From 5c6f7bdc2ca8b7e0e817e090626e0f48f4780491 Mon Sep 17 00:00:00 2001 From: Markus Strehle <11627201+strehle@users.noreply.github.com> Date: Fri, 5 May 2023 16:32:11 +0200 Subject: [PATCH] KeyInfo fixes (#2284) * Fix KeyInfo * tests * more flexible key parser * sonar * some more tests * renamed * renamed * remove deprecated zone holder * sonar issues * KeyInfo fixes * cleanup * small refactorings * rebase * rebase * review * sonar --- .../identity/uaa/oauth/jwk/JsonWebKey.java | 57 ++-- .../identity/uaa/util/UaaStringUtils.java | 2 + .../oauth/jwk/JsonWebKeyDeserializerTest.java | 40 +++ .../identity/uaa/util/UaaStringUtilsTest.java | 6 + .../uaa/oauth/jwk/JwkSet-ECProvider.json | 13 + .../identity/uaa/oauth/jwk/JwkSet-Hmac.json | 11 + .../identity/uaa/oauth/KeyInfo.java | 317 +++++------------- .../identity/uaa/oauth/KeyInfoBuilder.java | 13 +- .../oauth/jwt/ChainedSignatureVerifier.java | 2 +- .../oauth/jwt/CommonSignatureVerifier.java | 38 ++- .../identity/uaa/oauth/jwt/JwtAlgorithms.java | 11 +- .../uaa/oauth/KeyInfoBuilderTest.java | 21 +- .../identity/uaa/oauth/KeyInfoTest.java | 23 +- .../uaa/oauth/jwk/JsonWebKeySetTests.java | 30 +- .../jwt/ChainedSignatureVerifierTests.java | 35 +- 15 files changed, 334 insertions(+), 285 deletions(-) create mode 100644 model/src/test/resources/org/cloudfoundry/identity/uaa/oauth/jwk/JwkSet-ECProvider.json create mode 100644 model/src/test/resources/org/cloudfoundry/identity/uaa/oauth/jwk/JwkSet-Hmac.json diff --git a/model/src/main/java/org/cloudfoundry/identity/uaa/oauth/jwk/JsonWebKey.java b/model/src/main/java/org/cloudfoundry/identity/uaa/oauth/jwk/JsonWebKey.java index 7e4f6aa06ae..45b17117aa4 100644 --- a/model/src/main/java/org/cloudfoundry/identity/uaa/oauth/jwk/JsonWebKey.java +++ b/model/src/main/java/org/cloudfoundry/identity/uaa/oauth/jwk/JsonWebKey.java @@ -19,26 +19,29 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.nimbusds.jose.HeaderParameterNames; import com.nimbusds.jose.jwk.JWKParameterNames; -import org.apache.commons.codec.binary.Base64; -import org.apache.commons.codec.binary.BaseNCodec; +import org.cloudfoundry.identity.uaa.util.UaaStringUtils; import java.math.BigInteger; +import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.security.KeyFactory; import java.security.NoSuchAlgorithmException; import java.security.PublicKey; import java.security.spec.InvalidKeySpecException; import java.security.spec.RSAPublicKeySpec; +import java.util.Base64; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; +import static org.apache.commons.codec.binary.BaseNCodec.PEM_CHUNK_SIZE; import static org.cloudfoundry.identity.uaa.oauth.jwk.JsonWebKey.KeyType.MAC; import static org.cloudfoundry.identity.uaa.oauth.jwk.JsonWebKey.KeyType.RSA; -import static org.springframework.util.StringUtils.hasText; +import static org.cloudfoundry.identity.uaa.oauth.jwk.JsonWebKey.KeyType.oct; /** * See https://tools.ietf.org/html/rfc7517 @@ -48,7 +51,8 @@ @JsonSerialize(using = JsonWebKeySerializer.class) public class JsonWebKey { - private static final java.util.Base64.Encoder base64encoder = java.util.Base64.getMimeEncoder(BaseNCodec.PEM_CHUNK_SIZE, "\n".getBytes()); + private static final Base64.Encoder base64encoder = Base64.getMimeEncoder(PEM_CHUNK_SIZE, "\n".getBytes(Charset.defaultCharset())); + private static final Base64.Decoder base64decoder = Base64.getUrlDecoder(); // value is not defined in RFC 7517 public static final String PUBLIC_KEY_VALUE = "value"; @@ -58,9 +62,12 @@ public enum KeyUse { enc } + // RFC 7518 public enum KeyType { RSA, - MAC + EC, + MAC, + oct } public enum KeyOperation { @@ -81,7 +88,7 @@ public JsonWebKey(Map json) { throw new IllegalArgumentException("kty field is required"); } KeyType.valueOf((String) json.get(JWKParameterNames.KEY_TYPE)); - this.json = new HashMap(json); + this.json = new HashMap<>(json); } public Map getKeyProperties() { @@ -122,7 +129,7 @@ public JsonWebKey setX5t(String x5t) { public final KeyUse getUse() { String use = (String) getKeyProperties().get(JWKParameterNames.PUBLIC_KEY_USE); KeyUse result = null; - if (hasText(use)) { + if (UaaStringUtils.isNotEmpty(use)) { result = KeyUse.valueOf(use); } return result; @@ -153,10 +160,10 @@ public String getAlgorithm() { public String getValue() { String result = (String) getKeyProperties().get(PUBLIC_KEY_VALUE); if (result == null) { - if (RSA.equals(getKty())) { - result = pemEncodePublicKey(getRsaPublicKey(this)); + if (RSA == getKty()) { + result = pemEncodePublicKey(getRsaPublicKey(this)).orElse(UaaStringUtils.EMPTY_STRING); this.json.put(PUBLIC_KEY_VALUE, result); - } else if (MAC.equals(getKty())) { + } else if (MAC == getKty() || oct == getKty()) { result = (String) getKeyProperties().get(JWKParameterNames.OCT_KEY_VALUE); this.json.put(PUBLIC_KEY_VALUE, result); } @@ -172,27 +179,29 @@ public Set getKeyOps() { return result.stream().map(KeyOperation::valueOf).collect(Collectors.toSet()); } - public static String pemEncodePublicKey(PublicKey publicKey) { + public static Optional pemEncodePublicKey(PublicKey publicKey) { + if (publicKey == null) { + return Optional.empty(); + } String begin = "-----BEGIN PUBLIC KEY-----\n"; String end = "\n-----END PUBLIC KEY-----"; - byte[] data = publicKey.getEncoded(); - String base64encoded = new String(base64encoder.encode(data)); - return begin + base64encoded + end; + return Optional.of(begin + base64encoder.encodeToString(publicKey.getEncoded()) + end); } - public static PublicKey getRsaPublicKey(JsonWebKey key) { - final Base64 decoder = new Base64(true); + protected static PublicKey getRsaPublicKey(JsonWebKey key) { String e = (String) key.getKeyProperties().get(JWKParameterNames.RSA_EXPONENT); String n = (String) key.getKeyProperties().get(JWKParameterNames.RSA_MODULUS); - BigInteger modulus = new BigInteger(1, decoder.decode(n.getBytes(StandardCharsets.UTF_8))); - BigInteger exponent = new BigInteger(1, decoder.decode(e.getBytes(StandardCharsets.UTF_8))); - try { - return KeyFactory.getInstance(RSA.name()).generatePublic( - new RSAPublicKeySpec(modulus, exponent) - ); - } catch (InvalidKeySpecException | NoSuchAlgorithmException e1) { - throw new IllegalStateException(e1); + + if (e != null && n != null) { + BigInteger modulus = new BigInteger(1, base64decoder.decode(n.getBytes(StandardCharsets.UTF_8))); + BigInteger exponent = new BigInteger(1, base64decoder.decode(e.getBytes(StandardCharsets.UTF_8))); + try { + return KeyFactory.getInstance(RSA.name()).generatePublic(new RSAPublicKeySpec(modulus, exponent)); + } catch (InvalidKeySpecException | NoSuchAlgorithmException e1) { + throw new IllegalStateException(e1); + } } + return null; } } diff --git a/model/src/main/java/org/cloudfoundry/identity/uaa/util/UaaStringUtils.java b/model/src/main/java/org/cloudfoundry/identity/uaa/util/UaaStringUtils.java index 8e5e2b5d157..2790f23b0e9 100644 --- a/model/src/main/java/org/cloudfoundry/identity/uaa/util/UaaStringUtils.java +++ b/model/src/main/java/org/cloudfoundry/identity/uaa/util/UaaStringUtils.java @@ -286,6 +286,8 @@ public static boolean isNullOrEmpty(final String input) { return input == null || input.length() == 0; } + public static boolean isNotEmpty(final String input) { return !isNullOrEmpty(input); } + public static String convertISO8859_1_to_UTF_8(String s) { if (s==null) { return null; diff --git a/model/src/test/java/org/cloudfoundry/identity/uaa/oauth/jwk/JsonWebKeyDeserializerTest.java b/model/src/test/java/org/cloudfoundry/identity/uaa/oauth/jwk/JsonWebKeyDeserializerTest.java index 8f20137896c..107c999976b 100644 --- a/model/src/test/java/org/cloudfoundry/identity/uaa/oauth/jwk/JsonWebKeyDeserializerTest.java +++ b/model/src/test/java/org/cloudfoundry/identity/uaa/oauth/jwk/JsonWebKeyDeserializerTest.java @@ -4,6 +4,8 @@ import org.cloudfoundry.identity.uaa.util.JsonUtils; import org.junit.jupiter.api.Test; +import static org.cloudfoundry.identity.uaa.oauth.jwk.JsonWebKey.KeyType.EC; +import static org.cloudfoundry.identity.uaa.oauth.jwk.JsonWebKey.KeyType.oct; import static org.cloudfoundry.identity.uaa.test.ModelTestUtils.getResourceAsString; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @@ -17,6 +19,10 @@ class JsonWebKeyDeserializerTest { private static final String uaaLegacyJwkSet = getResourceAsString(JsonWebKeyDeserializerTest.class, "JwkSet-LegacyUaa.json"); // Keycloak server configuration https://www.keycloak.org/docs/latest/server_admin/, e.g. jwks_uri: http://localhost:8080/realms/{realm-name}/protocol/openid-connect/certs private static final String keyCloakJwkSet = getResourceAsString(JsonWebKeyDeserializerTest.class, "JwkSet-Keycloak.json"); + // HMAC standard attributes + private static final String keyOctedJwkSet = getResourceAsString(JsonWebKeyDeserializerTest.class, "JwkSet-Hmac.json"); + // elliptic cure + private static final String keyECJwkSet = getResourceAsString(JsonWebKeyDeserializerTest.class, "JwkSet-ECProvider.json"); @Test void testWebKeysMicrosoft() { @@ -63,4 +69,38 @@ void testWebKeysKeycloak() { assertEquals("Zv-dxo0VbAZrjp7gBP97yyjdxC8", key.getX5t()); } } + + @Test + void testWebKeysOcted() { + JsonWebKeySet keys = JsonUtils.readValue(keyOctedJwkSet, new TypeReference>() { + }); + assertNotNull(keys); + assertNotNull(keys.getKeys()); + assertEquals(1, keys.getKeys().size()); + for (JsonWebKey key : keys.getKeys()) { + assertNotNull(key); + assertEquals(oct, key.getKty()); + assertEquals("tokenKey", key.getValue()); + assertEquals("legacy-token-key", key.getKid()); + } + } + + @Test + void testWebKeysEllipticCurve() { + JsonWebKeySet keys = JsonUtils.readValue(keyECJwkSet, new TypeReference>() { + }); + assertNotNull(keys); + assertNotNull(keys.getKeys()); + assertEquals(1, keys.getKeys().size()); + for (JsonWebKey key : keys.getKeys()) { + assertNotNull(key); + assertNull(key.getValue()); + assertEquals(EC, key.getKty()); + assertEquals("ES256", key.getAlgorithm()); + assertEquals("ec-key-1", key.getKid()); + assertEquals("gI0GAILBdu7T53akrFmMyGcsF3n5dO7MmwNBHKW5SV0", key.getKeyProperties().get("x")); + assertEquals("SLW_xSffzlPWrHEVI30DHM_4egVwt3NQqeUD7nMFpps", key.getKeyProperties().get("y")); + assertEquals("P-256", key.getKeyProperties().get("crv")); + } + } } diff --git a/model/src/test/java/org/cloudfoundry/identity/uaa/util/UaaStringUtilsTest.java b/model/src/test/java/org/cloudfoundry/identity/uaa/util/UaaStringUtilsTest.java index 35783d01326..14929e831dc 100644 --- a/model/src/test/java/org/cloudfoundry/identity/uaa/util/UaaStringUtilsTest.java +++ b/model/src/test/java/org/cloudfoundry/identity/uaa/util/UaaStringUtilsTest.java @@ -404,6 +404,12 @@ void isNullOrEmpty_ShouldReturnTrue(final String input) { Assertions.assertThat(UaaStringUtils.isNullOrEmpty(input)).isTrue(); } + @ParameterizedTest + @NullAndEmptySource + void isNotEmpty_ShouldReturnFalse(final String input) { + Assertions.assertThat(UaaStringUtils.isNotEmpty(input)).isFalse(); + } + @ParameterizedTest @ValueSource(strings = { " ", " ", "\t", "\n", "abc" }) void isNullOrEmpty_ShouldReturnFalse(final String input) { diff --git a/model/src/test/resources/org/cloudfoundry/identity/uaa/oauth/jwk/JwkSet-ECProvider.json b/model/src/test/resources/org/cloudfoundry/identity/uaa/oauth/jwk/JwkSet-ECProvider.json new file mode 100644 index 00000000000..fd0dd7cd1cc --- /dev/null +++ b/model/src/test/resources/org/cloudfoundry/identity/uaa/oauth/jwk/JwkSet-ECProvider.json @@ -0,0 +1,13 @@ +{ + "keys": [ + { + "kty": "EC", + "use": "sig", + "kid": "ec-key-1", + "crv": "P-256", + "alg": "ES256", + "x": "gI0GAILBdu7T53akrFmMyGcsF3n5dO7MmwNBHKW5SV0", + "y": "SLW_xSffzlPWrHEVI30DHM_4egVwt3NQqeUD7nMFpps" + } + ] +} \ No newline at end of file diff --git a/model/src/test/resources/org/cloudfoundry/identity/uaa/oauth/jwk/JwkSet-Hmac.json b/model/src/test/resources/org/cloudfoundry/identity/uaa/oauth/jwk/JwkSet-Hmac.json new file mode 100644 index 00000000000..0f1158da2e2 --- /dev/null +++ b/model/src/test/resources/org/cloudfoundry/identity/uaa/oauth/jwk/JwkSet-Hmac.json @@ -0,0 +1,11 @@ +{ + "keys": [ + { + "kty": "oct", + "alg": "HS256", + "key_ops": ["verify"], + "k": "tokenKey", + "kid": "legacy-token-key" + } + ] +} diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/KeyInfo.java b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/KeyInfo.java index 8016ed235fd..76d93d3c5e4 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/KeyInfo.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/KeyInfo.java @@ -1,15 +1,18 @@ package org.cloudfoundry.identity.uaa.oauth; import com.nimbusds.jose.HeaderParameterNames; +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.jwk.JWK; import com.nimbusds.jose.jwk.JWKParameterNames; +import com.nimbusds.jose.jwk.OctetSequenceKey; import com.nimbusds.jose.util.Base64; import com.nimbusds.jose.util.Base64URL; import com.nimbusds.jose.util.X509CertUtils; -import org.bouncycastle.asn1.ASN1Sequence; import org.cloudfoundry.identity.uaa.oauth.jwk.JsonWebKey; import org.cloudfoundry.identity.uaa.oauth.jwt.JwtAlgorithms; import org.cloudfoundry.identity.uaa.oauth.jwt.JwtHelper; import org.cloudfoundry.identity.uaa.util.UaaUrlUtils; +import org.springframework.security.jwt.crypto.sign.EllipticCurveVerifier; import org.springframework.security.jwt.crypto.sign.MacSigner; import org.springframework.security.jwt.crypto.sign.RsaSigner; import org.springframework.security.jwt.crypto.sign.RsaVerifier; @@ -19,298 +22,160 @@ import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; -import java.security.KeyFactory; import java.security.KeyPair; -import java.security.NoSuchAlgorithmException; -import java.security.PrivateKey; import java.security.PublicKey; import java.security.cert.X509Certificate; +import java.security.interfaces.ECPublicKey; import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPublicKey; -import java.security.spec.InvalidKeySpecException; -import java.security.spec.KeySpec; -import java.security.spec.RSAPrivateCrtKeySpec; -import java.security.spec.RSAPublicKeySpec; -import java.security.spec.X509EncodedKeySpec; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Optional; -import java.util.regex.Matcher; -import java.util.regex.Pattern; +import static org.cloudfoundry.identity.uaa.oauth.jwk.JsonWebKey.KeyType.EC; import static org.cloudfoundry.identity.uaa.oauth.jwk.JsonWebKey.KeyType.MAC; import static org.cloudfoundry.identity.uaa.oauth.jwk.JsonWebKey.KeyType.RSA; -import static org.springframework.security.jwt.codec.Codecs.b64Decode; -import static org.springframework.security.jwt.codec.Codecs.utf8Encode; -public abstract class KeyInfo { - public abstract void verify(); - - public abstract SignatureVerifier getVerifier(); - - public abstract Signer getSigner(); - - public abstract String keyId(); - - public abstract String keyURL(); - - public abstract String type(); - - public abstract String verifierKey(); - public abstract Optional verifierCertificate(); - - public abstract Map getJwkMap(); - - public abstract String algorithm(); - - protected static String validateAndConstructTokenKeyUrl(String keyUrl) { - if (!UaaUrlUtils.isUrl(keyUrl)) { - throw new IllegalArgumentException("Invalid Key URL"); - } - - return UriComponentsBuilder.fromHttpUrl(keyUrl).scheme("https").path("/token_keys").build().toUriString(); - } -} - -class HmacKeyInfo extends KeyInfo { - private static final String DEFAULT_HMAC_ALGORITHM = "HMACSHA256"; +public class KeyInfo { + private final boolean isAsymmetric; private Signer signer; private SignatureVerifier verifier; private final String keyId; private final String keyUrl; private final String verifierKey; private final Optional verifierCertificate; + private final JsonWebKey.KeyType type; + private final JWK jwk; - public HmacKeyInfo(String keyId, String signingKey, String keyUrl) { - this(keyId, signingKey, keyUrl, null); + public KeyInfo(String keyId, String signingKey, String keyUrl) { + this(keyId, signingKey, keyUrl, null, null); } - public HmacKeyInfo(String keyId, String signingKey, String keyUrl, String sigAlg) { - this.keyUrl = validateAndConstructTokenKeyUrl(keyUrl); - - String algorithm = Optional.ofNullable(sigAlg).map(JwtAlgorithms::sigAlgJava).orElse(DEFAULT_HMAC_ALGORITHM); - SecretKey hmacKey = new SecretKeySpec(signingKey.getBytes(), algorithm); - this.signer = new MacSigner(algorithm, hmacKey); - this.verifier = new MacSigner(algorithm, hmacKey); - this.verifierCertificate = Optional.empty(); - + public KeyInfo(String keyId, String signingKey, String keyUrl, String sigAlg, String signingCert) { this.keyId = keyId; - this.verifierKey = signingKey; - } - - @Override - public void verify() { - + this.keyUrl = validateAndConstructTokenKeyUrl(keyUrl); + this.isAsymmetric = isAsymmetric(signingKey); + String algorithm; + if (this.isAsymmetric) { + String jwtAlg; + KeyPair keyPair; + try { + jwk = JWK.parseFromPEMEncodedObjects(signingKey); + jwtAlg = jwk.getKeyType().getValue(); + if (jwtAlg.startsWith("RSA")) { + algorithm = Optional.ofNullable(sigAlg).map(JwtAlgorithms::sigAlgJava).orElse(JwtAlgorithms.DEFAULT_RSA); + keyPair = jwk.toRSAKey().toKeyPair(); + PublicKey rsaPublicKey = keyPair.getPublic(); + this.signer = new RsaSigner((RSAPrivateKey) keyPair.getPrivate(), algorithm); + this.verifier = new RsaVerifier((RSAPublicKey) rsaPublicKey, algorithm); + this.type = RSA; + } else if (jwtAlg.startsWith("EC")) { + algorithm = Optional.ofNullable(sigAlg).map(JwtAlgorithms::sigAlgJava).orElse(JwtAlgorithms.DEFAULT_EC); + keyPair = jwk.toECKey().toKeyPair(); + this.signer = null; + this.verifier = new EllipticCurveVerifier((ECPublicKey) keyPair.getPublic(), algorithm); + this.type = EC; + } else { + throw new IllegalArgumentException("Invalid JWK"); + } + } catch (JOSEException e) { + throw new IllegalArgumentException(e); + } + this.verifierCertificate = Optional.ofNullable(signingCert); + this.verifierKey = JsonWebKey.pemEncodePublicKey(keyPair.getPublic()).orElse(null); + } else { + jwk = new OctetSequenceKey.Builder(signingKey.getBytes()).build(); + algorithm = Optional.ofNullable(sigAlg).map(JwtAlgorithms::sigAlgJava).orElse(JwtAlgorithms.DEFAULT_HMAC); + SecretKey hmacKey = new SecretKeySpec(signingKey.getBytes(), algorithm); + this.signer = new MacSigner(algorithm, hmacKey); + this.verifier = new MacSigner(algorithm, hmacKey); + this.verifierKey = signingKey; + this.verifierCertificate = Optional.empty(); + this.type = MAC; + } } - @Override public SignatureVerifier getVerifier() { return this.verifier; } - @Override public Signer getSigner() { return this.signer; } - @Override public String keyId() { return this.keyId; } - @Override public String keyURL() { return this.keyUrl; } - @Override public String type() { - return MAC.name(); + return this.type.name(); } - @Override public String verifierKey() { return this.verifierKey; } - @Override public Optional verifierCertificate() { return this.verifierCertificate; } - @Override public Map getJwkMap() { Map result = new HashMap<>(); result.put(HeaderParameterNames.ALGORITHM, this.algorithm()); - result.put(JsonWebKey.PUBLIC_KEY_VALUE, this.verifierKey); //new values per OpenID and JWK spec result.put(JWKParameterNames.PUBLIC_KEY_USE, JsonWebKey.KeyUse.sig.name()); result.put(HeaderParameterNames.KEY_ID, this.keyId); - result.put(JWKParameterNames.KEY_TYPE, MAC.name()); - return result; - } - - @Override - public String algorithm() { - return JwtAlgorithms.sigAlg(verifier.algorithm()); - } -} - -class RsaKeyInfo extends KeyInfo { - private static final String DEFAULT_RSA_ALGORITHM = "SHA256withRSA"; - private static Pattern PEM_DATA = Pattern.compile("-----BEGIN (.*)-----(.*)-----END (.*)-----", Pattern.DOTALL); - private final String keyId; - private final String keyUrl; - - private Signer signer; - private SignatureVerifier verifier; - private String verifierKey; - private Optional verifierCertificate; - - public RsaKeyInfo(String keyId, String signingKey, String keyUrl) { - this(keyId, signingKey, keyUrl, null, null); - } - public RsaKeyInfo(String keyId, String signingKey, String keyUrl, String sigAlg, String signingCert) { - this.keyUrl = validateAndConstructTokenKeyUrl(keyUrl); - - KeyPair keyPair = parseKeyPair(signingKey); - RSAPublicKey rsaPublicKey = (RSAPublicKey) keyPair.getPublic(); - String algorithm = Optional.ofNullable(sigAlg).map(JwtAlgorithms::sigAlgJava).orElse(DEFAULT_RSA_ALGORITHM); - String pemEncodePublicKey = JsonWebKey.pemEncodePublicKey(rsaPublicKey); - - this.signer = new RsaSigner((RSAPrivateKey) keyPair.getPrivate(), algorithm); - this.verifier = new RsaVerifier(rsaPublicKey, algorithm); - this.keyId = keyId; - this.verifierKey = pemEncodePublicKey; - this.verifierCertificate = Optional.ofNullable(signingCert); - } - - private static KeyPair parseKeyPair(String pemData) { - Matcher m = PEM_DATA.matcher(pemData.trim()); - - if (!m.matches()) { - throw new IllegalArgumentException("String is not PEM encoded data"); - } - - String type = m.group(1); - final byte[] content = b64Decode(utf8Encode(m.group(2))); - - PublicKey publicKey; - PrivateKey privateKey = null; - - try { - KeyFactory fact = KeyFactory.getInstance("RSA"); - if ("RSA PRIVATE KEY".equals(type)) { - ASN1Sequence seq = ASN1Sequence.getInstance(content); - if (seq.size() != 9) { - throw new IllegalArgumentException("Invalid RSA Private Key ASN1 sequence."); + result.put(JWKParameterNames.KEY_TYPE, type.name()); + if (this.isAsymmetric) { + // X509 releated values from JWK spec + if (this.verifierCertificate.isPresent()) { + X509Certificate x509Certificate = X509CertUtils.parse(verifierCertificate.get()); + if (x509Certificate != null) { + byte[] encoded = JwtHelper.getX509CertEncoded(x509Certificate); + result.put(HeaderParameterNames.X_509_CERT_CHAIN, Collections.singletonList(Base64.encode(encoded).toString())); + result.put(HeaderParameterNames.X_509_CERT_SHA_1_THUMBPRINT, JwtHelper.getX509CertThumbprint(encoded, "SHA-1")); + result.put(HeaderParameterNames.X_509_CERT_SHA_256_THUMBPRINT, JwtHelper.getX509CertThumbprint(encoded, "SHA-256")); } - - org.bouncycastle.asn1.pkcs.RSAPrivateKey key = org.bouncycastle.asn1.pkcs.RSAPrivateKey.getInstance(seq); - RSAPublicKeySpec pubSpec = new RSAPublicKeySpec(key.getModulus(), key.getPublicExponent()); - RSAPrivateCrtKeySpec privSpec = new RSAPrivateCrtKeySpec( - key.getModulus(), - key.getPublicExponent(), - key.getPrivateExponent(), - key.getPrime1(), - key.getPrime2(), - key.getExponent1(), - key.getExponent2(), - key.getCoefficient() - ); - publicKey = fact.generatePublic(pubSpec); - privateKey = fact.generatePrivate(privSpec); - } else if ("PUBLIC KEY".equals(type)) { - KeySpec keySpec = new X509EncodedKeySpec(content); - publicKey = fact.generatePublic(keySpec); - } else if ("RSA PUBLIC KEY".equals(type)) { - ASN1Sequence seq = ASN1Sequence.getInstance(content); - org.bouncycastle.asn1.pkcs.RSAPublicKey key = org.bouncycastle.asn1.pkcs.RSAPublicKey.getInstance(seq); - RSAPublicKeySpec pubSpec = new RSAPublicKeySpec(key.getModulus(), key.getPublicExponent()); - publicKey = fact.generatePublic(pubSpec); - } else { - throw new IllegalArgumentException(type + " is not a supported format"); } - - return new KeyPair(publicKey, privateKey); - } catch (InvalidKeySpecException e) { - throw new RuntimeException(e); - } catch (NoSuchAlgorithmException e) { - throw new IllegalStateException(e); + if (type == RSA) { + RSAPublicKey rsaKey; + try { + result.put(JsonWebKey.PUBLIC_KEY_VALUE, this.verifierKey); + rsaKey = jwk.toRSAKey().toRSAPublicKey(); + } catch (JOSEException e) { + throw new IllegalArgumentException(e); + } + String n = Base64URL.encode(rsaKey.getModulus()).toString(); + String e = Base64URL.encode(rsaKey.getPublicExponent()).toString(); + result.put(JWKParameterNames.RSA_MODULUS, n); + result.put(JWKParameterNames.RSA_EXPONENT, e); + } else if (type == EC) { + result.putAll(jwk.toJSONObject()); + } + return result; + } else { + result.put(JsonWebKey.PUBLIC_KEY_VALUE, this.verifierKey); + return result; } } - @Override - public void verify() { - } - - @Override - public SignatureVerifier getVerifier() { - return this.verifier; - } - - @Override - public Signer getSigner() { - return this.signer; - } - - @Override - public String keyId() { - return this.keyId; - } - - @Override - public String keyURL() { - return this.keyUrl; - } - - @Override - public String type() { - return RSA.name(); - } - - @Override - public String verifierKey() { - return this.verifierKey; - } - - @Override - public Optional verifierCertificate() { - return this.verifierCertificate; + public String algorithm() { + return JwtAlgorithms.sigAlg(verifier.algorithm()); } - @Override - public Map getJwkMap() { - Map result = new HashMap<>(); - result.put(HeaderParameterNames.ALGORITHM, this.algorithm()); - result.put(JsonWebKey.PUBLIC_KEY_VALUE, this.verifierKey); - //new values per OpenID and JWK spec - result.put(JWKParameterNames.PUBLIC_KEY_USE, JsonWebKey.KeyUse.sig.name()); - result.put(HeaderParameterNames.KEY_ID, this.keyId); - result.put(JWKParameterNames.KEY_TYPE, RSA.name()); - // X509 releated values from JWK spec - if (this.verifierCertificate.isPresent()) { - X509Certificate x509Certificate = X509CertUtils.parse(verifierCertificate.get()); - if (x509Certificate != null) { - byte[] encoded = JwtHelper.getX509CertEncoded(x509Certificate); - result.put(HeaderParameterNames.X_509_CERT_CHAIN, Collections.singletonList(Base64.encode(encoded).toString())); - result.put(HeaderParameterNames.X_509_CERT_SHA_1_THUMBPRINT, JwtHelper.getX509CertThumbprint(encoded, "SHA-1")); - result.put(HeaderParameterNames.X_509_CERT_SHA_256_THUMBPRINT, JwtHelper.getX509CertThumbprint(encoded, "SHA-256")); - } - } - RSAPublicKey rsaKey = (RSAPublicKey) parseKeyPair(verifierKey).getPublic(); - if (rsaKey != null) { - String n = Base64URL.encode(rsaKey.getModulus()).toString(); - String e = Base64URL.encode(rsaKey.getPublicExponent()).toString(); - result.put(JWKParameterNames.RSA_MODULUS, n); - result.put(JWKParameterNames.RSA_EXPONENT, e); + private static String validateAndConstructTokenKeyUrl(String keyUrl) { + if (!UaaUrlUtils.isUrl(keyUrl)) { + throw new IllegalArgumentException("Invalid Key URL"); } - return result; + return UriComponentsBuilder.fromHttpUrl(keyUrl).scheme("https").path("/token_keys").build().toUriString(); } - @Override - public String algorithm() { - return JwtAlgorithms.sigAlg(verifier.algorithm()); + private static boolean isAsymmetric(String key) { + return key.startsWith("-----BEGIN"); } } diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/KeyInfoBuilder.java b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/KeyInfoBuilder.java index ad79c1fa614..7a0af6d2462 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/KeyInfoBuilder.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/KeyInfoBuilder.java @@ -15,17 +15,6 @@ public static KeyInfo build(String keyId, String signingKey, String uaaUrl, Stri Assert.hasText(signingKey, "[Assertion failed] - this String argument must have text; it must not be null, empty, or blank"); signingKey = signingKey.trim(); - - if (isAssymetricKey(signingKey)) { - return new RsaKeyInfo(keyId, signingKey, uaaUrl, sigAlg, signingCert); - } - return new HmacKeyInfo(keyId, signingKey, uaaUrl, sigAlg); - } - - /** - * @return true if the string represents an asymmetric (RSA) key - */ - private static boolean isAssymetricKey(String key) { - return key.startsWith("-----BEGIN"); + return new KeyInfo(keyId, signingKey, uaaUrl, sigAlg, signingCert); } } diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/jwt/ChainedSignatureVerifier.java b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/jwt/ChainedSignatureVerifier.java index 0d4d6b57aa0..516aba4a64e 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/jwt/ChainedSignatureVerifier.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/jwt/ChainedSignatureVerifier.java @@ -32,7 +32,7 @@ public ChainedSignatureVerifier(JsonWebKeySet keys) { } List ds = new ArrayList<>(keys.getKeys().size()); for (JsonWebKey key : keys.getKeys()) { - ds.add(new CommonSignatureVerifier(key.getValue())); + ds.add(new CommonSignatureVerifier(key)); } delegates = Collections.unmodifiableList(ds); } diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/jwt/CommonSignatureVerifier.java b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/jwt/CommonSignatureVerifier.java index 418b25b72b1..ba6aa02a69b 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/jwt/CommonSignatureVerifier.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/jwt/CommonSignatureVerifier.java @@ -14,27 +14,49 @@ */ package org.cloudfoundry.identity.uaa.oauth.jwt; +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.jwk.ECKey; +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.RSAKey; +import org.cloudfoundry.identity.uaa.oauth.jwk.JsonWebKey; +import org.springframework.security.jwt.crypto.sign.EllipticCurveVerifier; import org.springframework.security.jwt.crypto.sign.MacSigner; import org.springframework.security.jwt.crypto.sign.RsaVerifier; import org.springframework.security.jwt.crypto.sign.SignatureVerifier; +import javax.crypto.spec.SecretKeySpec; +import java.text.ParseException; +import java.util.Optional; + public class CommonSignatureVerifier implements SignatureVerifier { private final SignatureVerifier delegate; - public CommonSignatureVerifier(String verificationKey) { + public CommonSignatureVerifier(JsonWebKey verificationKey) { if(verificationKey == null) { throw new IllegalArgumentException("verificationKey cannot be null"); - } else if(isAssymetricKey(verificationKey)) { - delegate = new RsaVerifier(verificationKey); + } else if(verificationKey.getKty() == JsonWebKey.KeyType.RSA) { + try { + RSAKey rsaKey = verificationKey.getValue() != null ? JWK.parseFromPEMEncodedObjects(verificationKey.getValue()).toRSAKey() : RSAKey.parse(verificationKey.getKeyProperties()); + String jwtAlg = Optional.ofNullable(verificationKey.getAlgorithm()).orElse(JWSAlgorithm.RS256.getName()); + delegate = new RsaVerifier(rsaKey.toRSAPublicKey(), JwtAlgorithms.sigAlgJava(jwtAlg)); + } catch (ParseException | JOSEException e) { + throw new IllegalArgumentException(e); + } + } else if(verificationKey.getKty() == JsonWebKey.KeyType.EC) { + try { + ECKey ecKey = ECKey.parse(verificationKey.getKeyProperties()); + String jwtAlg = Optional.ofNullable(verificationKey.getAlgorithm()).orElse(JWSAlgorithm.ES256.getName()); + delegate = new EllipticCurveVerifier(ecKey.toECPublicKey(), JwtAlgorithms.sigAlgJava(jwtAlg)); + } catch (ParseException | JOSEException e) { + throw new IllegalArgumentException(e); + } } else { - delegate = new MacSigner(verificationKey); + String jwtAlg = Optional.ofNullable(verificationKey.getAlgorithm()).orElse(JWSAlgorithm.HS256.getName()); + delegate = new MacSigner(JwtAlgorithms.sigAlgJava(jwtAlg), new SecretKeySpec(verificationKey.getValue().getBytes(), jwtAlg)); } } - private static boolean isAssymetricKey(String key) { - return key.startsWith("-----BEGIN"); - } - @Override public void verify(byte[] content, byte[] signature) { delegate.verify(content, signature); diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/jwt/JwtAlgorithms.java b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/jwt/JwtAlgorithms.java index 6fbe0a542f5..f660e3fb744 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/jwt/JwtAlgorithms.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/jwt/JwtAlgorithms.java @@ -21,21 +21,28 @@ * @author Luke Taylor */ public class JwtAlgorithms { + public static final String DEFAULT_HMAC = "HMACSHA256"; + public static final String DEFAULT_EC = "SHA256withECDSA"; + public static final String DEFAULT_RSA = "SHA256withRSA"; private static final Map sigAlgs = new HashMap(); private static final Map javaToSigAlgs = new HashMap(); private static final Map keyAlgs = new HashMap(); private static final Map javaToKeyAlgs = new HashMap(); static { - sigAlgs.put("HS256", "HMACSHA256"); + sigAlgs.put("HS256", DEFAULT_HMAC); sigAlgs.put("HS384" , "HMACSHA384"); sigAlgs.put("HS512" , "HMACSHA512"); - sigAlgs.put("RS256" , "SHA256withRSA"); + sigAlgs.put("RS256" , DEFAULT_RSA); sigAlgs.put("RS384" , "SHA384withRSA"); sigAlgs.put("RS512" , "SHA512withRSA"); sigAlgs.put("PS256" , "SHA256withRSAandMGF1"); sigAlgs.put("PS384" , "SHA384withRSAandMGF1"); sigAlgs.put("PS512" , "SHA512withRSAandMGF1"); + sigAlgs.put("ES256" , DEFAULT_EC); + sigAlgs.put("ES256K" , DEFAULT_EC); + sigAlgs.put("ES384" , "SHA384withECDSA"); + sigAlgs.put("ES512" , "SHA512withECDSA"); keyAlgs.put("RSA1_5" , "RSA/ECB/PKCS1Padding"); diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/KeyInfoBuilderTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/KeyInfoBuilderTest.java index bdbcd94ef72..0b4131f4094 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/KeyInfoBuilderTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/KeyInfoBuilderTest.java @@ -6,6 +6,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertEquals; public class KeyInfoBuilderTest { private static final String sampleRsaPrivateKey = "-----BEGIN RSA PRIVATE KEY-----\n" + @@ -24,6 +25,16 @@ public class KeyInfoBuilderTest { "waZKhM1W0oB8MX78M+0fG3xGUtywTx0D4N7pr1Tk2GTgNw==\n" + "-----END RSA PRIVATE KEY-----"; + private static final String sampleEcKeyPair = "-----BEGIN PRIVATE KEY-----\n" + + "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgevZzL1gdAFr88hb2\n" + + "OF/2NxApJCzGCEDdfSp6VQO30hyhRANCAAQRWz+jn65BtOMvdyHKcvjBeBSDZH2r\n" + + "1RTwjmYSi9R/zpBnuQ4EiMnCqfMPWiZqB4QdbAd0E7oH50VpuZ1P087G\n" + + "-----END PRIVATE KEY-----\n" + + "-----BEGIN PUBLIC KEY-----\n" + + "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEVs/o5+uQbTjL3chynL4wXgUg2R9\n" + + "q9UU8I5mEovUf86QZ7kOBIjJwqnzD1omageEHWwHdBO6B+dFabmdT9POxg==\n" + + "-----END PUBLIC KEY-----"; + @Rule public ExpectedException expectedException = ExpectedException.none(); @@ -47,4 +58,12 @@ public void whenProvidingNoSigningKey_shouldError() { KeyInfoBuilder.build("key-id", null, "https://localhost"); } -} \ No newline at end of file + + @Test + public void whenProvidingECKey_ShouldBuildJwkMap() { + KeyInfo keyInfo = KeyInfoBuilder.build("key-id", sampleEcKeyPair, "https://localhost"); + assertThat(keyInfo.type(), is("EC")); + assertThat(keyInfo.algorithm(), is("ES256")); + assertEquals(8, keyInfo.getJwkMap().size()); + } +} diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/KeyInfoTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/KeyInfoTest.java index 67e0dc2ba19..9498f99212a 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/KeyInfoTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/KeyInfoTest.java @@ -30,30 +30,37 @@ public class KeyInfoTest { @Test public void HmacKeyShouldSetFieldsCorrectly() { - HmacKeyInfo hmacKeyInfo = new HmacKeyInfo("key-id", "secret", "https://localhost"); + KeyInfo hmacKeyInfo = new KeyInfo("key-id", "secret", "https://localhost"); assertThat(hmacKeyInfo.type(), is("MAC")); } @Test public void HmacKeyShouldSetKeyUrlWithASecureProtocol() { - HmacKeyInfo hmacKeyInfo = new HmacKeyInfo("key-id", "secret", "http://localhost/path2"); + KeyInfo hmacKeyInfo = new KeyInfo("key-id", "secret", "http://localhost/path2"); assertThat(hmacKeyInfo.keyURL(), is("https://localhost/path2/token_keys")); } @Test public void RsaKeyShouldSetFieldsCorrectly() { - RsaKeyInfo hmacKeyInfo = new RsaKeyInfo("key-id", sampleRsaPrivateKey, "https://localhost"); + KeyInfo keyInfo = new KeyInfo("key-id", sampleRsaPrivateKey, "https://localhost"); - assertThat(hmacKeyInfo.type(), is("RSA")); + assertThat(keyInfo.type(), is("RSA")); + } + + @Test + public void Rsa512KeyShouldSetFieldsCorrectly() { + KeyInfo keyInfo = new KeyInfo("key-id", sampleRsaPrivateKey, "https://localhost", "RS512", null); + + assertThat(keyInfo.type(), is("RSA")); } @Test public void RsaKeyShouldSetKeyUrlWithASecureProtocol() { - RsaKeyInfo hmacKeyInfo = new RsaKeyInfo("key-id", sampleRsaPrivateKey, "http://localhost/path"); + KeyInfo keyInfo = new KeyInfo("key-id", sampleRsaPrivateKey, "http://localhost/path"); - assertThat(hmacKeyInfo.keyURL(), is("https://localhost/path/token_keys")); + assertThat(keyInfo.keyURL(), is("https://localhost/path/token_keys")); } @Test @@ -61,7 +68,7 @@ public void creatingHmacKeyWithInvalidUrlShouldFail() { expectedException.expect(IllegalArgumentException.class); expectedException.expectMessage("Invalid Key URL"); - new HmacKeyInfo("id", "secret", "foo bar"); + new KeyInfo("id", "secret", "foo bar"); } @@ -70,6 +77,6 @@ public void creatingRsaKeyWithInvalidUrlShouldFail() { expectedException.expect(IllegalArgumentException.class); expectedException.expectMessage("Invalid Key URL"); - new RsaKeyInfo("id", "secret", "foo bar"); + new KeyInfo("id", "secret", "foo bar"); } } \ No newline at end of file diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/jwk/JsonWebKeySetTests.java b/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/jwk/JsonWebKeySetTests.java index 7c8eb633454..54e60939fff 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/jwk/JsonWebKeySetTests.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/jwk/JsonWebKeySetTests.java @@ -73,6 +73,13 @@ public class JsonWebKeySetTests { " \"kid\": \"mac-id\",\n" + " \"kty\": \"MAC\",\n" + " \"key_ops\": [\"sign\",\"verify\"]\n" + + " },\n" + + " {\n" + + " \"alg\": \"HS256\",\n" + + " \"k\": \"test-oct-key\",\n" + + " \"kid\": \"oct-id\",\n" + + " \"kty\": \"oct\",\n" + + " \"key_ops\": [\"verify\"]\n" + " }\n" + " ]\n" + "}"; @@ -111,7 +118,7 @@ public class JsonWebKeySetTests { @Test public void test_multi_key() { JsonWebKeySet keys = test_key(multiKeyJson); - assertEquals(2, keys.getKeys().size()); + assertEquals(3, keys.getKeys().size()); JsonWebKey key = keys.getKeys().get(1); assertEquals("HMACSHA256", key.getAlgorithm()); @@ -129,6 +136,27 @@ public void test_multi_key() { assertEquals(new LinkedHashSet<>(Arrays.asList(JsonWebKey.KeyOperation.sign, JsonWebKey.KeyOperation.verify)), key.getKeyOps()); } + @Test + public void test_multi_key_rfc7518() { + JsonWebKeySet keys = test_key(multiKeyJson); + assertEquals(3, keys.getKeys().size()); + JsonWebKey key = keys.getKeys().get(2); + assertEquals("HS256", key.getAlgorithm()); + + assertEquals( + "test-oct-key", + key.getValue() + ); + + assertEquals( + "test-oct-key", + key.getKeyProperties().get("k") + ); + + assertNull(key.getUse()); + assertEquals(new LinkedHashSet<>(Arrays.asList(JsonWebKey.KeyOperation.verify)), key.getKeyOps()); + } + @Test public void test_single_key() { test_key(singleKeyJson); diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/jwt/ChainedSignatureVerifierTests.java b/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/jwt/ChainedSignatureVerifierTests.java index d9ec023a0ab..84bbe0bf427 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/jwt/ChainedSignatureVerifierTests.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/jwt/ChainedSignatureVerifierTests.java @@ -137,14 +137,14 @@ public void check_that_we_use_common_signer() { @Test public void unsupported_key_types_are_ignored() { Map p = new HashMap<>(); - p.put("kty", "EC"); + p.put("kty", "ES"); p.put("kid", "ecid"); p.put("x", "test-ec-key-x"); p.put("y", "test-ec-key-y"); p.put("use", "sig"); p.put("crv", "test-crv"); Map q = new HashMap<>(); - q.put("kty", "oct"); + q.put("kty", "MC"); q.put("k", "octkeyvalue"); JsonWebKeySet keySet = JsonUtils.convertValue(singletonMap("keys", Arrays.asList(validKey, p, q)), JsonWebKeySet.class); verifier = new ChainedSignatureVerifier(keySet); @@ -173,6 +173,37 @@ public void no_supported_key_types_causes_error() { verifier = new ChainedSignatureVerifier(keySet); } + @Test + public void test_single_hmackey_valid() { + Map q = new HashMap<>(); + q.put("kid", "test"); + q.put("kty", "oct"); + q.put("k", "octkeyvalue"); + JsonWebKeySet keySet = JsonUtils.convertValue(singletonMap("keys", Arrays.asList(q)), JsonWebKeySet.class); + verifier = new ChainedSignatureVerifier(keySet); + List delegates = new ArrayList((List) ReflectionTestUtils.getField(verifier, verifier.getClass(), "delegates")); + assertNotNull(delegates); + assertEquals(1, delegates.size()); + assertEquals("HMACSHA256", delegates.get(0).algorithm()); + } + + @Test + public void test_single_eckey_valid() { + Map q = new HashMap<>(); + q.put("kid", "ec-key"); + q.put("kty", "EC"); + q.put("crv", "P-256"); + q.put("x", "gI0GAILBdu7T53akrFmMyGcsF3n5dO7MmwNBHKW5SV0"); + q.put("y", "SLW_xSffzlPWrHEVI30DHM_4egVwt3NQqeUD7nMFpps"); + JsonWebKeySet keySet = JsonUtils.convertValue(singletonMap("keys", Arrays.asList(q)), JsonWebKeySet.class); + verifier = new ChainedSignatureVerifier(keySet); + List delegates = new ArrayList((List) ReflectionTestUtils.getField(verifier, verifier.getClass(), "delegates")); + assertNotNull(delegates); + assertEquals(1, delegates.size()); + assertNotNull(delegates.get(0)); + assertEquals("SHA256withECDSA", delegates.get(0).algorithm()); + } + @Test public void test_multi_key_both_valid() { Map p = new HashMap<>();