Issue Verifiable Credentials in the SD-JWT-VC format (#27207)
closes #25942 Signed-off-by: Stefan Wiedemann <wistefan@googlemail.com> Co-authored-by: Francis Pouatcha <francis.pouatcha@adorsys.com>
This commit is contained in:
parent
dc9b1136ef
commit
6fc69b6a01
9 changed files with 514 additions and 34 deletions
|
@ -34,9 +34,8 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
* of existing VCs as well as the creation and signing of new ones.
|
* of existing VCs as well as the creation and signing of new ones.
|
||||||
* It integrates with Keycloak's SignatureSignerContext to facilitate
|
* It integrates with Keycloak's SignatureSignerContext to facilitate
|
||||||
* the generation of issuer signature.
|
* the generation of issuer signature.
|
||||||
*
|
*
|
||||||
* @author <a href="mailto:francis.pouatcha@adorsys.com">Francis Pouatcha</a>
|
* @author <a href="mailto:francis.pouatcha@adorsys.com">Francis Pouatcha</a>
|
||||||
*
|
|
||||||
*/
|
*/
|
||||||
public class IssuerSignedJWT extends SdJws {
|
public class IssuerSignedJWT extends SdJws {
|
||||||
|
|
||||||
|
@ -54,7 +53,7 @@ public class IssuerSignedJWT extends SdJws {
|
||||||
}
|
}
|
||||||
|
|
||||||
private IssuerSignedJWT(List<SdJwtClaim> claims, List<DecoyClaim> decoyClaims, String hashAlg,
|
private IssuerSignedJWT(List<SdJwtClaim> claims, List<DecoyClaim> decoyClaims, String hashAlg,
|
||||||
boolean nestedDisclosures) {
|
boolean nestedDisclosures) {
|
||||||
super(generatePayloadString(claims, decoyClaims, hashAlg, nestedDisclosures));
|
super(generatePayloadString(claims, decoyClaims, hashAlg, nestedDisclosures));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,7 +62,7 @@ public class IssuerSignedJWT extends SdJws {
|
||||||
}
|
}
|
||||||
|
|
||||||
private IssuerSignedJWT(List<SdJwtClaim> claims, List<DecoyClaim> decoyClaims, String hashAlg,
|
private IssuerSignedJWT(List<SdJwtClaim> claims, List<DecoyClaim> decoyClaims, String hashAlg,
|
||||||
boolean nestedDisclosures, SignatureSignerContext signer, String jwsType) {
|
boolean nestedDisclosures, SignatureSignerContext signer, String jwsType) {
|
||||||
super(generatePayloadString(claims, decoyClaims, hashAlg, nestedDisclosures), signer, jwsType);
|
super(generatePayloadString(claims, decoyClaims, hashAlg, nestedDisclosures), signer, jwsType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,7 +71,7 @@ public class IssuerSignedJWT extends SdJws {
|
||||||
* of claims.
|
* of claims.
|
||||||
*/
|
*/
|
||||||
private static JsonNode generatePayloadString(List<SdJwtClaim> claims, List<DecoyClaim> decoyClaims, String hashAlg,
|
private static JsonNode generatePayloadString(List<SdJwtClaim> claims, List<DecoyClaim> decoyClaims, String hashAlg,
|
||||||
boolean nestedDisclosures) {
|
boolean nestedDisclosures) {
|
||||||
|
|
||||||
SdJwtUtils.requireNonEmpty(hashAlg, "hashAlg must not be null or empty");
|
SdJwtUtils.requireNonEmpty(hashAlg, "hashAlg must not be null or empty");
|
||||||
final List<SdJwtClaim> claimsInternal = claims == null ? Collections.emptyList()
|
final List<SdJwtClaim> claimsInternal = claims == null ? Collections.emptyList()
|
||||||
|
@ -150,7 +149,7 @@ public class IssuerSignedJWT extends SdJws {
|
||||||
private SignatureSignerContext signer;
|
private SignatureSignerContext signer;
|
||||||
private List<DecoyClaim> decoyClaims;
|
private List<DecoyClaim> decoyClaims;
|
||||||
private boolean nestedDisclosures;
|
private boolean nestedDisclosures;
|
||||||
private String jwsType = "vc+sd-jwt";
|
private String jwsType;
|
||||||
|
|
||||||
public Builder withClaims(List<SdJwtClaim> claims) {
|
public Builder withClaims(List<SdJwtClaim> claims) {
|
||||||
this.claims = claims;
|
this.claims = claims;
|
||||||
|
@ -185,6 +184,7 @@ public class IssuerSignedJWT extends SdJws {
|
||||||
public IssuerSignedJWT build() {
|
public IssuerSignedJWT build() {
|
||||||
// Preinitialize hashAlg to sha-256 if not provided
|
// Preinitialize hashAlg to sha-256 if not provided
|
||||||
hashAlg = hashAlg == null ? "sha-256" : hashAlg;
|
hashAlg = hashAlg == null ? "sha-256" : hashAlg;
|
||||||
|
jwsType = jwsType == null ? "vc+sd-jwt" : jwsType;
|
||||||
// send an empty lise if claims not set.
|
// send an empty lise if claims not set.
|
||||||
claims = claims == null ? Collections.emptyList() : claims;
|
claims = claims == null ? Collections.emptyList() : claims;
|
||||||
decoyClaims = decoyClaims == null ? Collections.emptyList() : decoyClaims;
|
decoyClaims = decoyClaims == null ? Collections.emptyList() : decoyClaims;
|
||||||
|
|
|
@ -34,9 +34,8 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main entry class for selective disclosure jwt (SD-JWT).
|
* Main entry class for selective disclosure jwt (SD-JWT).
|
||||||
*
|
*
|
||||||
* @author <a href="mailto:francis.pouatcha@adorsys.com">Francis Pouatcha</a>
|
* @author <a href="mailto:francis.pouatcha@adorsys.com">Francis Pouatcha</a>
|
||||||
*
|
|
||||||
*/
|
*/
|
||||||
public class SdJwt {
|
public class SdJwt {
|
||||||
public static final String DELIMITER = "~";
|
public static final String DELIMITER = "~";
|
||||||
|
@ -45,11 +44,11 @@ public class SdJwt {
|
||||||
private final List<SdJwtClaim> claims;
|
private final List<SdJwtClaim> claims;
|
||||||
private final List<String> disclosures = new ArrayList<>();
|
private final List<String> disclosures = new ArrayList<>();
|
||||||
|
|
||||||
private Optional<String> sdJwtString = Optional.empty();
|
|
||||||
|
|
||||||
private SdJwt(DisclosureSpec disclosureSpec, JsonNode claimSet, List<SdJwt> nesteSdJwts,
|
private SdJwt(DisclosureSpec disclosureSpec, JsonNode claimSet, List<SdJwt> nesteSdJwts,
|
||||||
Optional<KeyBindingJWT> keyBindingJWT,
|
Optional<KeyBindingJWT> keyBindingJWT,
|
||||||
SignatureSignerContext signer) {
|
SignatureSignerContext signer,
|
||||||
|
String hashAlgorithm,
|
||||||
|
String jwsType) {
|
||||||
claims = new ArrayList<>();
|
claims = new ArrayList<>();
|
||||||
claimSet.fields()
|
claimSet.fields()
|
||||||
.forEachRemaining(entry -> claims.add(createClaim(entry.getKey(), entry.getValue(), disclosureSpec)));
|
.forEachRemaining(entry -> claims.add(createClaim(entry.getKey(), entry.getValue(), disclosureSpec)));
|
||||||
|
@ -59,12 +58,16 @@ public class SdJwt {
|
||||||
.withDecoyClaims(createdDecoyClaims(disclosureSpec))
|
.withDecoyClaims(createdDecoyClaims(disclosureSpec))
|
||||||
.withNestedDisclosures(!nesteSdJwts.isEmpty())
|
.withNestedDisclosures(!nesteSdJwts.isEmpty())
|
||||||
.withSigner(signer)
|
.withSigner(signer)
|
||||||
|
.withHashAlg(hashAlgorithm)
|
||||||
|
.withJwsType(jwsType)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
nesteSdJwts.stream().forEach(nestedJwt -> this.disclosures.addAll(nestedJwt.getDisclosures()));
|
nesteSdJwts.stream().forEach(nestedJwt -> this.disclosures.addAll(nestedJwt.getDisclosures()));
|
||||||
this.disclosures.addAll(getDisclosureStrings(claims));
|
this.disclosures.addAll(getDisclosureStrings(claims));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Optional<String> sdJwtString = Optional.empty();
|
||||||
|
|
||||||
private List<DecoyClaim> createdDecoyClaims(DisclosureSpec disclosureSpec) {
|
private List<DecoyClaim> createdDecoyClaims(DisclosureSpec disclosureSpec) {
|
||||||
return disclosureSpec.getDecoyClaims().stream()
|
return disclosureSpec.getDecoyClaims().stream()
|
||||||
.map(disclosureData -> DecoyClaim.builder().withSalt(disclosureData.getSalt()).build())
|
.map(disclosureData -> DecoyClaim.builder().withSalt(disclosureData.getSalt()).build())
|
||||||
|
@ -73,9 +76,9 @@ public class SdJwt {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prepare to a nested payload to this SD-JWT.
|
* Prepare to a nested payload to this SD-JWT.
|
||||||
*
|
* <p>
|
||||||
* droping the algo claim.
|
* droping the algo claim.
|
||||||
*
|
*
|
||||||
* @param nestedSdJwt
|
* @param nestedSdJwt
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
|
@ -147,8 +150,8 @@ public class SdJwt {
|
||||||
}
|
}
|
||||||
|
|
||||||
private SdJwtClaim createArrayDisclosure(String claimName, JsonNode claimValue,
|
private SdJwtClaim createArrayDisclosure(String claimName, JsonNode claimValue,
|
||||||
Map<Integer, DisclosureSpec.DisclosureData> undisclosedArrayElts,
|
Map<Integer, DisclosureSpec.DisclosureData> undisclosedArrayElts,
|
||||||
Map<Integer, DisclosureSpec.DisclosureData> decoyArrayElts) {
|
Map<Integer, DisclosureSpec.DisclosureData> decoyArrayElts) {
|
||||||
ArrayNode arrayNode = validateArrayNode(claimName, claimValue);
|
ArrayNode arrayNode = validateArrayNode(claimName, claimValue);
|
||||||
ArrayDisclosure.Builder arrayDisclosureBuilder = ArrayDisclosure.builder().withClaimName(claimName);
|
ArrayDisclosure.Builder arrayDisclosureBuilder = ArrayDisclosure.builder().withClaimName(claimName);
|
||||||
|
|
||||||
|
@ -175,7 +178,7 @@ public class SdJwt {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void processArrayElement(ArrayDisclosure.Builder builder, JsonNode elementValue,
|
private void processArrayElement(ArrayDisclosure.Builder builder, JsonNode elementValue,
|
||||||
DisclosureSpec.DisclosureData disclosureData) {
|
DisclosureSpec.DisclosureData disclosureData) {
|
||||||
if (disclosureData != null) {
|
if (disclosureData != null) {
|
||||||
builder.withUndisclosedElement(disclosureData.getSalt(), elementValue);
|
builder.withUndisclosedElement(disclosureData.getSalt(), elementValue);
|
||||||
} else {
|
} else {
|
||||||
|
@ -198,6 +201,8 @@ public class SdJwt {
|
||||||
private Optional<KeyBindingJWT> keyBindingJWT = Optional.empty();
|
private Optional<KeyBindingJWT> keyBindingJWT = Optional.empty();
|
||||||
private SignatureSignerContext signer;
|
private SignatureSignerContext signer;
|
||||||
private final List<SdJwt> nestedSdJwts = new ArrayList<>();
|
private final List<SdJwt> nestedSdJwts = new ArrayList<>();
|
||||||
|
private String hashAlgorithm;
|
||||||
|
private String jwsType;
|
||||||
|
|
||||||
public Builder withDisclosureSpec(DisclosureSpec disclosureSpec) {
|
public Builder withDisclosureSpec(DisclosureSpec disclosureSpec) {
|
||||||
this.disclosureSpec = disclosureSpec;
|
this.disclosureSpec = disclosureSpec;
|
||||||
|
@ -224,8 +229,18 @@ public class SdJwt {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Builder withHashAlgorithm(String hashAlgorithm) {
|
||||||
|
this.hashAlgorithm = hashAlgorithm;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withJwsType(String jwsType) {
|
||||||
|
this.jwsType = jwsType;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public SdJwt build() {
|
public SdJwt build() {
|
||||||
return new SdJwt(disclosureSpec, claimSet, nestedSdJwts, keyBindingJWT, signer);
|
return new SdJwt(disclosureSpec, claimSet, nestedSdJwts, keyBindingJWT, signer, hashAlgorithm, jwsType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -105,7 +105,7 @@ public class JwtSigningService extends SigningService<String> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// retrieve the credential id from the given VC or generate one.
|
// retrieve the credential id from the given VC or generate one.
|
||||||
private String createCredentialId(VerifiableCredential verifiableCredential) {
|
static String createCredentialId(VerifiableCredential verifiableCredential) {
|
||||||
return Optional.ofNullable(
|
return Optional.ofNullable(
|
||||||
verifiableCredential.getId())
|
verifiableCredential.getId())
|
||||||
.orElse(URI.create(String.format(ID_TEMPLATE, UUID.randomUUID())))
|
.orElse(URI.create(String.format(ID_TEMPLATE, UUID.randomUUID())))
|
||||||
|
|
|
@ -17,8 +17,25 @@
|
||||||
|
|
||||||
package org.keycloak.protocol.oid4vc.issuance.signing;
|
package org.keycloak.protocol.oid4vc.issuance.signing;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.crypto.KeyWrapper;
|
||||||
|
import org.keycloak.crypto.SignatureProvider;
|
||||||
|
import org.keycloak.crypto.SignatureSignerContext;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.protocol.oid4vc.issuance.TimeProvider;
|
||||||
|
import org.keycloak.protocol.oid4vc.model.CredentialSubject;
|
||||||
import org.keycloak.protocol.oid4vc.model.VerifiableCredential;
|
import org.keycloak.protocol.oid4vc.model.VerifiableCredential;
|
||||||
|
import org.keycloak.sdjwt.DisclosureSpec;
|
||||||
|
import org.keycloak.sdjwt.SdJwt;
|
||||||
|
import org.keycloak.sdjwt.SdJwtUtils;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.StringJoiner;
|
||||||
|
import java.util.stream.IntStream;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@link VerifiableCredentialsSigningService} implementing the SD_JWT_VC format. It returns a String, containing
|
* {@link VerifiableCredentialsSigningService} implementing the SD_JWT_VC format. It returns a String, containing
|
||||||
|
@ -31,12 +48,98 @@ import org.keycloak.protocol.oid4vc.model.VerifiableCredential;
|
||||||
*/
|
*/
|
||||||
public class SdJwtSigningService extends SigningService<String> {
|
public class SdJwtSigningService extends SigningService<String> {
|
||||||
|
|
||||||
public SdJwtSigningService(KeycloakSession keycloakSession, String keyId, String algorithmType) {
|
private static final Logger LOGGER = Logger.getLogger(SdJwtSigningService.class);
|
||||||
|
|
||||||
|
private static final String ISSUER_CLAIM ="iss";
|
||||||
|
private static final String NOT_BEFORE_CLAIM ="nbf";
|
||||||
|
private static final String VERIFIABLE_CREDENTIAL_TYPE_CLAIM = "vct";
|
||||||
|
private static final String CREDENTIAL_ID_CLAIM = "jti";
|
||||||
|
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
private final SignatureSignerContext signatureSignerContext;
|
||||||
|
private final TimeProvider timeProvider;
|
||||||
|
private final String tokenType;
|
||||||
|
private final String hashAlgorithm;
|
||||||
|
private final int decoys;
|
||||||
|
private final List<String> visibleClaims;
|
||||||
|
protected final String issuerDid;
|
||||||
|
|
||||||
|
public SdJwtSigningService(KeycloakSession keycloakSession, ObjectMapper objectMapper, String keyId, String algorithmType, String tokenType, String hashAlgorithm, String issuerDid, int decoys, List<String> visibleClaims, TimeProvider timeProvider, Optional<String> kid) {
|
||||||
super(keycloakSession, keyId, algorithmType);
|
super(keycloakSession, keyId, algorithmType);
|
||||||
|
this.objectMapper = objectMapper;
|
||||||
|
this.issuerDid = issuerDid;
|
||||||
|
this.timeProvider = timeProvider;
|
||||||
|
this.tokenType = tokenType;
|
||||||
|
this.hashAlgorithm = hashAlgorithm;
|
||||||
|
this.decoys = decoys;
|
||||||
|
this.visibleClaims = visibleClaims;
|
||||||
|
KeyWrapper signingKey = getKey(keyId, algorithmType);
|
||||||
|
if (signingKey == null) {
|
||||||
|
throw new SigningServiceException(String.format("No key for id %s and algorithm %s available.", keyId, algorithmType));
|
||||||
|
}
|
||||||
|
// set the configured kid if present.
|
||||||
|
if (kid.isPresent()) {
|
||||||
|
// we need to clone the key first, to not change the kid of the original key so that the next request still can find it.
|
||||||
|
signingKey = signingKey.cloneKey();
|
||||||
|
signingKey.setKid(keyId);
|
||||||
|
}
|
||||||
|
kid.ifPresent(signingKey::setKid);
|
||||||
|
SignatureProvider signatureProvider = keycloakSession.getProvider(SignatureProvider.class, algorithmType);
|
||||||
|
signatureSignerContext = signatureProvider.signer(signingKey);
|
||||||
|
|
||||||
|
LOGGER.debugf("Successfully initiated the SD-JWT Signing Service with algorithm %s.", algorithmType);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String signCredential(VerifiableCredential verifiableCredential) {
|
public String signCredential(VerifiableCredential verifiableCredential) {
|
||||||
throw new UnsupportedOperationException("SD-JWT Signing is not yet supported.");
|
|
||||||
|
DisclosureSpec.Builder disclosureSpecBuilder = DisclosureSpec.builder();
|
||||||
|
CredentialSubject credentialSubject = verifiableCredential.getCredentialSubject();
|
||||||
|
JsonNode claimSet = objectMapper.valueToTree(credentialSubject);
|
||||||
|
// put all claims into the disclosure spec, except the one to be kept visible
|
||||||
|
credentialSubject.getClaims()
|
||||||
|
.entrySet()
|
||||||
|
.stream()
|
||||||
|
.filter(entry -> !visibleClaims.contains(entry.getKey()))
|
||||||
|
.forEach(entry -> {
|
||||||
|
if (entry instanceof List<?> listValue) {
|
||||||
|
IntStream.range(0, listValue.size())
|
||||||
|
.forEach(i -> disclosureSpecBuilder.withUndisclosedArrayElt(entry.getKey(), i, SdJwtUtils.randomSalt()));
|
||||||
|
} else {
|
||||||
|
disclosureSpecBuilder.withUndisclosedClaim(entry.getKey(), SdJwtUtils.randomSalt());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// add the configured number of decoys
|
||||||
|
if (decoys != 0) {
|
||||||
|
IntStream.range(0, decoys)
|
||||||
|
.forEach(i -> disclosureSpecBuilder.withDecoyClaim(SdJwtUtils.randomSalt()));
|
||||||
|
}
|
||||||
|
|
||||||
|
ObjectNode rootNode = claimSet.withObject("");
|
||||||
|
rootNode.put(ISSUER_CLAIM, issuerDid);
|
||||||
|
|
||||||
|
// 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());
|
||||||
|
rootNode.put(NOT_BEFORE_CLAIM, iat);
|
||||||
|
if (verifiableCredential.getType() == null || verifiableCredential.getType().size() != 1) {
|
||||||
|
throw new SigningServiceException("SD-JWT only supports single type credentials.");
|
||||||
|
}
|
||||||
|
rootNode.put(VERIFIABLE_CREDENTIAL_TYPE_CLAIM, verifiableCredential.getType().get(0));
|
||||||
|
rootNode.put(CREDENTIAL_ID_CLAIM, JwtSigningService.createCredentialId(verifiableCredential));
|
||||||
|
|
||||||
|
SdJwt sdJwt = SdJwt.builder()
|
||||||
|
.withDisclosureSpec(disclosureSpecBuilder.build())
|
||||||
|
.withClaimSet(claimSet)
|
||||||
|
.withSigner(signatureSignerContext)
|
||||||
|
.withHashAlgorithm(hashAlgorithm)
|
||||||
|
.withJwsType(tokenType)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return sdJwt.toSdJwtString();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -17,16 +17,21 @@
|
||||||
|
|
||||||
package org.keycloak.protocol.oid4vc.issuance.signing;
|
package org.keycloak.protocol.oid4vc.issuance.signing;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import org.keycloak.common.util.Time;
|
import org.keycloak.common.util.Time;
|
||||||
import org.keycloak.component.ComponentModel;
|
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.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provider Factory to create {@link SdJwtSigningService}s
|
* Provider Factory to create {@link SdJwtSigningService}s
|
||||||
|
@ -42,8 +47,24 @@ public class SdJwtSigningServiceProviderFactory implements VCSigningServiceProvi
|
||||||
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 hashAlgorithm = model.get(SigningProperties.HASH_ALGORITHM.getKey());
|
||||||
|
Optional<String> kid = Optional.ofNullable(model.get(SigningProperties.KID_HEADER.getKey()));
|
||||||
|
int decoys = Integer.valueOf(model.get(SigningProperties.DECOYS.getKey()));
|
||||||
|
|
||||||
return new SdJwtSigningService(session, keyId, algorithmType);
|
List<String> visibleClaims = Optional.ofNullable(model.get(SigningProperties.VISIBLE_CLAIMS.getKey()))
|
||||||
|
.map(visibileClaims -> visibileClaims.split(","))
|
||||||
|
.map(Arrays::asList)
|
||||||
|
.orElse(List.of());
|
||||||
|
|
||||||
|
String issuerDid = Optional.ofNullable(
|
||||||
|
session
|
||||||
|
.getContext()
|
||||||
|
.getRealm()
|
||||||
|
.getAttribute(ISSUER_DID_REALM_ATTRIBUTE_KEY))
|
||||||
|
.orElseThrow(() -> new VCIssuerException("No issuerDid configured."));
|
||||||
|
|
||||||
|
return new SdJwtSigningService(session, new ObjectMapper(), keyId, algorithmType, tokenType, hashAlgorithm, issuerDid, decoys, visibleClaims, new OffsetTimeProvider(), kid);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -55,7 +76,10 @@ public class SdJwtSigningServiceProviderFactory implements VCSigningServiceProvi
|
||||||
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())
|
||||||
.property(SigningProperties.DECOYS.asConfigProperty())
|
.property(SigningProperties.DECOYS.asConfigProperty())
|
||||||
|
.property(SigningProperties.KID_HEADER.asConfigProperty())
|
||||||
|
.property(SigningProperties.HASH_ALGORITHM.asConfigProperty())
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,7 +91,9 @@ public class SdJwtSigningServiceProviderFactory implements VCSigningServiceProvi
|
||||||
@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.HASH_ALGORITHM.asConfigProperty())
|
||||||
.checkRequired(SigningProperties.ALGORITHM_TYPE.asConfigProperty())
|
.checkRequired(SigningProperties.ALGORITHM_TYPE.asConfigProperty())
|
||||||
|
.checkRequired(SigningProperties.TOKEN_TYPE.asConfigProperty())
|
||||||
.checkInt(SigningProperties.DECOYS.asConfigProperty(), true);
|
.checkInt(SigningProperties.DECOYS.asConfigProperty(), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,11 +29,14 @@ public enum SigningProperties {
|
||||||
|
|
||||||
ISSUER_DID("issuerDid", "Did of the issuer.", "Provide the DID of the issuer. Needs to match the provided key material.", ProviderConfigProperty.STRING_TYPE, null),
|
ISSUER_DID("issuerDid", "Did of the issuer.", "Provide the DID of the issuer. Needs to match the provided key material.", 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),
|
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),
|
||||||
|
KID_HEADER("kidHeader", "Kid to be set for the JWT.", "The kid to be set in the jwt-header. Depending on the did-schema, the pure key-id might not be enough and can be overwritten here.", 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"),
|
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),
|
||||||
|
HASH_ALGORITHM("hashAlgorithm", "Hash algorithm for SD-JWTs.", "The hash algorithm to be used for the SD-JWTs.", ProviderConfigProperty.STRING_TYPE, "sha-256"),
|
||||||
|
VISIBLE_CLAIMS("visibleClaims", "Visible claims of the SD-JWT.", "List of claims to stay disclosed in the SD-JWT.", ProviderConfigProperty.MULTIVALUED_STRING_TYPE, null);
|
||||||
|
|
||||||
private final String key;
|
private final String key;
|
||||||
private final String label;
|
private final String label;
|
||||||
|
|
|
@ -214,17 +214,7 @@ public class JwtSigningServiceTest extends SigningServiceTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
@Override
|
||||||
public void configureTestRealm(RealmRepresentation testRealm) {
|
public void configureTestRealm(RealmRepresentation testRealm) {
|
||||||
if (testRealm.getComponents() != null) {
|
if (testRealm.getComponents() != null) {
|
||||||
|
|
|
@ -0,0 +1,331 @@
|
||||||
|
/*
|
||||||
|
* 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.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.keycloak.TokenVerifier;
|
||||||
|
import org.keycloak.common.VerificationException;
|
||||||
|
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.jose.jws.crypto.HashUtils;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.protocol.oid4vc.issuance.signing.JwtSigningService;
|
||||||
|
import org.keycloak.protocol.oid4vc.issuance.signing.SdJwtSigningService;
|
||||||
|
import org.keycloak.protocol.oid4vc.issuance.signing.SigningServiceException;
|
||||||
|
import org.keycloak.protocol.oid4vc.model.VerifiableCredential;
|
||||||
|
import org.keycloak.representations.JsonWebToken;
|
||||||
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
|
import org.keycloak.sdjwt.SdJwtUtils;
|
||||||
|
import org.keycloak.testsuite.runonserver.RunOnServerException;
|
||||||
|
|
||||||
|
import java.security.PublicKey;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.StringJoiner;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.junit.Assert.fail;
|
||||||
|
|
||||||
|
public class SdJwtSigningServiceTest extends SigningServiceTest {
|
||||||
|
|
||||||
|
private static ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
||||||
|
private static KeyWrapper rsaKey = getRsaKey();
|
||||||
|
|
||||||
|
// 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 SdJwtSigningService(
|
||||||
|
session,
|
||||||
|
new ObjectMapper(),
|
||||||
|
getKeyFromSession(session).getKid(),
|
||||||
|
"unsupported-algorithm",
|
||||||
|
"JWT",
|
||||||
|
"sha-256",
|
||||||
|
"did:web:test.org",
|
||||||
|
0,
|
||||||
|
List.of(),
|
||||||
|
new StaticTimeProvider(1000),
|
||||||
|
Optional.empty()));
|
||||||
|
} 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRsaSignedCredentialWithClaims() {
|
||||||
|
getTestingClient()
|
||||||
|
.server(TEST_REALM_NAME)
|
||||||
|
.run(session ->
|
||||||
|
testSignSDJwtCredential(
|
||||||
|
session,
|
||||||
|
Optional.empty(),
|
||||||
|
Algorithm.RS256,
|
||||||
|
Map.of("id", String.format("uri:uuid:%s", UUID.randomUUID()),
|
||||||
|
"test", "test",
|
||||||
|
"arrayClaim", List.of("a", "b", "c")),
|
||||||
|
0,
|
||||||
|
List.of()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRsaSignedCredentialWithVisibleClaims() {
|
||||||
|
getTestingClient()
|
||||||
|
.server(TEST_REALM_NAME)
|
||||||
|
.run(session ->
|
||||||
|
testSignSDJwtCredential(
|
||||||
|
session,
|
||||||
|
Optional.empty(),
|
||||||
|
Algorithm.RS256,
|
||||||
|
Map.of("id", String.format("uri:uuid:%s", UUID.randomUUID()),
|
||||||
|
"test", "test",
|
||||||
|
"arrayClaim", List.of("a", "b", "c")),
|
||||||
|
0,
|
||||||
|
List.of("test")));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRsaSignedCredentialWithClaimsAndDecoys() {
|
||||||
|
getTestingClient()
|
||||||
|
.server(TEST_REALM_NAME)
|
||||||
|
.run(session ->
|
||||||
|
testSignSDJwtCredential(
|
||||||
|
session,
|
||||||
|
Optional.empty(),
|
||||||
|
Algorithm.RS256,
|
||||||
|
Map.of("id", String.format("uri:uuid:%s", UUID.randomUUID()),
|
||||||
|
"test", "test",
|
||||||
|
"arrayClaim", List.of("a", "b", "c")),
|
||||||
|
6,
|
||||||
|
List.of()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRsaSignedCredentialWithKeyId() {
|
||||||
|
getTestingClient()
|
||||||
|
.server(TEST_REALM_NAME)
|
||||||
|
.run(session ->
|
||||||
|
testSignSDJwtCredential(
|
||||||
|
session,
|
||||||
|
Optional.of("did:web:test.org#key-id"),
|
||||||
|
Algorithm.RS256,
|
||||||
|
Map.of("id", String.format("uri:uuid:%s", UUID.randomUUID()),
|
||||||
|
"test", "test",
|
||||||
|
"arrayClaim", List.of("a", "b", "c")),
|
||||||
|
0,
|
||||||
|
List.of()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRsaSignedCredentialWithoutAdditionalClaims() {
|
||||||
|
getTestingClient()
|
||||||
|
.server(TEST_REALM_NAME)
|
||||||
|
.run(session ->
|
||||||
|
testSignSDJwtCredential(
|
||||||
|
session,
|
||||||
|
Optional.empty(),
|
||||||
|
Algorithm.RS256,
|
||||||
|
Map.of(),
|
||||||
|
0,
|
||||||
|
List.of()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void testSignSDJwtCredential(KeycloakSession session, Optional<String> keyId, String
|
||||||
|
algorithm, Map<String, Object> claims, int decoys, List<String> visibleClaims) {
|
||||||
|
KeyWrapper keyWrapper = getKeyFromSession(session);
|
||||||
|
|
||||||
|
SdJwtSigningService signingService = new SdJwtSigningService(
|
||||||
|
session,
|
||||||
|
new ObjectMapper(),
|
||||||
|
keyWrapper.getKid(),
|
||||||
|
algorithm,
|
||||||
|
"vc+sd-jwt",
|
||||||
|
"sha-256",
|
||||||
|
"did:web:test.org",
|
||||||
|
decoys,
|
||||||
|
visibleClaims,
|
||||||
|
new StaticTimeProvider(1000),
|
||||||
|
keyId);
|
||||||
|
|
||||||
|
VerifiableCredential testCredential = getTestCredential(claims);
|
||||||
|
|
||||||
|
String sdJwt = signingService.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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// the sd-jwt is dot-concatenated header.payload.signature~disclosure1~___~disclosureN
|
||||||
|
// we first split the disclosuers
|
||||||
|
String[] splittedSdToken = sdJwt.split("~");
|
||||||
|
// and then split the actual token part
|
||||||
|
String[] splittedToken = splittedSdToken[0].split("\\.");
|
||||||
|
|
||||||
|
String jwt = new StringJoiner(".")
|
||||||
|
// header
|
||||||
|
.add(splittedToken[0])
|
||||||
|
// payload
|
||||||
|
.add(splittedToken[1])
|
||||||
|
// signature
|
||||||
|
.add(splittedToken[2])
|
||||||
|
.toString();
|
||||||
|
TokenVerifier<JsonWebToken> verifier = TokenVerifier
|
||||||
|
.create(jwt, 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("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());
|
||||||
|
assertEquals("The type should be included", TEST_TYPES.get(0), theToken.getOtherClaims().get("vct"));
|
||||||
|
|
||||||
|
assertEquals("The nbf date should be included", TEST_ISSUANCE_DATE.toInstant().getEpochSecond(), theToken.getNbf().longValue());
|
||||||
|
|
||||||
|
List<String> sds = (List<String>) theToken.getOtherClaims().get("_sd");
|
||||||
|
if (sds != null && !sds.isEmpty()){
|
||||||
|
assertEquals("The algorithm should be included", "sha-256", theToken.getOtherClaims().get("_sd_alg"));
|
||||||
|
}
|
||||||
|
List<String> disclosed = Arrays.asList(splittedSdToken).subList(1, splittedSdToken.length);
|
||||||
|
int numSds = sds != null ? sds.size() : 0;
|
||||||
|
assertEquals("All undisclosed claims and decoys should be provided.", disclosed.size() + decoys, numSds);
|
||||||
|
verifyDisclosures(sds, disclosed);
|
||||||
|
|
||||||
|
visibleClaims
|
||||||
|
.forEach(vc -> assertTrue("The visible claims should be present within the token.",theToken.getOtherClaims().containsKey(vc)));
|
||||||
|
} catch (VerificationException e) {
|
||||||
|
fail("Was not able to extract the token.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void verifyDisclosures(List<String> undisclosed, List<String> disclosedList) {
|
||||||
|
disclosedList.stream()
|
||||||
|
.map(disclosed -> new String(Base64.getUrlDecoder().decode(disclosed)))
|
||||||
|
.map(disclosedString -> {
|
||||||
|
try {
|
||||||
|
return OBJECT_MAPPER.readValue(disclosedString, List.class);
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map(dl -> new DisclosedClaim((String) dl.get(0), (String) dl.get(1), dl.get(2)))
|
||||||
|
.forEach(dc -> assertTrue("Every disclosure claim should be provided in the undisclosures.", undisclosed.contains(dc.getHash())));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@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)))));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class DisclosedClaim {
|
||||||
|
private final String salt;
|
||||||
|
private final String key;
|
||||||
|
private final Object value;
|
||||||
|
private final String hash;
|
||||||
|
|
||||||
|
DisclosedClaim(String salt, String key, Object value) {
|
||||||
|
this.salt = salt;
|
||||||
|
this.key = key;
|
||||||
|
this.value = value;
|
||||||
|
this.hash = createHash(salt, key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String createHash(String salt, String key, Object value) {
|
||||||
|
try {
|
||||||
|
return SdJwtUtils.encodeNoPad(
|
||||||
|
HashUtils.hash("sha-256",
|
||||||
|
SdJwtUtils.encodeNoPad(
|
||||||
|
SdJwtUtils.printJsonArray(List.of(salt, key, value).toArray()).getBytes()).getBytes()));
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSalt() {
|
||||||
|
return salt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getKey() {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Object getValue() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getHash() {
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -25,6 +25,7 @@ import org.keycloak.common.util.MultivaluedHashMap;
|
||||||
import org.keycloak.common.util.PemUtils;
|
import org.keycloak.common.util.PemUtils;
|
||||||
import org.keycloak.crypto.KeyUse;
|
import org.keycloak.crypto.KeyUse;
|
||||||
import org.keycloak.crypto.KeyWrapper;
|
import org.keycloak.crypto.KeyWrapper;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.protocol.oid4vc.issuance.TimeProvider;
|
import org.keycloak.protocol.oid4vc.issuance.TimeProvider;
|
||||||
import org.keycloak.protocol.oid4vc.model.CredentialSubject;
|
import org.keycloak.protocol.oid4vc.model.CredentialSubject;
|
||||||
import org.keycloak.protocol.oid4vc.model.VerifiableCredential;
|
import org.keycloak.protocol.oid4vc.model.VerifiableCredential;
|
||||||
|
@ -161,6 +162,17 @@ public abstract class SigningServiceTest extends AbstractTestRealmKeycloakTest {
|
||||||
return componentExportRepresentation;
|
return componentExportRepresentation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected 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"));
|
||||||
|
}
|
||||||
|
|
||||||
static class StaticTimeProvider implements TimeProvider {
|
static class StaticTimeProvider implements TimeProvider {
|
||||||
private final int currentTimeInS;
|
private final int currentTimeInS;
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue