Issue Verifiable Credentials in jwt_vc format #25941 (#26484)

closes #25941 

Signed-off-by: Stefan Wiedemann <wistefan@googlemail.com>
This commit is contained in:
Stefan Wiedemann 2024-01-30 18:35:20 +01:00 committed by GitHub
parent 1213556eff
commit fa948f37e0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 597 additions and 6 deletions

View file

@ -17,7 +17,7 @@
--> -->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" <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> <parent>
<artifactId>keycloak-parent</artifactId> <artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId> <groupId>org.keycloak</groupId>
@ -28,7 +28,7 @@
<artifactId>keycloak-services</artifactId> <artifactId>keycloak-services</artifactId>
<name>Keycloak REST Services</name> <name>Keycloak REST Services</name>
<description /> <description/>
<properties> <properties>
<version.swagger.doclet>1.1.2</version.swagger.doclet> <version.swagger.doclet>1.1.2</version.swagger.doclet>
@ -222,6 +222,7 @@
<groupId>org.keycloak</groupId> <groupId>org.keycloak</groupId>
<artifactId>keycloak-model-storage-private</artifactId> <artifactId>keycloak-model-storage-private</artifactId>
</dependency> </dependency>
</dependencies> </dependencies>
<build> <build>
<plugins> <plugins>

View file

@ -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();
}
}

View file

@ -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();
}

View file

@ -17,8 +17,19 @@
package org.keycloak.protocol.oid4vc.issuance.signing; 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.models.KeycloakSession;
import org.keycloak.protocol.oid4vc.issuance.TimeProvider;
import org.keycloak.protocol.oid4vc.model.VerifiableCredential; 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 * {@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 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); 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 @Override
public String signCredential(VerifiableCredential verifiableCredential) { 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();
}
} }

View file

@ -21,14 +21,19 @@ import org.keycloak.component.ComponentModel;
import org.keycloak.component.ComponentValidationException; import org.keycloak.component.ComponentValidationException;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; 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.protocol.oid4vc.model.Format;
import org.keycloak.provider.ConfigurationValidationHelper; import org.keycloak.provider.ConfigurationValidationHelper;
import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.provider.ProviderConfigProperty;
import java.util.List; import java.util.List;
import java.util.Optional;
/** /**
* Provider Factory to create {@link JwtSigningService}s * Provider Factory to create {@link JwtSigningService}s
*
* @author <a href="https://github.com/wistefan">Stefan Wiedemann</a> * @author <a href="https://github.com/wistefan">Stefan Wiedemann</a>
*/ */
public class JwtSigningServiceProviderFactory implements VCSigningServiceProviderFactory { public class JwtSigningServiceProviderFactory implements VCSigningServiceProviderFactory {
@ -38,10 +43,18 @@ public class JwtSigningServiceProviderFactory implements VCSigningServiceProvide
@Override @Override
public VerifiableCredentialsSigningService create(KeycloakSession session, ComponentModel model) { public VerifiableCredentialsSigningService create(KeycloakSession session, ComponentModel model) {
String keyId = model.get(SigningProperties.KEY_ID.getKey()); String keyId = model.get(SigningProperties.KEY_ID.getKey());
String algorithmType = model.get(SigningProperties.ALGORITHM_TYPE.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 @Override
@ -53,6 +66,7 @@ public class JwtSigningServiceProviderFactory implements VCSigningServiceProvide
public List<ProviderConfigProperty> getConfigProperties() { public List<ProviderConfigProperty> getConfigProperties() {
return VCSigningServiceProviderFactory.configurationBuilder() return VCSigningServiceProviderFactory.configurationBuilder()
.property(SigningProperties.ALGORITHM_TYPE.asConfigProperty()) .property(SigningProperties.ALGORITHM_TYPE.asConfigProperty())
.property(SigningProperties.TOKEN_TYPE.asConfigProperty())
.build(); .build();
} }
@ -64,6 +78,7 @@ public class JwtSigningServiceProviderFactory implements VCSigningServiceProvide
@Override @Override
public void validateSpecificConfiguration(KeycloakSession session, RealmModel realm, ComponentModel model) throws ComponentValidationException { public void validateSpecificConfiguration(KeycloakSession session, RealmModel realm, ComponentModel model) throws ComponentValidationException {
ConfigurationValidationHelper.check(model) ConfigurationValidationHelper.check(model)
.checkRequired(SigningProperties.TOKEN_TYPE.asConfigProperty())
.checkRequired(SigningProperties.ALGORITHM_TYPE.asConfigProperty()); .checkRequired(SigningProperties.ALGORITHM_TYPE.asConfigProperty());
} }

View file

@ -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), 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), 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), 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); 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; private final String key;

View file

@ -38,6 +38,12 @@ import java.time.Clock;
* @author <a href="https://github.com/wistefan">Stefan Wiedemann</a> * @author <a href="https://github.com/wistefan">Stefan Wiedemann</a>
*/ */
public interface VCSigningServiceProviderFactory extends ComponentFactory<VerifiableCredentialsSigningService, VerifiableCredentialsSigningService>, EnvironmentDependentProviderFactory { 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() { public static ProviderConfigurationBuilder configurationBuilder() {
return ProviderConfigurationBuilder.create() return ProviderConfigurationBuilder.create()
.property(SigningProperties.KEY_ID.asConfigProperty()); .property(SigningProperties.KEY_ID.asConfigProperty());

View file

@ -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)))));
}
}
}

View file

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

View file

@ -27,6 +27,7 @@ metrics,4
migration,4 migration,4
model,6 model,6
oauth,6 oauth,6
oid4vc,6
oidc,6 oidc,6
policy,6 policy,6
providers,4 providers,4