All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.nimbusds.openid.connect.provider.jwkset.JWKSetSpec Maven / Gradle / Ivy

Go to download

JSON Web Key (JWK) set specification, utilities and generator for Connect2id server deployments.

There is a newer version: 2.0
Show newest version
package com.nimbusds.openid.connect.provider.jwkset;


import com.nimbusds.jose.EncryptionMethod;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JWEAlgorithm;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.crypto.bc.BouncyCastleProviderSingleton;
import com.nimbusds.jose.crypto.impl.RSASSAProvider;
import com.nimbusds.jose.jwk.*;
import com.nimbusds.jose.jwk.gen.ECKeyGenerator;
import com.nimbusds.jose.jwk.gen.OctetKeyPairGenerator;
import com.nimbusds.jose.jwk.gen.OctetSequenceKeyGenerator;
import com.nimbusds.jose.jwk.gen.RSAKeyGenerator;
import com.nimbusds.jwt.util.DateUtils;
import org.checkerframework.checker.nullness.qual.Nullable;

import javax.crypto.SecretKey;
import java.util.*;


/**
 * Connect2id server OpenID Provider / OAuth 2.0 authorisation server JWK set
 * specification.
 */
public final class JWKSetSpec {
	
	
	/**
	 * The permitted RSA signing and encryption key bit sizes. The weak
	 * 1024-bit key requires a special {@code jose.allowWeakKeys=true}
	 * Connect2id server configuration to be accepted.
	 */
	public static final int[] RSA_KEY_BIT_SIZES = new int[]{1024, 2048, 3072, 4096};
	
	
	/**
	 * Rotated signing RSA keys.
	 */
	public static class RotatedRSASigning {
		
		
		/**
		 * The default RSA JWK size.
		 */
		public static final int KEY_BIT_SIZE = 2048;
		
		
		/**
		 * Returns a JWK matcher.
		 *
		 * @param alg The expected JWS algorithm. Should not be
		 *            {@code null}.
		 *
		 * @return The JWK matcher.
		 */
		public static JWKMatcher createKeyMatcher(final @Nullable JWSAlgorithm alg) {
			
			return new JWKMatcher.Builder()
				.keyType(KeyType.RSA)
				.privateOnly(true)
				.algorithms(alg, null) // The JWS alg may be omitted
				.keyUses(KeyUse.SIGNATURE)
				.keySizes(RSA_KEY_BIT_SIZES)
				.hasKeyID(true)
				.build();
		}
		
		
		/**
		 * Generates a JWK with the specified key ID.
		 *
		 * @param kid The key ID, {@code null} if not specified.
		 *
		 * @return The JWK.
		 */
		public static RSAKey generateKey(final String kid)
			throws JOSEException {
			
			return generateKey(kid, KEY_BIT_SIZE);
		}
		
		
		/**
		 * Generates a JWK with the specified key ID and key size.
		 *
		 * @param kid        The key ID, {@code null} if not specified.
		 * @param keyBitSize The key bit size.
		 *
		 * @return The JWK.
		 */
		public static RSAKey generateKey(final String kid, final int keyBitSize)
			throws JOSEException {
			
			return new RSAKeyGenerator(keyBitSize)
				.keyUse(KeyUse.SIGNATURE)
				.keyID(kid)
				.issueTime(DateUtils.nowWithSecondsPrecision())
				.generate();
		}
		
		
		/**
		 * Loads the matching JWKs from the specified JWK set.
		 *
		 * @param jwkSet The JWK set.
		 * @param jwsAlg The expected JWS algorithm. Should not be
		 *               {@code null}.
		 *
		 * @return The matching JWKs, empty set if none.
		 *
		 * @throws JOSEException If the JWS algorithm is not supported.
		 */
		public static List loadKeys(final JWKSet jwkSet, final JWSAlgorithm jwsAlg)
			throws JOSEException {
			
			if (! RSASSAProvider.SUPPORTED_ALGORITHMS.contains(jwsAlg)) {
				throw new JOSEException("Invalid / unsupported RSA signature algorithm: " + jwsAlg);
			}
			
			List jwkMatches = new JWKSelector(createKeyMatcher(jwsAlg)).select(jwkSet);
			List rsaJWKMatches = new LinkedList<>();
			jwkMatches.forEach(jwk -> rsaJWKMatches.add(jwk.toRSAKey()));
			return rsaJWKMatches;
		}
		
		
		private RotatedRSASigning() {}
	}
	
	
	/**
	 * Rotated signing EC keys.
	 */
	public static class RotatedECSigning {
		
		
		/**
		 * The supported EC curves.
		 */
		public static final Set SUPPORTED_CURVES = Collections.unmodifiableSet(
			new LinkedHashSet<>(Arrays.asList(Curve.P_256, Curve.P_384, Curve.P_521, Curve.SECP256K1))
		);
		
		
		/**
		 * Returns a signing EC JWK matcher.
		 *
		 * @param alg The expected EC DSA algorithm.
		 *
		 * @return The JWK matcher.
		 */
		public static JWKMatcher createKeyMatcher(final JWSAlgorithm alg) {
			
			Set curves = Curve.forJWSAlgorithm(alg);
			
			if (curves == null) {
				throw new IllegalArgumentException("Invalid / unsupported EC DSA algorithm: " + alg);
			}
			
			return new JWKMatcher.Builder()
				.keyType(KeyType.EC)
				.curves(curves)
				.privateOnly(true)
				.algorithms(alg, null) // The JWS alg may be unspecified
				.keyUses(KeyUse.SIGNATURE)
				.hasKeyID(true)
				.build();
		}
		
		
		/**
		 * Generates a JWK with the specified curve and key ID.
		 *
		 * @param crv The curve. Must not be {@code null}.
		 * @param kid The key ID, {@code null} if not specified.
		 *
		 * @return The JWK.
		 */
		public static ECKey generateKey(final Curve crv, final String kid)
			throws JOSEException {
			
			return new ECKeyGenerator(crv)
				.keyUse(KeyUse.SIGNATURE)
				.keyID(kid)
				.issueTime(DateUtils.nowWithSecondsPrecision())
				.provider(BouncyCastleProviderSingleton.getInstance())
				.generate();
		}
		
		
		/**
		 * Loads the matching JWKs from the specified JWK set.
		 *
		 * @param jwkSet The JWK set.
		 * @param jwsAlg The expected JWS algorithm. Should not be
		 *               {@code null}.
		 *
		 * @return The matching JWKs, empty set if none.
		 *
		 * @throws JOSEException If the JWS algorithm is not supported.
		 */
		public static List loadKeys(final JWKSet jwkSet, final JWSAlgorithm jwsAlg)
			throws JOSEException {
			
			JWKSelector jwkSelector;
			try {
				jwkSelector = new JWKSelector(createKeyMatcher(jwsAlg));
			} catch (IllegalArgumentException e) {
				throw new JOSEException(e.getMessage());
			}
			List jwkMatches = jwkSelector.select(jwkSet);
			List ecMatches = new LinkedList<>();
			jwkMatches.forEach(jwk -> ecMatches.add(jwk.toECKey()));
			return ecMatches;
		}
		
		
		private RotatedECSigning() {}
	}
	
	
	/**
	 * Rotated signing EdDSA keys.
	 */
	public static class RotatedEdDSASigning {
		
		
		/**
		 * The JWK matcher.
		 */
		public static final JWKMatcher KEY_MATCHER = new JWKMatcher.Builder()
				.keyType(KeyType.OKP)
				.curve(Curve.Ed25519)
				.privateOnly(true)
				.algorithms(JWSAlgorithm.EdDSA, null) // The JWS alg may be unspecified
				.keyUses(KeyUse.SIGNATURE)
				.hasKeyID(true)
				.build();
		
		
		/**
		 * Generates a JWK with the specified key ID.
		 *
		 * @param kid The key ID, {@code null} if not specified.
		 *
		 * @return The JWK.
		 */
		public static OctetKeyPair generateKey(final String kid)
			throws JOSEException {
			
			return new OctetKeyPairGenerator(Curve.Ed25519)
				.keyUse(KeyUse.SIGNATURE)
				.keyID(kid)
				.issueTime(DateUtils.nowWithSecondsPrecision())
				.generate();
		}
		
		
		/**
		 * Loads the matching JWKs from the specified JWK set.
		 *
		 * @param jwkSet The JWK set.
		 *
		 * @return The matching JWKs, empty set if none.
		 */
		public static List loadKeys(final JWKSet jwkSet) {
			
			List jwkMatches = new JWKSelector(KEY_MATCHER).select(jwkSet);
			List okpMatches = new LinkedList<>();
			jwkMatches.forEach(jwk -> okpMatches.add(jwk.toOctetKeyPair()));
			return okpMatches;
		}
		
		
		private RotatedEdDSASigning() {}
	}
	
	
	/**
	 * Rotated encryption RSA keys.
	 */
	public static class RotatedRSAEncryption {
		
		
		/**
		 * The default RSA JWK size.
		 */
		public static final int KEY_BIT_SIZE = 2048;
		
		
		/**
		 * Generates a JWK with the specified key ID.
		 *
		 * @param kid The key ID, {@code null} if not specified.
		 *
		 * @return The JWK.
		 */
		public static RSAKey generateKey(final String kid)
			throws JOSEException {
			
			return new RSAKeyGenerator(KEY_BIT_SIZE)
				.keyUse(KeyUse.ENCRYPTION)
				.keyID(kid)
				.issueTime(DateUtils.nowWithSecondsPrecision())
				.generate();
		}
		
		
		/**
		 * The JWK matcher.
		 */
		public static final JWKMatcher KEY_MATCHER = new JWKMatcher.Builder()
			.keyType(KeyType.RSA)
			.privateOnly(true)
			.keyUse(KeyUse.ENCRYPTION)
			.hasKeyID(true)
			.keySizes(RSA_KEY_BIT_SIZES)
			.build();
		
		
		private RotatedRSAEncryption() {}
	}
	
	
	/**
	 * Rotated ECDH encryption keys.
	 */
	public static class RotatedECDHEncryption {
		
		
		/**
		 * The supported EC curves.
		 */
		public static final Set SUPPORTED_CURVES = Collections.unmodifiableSet(
			new LinkedHashSet<>(Arrays.asList(Curve.P_256, Curve.P_384, Curve.P_521))
		);
		
		
		/**
		 * Generates a JWK the specified curve and key ID.
		 *
		 * @param crv The curve. Must not be {@code null}.
		 * @param kid The key ID, {@code null} if not specified.
		 *
		 * @return The JWK.
		 */
		public static ECKey generateKey(final Curve crv, final String kid)
			throws JOSEException {
			
			return new ECKeyGenerator(crv)
				.keyUse(KeyUse.ENCRYPTION)
				.keyID(kid)
				.issueTime(DateUtils.nowWithSecondsPrecision())
				.generate();
		}
		
		
		/**
		 * The JWK matcher.
		 */
		public static final JWKMatcher KEY_MATCHER = new JWKMatcher.Builder()
			.keyType(KeyType.EC)
			.privateOnly(true)
			.keyUse(KeyUse.ENCRYPTION)
			.hasKeyID(true)
			.build();
		
		
		private RotatedECDHEncryption() {}
	}
	
	
	/**
	 * Rotated AES and ChaCha20 direct encryption keys for JWT-encoded
	 * access tokens.
	 */
	public static class RotatedAccessTokenDirectEncryption {
		
		
		/**
		 * The JWK sizes.
		 */
		public static final int[] KEY_BIT_SIZES = new int[]{128, 192, 256, 384, 512};
		
		
		/**
		 * The JWK matcher.
		 *
		 * @deprecated Use {@link #createKeyMatcher} instead.
		 */
		@Deprecated
		public static final JWKMatcher KEY_MATCHER = new JWKMatcher.Builder()
			.keyType(KeyType.OCT)
			.keySizes(KEY_BIT_SIZES)
			.privateOnly(true)
			.algorithms(JWEAlgorithm.DIR, null) // The JWE alg must be 'dir' or unspecified
			.keyUses(KeyUse.ENCRYPTION)
			.hasKeyID(true)
			.build();
		
		
		/**
		 * Returns a JWK matcher for the specified JWE encryption
		 * method.
		 *
		 * @param enc The JWE encryption method.
		 *
		 * @return The JWK matcher.
		 */
		public static JWKMatcher createKeyMatcher(final EncryptionMethod enc) {
			
			return new JWKMatcher.Builder()
				.keyType(KeyType.OCT)
				.keySize(enc.cekBitLength())
				.privateOnly(true)
				.algorithms(JWEAlgorithm.DIR, null) // The JWE alg must be 'dir' or unspecified
				.keyUses(KeyUse.ENCRYPTION)
				.hasKeyID(true)
				.build();
		}
		
		
		/**
		 * Generates a 128 bit JWK with the specified key ID.
		 *
		 * @param kid The key ID, {@code null} if not specified.
		 *
		 * @return The JWK.
		 */
		public static OctetSequenceKey generateKey(final String kid)
			throws JOSEException {
			
			return generateKey(kid, KEY_BIT_SIZES[0]);
		}
		
		
		/**
		 * Generates a JWK with the specified key ID and bit size.
		 *
		 * @param kid     The key ID, {@code null} if not specified.
		 * @param bitSize The key bit size.
		 *
		 * @return The JWK.
		 */
		public static OctetSequenceKey generateKey(final String kid, final int bitSize)
			throws JOSEException {
			
			return new OctetSequenceKeyGenerator(bitSize)
				.keyUse(KeyUse.ENCRYPTION)
				.keyID(kid)
				.issueTime(DateUtils.nowWithSecondsPrecision())
				.generate();
		}
		
		
		/**
		 * Loads the matching JWKs from the specified JWK set.
		 *
		 * @param jwkSet The JWK set.
		 *
		 * @return The matching JWKs, empty set if none.
		 *
		 * @deprecated Use {@link #loadKeys(JWKSet, EncryptionMethod)}
		 * instead.
		 */
		@Deprecated
		public static List loadKeys(final JWKSet jwkSet) {
			
			List jwkMatches = new JWKSelector(KEY_MATCHER).select(jwkSet);
			List aesMatches = new LinkedList<>();
			jwkMatches.forEach(jwk -> aesMatches.add(jwk.toOctetSequenceKey()));
			return aesMatches;
		}
		
		
		/**
		 * Loads the matching JWKs from the specified JWK set.
		 *
		 * @param jwkSet The JWK set.
		 * @param enc    The JWE encryption method.
		 *
		 * @return The matching JWKs, empty set if none.
		 */
		public static List loadKeys(final JWKSet jwkSet, final EncryptionMethod enc) {
			
			List jwkMatches = new JWKSelector(createKeyMatcher(enc)).select(jwkSet);
			List aesMatches = new LinkedList<>();
			jwkMatches.forEach(jwk -> aesMatches.add(jwk.toOctetSequenceKey()));
			return aesMatches;
		}
		
		
		private RotatedAccessTokenDirectEncryption() {}
	}
	
	
	/**
	 * HMAC key for the subject sessions, authorisation codes, etc.
	 */
	public static class HMAC {
		
		
		/**
		 * The JWK ID.
		 */
		public static final String KEY_ID = "hmac";
		
		
		/**
		 * The JWK size.
		 */
		public static final int KEY_BIT_SIZE = 256;
		
		
		/**
		 * The JWK matcher.
		 */
		public static final JWKMatcher KEY_MATCHER = new JWKMatcher.Builder()
			.keyType(KeyType.OCT)
			.keySize(KEY_BIT_SIZE)
			.privateOnly(true)
			.keyUse(KeyUse.SIGNATURE)
			.keyID(KEY_ID)
			.build();
		
		
		/**
		 * Generates a JWK.
		 *
		 * @return The JWK.
		 */
		public static OctetSequenceKey generateKey()
			throws JOSEException {
			
			return new OctetSequenceKeyGenerator(KEY_BIT_SIZE)
				.keyUse(KeyUse.SIGNATURE)
				.keyID(KEY_ID)
				.issueTime(DateUtils.nowWithSecondsPrecision())
				.generate();
		}
		
		
		
