AWS Key Management Service (AWS KMS) makes it easy to create and manage cryptographic keys in your applications. The service supports both symmetric and asymmetric customer master keys (CMKs). The asymmetric CMKs offer digital signature capability, which data consumers can use to verify that data is from a trusted producer and is unaltered in transit.

AWS KMS provides convenient API methods over asymmetric CMKs that you can use to both sign and verify signatures. However, when you need to verify a signature in high-throughput or decoupled architectures, using the AWS KMS API might not be practical. This blog post describes a delegated authorization use case, to illustrate how to approach this problem at scale. In our use case, AWS KMS signs the data, but the verification is performed in independent, distributed environments.

If you’re interested in signing JSON Web Tokens (JWTs) with AWS KMS, and then later validating those signatures in large-scale distributed applications, read on for an example use case and working code examples.

About signature verification

Digital signatures

In public-key cryptography, producing a digital signature requires the private key in a public/private key pair, whereas verifying the signature requires only the public key. By using standard signing, hashing algorithms, and key parameters, receivers can verify signatures in any environment that has access to the public key.

In many cases, you verify signatures more often than you issue them—consider how an API token might be signed once every hour but is verified thousands of times every second. Signing entities might also be disconnected at verification time—consider how verifying SSL/TLS certificates doesn’t require a connection to the certificate issuer.

You can use these asymmetries to your advantage when you design high performance and distributed delegated authorization systems.

When to verify signatures outside of AWS KMS

Verifying signatures without calling AWS KMS can be necessary in the following scenarios:

  • AWS KMS isn’t accessible from your signature verification environment
  • Your system has low latency or high throughput requirements for signature verification, exceeding AWS KMS API request quotas
  • You want to optimize costs by minimizing AWS KMS API calls

Decoupled signing and verification

Figure 1 illustrates how to decouple a Signer, based on AWS KMS, from one or more independent Verifiers. This process involves the following steps:

  1. During system setup, the Signer is provisioned with an asymmetric key pair. Signers such as AWS KMS support internal hardware security module (HSM)-backed, high-entropy asymmetric key generation and management.
  2. The Verifiers are configured to trust the Signer through an offline import of the Signer’s public key.
  3. During runtime, AWS KMS is requested to hash and create a digital signature over some original data. AWS KMS hashes the provided data and uses the private key in the asymmetric key pair to compute the signature over the hash. The original data, along with its signature, is delivered to a client.
  4. The client forwards the data and signature to one or more Verifiers, and requests access to their protected resources.
  5. The Verifiers verify the signature that is associated with the original data. The Verifiers use the Signer’s public key in this process. If the verification succeeds, meaning that the original data that was conveyed is unaltered and authentic, the Verifiers grant the client access to their protected resources.
Figure 1: Digital signature signing and verification in decoupled environments

Figure 1: Digital signature signing and verification in decoupled environments

Example use case

If you already have a use case and would like to get straight to the code, you can skip to the Deploy the solution section. If you’d like to understand a sample use case in depth, keep reading.

We’ll look at how a healthcare authority in the COVID-19 Exposure Notification System (ENS) reference architecture can use AWS KMS fronted by AWS Lambda compute to certify (sign) a patient’s diagnosis that is embedded within JWT claims. This certification can allow independent healthcare authorities to verify a patient’s diagnosis while also preserving the privacy of the patient.

Our example uses Elliptic Curve Digital Signature Algorithm (ECDSA) signatures, although the techniques presented can be equally applied to other public-key cryptography algorithms, such as RSA. We provide example code in the Golang and Python programming languages, as well as example OpenSSL commands. A previous blog post describes AWS KMS RSA signature verification using OpenSSL commands. See the Appendix at the end of this post for information on our ECDSA key configuration in AWS KMS.

You can generalize from this use case to your own architecture by using the included code examples.

Scenario: Exposure notifications in COVID-19 digital contact tracing

As part of COVID-19 digital contact tracing, the Exposure Notifications system validates that diagnosis test results are from a legitimate, trusted testing facility, while protecting the identity of the patient. To facilitate privacy-preserving contact tracing, the system creates an anonymizing chain of trust with digital signatures.

