Implementing RFC7638 KIDs

Introduction

In a previous article, we explored the world of JSON Web Signatures (JWS), focusing on the methods for publishing signing keys, and advocating a standards-based approach to key identification. In this article, we look at how Key Identifiers (KIDs) work in practice, using the ForgeRock® Identity Platform.

Following the recommendations of the previous article, we look at how to configure ForgeRock® Access Management (AM) to generate standards-based KIDs, using the algorithm specified by RFC 7638 (JSON Web Key Thumbprint).

KIDs: The basics

Let’s start with an example OIDC token issued by a demonstration deployment of AM using the configuration shipped with AM.

Decoded OIDC token

{
"typ": "JWT",
"kid": "wU3ifIIaLOUAReRB/FG6eM1P1QM=",
"alg": "PS256"
}{
"at_hash": "SkLqBsiWonLraGruur9ECQ",
"sub": "jane.doe",
"auditTrackingId": "31ed9b9f-d8aa-4c1f-9f47-dce1a4e89b47-98",
"iss": "https://am.authdemo.org/oauth2/realms/root/realms/test",
"tokenName": "id_token",
"aud": "testclient",
"azp": "testclient",
"auth_time": 1598391410,
"realm": "/test",
"exp": 1598392012,
"tokenType": "JWTToken",
"iat": 1598391412
}

The KID specified in the JOSE header matches the following entry from the AM JWK, to let relying parties locate the key required to verify the signatures on tokens issued by AM.

JWK entry

{
"kty": "RSA",
"kid": "wU3ifIIaLOUAReRB/FG6eM1P1QM=",
"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_UmkGv_
se0BkRkwMZavGD_Nqjq8x3-
fKNI45nU7E7COAh8gjn6LCXfug57EQfi0gOgKhOhVcLmKqIEXPmqeagvMndsXWIy6k8W
PPwBzSkN5PDLKBXKG_X1BwVvOE9276nrx6lJq3CgNbmiEihovNt_6g5pCxiSarIk2uaG
3T3Ve6hUJrM0W35QmqrNM9rL3laPgXtCuz4sJJN3rGnQq_25YbUawW9L1MTVbqKxWiyN
5WbXoWUg8to1DhoQnXzDymIMhFa45NTLhxtdH9CDprXWXWBaWzo8mIFes5yI4AJW4ZSg
1PPO2UJSQ",
"e": "AQAB",
"alg": "PS256"
}

Generating a KID

AM ships with a proprietary algorithm to create KIDs, based on a combination of the keystore alias name and the signing key details. This is fine for most environments, since the KID is usually treated as an opaque string: as long as there is a matching KID in the AM JWK, then relying parties (and AM itself) can validate a token signature. However, in some cases we need to use a speciBc algorithm to generate KIDs, for example to provide consistency among participants in a particular ecosystem. In the previous article (LINK), we cited the example of the UK Open Banking ecosystem.

Fortunately, Access Management provides a pluggable interface for implementing your own KID creation algorithms. As per the ForgeRock documentation, the process for doing this is:

  1. Write your own implementation of the KeyStoreKeyIdProvider interface.
  2. Add your implementation classes to the AM web application directory/archive.
  3. Configure AM with the advanced property org.forgerock.openam.secrets.keystore.keyid.provider with the name of your implementation entry class.
  4. Restart AM From this point on, the key identifiers in signed tokens and the AM JWK document will be built according to your implementation of the KID provider.

An example KID Provider with RFC 7638 support

Let’s look at how to build our own Key ID provider for ForgeRock AM. We’ll be writing our own implementation of the KeyStoreKeyIdProvider interface to create KIDs as per the RFC 7638 standard. First, you’ll need access to the ForgeRock dependencies—details of how to configure your environment are provided in the ForgeRock Knowledge Base article on accessing ForgeRock Maven repositories. LG: Should we provide a link?

