JSON Web Signatures, KIDs and Thumbprints: Sticking to Standard

Introduction

In this article, we’ll explore the world of JSON web signatures (JWS), focusing specifically on how third parties can access and identify your keys in order to verify your signatures. In a follow up article (LINK), we’ll look at a practical standards based approach using the ForgeRock® Identity Platform.

Terminology

As a starting point, let’s look at a sample authentication token which uses the OpenID Connect (OIDC) standard to identify an end user.

Raw OIDC token

In JWx terms, this is a JSON Web Token (JWT) secured by a JSON Web Signature (JWS). The token consists of three elements, each of which are base64url-encoded, and then concatenated with dot delimiters. If we break down the three elements of our token and decode them, we get the following:

  • The JSON Object Signing and Encryption (JOSE) header. More on this later:
{
"typ": "JWT",
"kid": "EF71iSaosbC5C4tC6Syq1Gm647M",
"alg": "PS256"
}
  • The JOSE payload (the signed data). In the case of OIDC, this contains details about the identity we have authenticated:
{
"at_hash": "lEjlYjYX-a1zv9uVKl89VQ",
"sub": "jane.doe",
"auditTrackingId": "cb158eb8-8c8f-40b5-a40e-a54642925e2a-252",
"iss": "https://am.authdemo.org/oauth2/realms/root/realms/test",
"tokenName": "id_token",
"aud": "testclient",
"azp": "testclient",
"auth_time": 1598288890,
"realm": "/test",
"exp": 1598289493,
"tokenType": "JWTToken",
"iat": 1598288893
}
  • The JOSE signature computed across the header and payload (binary value when decoded, so not shown here).

The method required to verify the signature depends on the method used to create it. There are two broad types of key used for signature: symmetric and asymmetric.

Symmetric key based signatures use a shared key. You need a copy of the signing key in order to verify the signature. In this case, you need a secure way of sharing the signing key between parties, and you need to trust each party you share the key with.

Asymmetric key based signatures use a private/public key pair. In this case you only need the public key to verify the signature. Since the public key is not sensitive (for example, you can’t use it to forge signatures), you can freely publish the key to let anyone verify your signatures.

We are going to focus on asymmetric keys, because they offer the best security and most flexible deployment options.

Sharing your keys

In order for relying parties to be able to verify your signatures, they need access to your keys. So how do you give them this access?

JSON Web Key (JWK) standard. This is a JSON representation of keys used for signing, encryption, and various other purposes. The following is an example JWK with a couple of signing keys—one RSA key and one Elliptic Curve.

Sample JWK document

