From e3fff91dd175ed30346520a870db4b9d19237d75 Mon Sep 17 00:00:00 2001 From: exceptionfactory Date: Sat, 28 Dec 2024 09:39:16 -0600 Subject: [PATCH] NIFI-14048 Added fallback to RSA for Framework Application Tokens This closes #9603. - Added KeyPairGeneratorConfiguration with Security Provider detection for Ed25519 and fallback to RSA when not found - Added StandardJWSVerifierFactory supporting either EdDSA for Ed25519 or PS512 for RSA signatures - Updated KeyGenerationCommand with provided KeyPairGenerator and conditional JWS Algorithm selection Signed-off-by: Joseph Witt --- .../AuthenticationSecurityConfiguration.java | 1 + ...wtAuthenticationSecurityConfiguration.java | 6 +- .../JwtDecoderConfiguration.java | 4 +- .../KeyPairGeneratorConfiguration.java | 64 +++++++++++++++++ ...y.java => StandardJWSVerifierFactory.java} | 29 +++++--- .../jwt/key/command/KeyGenerationCommand.java | 42 ++++++++--- .../KeyPairGeneratorConfigurationTest.java | 69 +++++++++++++++++++ .../key/command/KeyGenerationCommandTest.java | 11 ++- 8 files changed, 198 insertions(+), 28 deletions(-) create mode 100644 nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/configuration/KeyPairGeneratorConfiguration.java rename nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/key/{Ed25519VerifierFactory.java => StandardJWSVerifierFactory.java} (72%) create mode 100644 nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/configuration/KeyPairGeneratorConfigurationTest.java diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/configuration/AuthenticationSecurityConfiguration.java b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/configuration/AuthenticationSecurityConfiguration.java index d100298ee77a..cd2269e1fc04 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/configuration/AuthenticationSecurityConfiguration.java +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/configuration/AuthenticationSecurityConfiguration.java @@ -35,6 +35,7 @@ @Configuration @Import({ ClientRegistrationConfiguration.class, + KeyPairGeneratorConfiguration.class, JwtAuthenticationSecurityConfiguration.class, JwtDecoderConfiguration.class, OidcSecurityConfiguration.class, diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/configuration/JwtAuthenticationSecurityConfiguration.java b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/configuration/JwtAuthenticationSecurityConfiguration.java index 1057c50a1e8c..f9acf2d81253 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/configuration/JwtAuthenticationSecurityConfiguration.java +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/configuration/JwtAuthenticationSecurityConfiguration.java @@ -45,6 +45,7 @@ import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver; import org.springframework.security.oauth2.server.resource.web.authentication.BearerTokenAuthenticationFilter; +import java.security.KeyPairGenerator; import java.time.Duration; /** @@ -180,11 +181,12 @@ public StandardJwsSignerProvider jwsSignerProvider() { /** * Key Generation Command responsible for rotating JSON Web Signature key pairs based on configuration * + * @param keyPairGenerator Key Pair Generator for JSON Web Signatures * @return Key Generation Command scheduled according to application properties */ @Bean - public KeyGenerationCommand keyGenerationCommand() { - final KeyGenerationCommand command = new KeyGenerationCommand(jwsSignerProvider(), verificationKeySelector); + public KeyGenerationCommand keyGenerationCommand(final KeyPairGenerator keyPairGenerator) { + final KeyGenerationCommand command = new KeyGenerationCommand(jwsSignerProvider(), verificationKeySelector, keyPairGenerator); commandScheduler().scheduleAtFixedRate(command, keyRotationPeriod); return command; } diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/configuration/JwtDecoderConfiguration.java b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/configuration/JwtDecoderConfiguration.java index 597b4096375c..e69b852e45f2 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/configuration/JwtDecoderConfiguration.java +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/configuration/JwtDecoderConfiguration.java @@ -27,7 +27,7 @@ import org.apache.nifi.util.NiFiProperties; import org.apache.nifi.web.security.jwt.converter.StandardIssuerJwtDecoder; import org.apache.nifi.web.security.jwt.jws.StandardJWSKeySelector; -import org.apache.nifi.web.security.jwt.key.Ed25519VerifierFactory; +import org.apache.nifi.web.security.jwt.key.StandardJWSVerifierFactory; import org.apache.nifi.web.security.jwt.key.StandardVerificationKeySelector; import org.apache.nifi.web.security.jwt.key.service.StandardVerificationKeyService; import org.apache.nifi.web.security.jwt.key.service.VerificationKeyService; @@ -126,7 +126,7 @@ public JWTProcessor jwtProcessor() { final JWTClaimsSetVerifier claimsSetVerifier = new DefaultJWTClaimsVerifier<>(null, REQUIRED_CLAIMS); jwtProcessor.setJWTClaimsSetVerifier(claimsSetVerifier); - jwtProcessor.setJWSVerifierFactory(new Ed25519VerifierFactory()); + jwtProcessor.setJWSVerifierFactory(new StandardJWSVerifierFactory()); return jwtProcessor; } diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/configuration/KeyPairGeneratorConfiguration.java b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/configuration/KeyPairGeneratorConfiguration.java new file mode 100644 index 000000000000..63f8a7eaafb8 --- /dev/null +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/configuration/KeyPairGeneratorConfiguration.java @@ -0,0 +1,64 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.web.security.configuration; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.Provider; +import java.security.Security; + +@Configuration +public class KeyPairGeneratorConfiguration { + /** Standard Key Pair Algorithm for signing JSON Web Tokens */ + private static final String STANDARD_KEY_PAIR_ALGORITHM = "Ed25519"; + + private static final String STANDARD_KEY_PAIR_ALGORITHM_FILTER = "KeyPairGenerator.Ed25519"; + + /** Fallback Key Pair Algorithm when standard algorithm not supported in current Security Provider */ + private static final String FALLBACK_KEY_PAIR_ALGORITHM = "RSA"; + + private static final Logger logger = LoggerFactory.getLogger(KeyPairGeneratorConfiguration.class); + + /** + * JSON Web Token Key Pair Generator defaults to Ed25519 and falls back to RSA when current Security Providers do + * not support Ed25519. The fallback strategy supports security configurations that have not included Ed25519 + * as an approved algorithm. This strategy works with restricted providers such as those that have not incorporated + * algorithm approvals described in FIPS 186-5 + * + * @return Key Pair Generator for JSON Web Token signing + * @throws NoSuchAlgorithmException Thrown on failure to get Key Pair Generator for selected algorithm + */ + @Bean + public KeyPairGenerator jwtKeyPairGenerator() throws NoSuchAlgorithmException { + final String keyPairAlgorithm; + + final Provider[] providers = Security.getProviders(STANDARD_KEY_PAIR_ALGORITHM_FILTER); + if (providers == null) { + keyPairAlgorithm = FALLBACK_KEY_PAIR_ALGORITHM; + } else { + keyPairAlgorithm = STANDARD_KEY_PAIR_ALGORITHM; + } + + logger.info("Configured Key Pair Algorithm [{}] for JSON Web Signatures", keyPairAlgorithm); + return KeyPairGenerator.getInstance(keyPairAlgorithm); + } +} diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/key/Ed25519VerifierFactory.java b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/key/StandardJWSVerifierFactory.java similarity index 72% rename from nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/key/Ed25519VerifierFactory.java rename to nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/key/StandardJWSVerifierFactory.java index eb20bd75ef08..1ca60b903348 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/key/Ed25519VerifierFactory.java +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/key/StandardJWSVerifierFactory.java @@ -21,44 +21,51 @@ import com.nimbusds.jose.JWSHeader; import com.nimbusds.jose.JWSVerifier; import com.nimbusds.jose.KeyTypeException; +import com.nimbusds.jose.crypto.RSASSAVerifier; import com.nimbusds.jose.jca.JCAContext; import com.nimbusds.jose.proc.JWSVerifierFactory; import java.security.Key; import java.security.PublicKey; +import java.security.interfaces.RSAPublicKey; import java.util.Set; /** - * Ed25519 implementation of Verifier Factory + * Standard implementation of JSON Web Signature Verifier Factory */ -public class Ed25519VerifierFactory implements JWSVerifierFactory { - private static final Set SUPPORTED_ALGORITHMS = Set.of(JWSAlgorithm.EdDSA); +public class StandardJWSVerifierFactory implements JWSVerifierFactory { + /** Supported Algorithms aligned with supported Signers */ + private static final Set SUPPORTED_ALGORITHMS = Set.of(JWSAlgorithm.EdDSA, JWSAlgorithm.PS512); private final JCAContext jcaContext = new JCAContext(); /** - * Create JSON Web Security Verifier for EdDSA using Ed25519 Public Key + * Create JSON Web Security Verifier for EdDSA using Ed25519 Public Key or PS512 using RSA Public Key * * @param jwsHeader JSON Web Security Header - * @param key Ed25519 Public Key required + * @param key Ed25519 or RSA Public Key required * @return JSON Web Security Verifier * @throws JOSEException Thrown on failure to create verifier */ @Override public JWSVerifier createJWSVerifier(final JWSHeader jwsHeader, final Key key) throws JOSEException { final JWSAlgorithm algorithm = jwsHeader.getAlgorithm(); + final JWSVerifier verifier; if (SUPPORTED_ALGORITHMS.contains(algorithm)) { - if (key instanceof PublicKey publicKey) { - final Ed25519Verifier verifier = new Ed25519Verifier(publicKey); - verifier.getJCAContext().setProvider(jcaContext.getProvider()); - return verifier; - } else { + if (key instanceof RSAPublicKey rsaPublicKey) { + verifier = new RSASSAVerifier(rsaPublicKey); + } else if (key instanceof PublicKey publicKey) { + verifier = new Ed25519Verifier(publicKey); + } else { throw new KeyTypeException(PublicKey.class); - } + } } else { throw new JOSEException("JWS Algorithm [%s] not supported".formatted(algorithm)); } + + verifier.getJCAContext().setProvider(jcaContext.getProvider()); + return verifier; } @Override diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/key/command/KeyGenerationCommand.java b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/key/command/KeyGenerationCommand.java index 75c939cd7149..34573eada26d 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/key/command/KeyGenerationCommand.java +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/key/command/KeyGenerationCommand.java @@ -18,6 +18,7 @@ import com.nimbusds.jose.JWSAlgorithm; import com.nimbusds.jose.JWSSigner; +import com.nimbusds.jose.crypto.RSASSASigner; import org.apache.nifi.web.security.jwt.jws.JwsSignerContainer; import org.apache.nifi.web.security.jwt.jws.SignerListener; import org.apache.nifi.web.security.jwt.key.Ed25519Signer; @@ -27,7 +28,7 @@ import java.security.KeyPair; import java.security.KeyPairGenerator; -import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; import java.util.Objects; import java.util.UUID; @@ -37,23 +38,31 @@ public class KeyGenerationCommand implements Runnable { private static final Logger LOGGER = LoggerFactory.getLogger(KeyGenerationCommand.class); - private static final String KEY_ALGORITHM = "Ed25519"; + private static final String RSA_KEY_ALGORITHM = "RSA"; - private static final JWSAlgorithm JWS_ALGORITHM = JWSAlgorithm.EdDSA; + private static final JWSAlgorithm RSA_JWS_ALGORITHM = JWSAlgorithm.PS512; + + private static final JWSAlgorithm DEFAULT_JWS_ALGORITHM = JWSAlgorithm.EdDSA; private final KeyPairGenerator keyPairGenerator; + private final JWSAlgorithm jwsAlgorithm; + private final SignerListener signerListener; private final VerificationKeyListener verificationKeyListener; - public KeyGenerationCommand(final SignerListener signerListener, final VerificationKeyListener verificationKeyListener) { + public KeyGenerationCommand(final SignerListener signerListener, final VerificationKeyListener verificationKeyListener, final KeyPairGenerator keyPairGenerator) { this.signerListener = Objects.requireNonNull(signerListener, "Signer Listener required"); this.verificationKeyListener = Objects.requireNonNull(verificationKeyListener, "Verification Key Listener required"); - try { - keyPairGenerator = KeyPairGenerator.getInstance(KEY_ALGORITHM); - } catch (final NoSuchAlgorithmException e) { - throw new IllegalArgumentException(e); + this.keyPairGenerator = Objects.requireNonNull(keyPairGenerator, "Key Pair Generator required"); + + // Configure JWS Algorithm based on Key Pair Generator algorithm with fallback to RSA when Ed25519 not supported + final String keyPairAlgorithm = keyPairGenerator.getAlgorithm(); + if (RSA_KEY_ALGORITHM.equals(keyPairAlgorithm)) { + this.jwsAlgorithm = RSA_JWS_ALGORITHM; + } else { + this.jwsAlgorithm = DEFAULT_JWS_ALGORITHM; } } @@ -64,11 +73,22 @@ public KeyGenerationCommand(final SignerListener signerListener, final Verificat public void run() { final KeyPair keyPair = keyPairGenerator.generateKeyPair(); final String keyIdentifier = UUID.randomUUID().toString(); - LOGGER.debug("Generated Key Pair [{}] Key Identifier [{}]", KEY_ALGORITHM, keyIdentifier); + LOGGER.debug("Generated Key Pair [{}] Key Identifier [{}]", keyPairGenerator.getAlgorithm(), keyIdentifier); verificationKeyListener.onVerificationKeyGenerated(keyIdentifier, keyPair.getPublic()); - final JWSSigner jwsSigner = new Ed25519Signer(keyPair.getPrivate()); - signerListener.onSignerUpdated(new JwsSignerContainer(keyIdentifier, JWS_ALGORITHM, jwsSigner)); + final PrivateKey privateKey = keyPair.getPrivate(); + final JWSSigner jwsSigner = getJwsSigner(privateKey); + signerListener.onSignerUpdated(new JwsSignerContainer(keyIdentifier, jwsAlgorithm, jwsSigner)); + } + + private JWSSigner getJwsSigner(final PrivateKey privateKey) { + final JWSSigner jwsSigner; + if (RSA_JWS_ALGORITHM.equals(jwsAlgorithm)) { + jwsSigner = new RSASSASigner(privateKey); + } else { + jwsSigner = new Ed25519Signer(privateKey); + } + return jwsSigner; } } diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/configuration/KeyPairGeneratorConfigurationTest.java b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/configuration/KeyPairGeneratorConfigurationTest.java new file mode 100644 index 000000000000..be2bd4a7abb6 --- /dev/null +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/configuration/KeyPairGeneratorConfigurationTest.java @@ -0,0 +1,69 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.web.security.configuration; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.Provider; +import java.security.Security; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class KeyPairGeneratorConfigurationTest { + private static final String STANDARD_KEY_PAIR_ALGORITHM_FILTER = "KeyPairGenerator.Ed25519"; + + private static final String STANDARD_KEY_PAIR_ALGORITHM = "Ed25519"; + + private static final String FALLBACK_KEY_PAIR_ALGORITHM = "RSA"; + + private KeyPairGeneratorConfiguration configuration; + + @BeforeEach + void setConfiguration() { + configuration = new KeyPairGeneratorConfiguration(); + } + + @Test + void testJwtKeyPairGenerator() throws NoSuchAlgorithmException { + final KeyPairGenerator keyPairGenerator = configuration.jwtKeyPairGenerator(); + + final String algorithm = keyPairGenerator.getAlgorithm(); + assertEquals(STANDARD_KEY_PAIR_ALGORITHM, algorithm); + } + + @Test + void testJwtKeyPairGeneratorFallbackAlgorithm() throws NoSuchAlgorithmException { + final Provider[] providers = Security.getProviders(STANDARD_KEY_PAIR_ALGORITHM_FILTER); + assertNotNull(providers); + + final Provider provider = providers[0]; + try { + Security.removeProvider(provider.getName()); + + final KeyPairGenerator keyPairGenerator = configuration.jwtKeyPairGenerator(); + + final String algorithm = keyPairGenerator.getAlgorithm(); + assertEquals(FALLBACK_KEY_PAIR_ALGORITHM, algorithm); + } finally { + Security.addProvider(provider); + } + } +} diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/key/command/KeyGenerationCommandTest.java b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/key/command/KeyGenerationCommandTest.java index eb327d30825d..a3c874d9c7bc 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/key/command/KeyGenerationCommandTest.java +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/key/command/KeyGenerationCommandTest.java @@ -29,8 +29,12 @@ import org.mockito.junit.jupiter.MockitoExtension; import java.security.Key; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.mockito.Mockito.verify; @ExtendWith(MockitoExtension.class) @@ -57,8 +61,10 @@ public class KeyGenerationCommandTest { private KeyGenerationCommand command; @BeforeEach - public void setCommand() { - command = new KeyGenerationCommand(signerListener, verificationKeyListener); + public void setCommand() throws NoSuchAlgorithmException { + final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(JWS_ALGORITHM.getName()); + + command = new KeyGenerationCommand(signerListener, verificationKeyListener, keyPairGenerator); } @Test @@ -72,5 +78,6 @@ public void testRun() { verify(verificationKeyListener).onVerificationKeyGenerated(keyIdentifierCaptor.capture(), keyCaptor.capture()); final Key key = keyCaptor.getValue(); assertEquals(KEY_ALGORITHM, key.getAlgorithm()); + assertInstanceOf(PublicKey.class, key); } }