The system makes it possible for healthcare authorities to send notifications of potential exposure through mobile devices to users whose phone was near the phone of a user with confirmed positive COVID-19 diagnosis. To protect identity and personal information, neither the originating user nor the receivers know each other’s identity or location of exposure.

Figure 2 illustrates the architecture. An Exposure Verification Server (EVS) provides a set of APIs that allow the mobile user (patient) and the public health authority worker to provide cryptographic evidence that a positive diagnosis was issued by a verified laboratory.

An Exposure Notification Server (ENS) receives trusted, anonymized exposure information from registered mobile applications, and in turn sends out a potential exposure notification to all mobile devices in the configured domain. Each mobile application uses the aggregated, anonymous information (Temporary Exposure Keys, or TEKs) to calculate potential exposure.

In this example, the EVS service is running in the AWS Cloud and uses AWS Lambda with access to AWS KMS for signing. The ENS is running elsewhere, administered by a third party.

The Exposure Notification System architecture has properties that limit direct use of the AWS KMS API:

  • The EVS and ENS servers have no connectivity, and are operated by different entities
  • The ENS server validates a high volume of signatures, potentially millions per day
Figure 2: COVID-19 Exposure Notification System with AWS-based EVS

Figure 2: COVID-19 Exposure Notification System with AWS-based EVS

Let’s walk through each step and show how you can use an offline digital signature verification to pass information through a chain of trust, without revealing identity, while helping to ensure that any tampering of diagnosis results can be detected.

  1. At system setup, an administrator for the EVS that is associated with a laboratory facility generates an ECDSA signing key pair by using AWS KMS. The administrator, on behalf of the laboratory facility, registers with the ENS by providing details about the laboratory and the public key of the ECDSA key pair. This information handoff also includes metadata about the key and signing parameters, such as the key identifier, the elliptic curve used in creation of the key, and the hashing algorithm that was used. The ENS establishes trust through a contractual, offline process. (“We know this is a valid facility.”)
  2. A user participating in Exposure Notifications is diagnosed with COVID-19. The Public Health Authority (PHA) issues a short-lived, human-readable, one-time code for the user to enter into their mobile application, issued by the EVS. The one-time code is short and easy for a human to use, but is therefore limited in the security protections it offers.
  3. The application exchanges the one-time code for a temporary API token, which improves the security of subsequent communications. The app walks the user through health-related questions without prompting for any identifying information.
  4. TEKs are generated on the phone and are transmitted to nearby phones by using Bluetooth Low Energy signals. When a phone detects the TEK of another phone, this means that the two phones have been physically nearby for a sufficient time to pose an exposure risk. The phones continuously transmit and collect TEKs. At a predefined interval, the phone gathers its collected TEKs over the period, applies a hash function, and sends the resulting hash to the EVS for signing. The temporary API token secures this API call.
  5. A Lambda function in EVS calls AWS KMS to sign a JWT by using the ECDSA key that was created in step 1. The JWT contains custom claims that a person associated with a mobile device was diagnosed with COVID-19 on a certain date, and the hash of the TEKs, but contains no identifying information about the person or the device. The EVS returns the signed JWT to the mobile application.
  6. The signed JWT and the TEKs are sent to the ENS for widespread distribution of the TEKs. The ENS validates the identity of the EVS by extracting the key identifier embedded in the received JWT and matching the key identifier against its previously configured value from step 1. The ENS then cryptographically verifies the digital signature. After this process is complete, the ENS has proof that a trusted lab provided the results, but doesn’t have information about the actual patient.
  7. The TEKs are distributed to the target geography. The mobile phone application checks the downloaded TEKs against its own list of generated TEKs, and alerts the user of potential exposure if a match is found.

Architecture benefits

The ENS server needs to know that JWTs that are submitted for distribution represent legitimate diagnoses. At a national scale, calling an external API for every digital signature check would be prohibitively expensive and complicated. By checking signatures transmitted with the message at the point of receipt with no external dependencies, the system can be faster and more reliable.

Although the public health authority representative needs to know the patient’s identity, the distribution server does not. By using digital signatures, you create a chain of anonymized trust—the notification server trusts the verification server, and the JWT’s signature from the verification server applies that trust and detects alterations of the data.

