BCFIPS approved mode: Some tests failing due the short secret for client-secret-jwt client authentication
Closes #16678
This commit is contained in:
parent
d2ef774788
commit
7f017f540e
8 changed files with 56 additions and 37 deletions
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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");
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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" : {
|
||||||
|
|
Loading…
Reference in a new issue