Skip to content

Commit

Permalink
NIFI-14048 Added fallback to RSA for Framework Application Tokens
Browse files Browse the repository at this point in the history
This closes apache#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 <[email protected]>
  • Loading branch information
exceptionfactory authored and joewitt committed Dec 31, 2024
1 parent bfd2092 commit e3fff91
Show file tree
Hide file tree
Showing 8 changed files with 198 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
@Configuration
@Import({
ClientRegistrationConfiguration.class,
KeyPairGeneratorConfiguration.class,
JwtAuthenticationSecurityConfiguration.class,
JwtDecoderConfiguration.class,
OidcSecurityConfiguration.class,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -126,7 +126,7 @@ public JWTProcessor<SecurityContext> jwtProcessor() {
final JWTClaimsSetVerifier<SecurityContext> claimsSetVerifier = new DefaultJWTClaimsVerifier<>(null, REQUIRED_CLAIMS);
jwtProcessor.setJWTClaimsSetVerifier(claimsSetVerifier);

jwtProcessor.setJWSVerifierFactory(new Ed25519VerifierFactory());
jwtProcessor.setJWSVerifierFactory(new StandardJWSVerifierFactory());
return jwtProcessor;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<JWSAlgorithm> SUPPORTED_ALGORITHMS = Set.of(JWSAlgorithm.EdDSA);
public class StandardJWSVerifierFactory implements JWSVerifierFactory {
/** Supported Algorithms aligned with supported Signers */
private static final Set<JWSAlgorithm> 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand All @@ -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;
}
}

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

0 comments on commit e3fff91

Please sign in to comment.