diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/GatewayResources.java b/gateway-server/src/main/java/org/apache/knox/gateway/GatewayResources.java index 6e1422d6c..697369b9b 100644 --- a/gateway-server/src/main/java/org/apache/knox/gateway/GatewayResources.java +++ b/gateway-server/src/main/java/org/apache/knox/gateway/GatewayResources.java @@ -95,9 +95,9 @@ public interface GatewayResources { @Resource( text="The private signing key was not found in the signing keystore using the alias name {0}. The alias could be missing or the password could be incorrect.") String privateSigningKeyNotFound( String alias ); - @Resource( text="The private signing key found in the signing keystore using the alias name {0} is not a RSAPrivateKey") + @Resource( text="The private signing key found in the signing keystore using the alias name {0} is not a supported private key type (expected an RSAPrivateKey or ECPrivateKey)") String privateSigningKeyWrongType( String alias ); - @Resource( text="The public signing key found in the signing keystore using the alias name {0} is not a RSAPublicKey") + @Resource( text="The public signing key found in the signing keystore using the alias name {0} is not a supported public key type (expected an RSAPublicKey or ECPublicKey)") String publicSigningKeyWrongType( String alias ); } diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/DefaultTokenAuthorityService.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/DefaultTokenAuthorityService.java index 2f046d7df..42f62a160 100644 --- a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/DefaultTokenAuthorityService.java +++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/DefaultTokenAuthorityService.java @@ -27,9 +27,12 @@ import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; import java.security.PublicKey; import java.security.UnrecoverableKeyException; import java.security.cert.Certificate; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPublicKey; import java.text.ParseException; @@ -54,10 +57,13 @@ import com.nimbusds.jose.JWSSigner; import com.nimbusds.jose.JWSVerifier; import com.nimbusds.jose.KeyLengthException; +import com.nimbusds.jose.crypto.ECDSASigner; +import com.nimbusds.jose.crypto.ECDSAVerifier; import com.nimbusds.jose.crypto.MACSigner; import com.nimbusds.jose.crypto.MACVerifier; import com.nimbusds.jose.crypto.RSASSASigner; import com.nimbusds.jose.crypto.RSASSAVerifier; +import com.nimbusds.jose.jwk.Curve; import com.nimbusds.jose.jwk.source.JWKSource; import com.nimbusds.jose.jwk.source.JWKSourceBuilder; import com.nimbusds.jose.proc.BadJOSEException; @@ -99,6 +105,7 @@ public class DefaultTokenAuthorityService implements JWTokenAuthority, Service { // Only standard RSA and HMAC signature algorithms are accepted // https://tools.ietf.org/html/rfc7518 private static final Set SUPPORTED_PKI_SIG_ALGS = new HashSet<>(Arrays.asList("RS256", "RS384", "RS512", "PS256", "PS384", "PS512")); + private static final Set SUPPORTED_EC_SIG_ALGS = new HashSet<>(Arrays.asList("ES256", "ES384", "ES512")); private static final Set SUPPORTED_HMAC_SIG_ALGS = new HashSet<>(Arrays.asList("HS256", "HS384", "HS512")); /* How long an issuer CA bundle JWK source is kept without use, e.g. after its issuer was re-registered */ @@ -118,7 +125,7 @@ public class DefaultTokenAuthorityService implements JWTokenAuthority, Service { private char[] cachedSigningKeyPassphrase; private byte[] cachedSigningHmacSecret; - private RSAPrivateKey signingKey; + private PrivateKey signingKey; /* Cache JWKS Key source which has its own cache for the jwk keys */ /* @@ -152,14 +159,14 @@ public JWT issueToken(JWTokenAttributes jwtAttributes) throws TokenServiceExcept } else { jwtAttributes.setKid(cachedSigningKeyID.isPresent() ? cachedSigningKeyID.get() : null); } - final JWT token = SUPPORTED_PKI_SIG_ALGS.contains(algorithm) || SUPPORTED_HMAC_SIG_ALGS.contains(algorithm) + final JWT token = SUPPORTED_PKI_SIG_ALGS.contains(algorithm) || SUPPORTED_EC_SIG_ALGS.contains(algorithm) || SUPPORTED_HMAC_SIG_ALGS.contains(algorithm) ? new JWTToken(jwtAttributes) : null; if (token != null) { if (SUPPORTED_HMAC_SIG_ALGS.contains(algorithm)) { signTokenWithHMAC(token); } else { - signTokenWithRSA(token, jwtAttributes.getSigningKeystoreName(), jwtAttributes.getSigningKeystoreAlias(), jwtAttributes.getSigningKeystorePassphrase()); + signTokenWithPKI(token, jwtAttributes.getSigningKeystoreName(), jwtAttributes.getSigningKeystoreAlias(), jwtAttributes.getSigningKeystorePassphrase()); } return token; } else { @@ -167,22 +174,44 @@ public JWT issueToken(JWTokenAttributes jwtAttributes) throws TokenServiceExcept } } - private void signTokenWithRSA(final JWT token, String signingKeystoreName, String signingKeystoreAlias, char[] signingKeystorePassphrase) throws TokenServiceException { + private void signTokenWithPKI(final JWT token, String signingKeystoreName, String signingKeystoreAlias, char[] signingKeystorePassphrase) throws TokenServiceException { try { - final RSAPrivateKey key = getSigningKey(signingKeystoreName, signingKeystoreAlias, signingKeystorePassphrase); - // allowWeakKey to not break existing 1024 bit certificates - final JWSSigner signer = new RSASSASigner(key, true); + final String algorithm = token.getSignatureAlgorithm().getName(); + final PrivateKey key = getSigningKey(signingKeystoreName, signingKeystoreAlias, signingKeystorePassphrase); + final JWSSigner signer; + if (key instanceof RSAPrivateKey) { + if (!SUPPORTED_PKI_SIG_ALGS.contains(algorithm)) { + throw new TokenServiceException("Cannot sign token with algorithm " + algorithm + " using an RSA signing key"); + } + // allowWeakKey to not break existing 1024 bit certificates + signer = new RSASSASigner((RSAPrivateKey) key, true); + } else if (key instanceof ECPrivateKey) { + if (!SUPPORTED_EC_SIG_ALGS.contains(algorithm)) { + throw new TokenServiceException("Cannot sign token with algorithm " + algorithm + " using an EC signing key"); + } + final ECPrivateKey ecKey = (ECPrivateKey) key; + final Curve actualCurve = Curve.forECParameterSpec(ecKey.getParams()); + final Set expectedCurves = Curve.forJWSAlgorithm(token.getSignatureAlgorithm()); + if (actualCurve == null || expectedCurves == null || !expectedCurves.contains(actualCurve)) { + throw new TokenServiceException("Cannot sign token with algorithm " + algorithm + + " using an EC signing key on curve " + (actualCurve != null ? actualCurve.getName() : "unknown")); + } + signer = new ECDSASigner(ecKey); + } else { + throw new TokenServiceException("Cannot sign token - unsupported signing key type: " + + (key == null ? "null" : key.getClass().getName())); + } token.sign(signer); - } catch (KeystoreServiceException e) { + } catch (KeystoreServiceException | JOSEException e) { throw new TokenServiceException(e); } } - private RSAPrivateKey getSigningKey(final String signingKeystoreName, final String signingKeystoreAlias, final char[] signingKeystorePassphrase) + private PrivateKey getSigningKey(final String signingKeystoreName, final String signingKeystoreAlias, final char[] signingKeystorePassphrase) throws KeystoreServiceException, TokenServiceException { if (signingKeystorePassphrase != null) { - return (RSAPrivateKey) keystoreService.getSigningKey(signingKeystoreName, getSigningKeyAlias(signingKeystoreAlias), getSigningKeyPassphrase(signingKeystorePassphrase)); + return (PrivateKey) keystoreService.getSigningKey(signingKeystoreName, getSigningKeyAlias(signingKeystoreAlias), getSigningKeyPassphrase(signingKeystorePassphrase)); } return signingKey; @@ -235,18 +264,26 @@ public boolean verifyToken(JWT token) throws TokenServiceException { @Override public boolean verifyToken(JWT token, RSAPublicKey publicKey) throws TokenServiceException { final String signatureAlgorithm = token.getSignatureAlgorithm().getName(); - return SUPPORTED_HMAC_SIG_ALGS.contains(signatureAlgorithm) ? verifyTokenUsingHMAC(token) : verifyTokenUsingRSA(token, publicKey); + return SUPPORTED_HMAC_SIG_ALGS.contains(signatureAlgorithm) ? verifyTokenUsingHMAC(token) : verifyTokenUsingPKI(token, publicKey); } - private boolean verifyTokenUsingRSA(JWT token, RSAPublicKey publicKey) throws TokenServiceException { + private boolean verifyTokenUsingPKI(JWT token, PublicKey publicKey) throws TokenServiceException { try { PublicKey key = publicKey; if (key == null) { key = selectVerificationKey(token); } - final JWSVerifier verifier = new RSASSAVerifier((RSAPublicKey) key); + final JWSVerifier verifier; + if (key instanceof RSAPublicKey) { + verifier = new RSASSAVerifier((RSAPublicKey) key); + } else if (key instanceof ECPublicKey) { + verifier = new ECDSAVerifier((ECPublicKey) key); + } else { + throw new TokenServiceException("Cannot verify token - unsupported public key type: " + + (key == null ? "null" : key.getClass().getName())); + } return token.verify(verifier); - } catch (KeyStoreException | KeystoreServiceException e) { + } catch (KeyStoreException | KeystoreServiceException | JOSEException e) { throw new TokenServiceException("Cannot verify token.", e); } } @@ -267,12 +304,21 @@ private PublicKey selectVerificationKey(JWT token) throws KeyStoreException, Key if (kid != null) { for (final String alias : aliases) { final Certificate cert = keystore.getCertificate(alias); - if (cert == null || !(cert.getPublicKey() instanceof RSAPublicKey)) { + if (cert == null) { continue; } + final PublicKey candidateKey = cert.getPublicKey(); try { - if (kid.equals(TokenUtils.getThumbprint((RSAPublicKey) cert.getPublicKey(), "SHA-256"))) { - return cert.getPublicKey(); + final String candidateKid; + if (candidateKey instanceof RSAPublicKey) { + candidateKid = TokenUtils.getThumbprint((RSAPublicKey) candidateKey, "SHA-256"); + } else if (candidateKey instanceof ECPublicKey) { + candidateKid = TokenUtils.getThumbprint((ECPublicKey) candidateKey, "SHA-256"); + } else { + continue; + } + if (kid.equals(candidateKid)) { + return candidateKey; } } catch (JOSEException e) { // Cannot compute this key's thumbprint; skip it and try the next alias. @@ -479,10 +525,15 @@ public void start() throws ServiceLifecycleException { if (publicKey == null) { throw new ServiceLifecycleException(RESOURCES.publicSigningKeyNotFound(signingKeyAlias)); } - else if (! (publicKey instanceof RSAPublicKey)) { + else if (publicKey instanceof RSAPublicKey) { + cachedSigningKeyID = Optional.of(TokenUtils.getThumbprint((RSAPublicKey) publicKey, "SHA-256")); + } + else if (publicKey instanceof ECPublicKey) { + cachedSigningKeyID = Optional.of(TokenUtils.getThumbprint((ECPublicKey) publicKey, "SHA-256")); + } + else { throw new ServiceLifecycleException(RESOURCES.publicSigningKeyWrongType(signingKeyAlias)); } - cachedSigningKeyID = Optional.of(TokenUtils.getThumbprint((RSAPublicKey) publicKey, "SHA-256")); } catch (KeyStoreException e) { throw new ServiceLifecycleException(RESOURCES.publicSigningKeyNotFound(signingKeyAlias), e); } catch (final JOSEException e) { @@ -497,10 +548,10 @@ else if (! (publicKey instanceof RSAPublicKey)) { if (key == null) { throw new ServiceLifecycleException(RESOURCES.privateSigningKeyNotFound(signingKeyAlias)); } - else if (! (key instanceof RSAPrivateKey)) { + else if (! (key instanceof RSAPrivateKey || key instanceof ECPrivateKey)) { throw new ServiceLifecycleException(RESOURCES.privateSigningKeyWrongType(signingKeyAlias)); } - signingKey = (RSAPrivateKey) key; + signingKey = (PrivateKey) key; } catch (KeyStoreException | NoSuchAlgorithmException | UnrecoverableKeyException e) { throw new ServiceLifecycleException(RESOURCES.privateSigningKeyNotFound(signingKeyAlias), e); } diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/DefaultTokenAuthorityServiceTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/DefaultTokenAuthorityServiceTest.java index 41aebdab7..a7143d1cd 100644 --- a/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/DefaultTokenAuthorityServiceTest.java +++ b/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/DefaultTokenAuthorityServiceTest.java @@ -22,6 +22,7 @@ import java.security.KeyPairGenerator; import java.security.KeyStore; import java.security.Principal; +import java.security.cert.Certificate; import java.security.interfaces.RSAPublicKey; import java.util.Arrays; import java.util.Collections; @@ -715,6 +716,128 @@ public void testGetSigningCertKid() throws Exception { opt = ta.getCachedSigningKeyID(); assertTrue("Missing expected KID value", opt.isPresent()); } + + /** + * When the signing keystore holds an EC key, issueToken/verifyToken/start() must all take the + * EC branches introduced for KNOX-3281 instead of hard-failing on the RSA-only casts. + */ + @Test + public void testTokenCreationWithECSigningKey() throws Exception { + final String userName = "john.doe@example.com"; + final String alias = "gateway-identity"; + final char[] passphrase = "horton".toCharArray(); + + final KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC"); + kpg.initialize(256); + final KeyPair ecPair = kpg.generateKeyPair(); + + final KeyStore signingKeystore = KeyStore.getInstance("JKS"); + signingKeystore.load(null, null); + final Certificate cert = X509CertificateUtil.generateCertificate("CN=ec-signing", ecPair, 365, "SHA256withECDSA"); + signingKeystore.setKeyEntry(alias, ecPair.getPrivate(), passphrase, new Certificate[]{cert}); + + final GatewayConfig config = EasyMock.createNiceMock(GatewayConfig.class); + EasyMock.expect(config.getSigningKeyAlias()).andReturn(alias).anyTimes(); + + final KeystoreService ks = EasyMock.createNiceMock(KeystoreService.class); + EasyMock.expect(ks.getSigningKeystore()).andReturn(signingKeystore).anyTimes(); + + final AliasService as = EasyMock.createNiceMock(AliasService.class); + EasyMock.expect(as.getSigningKeyPassphrase()).andReturn(passphrase).anyTimes(); + + EasyMock.replay(config, ks, as); + + final DefaultTokenAuthorityService ta = new DefaultTokenAuthorityService(); + ta.setAliasService(as); + ta.setKeystoreService(ks); + ta.init(config, new HashMap<>()); + ta.start(); + + final JWT token = ta.issueToken(new JWTokenAttributesBuilder().setUserName(userName).setAlgorithm("ES256").build()); + assertEquals("john.doe@example.com", token.getSubject()); + assertTrue(token.getHeader().contains("ES256")); + + assertTrue(ta.verifyToken(token)); + } + + /** + * A requested algorithm from the wrong key family (EC algorithm against an RSA signing key) + * must fail fast with a TokenServiceException, not silently produce an unsigned token via + * JWTToken.sign()'s swallowed JOSEException. + */ + @Test(expected = TokenServiceException.class) + public void testTokenCreationAlgorithmKeyMismatch() throws Exception { + final String alias = "gateway-identity"; + final char[] passphrase = "horton".toCharArray(); + + final KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); + kpg.initialize(2048); + final KeyPair rsaPair = kpg.generateKeyPair(); + + final KeyStore signingKeystore = KeyStore.getInstance("JKS"); + signingKeystore.load(null, null); + final Certificate cert = X509CertificateUtil.generateCertificate("CN=rsa-signing", rsaPair, 365, "SHA256withRSA"); + signingKeystore.setKeyEntry(alias, rsaPair.getPrivate(), passphrase, new Certificate[]{cert}); + + final GatewayConfig config = EasyMock.createNiceMock(GatewayConfig.class); + EasyMock.expect(config.getSigningKeyAlias()).andReturn(alias).anyTimes(); + + final KeystoreService ks = EasyMock.createNiceMock(KeystoreService.class); + EasyMock.expect(ks.getSigningKeystore()).andReturn(signingKeystore).anyTimes(); + + final AliasService as = EasyMock.createNiceMock(AliasService.class); + EasyMock.expect(as.getSigningKeyPassphrase()).andReturn(passphrase).anyTimes(); + + EasyMock.replay(config, ks, as); + + final DefaultTokenAuthorityService ta = new DefaultTokenAuthorityService(); + ta.setAliasService(as); + ta.setKeystoreService(ks); + ta.init(config, new HashMap<>()); + ta.start(); + + ta.issueToken(new JWTokenAttributesBuilder().setUserName("john.doe@example.com").setAlgorithm("ES256").build()); + } + + /** + * start() must accept an EC signing key: the public-key branch computes the cached KID via the + * new EC thumbprint overload, and the private-key branch accepts ECPrivateKey. + */ + @Test + public void testServiceStartWithECSigningKey() throws Exception { + final String alias = "gateway-identity"; + final char[] passphrase = "horton".toCharArray(); + + final KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC"); + kpg.initialize(256); + final KeyPair ecPair = kpg.generateKeyPair(); + + final KeyStore signingKeystore = KeyStore.getInstance("JKS"); + signingKeystore.load(null, null); + final Certificate cert = X509CertificateUtil.generateCertificate("CN=ec-signing", ecPair, 365, "SHA256withECDSA"); + signingKeystore.setKeyEntry(alias, ecPair.getPrivate(), passphrase, new Certificate[]{cert}); + + final GatewayConfig config = EasyMock.createNiceMock(GatewayConfig.class); + EasyMock.expect(config.getSigningKeyAlias()).andReturn(alias).anyTimes(); + + final KeystoreService ks = EasyMock.createNiceMock(KeystoreService.class); + EasyMock.expect(ks.getSigningKeystore()).andReturn(signingKeystore).anyTimes(); + + final AliasService as = EasyMock.createNiceMock(AliasService.class); + EasyMock.expect(as.getSigningKeyPassphrase()).andReturn(passphrase).anyTimes(); + + EasyMock.replay(config, ks, as); + + final DefaultTokenAuthorityService ta = new DefaultTokenAuthorityService(); + ta.setAliasService(as); + ta.setKeystoreService(ks); + ta.init(config, new HashMap<>()); + ta.start(); + + final Optional kid = ta.getCachedSigningKeyID(); + assertTrue("Missing expected KID value for EC signing key", kid.isPresent()); + } + /* * The default (gateway truststore) JWKS path must use the Apache HttpClient retriever, not * Nimbus's HttpURLConnection one - that transport is the entire fix, and a retriever that is @@ -754,5 +877,4 @@ private static DefaultTokenAuthorityService tokenAuthorityService() throws Excep ta.init(config, new HashMap<>()); return ta; } - } diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenUtils.java b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenUtils.java index e3149a854..c52814794 100644 --- a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenUtils.java +++ b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenUtils.java @@ -17,6 +17,8 @@ package org.apache.knox.gateway.services.security.token; import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.jwk.Curve; +import com.nimbusds.jose.jwk.ECKey; import com.nimbusds.jose.jwk.KeyType; import com.nimbusds.jose.jwk.ThumbprintUtils; import com.nimbusds.jose.util.Base64URL; @@ -30,6 +32,7 @@ import jakarta.servlet.FilterConfig; import jakarta.servlet.ServletContext; +import java.security.interfaces.ECPublicKey; import java.security.interfaces.RSAPublicKey; import java.util.ArrayList; import java.util.Collections; @@ -167,6 +170,23 @@ public static String getThumbprint(final RSAPublicKey publicKey, final String ha return ThumbprintUtils.compute(hashAlgorithm, params).toString(); } + /** + * Utility method to calculate public key thumbprint + * @param publicKey + * @param hashAlgorithm + * @return + * @throws JOSEException + */ + public static String getThumbprint(final ECPublicKey publicKey, final String hashAlgorithm) + throws JOSEException { + final Curve curve = Curve.forECParameterSpec(publicKey.getParams()); + if (curve == null) { + throw new JOSEException("Unrecognized EC curve for public key"); + } + final ECKey ecKey = new ECKey.Builder(curve, publicKey).build(); + return ecKey.computeThumbprint(hashAlgorithm).toString(); + } + /** * @return true, if the HMAC secret is configured via the alias service for the gateway AND signing keystore name is not set ; false * otherwise diff --git a/gateway-spi/src/test/java/org/apache/knox/gateway/services/security/token/TokenUtilsTest.java b/gateway-spi/src/test/java/org/apache/knox/gateway/services/security/token/TokenUtilsTest.java index 8eaa63fd4..d949540be 100644 --- a/gateway-spi/src/test/java/org/apache/knox/gateway/services/security/token/TokenUtilsTest.java +++ b/gateway-spi/src/test/java/org/apache/knox/gateway/services/security/token/TokenUtilsTest.java @@ -23,6 +23,7 @@ import java.security.KeyPair; import java.security.KeyPairGenerator; +import java.security.interfaces.ECPublicKey; import java.security.interfaces.RSAPrivateKey; import java.util.ArrayList; import java.util.LinkedHashMap; @@ -41,6 +42,17 @@ */ public class TokenUtilsTest { + @Test + public void testGetThumbprintForECPublicKey() throws Exception { + final KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC"); + kpg.initialize(256); + final KeyPair ecPair = kpg.generateKeyPair(); + + final String thumbprint = TokenUtils.getThumbprint((ECPublicKey) ecPair.getPublic(), "SHA-256"); + assertNotNull(thumbprint); + assertTrue(!thumbprint.isEmpty()); + } + @Test public void testExtractActorChain_NoActClaim() throws Exception { // Create a JWT without an 'act' claim