{
“keys”: [
{
“kty”: “RSA”,
“kid”: “EF71iSaosbC5C4tC6Syq1Gm647M”,
“use”: “sig”,
“x5t”: “5eOfy1Nn2MMIKVRRkq0OgFAw348”,
“x5c”: [
“MIIDdzCCAl+gAwIBAgIES3eb+zANBgkqhkiG9w0BAQsFADBsMRAwDgYDVQQGEwdVbmt
ub3duMRAwDgYDVQQIEwdVbmtub3duMRAwDgYDVQQHEwdVbmtub3duMRAwDgYDVQQKEwd
Vbmtub3duMRAwDgYDVQQLEwdVbmtub3duMRAwDgYDVQQDEwdVbmtub3duMB4XDTE2MDU
yNDEzNDEzN1oXDTI2MDUyMjEzNDEzN1owbDEQMA4GA1UEBhMHVW5rbm93bjEQMA4GA1U
ECBMHVW5rbm93bjEQMA4GA1UEBxMHVW5rbm93bjEQMA4GA1UEChMHVW5rbm93bjEQMA4
GA1UECxMHVW5rbm93bjEQMA4GA1UEAxMHVW5rbm93bjCCASIwDQYJKoZIhvcNAQEBBQA
DggEPADCCAQoCggEBANdIhkOZeSHagT9ZecG+QQwWaUsi7OMv1JvpBr/7HtAZEZMDGWr
xg/zao6vMd/nyjSOOZ1OxOwjgIfII5+iwl37oOexEH4tIDoCoToVXC5iqiBFz5qnmoLz
J3bF1iMupPFjz8Ac0pDeTwyygVyhv19QcFbzhPdu+p68epSatwoDW5ohIoaLzbf+oOaQ
sYkmqyJNrmht091XuoVCazNFt+UJqqzTPay95Wj4F7Qrs+LCSTd6xp0Kv9uWG1GsFvS9
TE1W6isVosjeVm16FlIPLaNQ4aEJ18w8piDIRWuOTUy4cbXR/Qg6a11l1gWls6PJiBXr
OciOACVuGUoNTzztlCUkCAwEAAaMhMB8wHQYDVR0OBBYEFMm4/1hF4WEPYS5gMXRmmH0
gs6XjMA0GCSqGSIb3DQEBCwUAA4IBAQDVH/Md9lCQWxbSbie5lPdPLB72F4831glHlaq
ms7kzAM6IhRjXmd0QTYq3Ey1J88KSDf8A0HUZefhudnFaHmtxFv0SF5VdMUY14bJ9Usx
J5f4oP4CVh57fHK0w+EaKGGIw6TQEkL5L/+5QZZAywKgPz67A3o+uk45aKpF3GaNWjGR
WEPqcGkyQ0sIC2o7FUTV+MV1KHDRuBgreRCEpqMoY5XGXe/IJc1EJLFDnsjIOQU1rrUz
fM+WP/DigEQTPpkKWHJpouP+LLrGRj2ziYVbBDveP8KtHvLFsnexA/TidjOOxChKSLT9
LYFyQqsvUyCagBb4aLs009kbW6inN8zA6”
],
“n”: “10iGQ5l5IdqBP1l5wb5BDBZpSyLs4y_Um-kGv_se0BkRkwMZavGD_Nqjq8x3-
fKNI45nU7E7COAh8gjn6LCXfug57EQfi0gOgKhOhVcLmKqIEXPmqeagvMndsXWIy6k8W
PPwBzSkN5PDLKBXKG_X1BwVvOE9276nrx6lJq3CgNbmiEihovNt_6g5pCxiSarIk2uaG
3T3Ve6hUJrM0W35QmqrNM9rL3laPgXtCuz4sJJN3rGnQq_25YbUawW9L1MTVbqKxWiyN
5WbXoWUg8to1DhoQnXzDymIMhFa45NTLhxtdH9CDprXWXWBaWzo8mIFes5yI4AJW4ZSg
1PPO2UJSQ”,
“e”: “AQAB”,
“alg”: “PS256”
},
{
“kty”: “EC”,
“kid”: “WhUPrWNhvLWLxtrU3–1KMKn2o8I”,
“use”: “sig”,
“x5t”: “MUOPc5byMEN9q_9gqArkd1EDajg”,
“x5c”: [
“MIIBwjCCAWkCCQCw3GyPBTSiGzAJBgcqhkjOPQQBMGoxCzAJBgNVBAYTAlVLMRAwDgY
DVQQIEwdCcmlzdG9sMRAwDgYDVQQHEwdCcmlzdG9sMRIwEAYDVQQKEwlGb3JnZVJvY2s
xDzANBgNVBAsTBk9wZW5BTTESMBAGA1UEAxMJZXMyNTZ0ZXN0MB4XDTE3MDIwMzA5MzQ
0NloXDTIwMTAzMDA5MzQ0NlowajELMAkGA1UEBhMCVUsxEDAOBgNVBAgTB0JyaXN0b2w
xEDAOBgNVBAcTB0JyaXN0b2wxEjAQBgNVBAoTCUZvcmdlUm9jazEPMA0GA1UECxMGT3B
lbkFNMRIwEAYDVQQDEwllczI1NnRlc3QwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQ
3sy05tV/3YUlPBi9jZm9NVPeuBmntrtcO3NP/1HDsgLsTZsqKHD6KWIeJNRQnONcriWV
aIcZYTKNykyCVUz93MAkGByqGSM49BAEDSAAwRQIgZhTox7WpCb9krZMyHfgCzHwfu0F
VqaJsO2Nl2ArhCX0CIQC5GgWD5jjCRlIWSEFSDo4DZgoQFXaQkJUSUbJZYpi9dA==”
],
“x”: “N7MtObVf92FJTwYvY2ZvTVT3rgZp7a7XDtzT_9Rw7IA”,
“y”: “uxNmyoocPopYh4k1FCc41yuJZVohxlhMo3KTIJVTP3c”,
“crv”: “P-256”,
“alg”: “ES256”
}
]
}
}
}