		/**
		 * Loads the JWK from the specified JWK set.
		 *
		 * @param jwkSet The JWK set.
		 *
		 * @return The HMAC key as Java SecretKey.
		 *
		 * @throws JOSEException If no matching JWK could be found.
		 */
		public static SecretKey loadKey(final JWKSet jwkSet)
			throws JOSEException {
			
			ensureNotEmpty(jwkSet);
			
			List matches = new JWKSelector(KEY_MATCHER).select(jwkSet);
			
			if (matches.isEmpty()) {
				throw new JOSEException(
					"Couldn't find eligible secret JSON Web Key (JWK) " +
					"for applying HMAC to objects: " +
					"Required key ID \"" + KEY_ID + "\", " +
					"required key use \"sig\", " +
					"required key size " + KEY_BIT_SIZE + " bits"
				);
			}
			
			return ((OctetSequenceKey)matches.get(0)).toSecretKey("HmacSha256");
		}
		
		
		private HMAC() {}
	}
	
	
	/**
	 * AES/SIV key for pairwise subject encryption.
	 */
	public static class SubjectEncryption {
		
		
		/**
		 * The JWK ID.
		 */
		public static final String KEY_ID = "subject-encrypt";
		
		
		/**
		 * The JWK sizes.
		 */
		public static final int[] KEY_BIT_SIZES = new int[]{256, 384, 512};
		
		
		/**
		 * The JWK matcher.
		 */
		public static final JWKMatcher KEY_MATCHER = new JWKMatcher.Builder()
			.keyType(KeyType.OCT)
			.keySizes(KEY_BIT_SIZES)
			.privateOnly(true)
			.keyUse(KeyUse.ENCRYPTION)
			.keyID(KEY_ID)
			.build();
		
		
		/**
		 * Generates a 256 bit JWK.
		 *
		 * @return The JWK.
		 */
		public static OctetSequenceKey generateKey()
			throws JOSEException {
			
			return new OctetSequenceKeyGenerator(KEY_BIT_SIZES[0])
				.keyUse(KeyUse.ENCRYPTION)
				.keyID(KEY_ID)
				.issueTime(DateUtils.nowWithSecondsPrecision())
				.generate();
		}
		
		
		/**
		 * Loads the JWK from the specified JWK set.
		 *
		 * @param jwkSet The JWK set.
		 *
		 * @return The subject encryption key as Java secret key.
		 *
		 * @throws JOSEException If no matching JWK could be found.
		 */
		public static SecretKey loadKey(final JWKSet jwkSet)
			throws JOSEException {
			
			ensureNotEmpty(jwkSet);
			
			List keyMatches = new JWKSelector(KEY_MATCHER).select(jwkSet);
			
			if (keyMatches.isEmpty()) {
				throw new JOSEException(
					"Couldn't find eligible secret JSON Web Key (JWK) " +
					"for pairwise subject encryption: " +
					"Required key ID \"" + KEY_ID + "\", " +
					"required key use \"enc\", " +
					"required key sizes " + Arrays.toString(KEY_BIT_SIZES) + " bits"
				);
			}
			
			if (keyMatches.size() > 1) {
				throw new JOSEException("Too many pairwise subject encryption keys, must be one");
			}
			
			return ((OctetSequenceKey)keyMatches.get(0)).toSecretKey("AES");
		}
		
		
		private SubjectEncryption() {}
	}
	
	
	/**
	 * AES/SIV key for refresh token payload encryption.
	 */
	public static class RefreshTokenEncryption {
		
		
		/**
		 * The JWK ID.
		 */
		public static final String KEY_ID = "refresh-token-encrypt";
		
		
		/**
		 * The JWK size.
		 */
		public static final int KEY_BIT_SIZE = 256;
		
		
		/**
		 * The JWK matcher.
		 */
		public static final JWKMatcher KEY_MATCHER = new JWKMatcher.Builder()
			.keyType(KeyType.OCT)
			.keySize(KEY_BIT_SIZE)
			.privateOnly(true)
			.keyUse(KeyUse.ENCRYPTION)
			.keyID(KEY_ID)
			.build();
		
		
		/**
		 * Generates a JWK.
		 *
		 * @return The JWK.
		 */
		public static OctetSequenceKey generateKey()
			throws JOSEException {
			
			return new OctetSequenceKeyGenerator(KEY_BIT_SIZE)
				.keyUse(KeyUse.ENCRYPTION)
				.keyID(KEY_ID)
				.issueTime(DateUtils.nowWithSecondsPrecision())
				.generate();
		}
		
		
		/**
		 * Loads the JWK from the specified JWK set.
		 *
		 * @param jwkSet The JWK set.
		 *
		 * @return The refresh token encryption key as Java secret key.
		 *
		 * @throws JOSEException If no matching JWK could be found.
		 */
		public static SecretKey loadKey(final JWKSet jwkSet)
			throws JOSEException {
			
			ensureNotEmpty(jwkSet);
			
			List keyMatches = new JWKSelector(KEY_MATCHER).select(jwkSet);
			
			if (keyMatches.isEmpty()) {
				throw new JOSEException(
					"Couldn't find eligible secret JSON Web Key (JWK) " +
					"for refresh token encryption: " +
					"Required key ID \"" + KEY_ID + "\", " +
					"required key use \"enc\", " +
					"required key size " + KEY_BIT_SIZE + " bits"
				);
			}
			
			if (keyMatches.size() > 1) {
				throw new JOSEException("Too many refresh token encryption keys, must be one");
			}
			
			return ((OctetSequenceKey)keyMatches.get(0)).toSecretKey("AES");
		}
		
		
		private RefreshTokenEncryption() {}
	}
	
	
	private static void ensureNotEmpty(final JWKSet jwkSet)
		throws JOSEException {
		
		if (jwkSet == null || jwkSet.getKeys().isEmpty()) {
			
			throw new JOSEException("Missing or empty JSON Web Key (JWK) set");
		}
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy