BCFIPS approved mode: Some tests failing due the short secret for client-secret-jwt client authentication

Closes #16678
This commit is contained in:
mposolda 2023-01-27 15:30:39 +01:00 committed by Marek Posolda
parent d2ef774788
commit 7f017f540e
8 changed files with 56 additions and 37 deletions

View file

@ -17,16 +17,20 @@
package org.keycloak.jose; package org.keycloak.jose;
import org.jboss.logging.Logger;
import org.junit.Assert; import org.junit.Assert;
import org.junit.ClassRule; import org.junit.ClassRule;
import org.junit.Test; import org.junit.Test;
import org.keycloak.common.util.BouncyIntegration;
import org.keycloak.jose.jws.JWSBuilder; import org.keycloak.jose.jws.JWSBuilder;
import org.keycloak.jose.jws.JWSInput; import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.crypto.HMACProvider; import org.keycloak.jose.jws.crypto.HMACProvider;
import org.keycloak.rule.CryptoInitRule; import org.keycloak.rule.CryptoInitRule;
import javax.crypto.SecretKey; import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.SecretKeySpec; import javax.crypto.spec.SecretKeySpec;
import java.util.UUID; import java.util.UUID;
/** /**
@ -35,17 +39,29 @@ import java.util.UUID;
*/ */
public abstract class HmacTest { public abstract class HmacTest {
private final Logger logger = Logger.getLogger(getClass().getName());
@ClassRule @ClassRule
public static CryptoInitRule cryptoInitRule = new CryptoInitRule(); public static CryptoInitRule cryptoInitRule = new CryptoInitRule();
@Test @Test
public void testHmacSignatures() throws Exception { public void testHmacSignaturesWithRandomSecretKey() throws Exception {
SecretKey secret = new SecretKeySpec(UUID.randomUUID().toString().getBytes(), "HmacSHA256"); SecretKey secretKey = new SecretKeySpec(UUID.randomUUID().toString().getBytes(), "HmacSHA256");
testHMACSignAndVerify(secretKey, "testHmacSignaturesWithRandomSecretKey");
}
@Test
public void testHmacSignaturesWithShortSecretKey() throws Exception {
SecretKey secretKey = new SecretKeySpec("secret".getBytes(), "HmacSHA256");
testHMACSignAndVerify(secretKey, "testHmacSignaturesWithShortSecretKey");
}
protected void testHMACSignAndVerify(SecretKey secretKey, String test) throws Exception {
String encoded = new JWSBuilder().content("12345678901234567890".getBytes()) String encoded = new JWSBuilder().content("12345678901234567890".getBytes())
.hmac256(secret); .hmac256(secretKey);
System.out.println("length: " + encoded.length()); logger.infof("%s: Length of encoded content: %d, Length of secret key: %d", test, encoded.length(), secretKey.getEncoded().length);
JWSInput input = new JWSInput(encoded); JWSInput input = new JWSInput(encoded);
Assert.assertTrue(HMACProvider.verify(input, secret)); Assert.assertTrue(HMACProvider.verify(input, secretKey));
} }
} }

View file