Secondly, you need a common way to communicate this key information. One option is to publish your JWK online through HTTPS, via a known JWK URI. This allows dynamic changes such as algorithm updates and key rollover, and makes relying party configuration very simple. A common way to advertise your JWK URI is via your OpenID Connect Discovery endpoint.

Key Identifiers

You’ll see from the sample JWK document above that a JWK can include multiple keys, covering different key generations, purposes, and algorithms. When verifying the signature on a JWS, there needs to be a way to identify the required key within a JWK. Step forward the Key Identifier (KID). The JWS can include a signing KID in the kid header field, which must match a kid entry in your JWK. Revisiting our sample OIDC token, we can see the kid parameter in the JOSE header.

OIDC token header

{
"typ": "JWT",
"kid": "EF71iSaosbC5C4tC6Syq1Gm647M",
"alg": "PS256"
}

Looking at the key entries in the sample JWK, we can find a matching entry with the same kid parameter as follows.

JWK key entry

{
“kty”: “RSA”,
“kid”: “EF71iSaosbC5C4tC6Syq1Gm647M”,
“use”: “sig”,
“x5t”: “5eOfy1Nn2MMIKVRRkq0OgFAw348”,
“x5c”: [
“MIIDdzCC...kbW6inN8zA6”
],
“n”:
“10iGQ5l5Idq...zDymIMhFa45NTLhxtdH9CDprXWXWBaWzo8mIFes5yI4AJW4ZSg1PP
O2UJSQ”,
“e”: “AQAB”,
“alg”: “PS256”
}

We can now extract the public key from this JWK entry and use it to verify the signature on our OIDC token.

Testing

We can use openssl to test the end-to-end process for signature verification. Of course, this would normally be performed automatically by the relying party platform. For a scripted approach, have a look at the sample jwtx script available here. The steps (using the bash shell on *nix) are as follows:

  • Load the OIDC token:
oidc_token='eyJ0eXAiOiJKV1QiLCJraWQiOiJFRjcxaVNhb3NiQzVDNHRDNlN5cTFH
bTY0N00iLCJhbGciOiJQUzI1NiJ9.eyJhdF9oYXNoIjoibEVqbFlqWVgtYTF6djl1Vkt
sODlWUSIsInN1YiI6ImphbmUuZG9lIiwiYXVkaXRUcmFja2luZ0lkIjoiY2IxNThlYjg
tOGM4Zi00MGI1LWE0MGUtYTU0NjQyOTI1ZTJhLTI1MiIsImlzcyI6Imh0dHBzOi8vYW0
uYXV0aGRlbW8ub3JnL29hdXRoMi9yZWFsbXMvcm9vdC9yZWFsbXMvdGVzdCIsInRva2V
uTmFtZSI6ImlkX3Rva2VuIiwiYXVkIjoidGVzdGNsaWVudCIsImF6cCI6InRlc3RjbGl
lbnQiLCJhdXRoX3RpbWUiOjE1OTgyODg4OTAsInJlYWxtIjoiL3Rlc3QiLCJleHAiOjE
1OTgyODk0OTMsInRva2VuVHlwZSI6IkpXVFRva2VuIiwiaWF0IjoxNTk4Mjg4ODkzfQ.
NNKNdsOD2h0Y1kz75Ljqluu3QWzgVZyqrOxmBnMI9I6nPAqhd4rkxo3HsQ_E1e_0dpa_
jp-xB4-
FXk0RLI2xqFp7fEehW9NdaMZm2nT75Id2O_IAoNhqV_iski6HlKSwB3qJ5MwjBS2R2EG
_3Co3KDn2NuyIuqpu1RS6Ut1TnYH8P4-jse4AIIRr9kM-Id52-
TU1NlKkSAcHvjqyoPhXt6L_6nA60ZtduXWVwkWCuvhH32myG5K8UEQxNUlfO8L7VAWQPRPDPo1fDqlyMKeWQHlGA8TrgXRbdry1p0JvETFFXE_
GlxkOO5MFeOB3Hg
wftW6Mhf-N9g3Wewx3HMhgQ'
  • Extract the OIDC token header and payload (the first two elements of the token) as the input data for signature verification. Be careful not to add a new line:
awk -F. '{printf("%s.%s",$1,$2)}' <<< "$oidc_token" > signed.data
  • Extract the OIDC token signature (the third element of the token) and convert to binary (openssl doesn’t support base64url-decoding, so we have to do a bit of manipulation with sed to convert it to plain base64):
awk -F. '{printf($3)}' <<< "$oidc_token" | sed 's/-/+/g; s/_/\//g; s/$/==/' | openssl base64 -d -A -out signature.bin
  • Search the JWK document for the KID from the token header, and extract the public key from the corresponding certificate (the x5c parameter in the JWK). The certificate will be all on one line, so we need to feed it through openssl twice:
echo
'MIIDdzCCAl+gAwIBAgIES3eb+zANBgkqhkiG9w0BAQsFADBsMRAwDgYDVQQGEwdVbmt
ub3duMRAwDgYDVQQIEwdVbmtub3duMRAwDgYDVQQHEwdVbmtub3duMRAwDgYDVQQKEwd
Vbmtub3duMRAwDgYDVQQLEwdVbmtub3duMRAwDgYDVQQDEwdVbmtub3duMB4XDTE2MDU
yNDEzNDEzN1oXDTI2MDUyMjEzNDEzN1owbDEQMA4GA1UEBhMHVW5rbm93bjEQMA4GA1U
ECBMHVW5rbm93bjEQMA4GA1UEBxMHVW5rbm93bjEQMA4GA1UEChMHVW5rbm93bjEQMA4
GA1UECxMHVW5rbm93bjEQMA4GA1UEAxMHVW5rbm93bjCCASIwDQYJKoZIhvcNAQEBBQA
DggEPADCCAQoCggEBANdIhkOZeSHagT9ZecG+QQwWaUsi7OMv1JvpBr/7HtAZEZMDGWr
xg/zao6vMd/nyjSOOZ1OxOwjgIfII5+iwl37oOexEH4tIDoCoToVXC5iqiBFz5qnmoLz
J3bF1iMupPFjz8Ac0pDeTwyygVyhv19QcFbzhPdu+p68epSatwoDW5ohIoaLzbf+oOaQ
sYkmqyJNrmht091XuoVCazNFt+UJqqzTPay95Wj4F7Qrs+LCSTd6xp0Kv9uWG1GsFvS9
TE1W6isVosjeVm16FlIPLaNQ4aEJ18w8piDIRWuOTUy4cbXR/Qg6a11l1gWls6PJiBXr
OciOACVuGUoNTzztlCUkCAwEAAaMhMB8wHQYDVR0OBBYEFMm4/1hF4WEPYS5gMXRmmH0
gs6XjMA0GCSqGSIb3DQEBCwUAA4IBAQDVH/Md9lCQWxbSbie5lPdPLB72F4831glHlaq
ms7kzAM6IhRjXmd0QTYq3Ey1J88KSDf8A0HUZefhudnFaHmtxFv0SF5VdMUY14bJ9Usx
J5f4oP4CVh57fHK0w+EaKGGIw6TQEkL5L/+5QZZAywKgPz67A3o+uk45aKpF3GaNWjGR
WEPqcGkyQ0sIC2o7FUTV+MV1KHDRuBgreRCEpqMoY5XGXe/IJc1EJLFDnsjIOQU1rrUz
fM+WP/DigEQTPpkKWHJpouP+LLrGRj2ziYVbBDveP8KtHvLFsnexA/TidjOOxChKSLT9
LYFyQqsvUyCagBb4aLs009kbW6inN8zA6' | openssl base64 -d -A | openssl
x509 -inform der -noout -pubkey > pubkey.pem
  • Finally, verify the signature. Our sample token is signed with the PS256 algorithm, so we need to tell openssl about the padding mode (PSS) and hashing algorithm (SHA256):
openssl dgst -sigopt rsa_padding_mode:pss -verify pubkey.pem -sha256
-signature signature.bin signed.data

All being well, openssl will output the following message:

Verified OK

There are various niceties around using openssl for JWS verification—the jwtx script has more examples.

Standards-based key identifiers

KIDs can be any arbitrary value, as long as they are unique within the JWK. So, if you can use any unique string for your key ID, why would you want to use

a standards-based algorithm for creating a KID? Why not just use your own method of deriving KIDs, or even just a vanilla UUID?