In the case of most AM extension points, ForgeRock includes sample code in the public am-external repository. However, at the time of writing, the am-external repository does not include a sample Key ID provider. We are therefore going to look at some sample code to illustrate how to implement this interface. Note that this is sample code for demonstration purposes only; you will need to review and enhance the code for production environments. Note also that caution is required when changing the KID generation algorithm on a live service. You need to consider the tokens you have already issued with the previous KID values. More on this later.

This sample consists of the following:

  • A pom.xml file with the information Maven needs to build the plugin
  • The main class for the Key ID provider which implements the KeyStoreKeyIdProvider interface
  • A supporting utility class for handling the key algorithm itself

The sample project directory structure is as follows:

./pom.xml
./src/main/java/org/authdemo/keyidprovider/DemoKeyIdProvider.java
./src/main/java/org/authdemo/keyidprovider/ThumbprintUtils.java

Source code

As mentioned above, this sample source code is provided for demo purposes only, and is not intended for production use as-is.

pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<groupId>org.authdemo.keyidprovider</groupId>
<artifactId>am-demokeyidprovider</artifactId>
<version>1.0</version>
<modelVersion>4.0.0</modelVersion>
<name>AM Custom Library</name>
<repositories>
<repository>
<id>forgerock-build-dependencies</id>
<url>https://maven.forgerock.org/repo/forgerock-openam-6.5.2.1-
dependencies</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>org.forgerock.am</groupId>
<artifactId>openam-secrets</artifactId>
<version>6.5.2.3</version>
</dependency>
</dependencies>
</project

DemoKeyIdProvider.java

package org.authdemo.keyidprovider;
import java.security.PublicKey;
import java.security.cert.Certificate;
import java.util.Optional;
import org.forgerock.openam.secrets.KeyStoreKeyIdProvider;
import org.forgerock.json.jose.jwk.KeyUse;
/**
* KeyStoreKeyIdProvider implementation for ForgeRock AM.
*
* Provides rfc7638 compliant key ids built from SHA1 hash of JWK
thumbprint
*/
public class DemoKeyIdProvider implements KeyStoreKeyIdProvider {
@Override
public String getKeyId(KeyUse keyUse, String alias, PublicKey
publicKey, Optional<Certificate> certificate) {
return
ThumbprintUtils.getRfc7638ThumbprintFromKey(publicKey);
}
}

ThumprintUtils.java