Signed JWTs are the key architectural component that makes this system possible. Now that you’ve seen why you might want to use signed JWTs, let’s look at how to work with them.

Deploy the solution

In this section, we provide code examples that implement steps in the example scenario. We’ve kept the signing code domain-agnostic; you can adapt it to your own use case.

Sign a JWT

In step 5 in the preceding section, the ENV server signs a JWT, which indicates that a known, trusted lab has provided results.

The JWT is signed according to the ES256 standard (ECDSA using the P-256 curve and the SHA-256 hash) as required by the EVS specification. The EVS system performs the following steps:

  1. Creates a JWT header that carries signing metadata, including the signature and hashing algorithm (for example, ES256) and the key identifier used.
  2. Creates a JWT payload that contains claims that assert the patient’s COVID-19 diagnosis and the hash of TEKs.
  3. Serializes the JWT header and payload JSON objects into strings and encodes them to base64url format. The system then concatenates the two strings together, separated by a period (“.”) character to form the string to be signed.
  4. Invokes the AWS KMS sign() API, inputting the ECDSA CMK ID, the string to be signed, and the signing algorithm to use.
  5. Encodes the binary signature bytes that were returned from AWS KMS into base64url format.
  6. Appends the base64url signature string to the first two parts of the JWT, separated by a period character to produce the final JWT.
  7. Returns the JWT back to the patient’s mobile app.

You can implement the preceding steps in any programming language by using the AWS KMS SDK or the AWS KMS HTTP API. The following code shows an example implementation in Golang that uses the AWS SDK for Go V2.

Note: You’ll need to change the values tagged “<your…>” in all of the code samples below.