Well, for one thing, following a known standard ensures that you are using robust, collision-resistant identifiers. Also, and perhaps more crucially, it means that you can agree on a common standard with third parties in cases where you need a consistent approach to Key IDs.

For example, in the case of the UK Open Banking ecosystem, your signing keys are published on your behalf by the Open Banking Directory. The Directory creates and publishes your JWK via a central trusted location. The key identifiers used within the JWK are created by the Directory, using the algorithm specified by the RFC 7638 standard.

As an Open Banking participant, when you construct a JWS such as an OIDC token, you need to make sure that the KID in your token header matches the KID in the JWK published by the Directory. Otherwise, the relying party will not be able to find the public key required to verify your signature. The smartest way to ensure your tokens contain the right KIDs is to use the same algorithm as the directory.

The RFC 7638 algorithm

Let’s have a look at the RFC 7638 algorithm, in order to understand the requirements for its implementation. According to the standard, each KID is a JWK Thumbprint, based on the details of the key from the JWK. This thumbprint is essentially a hash of a specific subset of JWK fields. The exact subset of fields depends on the key type (Elliptic Curve, RSA, or symmetric).

Taking our sample JWK as a working example, we can look at how to build a KID for the key used to sign our OIDC token. First, we extract the JWK entry for the OIDC signing key. Then we remove all of the parameters except the e, kty, and n parameters (this is the specified subset for an RSA key). We then put them in alphabetical order, ending up with the following:

{
“e”: “AQAB”,
“kty”: “RSA”,
“n”: “10iGQ5l5IdqBP1l5wb5BDBZpSyLs4y_Um-kGv_se0BkRkwMZavGD_Nqjq8x3-
fKNI45nU7E7COAh8gjn6LCXfug57EQfi0gOgKhOhVcLmKqIEXPmqeagvMndsXWIy6k8W
PPwBzSkN5PDLKBXKG_X1BwVvOE9276nrx6lJq3CgNbmiEihovNt_6g5pCxiSarIk2uaG
3T3Ve6hUJrM0W35QmqrNM9rL3laPgXtCuz4sJJN3rGnQq_25YbUawW9L1MTVbqKxWiyN
5WbXoWUg8to1DhoQnXzDymIMhFa45NTLhxtdH9CDprXWXWBaWzo8mIFes5yI4AJW4ZSg
1PPO2UJSQ”
}

We then remove all whitespace:

{“e”:“AQAB”,“kty”:“RSA”,“n”:“10iGQ5l5IdqBP1l5wb5BDBZpSyLs4y_UmkGv_
se0BkRkwMZavGD_Nqjq8x3-
fKNI45nU7E7COAh8gjn6LCXfug57EQfi0gOgKhOhVcLmKqIEXPmqeagvMndsXWIy6k8W
PPwBzSkN5PDLKBXKG_X1BwVvOE9276nrx6lJq3CgNbmiEihovNt_6g5pCxiSarIk2uaG
3T3Ve6hUJrM0W35QmqrNM9rL3laPgXtCuz4sJJN3rGnQq_25YbUawW9L1MTVbqKxWiyN
5WbXoWUg8to1DhoQnXzDymIMhFa45NTLhxtdH9CDprXWXWBaWzo8mIFes5yI4AJW4ZSg
1PPO2UJSQ”}

Finally, we create a hash of the resulting JSON, and base64url encode the hash to get the thumbprint. RFC 7638 does not mandate a specific hashing algorithm, but leaves it up to participants to agree the algorithm among themselves if necessary. In the case of the Open Banking ecosystem, the hashing algorithm used is SHA1. For our sample JWK, we can compute the hash using openssl as follows (as before, we use sed to manually transform the base64-encoding to base64url).

This gives us the following thumbprint:

EF71iSaosbC5C4tC6Syq1Gm647M

By using this value in the kid field of our token header, we can be sure that relying parties can find the corresponding public key in the JWK published by the Open Banking Directory.

Conclusion

Key identifiers are a critical part of the token validation process for OpenID Connect and any other JWS based trust model. They provide the means for correlating JOSE signatures with their corresponding keys, consideration when building an authentication and authorization ecosystem. Using a standard such as RFC 7638 ensures robust and unique key identifiers in large scale environments, with consistency of KIDs between all participants.

In the next article, we’ll look at how to implement standards based key identifiers in real life, using the ForgeRock Identity Platform.

Other Articles by This Author