package org.authdemo.keyidprovider;
import java.math.BigInteger;
import java.security.PublicKey;
import java.security.interfaces.ECPublicKey;
import java.security.interfaces.RSAPublicKey;
import java.util.Base64;
import java.util.SortedMap;
import java.util.TreeMap;
import org.forgerock.json.jose.jws.SupportedEllipticCurve;
import org.forgerock.json.JsonValue;
import java.security.MessageDigest;
import static java.nio.charset.StandardCharsets.UTF_8;
import java.util.Arrays;
public class ThumbprintUtils {
static String getRfc7638ThumbprintFromKey(PublicKey publicKey){
String thumbprint = null;
final SortedMap<String, String> essentialKeys = new
TreeMap();
if (publicKey instanceof ECPublicKey) {
ECPublicKey ecPublicKey = (ECPublicKey) publicKey;
String curve = getCurve(ecPublicKey);
BigInteger x = ecPublicKey.getW().getAffineX();
BigInteger y = ecPublicKey.getW().getAffineY();
essentialKeys.put("kty", "EC");
essentialKeys.put("crv", curve);
essentialKeys.put("x",
jwkBase64(toByteArrayUnsigned(x)));
essentialKeys.put("y",
jwkBase64(toByteArrayUnsigned(y)));
} else if (publicKey instanceof RSAPublicKey) {
RSAPublicKey rsaPublicKey = (RSAPublicKey) publicKey;
BigInteger n = rsaPublicKey.getModulus();
BigInteger e = rsaPublicKey.getPublicExponent();
essentialKeys.put("kty", "RSA");
essentialKeys.put("n",
jwkBase64(toByteArrayUnsigned(n)));
essentialKeys.put("e",
jwkBase64(toByteArrayUnsigned(e)));
} else {
throw new IllegalArgumentException("Public key type not
supported.");
}
final JsonValue jsonValue = new JsonValue(essentialKeys);
try {
final MessageDigest md = MessageDigest.getInstance("SHA-
1");
String macInput =
jsonValue.toString().replaceAll("\\s+", "");
byte[] digest = md.digest(macInput.getBytes(UTF_8));
thumbprint = jwkBase64(digest);
}
catch (Exception e) {
throw new IllegalArgumentException("Error building
digest [" + e + "]");
}
return thumbprint;
}
/**
* toByteArrayUnsigned
* @param bi - input BigInteger
* @return - unsigned version as byte array
*/
private static byte[] toByteArrayUnsigned(BigInteger bi) {
byte[] extractedBytes = bi.toByteArray();
int skipped = 0;
boolean skip = true;
for (byte b : extractedBytes) {
boolean signByte = b == (byte) 0x00;
if (skip && signByte) {
skipped++;
continue;
} else if (skip) {
skip = false;
}
}
extractedBytes = Arrays.copyOfRange(extractedBytes, skipped,
extractedBytes.length);
return extractedBytes;
}
/**
* jwkBase64
* @param input byte array to encode
* @return Base64 encoded with trailing equals sign(s) removed
*/
private static String jwkBase64(byte[] input)
{
return
Base64.getUrlEncoder().encodeToString(input).replaceAll("=","");
}
/**
* Figure out curve from EC public key
*/
private static String getCurve(ECPublicKey ecPublicKey) {
String curve = null;
switch (SupportedEllipticCurve.forKey(ecPublicKey))
{
case P256:
curve = "P-256";
break;
case P384:
curve = "P-384";
break;
case P521:
curve = "P-521";
break;
}
return curve;
}
}

Building and Deploying the Plugin

The plugin can be built from the base directory of the sample project structure as follows:

mvn clean install

This will create a target directory containing the .jar file with the plugin class files. This .jar file should be copied to the AM webapp directory under the WEB-INF/lib subdirectory (or packaged into a custom AM war file), as per any other custom extensions.

Configuring AM to use the plugin

The final step is to configure AM to use our custom plugin instead of the default Key ID provider. As per the ForgeRock documentation, you need to add the server advanced property org.forgerock.openam.secrets.keystore.keyid.provider with the name of the custom class. If you are using the AM admin console, add the following entry under Configure>Server Defaults>Advanced:

org.forgerock.openam.secrets.keystore.keyid.provider : org.authdemo.keyidprovider.DemoKeyIdProvider

AM is shipped with a proprietary algorithm to create KIDs, based on a combination of the keystore alias name and the signing key details. This is fine for most environments, since the KID is usually treated as an opaque string: as long as there is a matching KID in the AM JWK, then relying parties (and AM itself) can validate a token signature.

However, in some cases we need to use a specific algorithm to generate KIDs; for example, to provide consistency among participants in a particular ecosystem. In the previous article, we cited the example of the UK Open Banking ecosystem. Fortunately, AM provides a pluggable interface for implementing your own KID creation algorithms. As per the ForgeRock documentation, the process for doing this is

Write your own implementation of the KeyStoreKeyIdProvider interface

Add your implementation classes to the AM web application directory/archive

Configure AM with the advanced property org.forgerock.openam.secrets.keystore.keyid.provider with the name of your implementation entry class

Restart AM

From this point on, the key identifiers in signed tokens and the AM JWK document will be built according to your implementation of the KID provider.

An example KID Provider with RFC 7638 support

Let’s look at how to build our own KID provider for AM. We’ll be writing our own implementation of the KeyStoreKeyIdProvider interface to create Key IDs as per the RFC 7638 standard.

First, you’ll need access to the ForgeRock dependencies — details of how to configure your environment are provided in the ForgeRock knowledge base article on accessing ForgeRock Maven repositories. In the case of most AM extension points, ForgeRock includes sample code in the public am-external repository. However, at the time of writing, the am-external repository does not include a sample Key ID provider. We are therefore going to look at some sample code to illustrate how to implement this interface.

Note that this is sample code for demonstration purposes only — you will need to review and enhance the code for production environments.

Note also that caution is required when changing the KID generation algorithm on a live service — you need to consider the tokens you have already issued with the previous KID values. More on this later.

This sample consists of the following:

  • A pom.xml file with the information Maven needs to build the plugin
  • The main class for the Key ID provider which implements the

KeyStoreKeyIdProvider interface

A supporting utility class for handling the key algorithm itself

The sample project directory structure is as follows:

Building and deploying the plugin

The plugin can be built from the base directory of the sample project structure as follows mvn clean install. This will create a target directory containing the jar file with the plugin class files. This jar file should be copied to the Access Management webapp directory under the WEB-INF/lib subdirectory (or packaged into a custom AM .war file), as per any other custom extensions.

Configuring AM to use the plugin

The final step is to configure AM to use our custom plugin instead of the default Key ID provider. As per the ForgeRock documentation, you need to add the server advanced propertyorg.forgerock.openam.secrets.keystore.keyid.provider with the name of the custom class. If using the AM admin console, add the following entry under Configure>Server Defaults>Advanced.

Restart the AM service, and fetch the AM JWK. You’ll see that the KIDs have changed within the JWK. E.g. in the case of the sample JWK earlier, the KID will change from this:

{
"kty": "RSA",
"kid": "wU3ifIIaLOUAReRB/FG6eM1P1QM=",
"use": "sig",
"x5t": "5eOfy1Nn2MMIKVRRkq0OgFAw348",
"x5c": [
"MIIDdzCCAl+gAwIBAgIES3eb+zANBgkqhkiG9w0BAQsFADBsMRAwDgYDVQQGEwdVbmt
ub3duMRAwDgYDVQQIEwdVbmtub3duMRAwDgYDV.....

to this:

{
"kty": "RSA",
"kid": "EF71iSaosbC5C4tC6Syq1Gm647M",
"use": "sig",
"x5t": "5eOfy1Nn2MMIKVRRkq0OgFAw348",
"x5c": [
"MIIDdzCCAl+gAwIBAgIES3eb+zANBgkqhkiG9w0BAQsFADBsMRAwDgYDVQQGEwdVbmt
ub3duMRAwDgYDVQQIEwdVbmtub3duMRAwDgYDV.....

A Note of Caution

Here’s the cautious bit. Firstly, note that any customization of the ForgeRock platform will have an impact on future maintenance and support, so you should only customize the KID provider if you have a specific reason for doing so. Otherwise, it is best left to the default provider.

Secondly, as we hinted at earlier, there is a further caveat specific to the role of the KID provider.

As we have seen, when changing the KID provider, this immediately changes KIDs within the JWK published by AM. For any tokens issued after the change of provider, this is not an issue, because new tokens will contain the new KID to match the JWK. For tokens issued before the change of provider, there is a potential problem, in that these tokens will contain the previous KIDs, which will not be found in the JWK. This will cause problems for relying parties (and AM itself) when verifying token signatures.

When implementing a custom KID provider on a live system with active tokens in the wild, consideration needs to be made for handling this scenario. For example, you could apply the new algorithm only to key aliases with a specific naming format. In this case, existing keys will use the default algorithm and new keys with the given naming pattern will use the new algorithm. A cruder approach would be to use the key alias names as the KID, and apply the required algorithm to the alias names instead.

Conclusion

The extensibility of the ForgeRock platform lets us implement any preferred algorithm for generating JWS Key Identifiers. The defaultForgeRock KID provider is perfectly adequate for most use cases. However, in cases where a specific algorithm is required for a given use case, there is full flexibility to implement your own algorithm, whether it is based on proprietary mechanisms, or on standards such as RFC7638.

Other Articles by This Author