Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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<String> SUPPORTED_PKI_SIG_ALGS = new HashSet<>(Arrays.asList("RS256", "RS384", "RS512", "PS256", "PS384", "PS512"));
private static final Set<String> SUPPORTED_EC_SIG_ALGS = new HashSet<>(Arrays.asList("ES256", "ES384", "ES512"));
private static final Set<String> 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 */
Expand All @@ -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 */
/*
Expand Down Expand Up @@ -152,37 +159,59 @@ 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 {
throw new TokenServiceException("Cannot issue token - Unsupported algorithm: " + algorithm);
}
}

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<Curve> 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;
Expand Down Expand Up @@ -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);
}
}
Expand All @@ -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.
Expand Down Expand Up @@ -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) {
Expand All @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<String> 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
Expand Down Expand Up @@ -754,5 +877,4 @@ private static DefaultTokenAuthorityService tokenAuthorityService() throws Excep
ta.init(config, new HashMap<>());
return ta;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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
Expand Down
Loading