@ -42,13 +42,9 @@ public class ElytronHmacTest extends HmacTest {
SecureRandom random = isWindows() ? SecureRandom.getInstance("Windows-PRNG") : SecureRandom.getInstance("NativePRNG"); SecureRandom random = isWindows() ? SecureRandom.getInstance("Windows-PRNG") : SecureRandom.getInstance("NativePRNG");
random.setSeed(UUID.randomUUID().toString().getBytes()); random.setSeed(UUID.randomUUID().toString().getBytes());
keygen.init(random); keygen.init(random);
SecretKey secret = keygen.generateKey(); SecretKey secretKey = keygen.generateKey();
String encoded = new JWSBuilder().content("12345678901234567890".getBytes()) testHMACSignAndVerify(secretKey, "testHmacSignaturesUsingKeyGen");
.hmac256(secret);
System.out.println("length: " + encoded.length());
JWSInput input = new JWSInput(encoded);
Assert.assertTrue(HMACProvider.verify(input, secret));
} }
private boolean isWindows(){ private boolean isWindows(){
return System.getProperty("os.name").startsWith("Windows"); return System.getProperty("os.name").startsWith("Windows");

View file

@ -1,21 +1,19 @@
package org.keycloak.crypto.fips.test; package org.keycloak.crypto.fips.test;
import java.util.UUID; import java.util.UUID;
import javax.crypto.SecretKey; import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory; import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.SecretKeySpec; import javax.crypto.spec.SecretKeySpec;
import org.junit.Assert; import org.bouncycastle.crypto.CryptoServicesRegistrar;
import org.junit.Assume; import org.junit.Assume;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.keycloak.common.util.BouncyIntegration; import org.keycloak.common.util.BouncyIntegration;
import org.keycloak.common.util.Environment; import org.keycloak.common.util.Environment;
import org.keycloak.jose.HmacTest; import org.keycloak.jose.HmacTest;
import org.keycloak.jose.jws.JWSBuilder;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.crypto.HMACProvider;
/** /**
@ -31,15 +29,16 @@ public class FIPS1402HmacTest extends HmacTest {
} }
@Test @Test
public void testHmacSignaturesFIPS() throws Exception { public void testHmacSignaturesWithRandomSecretKeyCreatedByFactory() throws Exception {
//
SecretKeyFactory skFact = SecretKeyFactory.getInstance("HmacSHA256", BouncyIntegration.PROVIDER ); SecretKeyFactory skFact = SecretKeyFactory.getInstance("HmacSHA256", BouncyIntegration.PROVIDER );
SecretKey secret = skFact.generateSecret(new SecretKeySpec(UUID.randomUUID().toString().getBytes(), "HmacSHA256")); SecretKey secretKey = skFact.generateSecret(new SecretKeySpec(UUID.randomUUID().toString().getBytes(), "HmacSHA256"));
String encoded = new JWSBuilder().content("12345678901234567890".getBytes()) testHMACSignAndVerify(secretKey, "testHmacSignaturesWithRandomSecretKeyCreatedByFactory");
.hmac256(secret); }
System.out.println("length: " + encoded.length());
JWSInput input = new JWSInput(encoded); @Override
Assert.assertTrue(HMACProvider.verify(input, secret)); public void testHmacSignaturesWithShortSecretKey() throws Exception {
// With BCFIPS approved mode, secret key used for HmacSHA256 must be at least 112 bits long (14 characters). Short key won't work
Assume.assumeFalse(CryptoServicesRegistrar.isInApprovedOnlyMode());
super.testHmacSignaturesWithShortSecretKey();
} }
} }

View file

@ -89,7 +89,10 @@ which means even stricter security requirements on cryptography and security alg
``` ```
--spi-password-hashing-pbkdf2-sha256-max-padding-length=14 --spi-password-hashing-pbkdf2-sha256-max-padding-length=14
``` ```
- RSA keys of 1024 bits don't work (2048 is the minimum) - RSA keys of 1024 bits don't work (2048 is the minimum). This applies for keys used by Keycloak realm itself (Realm keys from the `Keys` tab), but also client keys and IDP keys
- HMAC SHA-XXX keys must be at least 112 bits (or 14 characters long). For example if you use OIDC clients with the client
authentication `Signed Jwt with Client Secret` (aka `client-secret-jwt`), then your client secrets should be at least 14 characters long.
But anyway, it is recommended to use client secrets generated by Keycloak server, which always matches this requirement.
- Also `jks` and `pkcs12` keystores/trustores are not supported. - Also `jks` and `pkcs12` keystores/trustores are not supported.
When starting server at startup, you can check that startup log contains `KC` provider contains KC provider with the note about `Approved Mode` like this: When starting server at startup, you can check that startup log contains `KC` provider contains KC provider with the note about `Approved Mode` like this:

View file

@ -1,6 +1,5 @@
package org.keycloak.testsuite.broker; package org.keycloak.testsuite.broker;
import static org.keycloak.testsuite.broker.BrokerTestConstants.CLIENT_SECRET;
import static org.keycloak.testsuite.broker.BrokerTestConstants.IDP_OIDC_ALIAS; import static org.keycloak.testsuite.broker.BrokerTestConstants.IDP_OIDC_ALIAS;
import static org.keycloak.testsuite.broker.BrokerTestConstants.IDP_OIDC_PROVIDER_ID; import static org.keycloak.testsuite.broker.BrokerTestConstants.IDP_OIDC_PROVIDER_ID;
import static org.keycloak.testsuite.broker.BrokerTestTools.createIdentityProvider; import static org.keycloak.testsuite.broker.BrokerTestTools.createIdentityProvider;
@ -16,6 +15,9 @@ import org.keycloak.representations.idm.IdentityProviderRepresentation;
public class KcOidcBrokerClientSecretJwtTest extends AbstractBrokerTest { public class KcOidcBrokerClientSecretJwtTest extends AbstractBrokerTest {
// BCFIPS approved mode requires at least 112 bits (14 characters) long SecretKey for "client-secret-jwt" authentication
private static final String CLIENT_SECRET = "atleast-14chars-password";
@Override @Override
protected BrokerConfiguration getBrokerConfiguration() { protected BrokerConfiguration getBrokerConfiguration() {
return new KcOidcBrokerConfigurationWithJWTAuthentication(); return new KcOidcBrokerConfigurationWithJWTAuthentication();
@ -39,6 +41,7 @@ public class KcOidcBrokerClientSecretJwtTest extends AbstractBrokerTest {
IdentityProviderRepresentation idp = createIdentityProvider(IDP_OIDC_ALIAS, IDP_OIDC_PROVIDER_ID); IdentityProviderRepresentation idp = createIdentityProvider(IDP_OIDC_ALIAS, IDP_OIDC_PROVIDER_ID);
Map<String, String> config = idp.getConfig(); Map<String, String> config = idp.getConfig();
applyDefaultConfiguration(config, syncMode); applyDefaultConfiguration(config, syncMode);
config.put("clientSecret", CLIENT_SECRET);
config.put("clientAuthMethod", OIDCLoginProtocol.CLIENT_SECRET_JWT); config.put("clientAuthMethod", OIDCLoginProtocol.CLIENT_SECRET_JWT);
return idp; return idp;
} }

View file

@ -316,7 +316,7 @@ public class FAPI1Test extends AbstractClientPoliciesTest {
// Register client (default authenticator) // Register client (default authenticator)
String clientUUID = createClientByAdmin("foo", (ClientRepresentation clientRep) -> { String clientUUID = createClientByAdmin("foo", (ClientRepresentation clientRep) -> {
clientRep.setClientAuthenticatorType(JWTClientSecretAuthenticator.PROVIDER_ID); clientRep.setClientAuthenticatorType(JWTClientSecretAuthenticator.PROVIDER_ID);
clientRep.setSecret("secret"); clientRep.setSecret("atleast-14chars-password");
}); });
ClientRepresentation client = getClientByAdmin(clientUUID); ClientRepresentation client = getClientByAdmin(clientUUID);
Assert.assertFalse(client.isPublicClient()); Assert.assertFalse(client.isPublicClient());
@ -336,7 +336,7 @@ public class FAPI1Test extends AbstractClientPoliciesTest {
// Check PKCE with S256, redirectUri and nonce/state set. Login should be successful // Check PKCE with S256, redirectUri and nonce/state set. Login should be successful
successfulLoginAndLogout("foo", false, (String code) -> { successfulLoginAndLogout("foo", false, (String code) -> {
String signedJwt = getClientSecretSignedJWT("secret", Algorithm.HS256); String signedJwt = getClientSecretSignedJWT("atleast-14chars-password", Algorithm.HS256);
return doAccessTokenRequestWithClientSignedJWT(code, signedJwt, codeVerifier, DefaultHttpClient::new); return doAccessTokenRequestWithClientSignedJWT(code, signedJwt, codeVerifier, DefaultHttpClient::new);
}); });
} }

View file

@ -92,6 +92,9 @@ public class ClientAuthSecretSignedJWTTest extends AbstractKeycloakTest {
private static final String PROFILE_NAME = "ClientSecretRotationProfile"; private static final String PROFILE_NAME = "ClientSecretRotationProfile";
private static final String POLICY_NAME = "ClientSecretRotationPolicy"; private static final String POLICY_NAME = "ClientSecretRotationPolicy";
private static final String OIDC = "openid-connect"; private static final String OIDC = "openid-connect";
// BCFIPS approved mode requires at least 112 bits (14 characters) long SecretKey for "client-secret-jwt" authentication
private static final String CLIENT_SECRET = "atleast-14chars-password";
private static final ObjectMapper objectMapper = new ObjectMapper(); private static final ObjectMapper objectMapper = new ObjectMapper();
@Rule @Rule
@ -142,7 +145,7 @@ public class ClientAuthSecretSignedJWTTest extends AbstractKeycloakTest {
} }
}; };
String algorithm = Algorithm.HS256; String algorithm = Algorithm.HS256;
jwtProvider.setClientSecret("password", algorithm); jwtProvider.setClientSecret(CLIENT_SECRET, algorithm);
String jwt = jwtProvider.createSignedRequestToken(oauth.getClientId(), getRealmInfoUrl(), algorithm); String jwt = jwtProvider.createSignedRequestToken(oauth.getClientId(), getRealmInfoUrl(), algorithm);
OAuthClient.AccessTokenResponse response = doAccessTokenRequest(code, OAuthClient.AccessTokenResponse response = doAccessTokenRequest(code,
jwt); jwt);
@ -180,7 +183,6 @@ public class ClientAuthSecretSignedJWTTest extends AbstractKeycloakTest {
ClientRepresentation clientRep = null; ClientRepresentation clientRep = null;
final String realmName = "test"; final String realmName = "test";
final String clientId = "test-app"; final String clientId = "test-app";
final String clientSecret = "password";
try { try {
clientResource = ApiUtil.findClientByClientId(adminClient.realm(realmName), clientId); clientResource = ApiUtil.findClientByClientId(adminClient.realm(realmName), clientId);
clientRep = clientResource.toRepresentation(); clientRep = clientResource.toRepresentation();
@ -188,11 +190,11 @@ public class ClientAuthSecretSignedJWTTest extends AbstractKeycloakTest {
clientResource.update(clientRep); clientResource.update(clientRep);
oauth.clientId(clientId); oauth.clientId(clientId);
oauth.doLogin("test-user@localhost", clientSecret); oauth.doLogin("test-user@localhost", "password");
events.expectLogin().client(clientId).assertEvent(); events.expectLogin().client(clientId).assertEvent();
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
OAuthClient.AccessTokenResponse response = doAccessTokenRequest(code, getClientSignedJWT(clientSecret, 20, Algorithm.HS256)); OAuthClient.AccessTokenResponse response = doAccessTokenRequest(code, getClientSignedJWT(CLIENT_SECRET, 20, Algorithm.HS256));
assertEquals(400, response.getStatusCode()); assertEquals(400, response.getStatusCode());
assertEquals("invalid_client", response.getError()); assertEquals("invalid_client", response.getError());
} catch (Exception e) { } catch (Exception e) {
@ -213,7 +215,7 @@ public class ClientAuthSecretSignedJWTTest extends AbstractKeycloakTest {
.assertEvent(); .assertEvent();
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
OAuthClient.AccessTokenResponse response = doAccessTokenRequest(code, getClientSignedJWT("password", 20, algorithm)); OAuthClient.AccessTokenResponse response = doAccessTokenRequest(code, getClientSignedJWT(CLIENT_SECRET, 20, algorithm));
assertEquals(200, response.getStatusCode()); assertEquals(200, response.getStatusCode());
oauth.verifyToken(response.getAccessToken()); oauth.verifyToken(response.getAccessToken());
@ -245,7 +247,7 @@ public class ClientAuthSecretSignedJWTTest extends AbstractKeycloakTest {
} }
private void processAuthenticateWithAlgorithm(String algorithm, Integer secretLength) throws Exception{ private void processAuthenticateWithAlgorithm(String algorithm, Integer secretLength) throws Exception{
String cidConfidential= createClientByAdmin("jwt-client","jwt-client","password",algorithm); String cidConfidential= createClientByAdmin("jwt-client","jwt-client",CLIENT_SECRET,algorithm);
ClientResource clientResource = adminClient.realm(REALM_NAME).clients().get(cidConfidential); ClientResource clientResource = adminClient.realm(REALM_NAME).clients().get(cidConfidential);
configureDefaultProfileAndPolicy(); configureDefaultProfileAndPolicy();
@ -292,7 +294,7 @@ public class ClientAuthSecretSignedJWTTest extends AbstractKeycloakTest {
.assertEvent(); .assertEvent();
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
String clientSignedJWT = getClientSignedJWT("password", 20); String clientSignedJWT = getClientSignedJWT(CLIENT_SECRET, 20);
OAuthClient.AccessTokenResponse response = doAccessTokenRequest(code, clientSignedJWT); OAuthClient.AccessTokenResponse response = doAccessTokenRequest(code, clientSignedJWT);
assertEquals(200, response.getStatusCode()); assertEquals(200, response.getStatusCode());
@ -329,7 +331,7 @@ public class ClientAuthSecretSignedJWTTest extends AbstractKeycloakTest {
*/ */
@Test @Test
public void authenticateWithInvalidRotatedClientSecretPolicyIsEnable() throws Exception { public void authenticateWithInvalidRotatedClientSecretPolicyIsEnable() throws Exception {
String cidConfidential= createClientByAdmin("jwt-client","jwt-client","password",Algorithm.HS256); String cidConfidential= createClientByAdmin("jwt-client","jwt-client",CLIENT_SECRET,Algorithm.HS256);
ClientResource clientResource = adminClient.realm(REALM_NAME).clients().get(cidConfidential); ClientResource clientResource = adminClient.realm(REALM_NAME).clients().get(cidConfidential);
configureDefaultProfileAndPolicy(); configureDefaultProfileAndPolicy();
String firstSecret = clientResource.getSecret().getValue(); String firstSecret = clientResource.getSecret().getValue();

View file

@ -29,7 +29,7 @@
], ],
"adminUrl": "http://localhost:8180/auth/realms/master/app/admin", "adminUrl": "http://localhost:8180/auth/realms/master/app/admin",
"clientAuthenticatorType": "client-secret-jwt", "clientAuthenticatorType": "client-secret-jwt",
"secret": "password" "secret": "atleast-14chars-password"
} }
], ],
"roles" : { "roles" : {