closes #25941 Signed-off-by: Stefan Wiedemann <wistefan@googlemail.com>
This commit is contained in:
parent
1213556eff
commit
fa948f37e0
10 changed files with 597 additions and 6 deletions
|
@ -17,7 +17,7 @@
|
|||
-->
|
||||
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<parent>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
|
@ -28,7 +28,7 @@
|
|||
|
||||
<artifactId>keycloak-services</artifactId>
|
||||
<name>Keycloak REST Services</name>
|
||||
<description />
|
||||
<description/>
|
||||
|
||||
<properties>
|
||||
<version.swagger.doclet>1.1.2</version.swagger.doclet>
|
||||
|
@ -222,6 +222,7 @@
|
|||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-model-storage-private</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
<build>
|
||||
<plugins>
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* Copyright 2024 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed 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.keycloak.protocol.oid4vc.issuance;
|
||||
|
||||
import org.keycloak.common.util.Time;
|
||||
|
||||
/**
|
||||
* Implementation of the {@link TimeProvider} that delegates calls to the common {@link Time} class.
|
||||
*/
|
||||
public class OffsetTimeProvider implements TimeProvider {
|
||||
|
||||
@Override
|
||||
public int currentTimeSeconds() {
|
||||
return Time.currentTime();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long currentTimeMillis() {
|
||||
return Time.currentTimeMillis();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Copyright 2024 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed 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.keycloak.protocol.oid4vc.issuance;
|
||||
|
||||
/**
|
||||
* Interface to provide the current time
|
||||
*/
|
||||
public interface TimeProvider {
|
||||
|
||||
/**
|
||||
* Returns current time in seconds
|
||||
*
|
||||
* @return see description
|
||||
*/
|
||||
int currentTimeSeconds();
|
||||
|
||||
/**
|
||||
* Returns current time in milliseconds
|
||||
*
|
||||
* @return see description
|
||||
*/
|
||||
long currentTimeMillis();
|
||||
|
||||
}
|
|
@ -17,8 +17,19 @@
|
|||
|
||||
package org.keycloak.protocol.oid4vc.issuance.signing;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.crypto.KeyWrapper;
|
||||
import org.keycloak.crypto.SignatureProvider;
|
||||
import org.keycloak.crypto.SignatureSignerContext;
|
||||
import org.keycloak.jose.jws.JWSBuilder;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.protocol.oid4vc.issuance.TimeProvider;
|
||||
import org.keycloak.protocol.oid4vc.model.VerifiableCredential;
|
||||
import org.keycloak.representations.JsonWebToken;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* {@link VerifiableCredentialsSigningService} implementing the JWT_VC format. It returns a string, containing the
|
||||
|
@ -29,14 +40,75 @@ import org.keycloak.protocol.oid4vc.model.VerifiableCredential;
|
|||
*/
|
||||
public class JwtSigningService extends SigningService<String> {
|
||||
|
||||
public JwtSigningService(KeycloakSession keycloakSession, String keyId, String algorithmType) {
|
||||
private static final Logger LOGGER = Logger.getLogger(JwtSigningService.class);
|
||||
|
||||
private static final String ID_TEMPLATE = "urn:uuid:%s";
|
||||
private static final String TOKEN_TYPE = "JWT";
|
||||
private static final String VC_CLAIM_KEY = "vc";
|
||||
private static final String ID_CLAIM_KEY = "id";
|
||||
|
||||
|
||||
private final SignatureSignerContext signatureSignerContext;
|
||||
private final TimeProvider timeProvider;
|
||||
private final String tokenType;
|
||||
protected final String issuerDid;
|
||||
|
||||
public JwtSigningService(KeycloakSession keycloakSession, String keyId, String algorithmType, String tokenType, String issuerDid, TimeProvider timeProvider) {
|
||||
super(keycloakSession, keyId, algorithmType);
|
||||
this.issuerDid = issuerDid;
|
||||
this.timeProvider = timeProvider;
|
||||
this.tokenType = tokenType;
|
||||
KeyWrapper signingKey = getKey(keyId, algorithmType);
|
||||
if (signingKey == null) {
|
||||
throw new SigningServiceException(String.format("No key for id %s and algorithm %s available.", keyId, algorithmType));
|
||||
}
|
||||
SignatureProvider signatureProvider = keycloakSession.getProvider(SignatureProvider.class, algorithmType);
|
||||
signatureSignerContext = signatureProvider.signer(signingKey);
|
||||
|
||||
LOGGER.debugf("Successfully initiated the JWT Signing Service with algorithm %s.", algorithmType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String signCredential(VerifiableCredential verifiableCredential) {
|
||||
LOGGER.debugf("Sign credentials to jwt-vc format.");
|
||||
|
||||
throw new UnsupportedOperationException("JWT-VC Credentials Signing is not yet supported.");
|
||||
// Get the issuance date from the credential. Since nbf is mandatory, we set it to the current time if not
|
||||
// provided
|
||||
long iat = Optional.ofNullable(verifiableCredential.getIssuanceDate())
|
||||
.map(issuanceDate -> issuanceDate.toInstant().getEpochSecond())
|
||||
.orElse((long) timeProvider.currentTimeSeconds());
|
||||
|
||||
// set mandatory fields
|
||||
JsonWebToken jsonWebToken = new JsonWebToken()
|
||||
.issuer(verifiableCredential.getIssuer().toString())
|
||||
.nbf(iat)
|
||||
.id(createCredentialId(verifiableCredential));
|
||||
jsonWebToken.setOtherClaims(VC_CLAIM_KEY, verifiableCredential);
|
||||
|
||||
// expiry is optional
|
||||
Optional.ofNullable(verifiableCredential.getExpirationDate())
|
||||
.ifPresent(d -> jsonWebToken.exp(d.toInstant().getEpochSecond()));
|
||||
|
||||
// subject id should only be set if the credential subject has an id.
|
||||
Optional.ofNullable(
|
||||
verifiableCredential
|
||||
.getCredentialSubject()
|
||||
.getClaims()
|
||||
.get(ID_CLAIM_KEY))
|
||||
.map(Object::toString)
|
||||
.ifPresent(jsonWebToken::subject);
|
||||
|
||||
return new JWSBuilder()
|
||||
.type(tokenType)
|
||||
.jsonContent(jsonWebToken)
|
||||
.sign(signatureSignerContext);
|
||||
}
|
||||
|
||||
// retrieve the credential id from the given VC or generate one.
|
||||
private String createCredentialId(VerifiableCredential verifiableCredential) {
|
||||
return Optional.ofNullable(
|
||||
verifiableCredential.getId())
|
||||
.orElse(URI.create(String.format(ID_TEMPLATE, UUID.randomUUID())))
|
||||
.toString();
|
||||
}
|
||||
}
|
|
@ -21,14 +21,19 @@ import org.keycloak.component.ComponentModel;
|
|||
import org.keycloak.component.ComponentValidationException;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.protocol.oid4vc.issuance.OffsetTimeProvider;
|
||||
import org.keycloak.protocol.oid4vc.issuance.VCIssuerException;
|
||||
import org.keycloak.protocol.oid4vc.model.Format;
|
||||
import org.keycloak.provider.ConfigurationValidationHelper;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
|
||||
/**
|
||||
* Provider Factory to create {@link JwtSigningService}s
|
||||
*
|
||||
* @author <a href="https://github.com/wistefan">Stefan Wiedemann</a>
|
||||
*/
|
||||
public class JwtSigningServiceProviderFactory implements VCSigningServiceProviderFactory {
|
||||
|
@ -38,10 +43,18 @@ public class JwtSigningServiceProviderFactory implements VCSigningServiceProvide
|
|||
|
||||
@Override
|
||||
public VerifiableCredentialsSigningService create(KeycloakSession session, ComponentModel model) {
|
||||
|
||||
String keyId = model.get(SigningProperties.KEY_ID.getKey());
|
||||
String algorithmType = model.get(SigningProperties.ALGORITHM_TYPE.getKey());
|
||||
String tokenType = model.get(SigningProperties.TOKEN_TYPE.getKey());
|
||||
String issuerDid = Optional.ofNullable(
|
||||
session
|
||||
.getContext()
|
||||
.getRealm()
|
||||
.getAttribute(ISSUER_DID_REALM_ATTRIBUTE_KEY))
|
||||
.orElseThrow(() -> new VCIssuerException("No issuerDid configured."));
|
||||
|
||||
return new JwtSigningService(session, keyId, algorithmType);
|
||||
return new JwtSigningService(session, keyId, algorithmType, tokenType, issuerDid, new OffsetTimeProvider());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -53,6 +66,7 @@ public class JwtSigningServiceProviderFactory implements VCSigningServiceProvide
|
|||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return VCSigningServiceProviderFactory.configurationBuilder()
|
||||
.property(SigningProperties.ALGORITHM_TYPE.asConfigProperty())
|
||||
.property(SigningProperties.TOKEN_TYPE.asConfigProperty())
|
||||
.build();
|
||||
}
|
||||
|
||||
|
@ -64,6 +78,7 @@ public class JwtSigningServiceProviderFactory implements VCSigningServiceProvide
|
|||
@Override
|
||||
public void validateSpecificConfiguration(KeycloakSession session, RealmModel realm, ComponentModel model) throws ComponentValidationException {
|
||||
ConfigurationValidationHelper.check(model)
|
||||
.checkRequired(SigningProperties.TOKEN_TYPE.asConfigProperty())
|
||||
.checkRequired(SigningProperties.ALGORITHM_TYPE.asConfigProperty());
|
||||
}
|
||||
|
||||
|
|
|
@ -31,6 +31,8 @@ public enum SigningProperties {
|
|||
KEY_ID("keyId", "Id of the signing key.", "The id of the key to be used for signing credentials. The key needs to be provided as a realm key.", ProviderConfigProperty.STRING_TYPE, null),
|
||||
PROOF_TYPE("proofType", "Type of the LD-Proof.", "The type of LD-Proofs to be created. Needs to fit the provided signing key.", ProviderConfigProperty.STRING_TYPE, null),
|
||||
ALGORITHM_TYPE("algorithmType", "Type of the signing algorithm.", "The type of the algorithm to be used for signing. Needs to fit the provided signing key.", ProviderConfigProperty.STRING_TYPE, Algorithm.RS256),
|
||||
|
||||
TOKEN_TYPE("tokenType", "Type of the token.", "The type of the token to be created. Will be used as `typ` claim in the JWT-Header.", ProviderConfigProperty.STRING_TYPE, "JWT"),
|
||||
DECOYS("decoys", "Number of decoys to be added.", "The number of decoys to be added to the SD-JWT.", ProviderConfigProperty.STRING_TYPE, 0);
|
||||
|
||||
private final String key;
|
||||
|
|
|
@ -38,6 +38,12 @@ import java.time.Clock;
|
|||
* @author <a href="https://github.com/wistefan">Stefan Wiedemann</a>
|
||||
*/
|
||||
public interface VCSigningServiceProviderFactory extends ComponentFactory<VerifiableCredentialsSigningService, VerifiableCredentialsSigningService>, EnvironmentDependentProviderFactory {
|
||||
|
||||
/**
|
||||
* Key for the realm attribute providing the issuerDidy.
|
||||
*/
|
||||
String ISSUER_DID_REALM_ATTRIBUTE_KEY = "issuerDid";
|
||||
|
||||
public static ProviderConfigurationBuilder configurationBuilder() {
|
||||
return ProviderConfigurationBuilder.create()
|
||||
.property(SigningProperties.KEY_ID.asConfigProperty());
|
||||
|
|
|
@ -0,0 +1,237 @@
|
|||
/*
|
||||
* Copyright 2024 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed 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.keycloak.testsuite.oid4vc.issuance.signing;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.TokenVerifier;
|
||||
import org.keycloak.common.VerificationException;
|
||||
import org.keycloak.common.crypto.CryptoIntegration;
|
||||
import org.keycloak.common.util.KeyUtils;
|
||||
import org.keycloak.common.util.MultivaluedHashMap;
|
||||
import org.keycloak.crypto.Algorithm;
|
||||
import org.keycloak.crypto.AsymmetricSignatureVerifierContext;
|
||||
import org.keycloak.crypto.KeyWrapper;
|
||||
import org.keycloak.crypto.ServerECDSASignatureVerifierContext;
|
||||
import org.keycloak.crypto.SignatureVerifierContext;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.protocol.oid4vc.issuance.signing.JwtSigningService;
|
||||
import org.keycloak.protocol.oid4vc.issuance.signing.SigningServiceException;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialSubject;
|
||||
import org.keycloak.protocol.oid4vc.model.VerifiableCredential;
|
||||
import org.keycloak.representations.JsonWebToken;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.testsuite.runonserver.RunOnServerException;
|
||||
|
||||
import java.security.PublicKey;
|
||||
import java.time.Instant;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
|
||||
public class JwtSigningServiceTest extends SigningServiceTest {
|
||||
|
||||
private static final Logger LOGGER = Logger.getLogger(JwtSigningServiceTest.class);
|
||||
|
||||
private static KeyWrapper rsaKey = getRsaKey();
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
CryptoIntegration.init(this.getClass().getClassLoader());
|
||||
}
|
||||
|
||||
// If an unsupported algorithm is provided, the JWT Sigining Service should not be instantiated.
|
||||
@Test(expected = SigningServiceException.class)
|
||||
public void testUnsupportedAlgorithm() throws Throwable {
|
||||
try {
|
||||
getTestingClient()
|
||||
.server(TEST_REALM_NAME)
|
||||
.run(session ->
|
||||
new JwtSigningService(
|
||||
session,
|
||||
getKeyFromSession(session).getKid(),
|
||||
"did:web:test.org",
|
||||
"JWT",
|
||||
"unsupported-algorithm",
|
||||
new StaticTimeProvider(1000)));
|
||||
} catch (RunOnServerException ros) {
|
||||
throw ros.getCause();
|
||||
}
|
||||
}
|
||||
|
||||
// If no key is provided, the JWT Sigining Service should not be instantiated.
|
||||
@Test(expected = SigningServiceException.class)
|
||||
public void testFailIfNoKey() throws Throwable {
|
||||
try {
|
||||
getTestingClient()
|
||||
.server(TEST_REALM_NAME)
|
||||
.run(session ->
|
||||
new JwtSigningService(
|
||||
session,
|
||||
"no-such-key",
|
||||
Algorithm.RS256,
|
||||
"JWT",
|
||||
"did:web:test.org",
|
||||
new StaticTimeProvider(1000)));
|
||||
} catch (RunOnServerException ros) {
|
||||
throw ros.getCause();
|
||||
}
|
||||
}
|
||||
|
||||
// The provided credentials should be successfully signed as a JWT-VC.
|
||||
@Test
|
||||
public void testRsaSignedCredentialWithOutIssuanceDate() {
|
||||
getTestingClient()
|
||||
.server(TEST_REALM_NAME)
|
||||
.run(session ->
|
||||
testSignJwtCredential(
|
||||
session,
|
||||
Algorithm.RS256,
|
||||
Map.of("id", String.format("uri:uuid:%s", UUID.randomUUID()),
|
||||
"test", "test",
|
||||
"arrayClaim", List.of("a", "b", "c"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRsaSignedCredentialWithIssuanceDate() {
|
||||
getTestingClient()
|
||||
.server(TEST_REALM_NAME)
|
||||
.run(session ->
|
||||
testSignJwtCredential(
|
||||
session,
|
||||
Algorithm.RS256,
|
||||
Map.of("id", String.format("uri:uuid:%s", UUID.randomUUID()),
|
||||
"test", "test",
|
||||
"arrayClaim", List.of("a", "b", "c"),
|
||||
"issuanceDate", Date.from(Instant.ofEpochSecond(10)))));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRsaSignedCredentialWithoutAdditionalClaims() {
|
||||
getTestingClient()
|
||||
.server(TEST_REALM_NAME)
|
||||
.run(session ->
|
||||
testSignJwtCredential(
|
||||
session,
|
||||
Algorithm.RS256,
|
||||
Map.of()));
|
||||
}
|
||||
|
||||
|
||||
public static void testSignJwtCredential(KeycloakSession session, String algorithm, Map<String, Object> claims) {
|
||||
KeyWrapper keyWrapper = getKeyFromSession(session);
|
||||
|
||||
JwtSigningService jwtSigningService = new JwtSigningService(
|
||||
session,
|
||||
keyWrapper.getKid(),
|
||||
algorithm,
|
||||
"JWT",
|
||||
"did:web:test.org",
|
||||
new StaticTimeProvider(1000));
|
||||
|
||||
VerifiableCredential testCredential = getTestCredential(claims);
|
||||
|
||||
String jwtCredential = jwtSigningService.signCredential(testCredential);
|
||||
|
||||
SignatureVerifierContext verifierContext = null;
|
||||
switch (algorithm) {
|
||||
case Algorithm.ES256: {
|
||||
verifierContext = new ServerECDSASignatureVerifierContext(keyWrapper);
|
||||
break;
|
||||
}
|
||||
case Algorithm.RS256: {
|
||||
verifierContext = new AsymmetricSignatureVerifierContext(keyWrapper);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
fail("Algorithm not supported.");
|
||||
}
|
||||
}
|
||||
|
||||
TokenVerifier<JsonWebToken> verifier = TokenVerifier
|
||||
.create(jwtCredential, JsonWebToken.class)
|
||||
.verifierContext(verifierContext);
|
||||
verifier.publicKey((PublicKey) keyWrapper.getPublicKey());
|
||||
try {
|
||||
verifier.verify();
|
||||
} catch (VerificationException e) {
|
||||
fail("The credential should successfully be verified.");
|
||||
}
|
||||
|
||||
try {
|
||||
JsonWebToken theToken = verifier.getToken();
|
||||
|
||||
assertEquals("JWT claim in JWT encoded VC or VP MUST be used to set the value of the “expirationDate” of the VC", TEST_EXPIRATION_DATE.toInstant().getEpochSecond(), theToken.getExp().longValue());
|
||||
if (claims.containsKey("issuanceDate")) {
|
||||
assertEquals("VC Data Model v1.1 specifies that “issuanceDate” property MUST be represented as an nbf JWT claim, and not iat JWT claim.", ((Date) claims.get("issuanceDate")).toInstant().getEpochSecond(), theToken.getNbf().longValue());
|
||||
} else {
|
||||
// if not specific date is set, check against "currentTime"
|
||||
assertEquals("VC Data Model v1.1 specifies that “issuanceDate” property MUST be represented as an nbf JWT claim, and not iat JWT claim.", TEST_ISSUANCE_DATE.toInstant().getEpochSecond(), theToken.getNbf().longValue());
|
||||
}
|
||||
assertEquals("The issuer should be set in the token.", TEST_DID.toString(), theToken.getIssuer());
|
||||
assertEquals("The credential ID should be set as the token ID.", testCredential.getId().toString(), theToken.getId());
|
||||
Optional.ofNullable(testCredential.getCredentialSubject().getClaims().get("id")).ifPresent(id -> assertEquals("If the credentials subject id is set, it should be set as the token subject.", id.toString(), theToken.getSubject()));
|
||||
|
||||
assertNotNull("The credentials should be included at the vc-claim.", theToken.getOtherClaims().get("vc"));
|
||||
VerifiableCredential credential = new ObjectMapper().convertValue(theToken.getOtherClaims().get("vc"), VerifiableCredential.class);
|
||||
assertEquals("The types should be included", TEST_TYPES, credential.getType());
|
||||
assertEquals("The issuer should be included", TEST_DID, credential.getIssuer());
|
||||
assertEquals("The expiration date should be included", TEST_EXPIRATION_DATE, credential.getExpirationDate());
|
||||
if (claims.containsKey("issuanceDate")) {
|
||||
assertEquals("The issuance date should be included", claims.get("issuanceDate"), credential.getIssuanceDate());
|
||||
}
|
||||
|
||||
CredentialSubject subject = credential.getCredentialSubject();
|
||||
claims.entrySet().stream()
|
||||
.filter(e -> !e.getKey().equals("issuanceDate"))
|
||||
.forEach(e -> assertEquals(String.format("All additional claims should be set - %s is incorrect", e.getKey()), e.getValue(), subject.getClaims().get(e.getKey())));
|
||||
} catch (VerificationException e) {
|
||||
fail("Was not able to get the token from the verifier.");
|
||||
}
|
||||
}
|
||||
|
||||
private static KeyWrapper getKeyFromSession(KeycloakSession keycloakSession) {
|
||||
// we only set one key to the realm, thus can just take the first one
|
||||
// if run inside the testsuite, configure is called seperated from the test itself, thus we cannot just take
|
||||
// the key from the `configureTestRealm` method.
|
||||
return keycloakSession
|
||||
.keys()
|
||||
.getKeysStream(keycloakSession.getContext().getRealm())
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new RuntimeException("No key was configured"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureTestRealm(RealmRepresentation testRealm) {
|
||||
if (testRealm.getComponents() != null) {
|
||||
testRealm.getComponents().add("org.keycloak.keys.KeyProvider", getRsaKeyProvider(rsaKey));
|
||||
} else {
|
||||
testRealm.setComponents(new MultivaluedHashMap<>(
|
||||
Map.of("org.keycloak.keys.KeyProvider", List.of(getRsaKeyProvider(rsaKey)))));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,182 @@
|
|||
/*
|
||||
* Copyright 2024 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed 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.keycloak.testsuite.oid4vc.issuance.signing;
|
||||
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.common.util.CertificateUtils;
|
||||
import org.keycloak.common.util.KeyUtils;
|
||||
import org.keycloak.common.util.MultivaluedHashMap;
|
||||
import org.keycloak.common.util.PemUtils;
|
||||
import org.keycloak.crypto.KeyUse;
|
||||
import org.keycloak.crypto.KeyWrapper;
|
||||
import org.keycloak.protocol.oid4vc.issuance.TimeProvider;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialSubject;
|
||||
import org.keycloak.protocol.oid4vc.model.VerifiableCredential;
|
||||
import org.keycloak.representations.idm.ComponentExportRepresentation;
|
||||
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
|
||||
|
||||
import java.net.URI;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.NoSuchProviderException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.Security;
|
||||
import java.security.cert.Certificate;
|
||||
import java.time.Instant;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Super class for all signing service tests. Provides convenience methods to ease the testing.
|
||||
*/
|
||||
public abstract class SigningServiceTest extends AbstractTestRealmKeycloakTest {
|
||||
|
||||
private static final Logger LOGGER = Logger.getLogger(SigningServiceTest.class);
|
||||
protected static final String CONTEXT_URL = "https://www.w3.org/2018/credentials/v1";
|
||||
protected static final URI TEST_DID = URI.create("did:web:test.org");
|
||||
protected static final List<String> TEST_TYPES = List.of("VerifiableCredential");
|
||||
protected static final Date TEST_EXPIRATION_DATE = Date.from(Instant.ofEpochSecond(2000));
|
||||
protected static final Date TEST_ISSUANCE_DATE = Date.from(Instant.ofEpochSecond(1000));
|
||||
|
||||
protected static CredentialSubject getCredentialSubject(Map<String, Object> claims) {
|
||||
CredentialSubject credentialSubject = new CredentialSubject();
|
||||
claims.forEach(credentialSubject::setClaims);
|
||||
return credentialSubject;
|
||||
}
|
||||
|
||||
protected static VerifiableCredential getTestCredential(Map<String, Object> claims) {
|
||||
|
||||
VerifiableCredential testCredential = new VerifiableCredential();
|
||||
testCredential.setId(URI.create(String.format("uri:uuid:%s", UUID.randomUUID())));
|
||||
testCredential.setContext(List.of(CONTEXT_URL));
|
||||
testCredential.setType(TEST_TYPES);
|
||||
testCredential.setIssuer(TEST_DID);
|
||||
testCredential.setExpirationDate(TEST_EXPIRATION_DATE);
|
||||
if (claims.containsKey("issuanceDate")) {
|
||||
testCredential.setIssuanceDate((Date) claims.get("issuanceDate"));
|
||||
}
|
||||
|
||||
testCredential.setCredentialSubject(getCredentialSubject(claims));
|
||||
return testCredential;
|
||||
}
|
||||
|
||||
|
||||
public static KeyWrapper getECKey(String keyId) {
|
||||
try {
|
||||
Security.addProvider(new BouncyCastleProvider());
|
||||
KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC", "BC");
|
||||
kpg.initialize(256);
|
||||
var keyPair = kpg.generateKeyPair();
|
||||
KeyWrapper kw = new KeyWrapper();
|
||||
kw.setPrivateKey(keyPair.getPrivate());
|
||||
kw.setPublicKey(keyPair.getPublic());
|
||||
kw.setUse(KeyUse.SIG);
|
||||
kw.setKid(keyId);
|
||||
kw.setType("EC");
|
||||
kw.setAlgorithm("ES256");
|
||||
return kw;
|
||||
|
||||
} catch (NoSuchAlgorithmException | NoSuchProviderException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static KeyWrapper getEd25519Key(String keyId) {
|
||||
try {
|
||||
Security.addProvider(new BouncyCastleProvider());
|
||||
KeyPairGenerator kpg = KeyPairGenerator.getInstance("Ed25519", "BC");
|
||||
var keyPair = kpg.generateKeyPair();
|
||||
KeyWrapper kw = new KeyWrapper();
|
||||
kw.setPrivateKey(keyPair.getPrivate());
|
||||
kw.setPublicKey(keyPair.getPublic());
|
||||
kw.setUse(KeyUse.SIG);
|
||||
kw.setKid(keyId);
|
||||
kw.setType("Ed25519");
|
||||
return kw;
|
||||
|
||||
} catch (NoSuchAlgorithmException | NoSuchProviderException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static KeyWrapper getRsaKey() {
|
||||
try {
|
||||
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
|
||||
kpg.initialize(2048);
|
||||
var keyPair = kpg.generateKeyPair();
|
||||
KeyWrapper kw = new KeyWrapper();
|
||||
kw.setPrivateKey(keyPair.getPrivate());
|
||||
kw.setPublicKey(keyPair.getPublic());
|
||||
kw.setUse(KeyUse.SIG);
|
||||
kw.setKid(KeyUtils.createKeyId(keyPair.getPublic()));
|
||||
kw.setType("RSA");
|
||||
kw.setAlgorithm("RS256");
|
||||
return kw;
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
protected ComponentExportRepresentation getRsaKeyProvider(KeyWrapper keyWrapper) {
|
||||
ComponentExportRepresentation componentExportRepresentation = new ComponentExportRepresentation();
|
||||
componentExportRepresentation.setName("rsa-key-provider");
|
||||
componentExportRepresentation.setId(UUID.randomUUID().toString());
|
||||
componentExportRepresentation.setProviderId("rsa");
|
||||
|
||||
|
||||
Certificate certificate = CertificateUtils.generateV1SelfSignedCertificate(
|
||||
new KeyPair((PublicKey) keyWrapper.getPublicKey(), (PrivateKey) keyWrapper.getPrivateKey()), "TestKey");
|
||||
|
||||
componentExportRepresentation.setConfig(new MultivaluedHashMap<>(
|
||||
Map.of(
|
||||
"privateKey", List.of(PemUtils.encodeKey(keyWrapper.getPrivateKey())),
|
||||
"certificate", List.of(PemUtils.encodeCertificate(certificate)),
|
||||
"active", List.of("true"),
|
||||
"priority", List.of("0"),
|
||||
"enabled", List.of("true"),
|
||||
"algorithm", List.of("RS256")
|
||||
)
|
||||
));
|
||||
return componentExportRepresentation;
|
||||
}
|
||||
|
||||
static class StaticTimeProvider implements TimeProvider {
|
||||
private final int currentTimeInS;
|
||||
|
||||
StaticTimeProvider(int currentTimeInS) {
|
||||
this.currentTimeInS = currentTimeInS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int currentTimeSeconds() {
|
||||
return currentTimeInS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long currentTimeMillis() {
|
||||
return currentTimeInS * 1000L;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -27,6 +27,7 @@ metrics,4
|
|||
migration,4
|
||||
model,6
|
||||
oauth,6
|
||||
oid4vc,6
|
||||
oidc,6
|
||||
policy,6
|
||||
providers,4
|
||||
|
@ -44,4 +45,4 @@ util,4
|
|||
validation,6
|
||||
vault,4
|
||||
welcomepage,6
|
||||
x509,4
|
||||
x509,4
|
Loading…
Reference in a new issue