package main import ( "context" "encoding/base64" "fmt" "log" "strings" "" "" ""
) func main() { // Load the Shared AWS Configuration (~/.aws/config) cfg, err := config.LoadDefaultConfig(context.TODO()) if err != nil { log.Fatal(err) } // Create an AWS KMS service client client := kms.NewFromConfig(cfg) // As per the JWT specification (, // the JWT header, payload, and signature are base64url encoded strings // concatenated by a period (‘.’) character. The JWT signing string // is the JWT header and payload strings joined by a period character. // Example signingString := "eyJhbGciOiJFUzI1NiIsImtpZCI6ImFsaWFzL0VOVlNfVlRfU0lHTklOR19LRVkiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJhd3MtZXhwb3N1cmUtbm90aWZpY2F0aW9ucy12ZXJpZmljYXRpb24tc2VydmVyIiwiZXhwIjoxNTk5NTg5NTM0LCJqdGkiOiJxMHg1N0lrRkMvSU9hdzAvNkpYVWhvaHFnV3RqZFVSaUNWMGpuVEpvR1BxTkNsbHdBWWhtTVJLUk1YOXUwb1I2bEtyTXNVSkdGZFJ6aCtncEJiakpCTWR4dVJBN3llYzEyWmE1SzJUMEFWWjZhMVdjNklYQ1ZlNGR6aHkyckJFbiIsImlhdCI6MTU5OTU4OTIzNCwiaXNzIjoiYXdzLWV4cG9zdXJlLW5vdGlmaWNhdGlvbnMtdmVyaWZpY2F0aW9uLXNlcnZlciIsInN1YiI6Im5lZ2F0aXZlLiJ9" signingString := "<your-jwt-header-and-payload-b64url-joined-by-period>" // Populate signing parameters for the AWS KMS Sign() method // signingAlgorithm := types.SigningAlgorithmSpecEcdsaSha256 kid := "<your-aws-kms-key-id-or-alias>" signInput := kms.SignInput{ KeyId: &kid, Message: []byte(signingString), SigningAlgorithm: signingAlgorithm, } fmt.Printf("Sign Input: %#v\n", signInput) // Invoke the AWS KMS Sign() API // signOutput, err := client.Sign(context.TODO(), &signInput) if err != nil { panic(err) } fmt.Printf("Sign Output Signature: %v\n", signOutput.Signature) // Convert signature bytes to base-64 URL encoding (without padding) sigB64 := base64.RawURLEncoding.EncodeToString(signOutput.Signature) // Append signature to first two parts of JWT to produce signed JWT signedJwt := strings.Join([]string{signingString, sigB64}, ".") fmt.Printf("Signed JWT: %s\n", signedJwt)

Verify signatures

Because AWS KMS produces standardized, interoperable ECDSA signatures, you can use any standard implementation of ECDSA for verifying the signature. This step is performed by the Exposure Notification Server in the example, but you can use this code anywhere you want to verify the signature that was produced in the previous section.

All three of the following examples achieve the same end result—they verify the signature. We’ve provided examples that use Golang, Python, and OpenSSL to demonstrate the flexibility in this approach.

Signature verification in Golang

The Golang code shown in this section uses its built-in crypto package to verify the signature. The code performs the following steps:

  • Splits the input JWT string into individual parts (header, payload, and signature) separated by a period (“.”) character
  • Computes a SHA-256 hash of the signed string, that is, the first two parts of the JWT string: the header and payload
  • Decodes the PEM-formatted ECDSA public key that was extracted from AWS KMS as part of the EVS-to-ENS trust establishment
  • Converts the signature string from base64url characters to binary
  • Unmarshals the ASN.1 encoded signature and extracts the R and S integer values of the signature
  • Calls the ecdsa.Verify() method with the public key, hash value, R, and S values of the signature arguments, and receives the verification outcome
package main import ( "crypto/ecdsa" "crypto/sha256" "crypto/x509" "encoding/asn1" "encoding/base64" "encoding/pem" "fmt" "math/big" "strings"
) func main() { /* Input your JWT string for signature verification. Note: The code example below does not show extraction of ‘kid’, ‘iss’, and ‘aud’ fields from the input JWT and the matching of these fields against previously configured values (at the time of offline trust establishment). You should consider implementing these checks as a security measure. The code assumes the ECDSA elliptic curve (NIST P-256) and the hashing algorithm (SHA-256) used by the JWT, but consider extracting the curve and the algorithm from the input JWT (the “alg” field in the JWT header) to apply the proper algorithms at runtime. You may use the sample JWT string and the following sample public key to run this code. The signature in the sample JWT string below was produced using the private key paired with the following sample public key. */ // jwtStr := "eyJhbGciOiJFUzI1NiIsImtpZCI6ImFsaWFzL0VOVlNfVlRfU0lHTklOR19LRVkiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJhd3MtZXhwb3N1cmUtbm90aWZpY2F0aW9ucy12ZXJpZmljYXRpb24tc2VydmVyIiwiZXhwIjoxNTk5NTg5NTM0LCJqdGkiOiJxMHg1N0lrRkMvSU9hdzAvNkpYVWhvaHFnV3RqZFVSaUNWMGpuVEpvR1BxTkNsbHdBWWhtTVJLUk1YOXUwb1I2bEtyTXNVSkdGZFJ6aCtncEJiakpCTWR4dVJBN3llYzEyWmE1SzJUMEFWWjZhMVdjNklYQ1ZlNGR6aHkyckJFbiIsImlhdCI6MTU5OTU4OTIzNCwiaXNzIjoiYXdzLWV4cG9zdXJlLW5vdGlmaWNhdGlvbnMtdmVyaWZpY2F0aW9uLXNlcnZlciIsInN1YiI6Im5lZ2F0aXZlLiJ9.MEYCIQCiLqsE2bxKdDi3NvX0mXqcHbvvDtI9zcCwPUHQiQutoQIhAJDhhCdRSlk_QYU_7_9X11yEcPzNHWF4qq2wRG66w7Lh" jwtStr := "<your-jwt-string>" jwtParts := strings.Split(jwtStr, ".") // Compute a SHA-256 hash of the header and payload parts of the JWT string hashInput := strings.Join(jwtParts[0:2], ".") digest := sha256.Sum256([]byte(hashInput)) /* Decode the ECDSA public key copied from AWS KMS Sample public key -- pubPemKey := "-----BEGIN PUBLIC KEY-----\n" + "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAErPPPHw8ilBwBNBhRZjyVOnKoHOri\n" + "nS1ifFDScjQR4GRIcAzsTwlKjblMOcmxwy9TNOGrGnHTjw1XnIrBBhOPhg==\n" + "-----END PUBLIC KEY-----" */ pubPemKey := "-----BEGIN PUBLIC KEY-----<your-key>-----END PUBLIC KEY-----" block, _ := pem.Decode([]byte(pubPemKey)) if block == nil || block.Type != "PUBLIC KEY" { panic("Failed to decode PEM public key") } pubEcdsaKey, err := x509.ParsePKIXPublicKey(block.Bytes) if err != nil { panic("Failed to parse ECDSA public key") } // Decode the signature to extract a DER-encoded byte string sigDer, err := base64.RawURLEncoding.DecodeString(jwtParts[2]) if err != nil { panic(err) } // Unmarshal the R and S components of the ASN.1-encoded signature type ECDSASignature struct { R, S *big.Int } sigRS := &ECDSASignature{} _, err = asn1.Unmarshal(sigDer, sigRS) if err != nil { panic(err) } // Verify signature ok := ecdsa.Verify(pubEcdsaKey.(*ecdsa.PublicKey), digest[:], sigRS.R, sigRS.S) if !ok { panic("Signature verification failed") } fmt.Println("Signature verification successful")

Signature verification in Python

The Python code shown in this section uses the python-ecdsa module to verify the signature. The code performs the following steps:

  • Splits the input JWT string into individual parts (header, payload, and signature) separated by a period (“.”) character
  • Converts the signature string from base64url characters to binary
  • Verifies the signature with the public key, signed string, and hash algorithm arguments, and receives the verification outcome
import base64
from hashlib import sha256 import ecdsa
from ecdsa.util import sigdecode_der """
Input your JWT string for signature verification.
You may use the sample JWT string and the following sample public key to run this code. The signature in the sample JWT string below was produced using the private key paired with the following sample public key.
#jwtStr = "eyJhbGciOiJFUzI1NiIsImtpZCI6ImFsaWFzL0VOVlNfVlRfU0lHTklOR19LRVkiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJhd3MtZXhwb3N1cmUtbm90aWZpY2F0aW9ucy12ZXJpZmljYXRpb24tc2VydmVyIiwiZXhwIjoxNTk5NTg5NTM0LCJqdGkiOiJxMHg1N0lrRkMvSU9hdzAvNkpYVWhvaHFnV3RqZFVSaUNWMGpuVEpvR1BxTkNsbHdBWWhtTVJLUk1YOXUwb1I2bEtyTXNVSkdGZFJ6aCtncEJiakpCTWR4dVJBN3llYzEyWmE1SzJUMEFWWjZhMVdjNklYQ1ZlNGR6aHkyckJFbiIsImlhdCI6MTU5OTU4OTIzNCwiaXNzIjoiYXdzLWV4cG9zdXJlLW5vdGlmaWNhdGlvbnMtdmVyaWZpY2F0aW9uLXNlcnZlciIsInN1YiI6Im5lZ2F0aXZlLiJ9.MEYCIQCiLqsE2bxKdDi3NvX0mXqcHbvvDtI9zcCwPUHQiQutoQIhAJDhhCdRSlk_QYU_7_9X11yEcPzNHWF4qq2wRG66w7Lh"
jwtStr = "<your-jwt-string>"
jwtParts = jwtStr.split(".") # Compute a SHA-256 hash of the header and payload parts of the JWT string
signedStr = ".".join(jwtParts[0:2]).encode(encoding="ASCII")
signature = base64.urlsafe_b64decode(jwtParts[2]) """ Decode the ECDSA public key copied from AWS KMS
Sample public key --
pubPemKey = ("-----BEGIN PUBLIC KEY-----\n" "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAErPPPHw8ilBwBNBhRZjyVOnKoHOri\n" "nS1ifFDScjQR4GRIcAzsTwlKjblMOcmxwy9TNOGrGnHTjw1XnIrBBhOPhg==\n" "-----END PUBLIC KEY-----")
pubPemKey = "-----BEGIN PUBLIC KEY-----<your-key>-----END PUBLIC KEY-----" verifyKey = ecdsa.VerifyingKey.from_pem(pubPemKey)
if verifyKey.verify(signature, signedStr, sha256, sigdecode=sigdecode_der): print ("Signature verification successful")
else: print ("Signature verification failed")

Signature verification with OpenSSL

You can use the openssl command line utility as shown following to verify the signature.

openssl dgst -sha256 -verify pubkey.pem -signature sig.der msg.txt

In this command, you instruct openssl to compute a SHA-256 digest of an input message stored in msg.txt. Continuing with our JWT example, this file will contain the first two parts of the JWT string—the header and payload separated by a period (“.”) character. This is the original message that was hashed, and the resultant hash value will be verified against the provided signature by OpenSSL. A sample msg.txt is shown following.


You specify the file name that contains the ECDSA public key—pubkey.pem. This is the ECDSA public key in PEM format that was extracted from AWS KMS.

vi pubkey.pem ———BEGIN PUBLIC KEY———<your-key>———END PUBLIC KEY———

A sample pubkey.pem is shown following.

-----END PUBLIC KEY-----

The sig.der argument contains the signature bytes in DER format. You can convert the base-64 encoded signature bytes in the JWT string into the DER format with the following command.

echo -n <your signature in base-64 encoded format> | base64 -D > sig.der

Sample signature bytes in base-64 are shown following.


The openssl command will produce the following output if all the parameters have been provided properly.

Verified OK

When to use AWS KMS for signature verification

Although this post shows how to verify signatures without calling AWS KMS in a distributed use case, many scenarios benefit from signature verification using AWS KMS. Such benefits include the following:

  • Control authorization of signature verification requests with AWS Identity and Access Management (IAM) and AWS KMS key policies
  • Achieve strong non-repudiation of the original signed message
  • Audit API calls with native AWS CloudTrail integration
  • Monitor the volume of verification requests with native Amazon CloudWatch integration
  • Immediately invalidate keys
  • Rotate key pairs to keep signature verification synchronized with updated keys

For more guidance on using public keys outside of AWS KMS, see Special considerations for downloading public keys.


Digital signatures make it possible for you to perform fast and inexpensive checks on the authenticity of information without having to reach out to an external source.

AWS KMS is powerful and convenient, and designed to solve a specific set of use cases very well. Sometimes, however, you might be designing a system that needs to share trust among different parties, or you might need to verify a signature with a public key that was generated outside of KMS, or have performance requirements that make calls to AWS KMS prohibitive. For these cases, you can use the techniques in this post to build trusted systems based on digital signatures.

Appendix: ECDSA key configuration in AWS KMS

AWS KMS supports two popular digital signing mechanisms—ECDSA and RSA. We use ECDSA in this post as required in the EVS specification.

An ECDSA key is referred to as an asymmetric customer master key (CMK) in AWS KMS and can be created as described in Creating keys in the AWS KMS documentation. Figure 3 shows the cryptographic configuration for an ECDSA key created in AWS KMS.

Figure 3: Cryptographic configuration of an ECDSA key pair in AWS KMS

Figure 3: Cryptographic configuration of an ECDSA key pair in AWS KMS

The Key Type field in this configuration indicates that this is an asymmetric key. The Origin field indicates that this key was generated within AWS KMS, as opposed to being imported.

The Key Spec field value of ECC_NIST_P256 indicates that this key can be used to produce NIST FIPS 186-4 (section 6.4)-compliant ECDSA signatures, where the key was generated using the curve labeled secp256r1.

The Key Usage field indicates that this key can be used for signing and verification only. The Signing algorithms field indicates that the SHA-256 hashing algorithm is used for computing the hash of the input message that is signed or verified.

Other ECDSA key specifications may be chosen based on your security requirements and on guidance from the AWS KMS documentation.

If you have feedback about this post, submit comments in the Comments section below. If you have questions about this post, start a new thread on the AWS KMS forum.

Want more AWS Security how-to content, news, and feature announcements? Follow us on Twitter.


Raj Jain

Raj is a Senior Cloud Architect at AWS. He is passionate about helping customers build well-architected applications in AWS. Raj is a published author in Bell Labs Technical Journal, has authored 3 IETF standards, and holds 12 patents in internet telephony and applied cryptography. In his spare time, Raj enjoys the outdoors, cooking, reading, and travel.


Davis Aites

Davis is a Principal Solutions Architect in the AWS Health and Human Services team. With over 25 years of software development and IT experience, he still finds that he learns something new every day. An inveterate tinkerer and confirmed bibliophile, when not designing solutions you can find him in his shop or trying a new recipe.