mirror of
https://github.com/keycloak/keycloak.git
synced 2026-01-05 22:39:52 -06:00
Rename ldp_vp to di_vp and restructure proofs object for Draft 16 compliance (#41982)
Closes #41576 Closes #41577 Closes #41581 Signed-off-by: forkimenjeckayang <forkimenjeckayang@gmail.com>
This commit is contained in:
committed by
GitHub
parent
fb35439479
commit
fc73537ba7
@@ -37,8 +37,6 @@ import jakarta.ws.rs.core.HttpHeaders;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.common.util.SecretGenerator;
|
||||
import org.keycloak.component.ComponentFactory;
|
||||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.events.Errors;
|
||||
import org.keycloak.events.EventBuilder;
|
||||
import org.keycloak.jose.jwe.JWE;
|
||||
@@ -77,7 +75,8 @@ import org.keycloak.protocol.oid4vc.model.NonceResponse;
|
||||
import org.keycloak.protocol.oid4vc.model.OfferUriType;
|
||||
import org.keycloak.protocol.oid4vc.model.PreAuthorizedCode;
|
||||
import org.keycloak.protocol.oid4vc.model.PreAuthorizedGrant;
|
||||
import org.keycloak.protocol.oid4vc.model.Proof;
|
||||
import org.keycloak.protocol.oid4vc.model.ProofType;
|
||||
import org.keycloak.protocol.oid4vc.model.Proofs;
|
||||
import org.keycloak.protocol.oid4vc.model.SupportedCredentialConfiguration;
|
||||
import org.keycloak.protocol.oid4vc.model.VerifiableCredential;
|
||||
import org.keycloak.protocol.oidc.grants.PreAuthorizedCodeGrantType;
|
||||
@@ -765,25 +764,33 @@ public class OID4VCIssuerEndpoint {
|
||||
* Enforce key binding: Validate proof and bind associated key to credential in issuance context.
|
||||
*/
|
||||
private void enforceKeyBindingIfProofProvided(VCIssuanceContext vcIssuanceContext) {
|
||||
Proof proof = vcIssuanceContext.getCredentialRequest().getProof();
|
||||
if (proof == null) {
|
||||
LOGGER.debugf("No proof provided, skipping key binding");
|
||||
Proofs proofs = vcIssuanceContext.getCredentialRequest().getProofs();
|
||||
if (proofs == null) {
|
||||
LOGGER.debugf("No proofs provided, skipping key binding");
|
||||
return;
|
||||
}
|
||||
|
||||
String proofType = proof.getProofType();
|
||||
// Validate each JWT proof if present
|
||||
if (proofs.getJwt() != null && !proofs.getJwt().isEmpty()) {
|
||||
validateProofs(vcIssuanceContext, ProofType.JWT);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateProofs(VCIssuanceContext vcIssuanceContext, String proofType) {
|
||||
ProofValidator proofValidator = session.getProvider(ProofValidator.class, proofType);
|
||||
if (proofValidator == null) {
|
||||
throw new BadRequestException(String.format("Unable to validate proofs of type %s", proofType));
|
||||
}
|
||||
|
||||
// Validate proof and bind public key to credential
|
||||
// Validate proof and bind public keys to credential
|
||||
try {
|
||||
Optional.ofNullable(proofValidator.validateProof(vcIssuanceContext))
|
||||
.ifPresent(jwk -> vcIssuanceContext.getCredentialBody().addKeyBinding(jwk));
|
||||
List<JWK> jwks = proofValidator.validateProof(vcIssuanceContext);
|
||||
if (jwks != null && !jwks.isEmpty()) {
|
||||
// Bind the first JWK to the credential
|
||||
vcIssuanceContext.getCredentialBody().addKeyBinding(jwks.get(0));
|
||||
}
|
||||
} catch (VCIssuerException e) {
|
||||
throw new BadRequestException("Could not validate provided proof", e);
|
||||
throw new BadRequestException(String.format("Could not validate provided %s proof", proofType), e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,22 +28,22 @@ import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.protocol.oid4vc.issuance.OID4VCIssuerWellKnownProvider;
|
||||
import org.keycloak.protocol.oid4vc.issuance.VCIssuanceContext;
|
||||
import org.keycloak.protocol.oid4vc.issuance.VCIssuerException;
|
||||
import org.keycloak.protocol.oid4vc.model.JwtProof;
|
||||
import org.keycloak.protocol.oid4vc.model.Proof;
|
||||
import org.keycloak.protocol.oid4vc.model.ProofType;
|
||||
import org.keycloak.protocol.oid4vc.model.ProofTypeJWT;
|
||||
import org.keycloak.protocol.oid4vc.model.ProofTypesSupported;
|
||||
import org.keycloak.protocol.oid4vc.model.Proofs;
|
||||
import org.keycloak.protocol.oid4vc.model.SupportedCredentialConfiguration;
|
||||
import org.keycloak.representations.AccessToken;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
/**
|
||||
* Validates the conformance and authenticity of presented JWT proofs.
|
||||
@@ -54,6 +54,7 @@ public class JwtProofValidator extends AbstractProofValidator {
|
||||
|
||||
public static final String PROOF_JWT_TYP = "openid4vci-proof+jwt";
|
||||
private static final String CRYPTOGRAPHIC_BINDING_METHOD_JWK = "jwk";
|
||||
private static final Logger LOGGER = Logger.getLogger(JwtProofValidator.class);
|
||||
|
||||
protected JwtProofValidator(KeycloakSession keycloakSession) {
|
||||
super(keycloakSession);
|
||||
@@ -64,7 +65,8 @@ public class JwtProofValidator extends AbstractProofValidator {
|
||||
return ProofType.JWT;
|
||||
}
|
||||
|
||||
public JWK validateProof(VCIssuanceContext vcIssuanceContext) throws VCIssuerException {
|
||||
@Override
|
||||
public List<JWK> validateProof(VCIssuanceContext vcIssuanceContext) throws VCIssuerException {
|
||||
try {
|
||||
return validateJwtProof(vcIssuanceContext);
|
||||
} catch (JWSInputException | VerificationException | IOException e) {
|
||||
@@ -87,20 +89,44 @@ public class JwtProofValidator extends AbstractProofValidator {
|
||||
* @throws IllegalStateException: is credential type badly configured
|
||||
* @throws IOException
|
||||
*/
|
||||
private JWK validateJwtProof(VCIssuanceContext vcIssuanceContext) throws VCIssuerException, JWSInputException, VerificationException, IOException {
|
||||
private List<JWK> validateJwtProof(VCIssuanceContext vcIssuanceContext) throws VCIssuerException, JWSInputException, VerificationException, IOException {
|
||||
|
||||
Optional<Proof> optionalProof = getProofFromContext(vcIssuanceContext);
|
||||
Optional<List<String>> optionalProof = getProofFromContext(vcIssuanceContext);
|
||||
|
||||
if (optionalProof.isEmpty() || !(optionalProof.get() instanceof JwtProof)) {
|
||||
if (optionalProof.isEmpty() || optionalProof.get().isEmpty()) {
|
||||
return null; // No proof support
|
||||
}
|
||||
|
||||
JwtProof proof = (JwtProof) optionalProof.get();
|
||||
List<String> jwtProofs = optionalProof.get();
|
||||
|
||||
// Check key binding config for jwt. Only type supported.
|
||||
checkCryptographicKeyBinding(vcIssuanceContext);
|
||||
|
||||
JWSInput jwsInput = getJwsInput(proof);
|
||||
// Validate all JWT proofs in the array
|
||||
List<JWK> validJwks = new ArrayList<>();
|
||||
|
||||
for (int i = 0; i < jwtProofs.size(); i++) {
|
||||
String jwt = jwtProofs.get(i);
|
||||
try {
|
||||
JWK jwk = validateSingleJwtProof(vcIssuanceContext, jwt);
|
||||
validJwks.add(jwk);
|
||||
LOGGER.debugf("Successfully validated JWT proof at index %d", i);
|
||||
} catch (VCIssuerException e) {
|
||||
// If any proof fails validation, throw the exception
|
||||
throw new VCIssuerException(String.format("Failed to validate JWT proof at index %d: %s", i, e.getMessage()), e);
|
||||
}
|
||||
}
|
||||
|
||||
if (validJwks.isEmpty()) {
|
||||
throw new VCIssuerException("No valid JWT proof found in the proofs array");
|
||||
}
|
||||
|
||||
LOGGER.debugf("Successfully validated %d JWT proofs", validJwks.size());
|
||||
return validJwks;
|
||||
}
|
||||
|
||||
private JWK validateSingleJwtProof(VCIssuanceContext vcIssuanceContext, String jwt) throws VCIssuerException, JWSInputException, VerificationException, IOException {
|
||||
JWSInput jwsInput = getJwsInput(jwt);
|
||||
JWSHeader jwsHeader = jwsInput.getHeader();
|
||||
validateJwsHeader(vcIssuanceContext, jwsHeader);
|
||||
|
||||
@@ -130,33 +156,24 @@ public class JwtProofValidator extends AbstractProofValidator {
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<Proof> getProofFromContext(VCIssuanceContext vcIssuanceContext) throws VCIssuerException {
|
||||
private Optional<List<String>> getProofFromContext(VCIssuanceContext vcIssuanceContext) throws VCIssuerException {
|
||||
return Optional.ofNullable(vcIssuanceContext.getCredentialConfig())
|
||||
.map(SupportedCredentialConfiguration::getProofTypesSupported)
|
||||
.flatMap(proofTypesSupported -> {
|
||||
Optional.ofNullable(proofTypesSupported.getSupportedProofTypes().get("jwt"))
|
||||
.orElseThrow(() -> new VCIssuerException("SD-JWT supports only jwt proof type."));
|
||||
|
||||
Proof proofObject = vcIssuanceContext.getCredentialRequest().getProof();
|
||||
if (proofObject == null) {
|
||||
Proofs proofs = vcIssuanceContext.getCredentialRequest().getProofs();
|
||||
if (proofs == null || proofs.getJwt() == null || proofs.getJwt().isEmpty()) {
|
||||
throw new VCIssuerException("Credential configuration requires a proof of type: " + ProofType.JWT);
|
||||
}
|
||||
|
||||
if (!(proofObject instanceof JwtProof)) {
|
||||
throw new VCIssuerException("Wrong proof type. Expected JwtProof, but got: " + proofObject.getClass().getSimpleName());
|
||||
}
|
||||
|
||||
Proof proof = (Proof) proofObject;
|
||||
if (!Objects.equals(proof.getProofType(), ProofType.JWT)) {
|
||||
throw new VCIssuerException("Wrong proof type");
|
||||
}
|
||||
|
||||
return Optional.of(proof);
|
||||
return Optional.of(proofs.getJwt());
|
||||
});
|
||||
}
|
||||
|
||||
private JWSInput getJwsInput(JwtProof proof) throws JWSInputException {
|
||||
return new JWSInput(proof.getJwt());
|
||||
private JWSInput getJwsInput(String jwt) throws JWSInputException {
|
||||
return new JWSInput(jwt);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -22,6 +22,8 @@ import org.keycloak.protocol.oid4vc.issuance.VCIssuanceContext;
|
||||
import org.keycloak.protocol.oid4vc.issuance.VCIssuerException;
|
||||
import org.keycloak.provider.Provider;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface ProofValidator extends Provider {
|
||||
|
||||
@Override
|
||||
@@ -31,10 +33,10 @@ public interface ProofValidator extends Provider {
|
||||
String getProofType();
|
||||
|
||||
/**
|
||||
* Validates a client-provided key binding proof.
|
||||
* Validates client-provided key binding proofs.
|
||||
*
|
||||
* @param vcIssuanceContext the issuance context with credential request and config
|
||||
* @return the JWK to bind to the credential
|
||||
* @return the list of JWKs to bind to credentials (one JWK per credential)
|
||||
*/
|
||||
JWK validateProof(VCIssuanceContext vcIssuanceContext) throws VCIssuerException;
|
||||
List<JWK> validateProof(VCIssuanceContext vcIssuanceContext) throws VCIssuerException;
|
||||
}
|
||||
|
||||
@@ -45,13 +45,8 @@ public class CredentialRequest {
|
||||
@JsonProperty("credential_identifier")
|
||||
private String credentialIdentifier;
|
||||
|
||||
@JsonProperty("proof")
|
||||
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "proof_type")
|
||||
@JsonSubTypes({
|
||||
@JsonSubTypes.Type(value = JwtProof.class, name = ProofType.JWT),
|
||||
@JsonSubTypes.Type(value = LdpVpProof.class, name = ProofType.LD_PROOF)
|
||||
})
|
||||
private Proof proof;
|
||||
@JsonProperty("proofs")
|
||||
private Proofs proofs;
|
||||
|
||||
// See: https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-format-identifier-3
|
||||
@JsonProperty("credential_definition")
|
||||
@@ -80,12 +75,12 @@ public class CredentialRequest {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Proof getProof() {
|
||||
return proof;
|
||||
public Proofs getProofs() {
|
||||
return proofs;
|
||||
}
|
||||
|
||||
public CredentialRequest setProof(Proof proof) {
|
||||
this.proof = proof;
|
||||
public CredentialRequest setProofs(Proofs proofs) {
|
||||
this.proofs = proofs;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
/*
|
||||
* Copyright 2025 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.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* W3C Verifiable Presentation as defined by [VC_DATA_2.0] or [VC_DATA] secured using Data Integrity [VC_Data_Integrity].
|
||||
* Used as a proof type in OID4VCI (Section F.2).
|
||||
*
|
||||
* @see <a href="https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-di_vp-proof-type">OID4VCI di_vp Proof Type</a>
|
||||
*/
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class DiVpProof {
|
||||
|
||||
@JsonProperty("@context")
|
||||
private List<String> context;
|
||||
|
||||
@JsonProperty("type")
|
||||
private List<String> type;
|
||||
|
||||
@JsonProperty("holder")
|
||||
private String holder;
|
||||
|
||||
@JsonProperty("proof")
|
||||
private List<DataIntegrityProof> proof;
|
||||
|
||||
public DiVpProof() {
|
||||
}
|
||||
|
||||
public DiVpProof(List<String> context, List<String> type, String holder, List<DataIntegrityProof> proof) {
|
||||
this.context = context;
|
||||
this.type = type;
|
||||
this.holder = holder;
|
||||
this.proof = proof;
|
||||
}
|
||||
|
||||
public List<String> getContext() {
|
||||
return context;
|
||||
}
|
||||
|
||||
public DiVpProof setContext(List<String> context) {
|
||||
this.context = context;
|
||||
return this;
|
||||
}
|
||||
|
||||
public List<String> getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public DiVpProof setType(List<String> type) {
|
||||
this.type = type;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getHolder() {
|
||||
return holder;
|
||||
}
|
||||
|
||||
public DiVpProof setHolder(String holder) {
|
||||
this.holder = holder;
|
||||
return this;
|
||||
}
|
||||
|
||||
public List<DataIntegrityProof> getProof() {
|
||||
return proof;
|
||||
}
|
||||
|
||||
public DiVpProof setProof(List<DataIntegrityProof> proof) {
|
||||
this.proof = proof;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Data Integrity Proof as defined in [VC_Data_Integrity].
|
||||
*/
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public static class DataIntegrityProof {
|
||||
|
||||
@JsonProperty("type")
|
||||
private String type;
|
||||
|
||||
@JsonProperty("cryptosuite")
|
||||
private String cryptosuite;
|
||||
|
||||
@JsonProperty("proofPurpose")
|
||||
private String proofPurpose;
|
||||
|
||||
@JsonProperty("verificationMethod")
|
||||
private String verificationMethod;
|
||||
|
||||
@JsonProperty("created")
|
||||
private String created;
|
||||
|
||||
@JsonProperty("challenge")
|
||||
private String challenge;
|
||||
|
||||
@JsonProperty("domain")
|
||||
private String domain;
|
||||
|
||||
@JsonProperty("proofValue")
|
||||
private String proofValue;
|
||||
|
||||
public DataIntegrityProof() {
|
||||
}
|
||||
|
||||
public DataIntegrityProof(String type, String cryptosuite, String proofPurpose,
|
||||
String verificationMethod, String created, String challenge,
|
||||
String domain, String proofValue) {
|
||||
this.type = type;
|
||||
this.cryptosuite = cryptosuite;
|
||||
this.proofPurpose = proofPurpose;
|
||||
this.verificationMethod = verificationMethod;
|
||||
this.created = created;
|
||||
this.challenge = challenge;
|
||||
this.domain = domain;
|
||||
this.proofValue = proofValue;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public DataIntegrityProof setType(String type) {
|
||||
this.type = type;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getCryptosuite() {
|
||||
return cryptosuite;
|
||||
}
|
||||
|
||||
public DataIntegrityProof setCryptosuite(String cryptosuite) {
|
||||
this.cryptosuite = cryptosuite;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getProofPurpose() {
|
||||
return proofPurpose;
|
||||
}
|
||||
|
||||
public DataIntegrityProof setProofPurpose(String proofPurpose) {
|
||||
this.proofPurpose = proofPurpose;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getVerificationMethod() {
|
||||
return verificationMethod;
|
||||
}
|
||||
|
||||
public DataIntegrityProof setVerificationMethod(String verificationMethod) {
|
||||
this.verificationMethod = verificationMethod;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getCreated() {
|
||||
return created;
|
||||
}
|
||||
|
||||
public DataIntegrityProof setCreated(String created) {
|
||||
this.created = created;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getChallenge() {
|
||||
return challenge;
|
||||
}
|
||||
|
||||
public DataIntegrityProof setChallenge(String challenge) {
|
||||
this.challenge = challenge;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getDomain() {
|
||||
return domain;
|
||||
}
|
||||
|
||||
public DataIntegrityProof setDomain(String domain) {
|
||||
this.domain = domain;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getProofValue() {
|
||||
return proofValue;
|
||||
}
|
||||
|
||||
public DataIntegrityProof setProofValue(String proofValue) {
|
||||
this.proofValue = proofValue;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
/*
|
||||
* 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.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
/**
|
||||
* JWT Proof for Credential Request in OID4VCI (Section 8.2.1.1).
|
||||
* Represents a signed JWT for holder binding.
|
||||
*
|
||||
* @author <a href="https://github.com/wistefan">Stefan Wiedemann</a>
|
||||
* @see <a href="https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-credential-request">OID4VCI Credential Request</a>
|
||||
*/
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class JwtProof implements Proof {
|
||||
|
||||
@JsonProperty("proof_type")
|
||||
private final String proofType = ProofType.JWT;
|
||||
|
||||
@JsonProperty("jwt")
|
||||
private String jwt;
|
||||
|
||||
public JwtProof() {
|
||||
}
|
||||
|
||||
public JwtProof(String jwt) {
|
||||
this.jwt = jwt;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getProofType() {
|
||||
return proofType;
|
||||
}
|
||||
|
||||
public String getJwt() {
|
||||
return jwt;
|
||||
}
|
||||
|
||||
public JwtProof setJwt(String jwt) {
|
||||
this.jwt = jwt;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
/*
|
||||
* 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.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
/**
|
||||
* LDP-VP Proof for Credential Request in OID4VCI (Section 8.2.1.1).
|
||||
* Represents a JSON-LD Verifiable Presentation for holder binding.
|
||||
*
|
||||
* @see <a href="https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-credential-request">OID4VCI Credential Request</a>
|
||||
*/
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class LdpVpProof implements Proof {
|
||||
|
||||
@JsonProperty("proof_type")
|
||||
private final String proofType = ProofType.LD_PROOF;
|
||||
|
||||
@JsonProperty("ldp_vp")
|
||||
private Object ldpVp;
|
||||
|
||||
public LdpVpProof() {
|
||||
}
|
||||
|
||||
public LdpVpProof(Object ldpVp) {
|
||||
this.ldpVp = ldpVp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getProofType() {
|
||||
return proofType;
|
||||
}
|
||||
|
||||
public Object getLdpVp() {
|
||||
return ldpVp;
|
||||
}
|
||||
|
||||
public LdpVpProof setLdpVp(Object ldpVp) {
|
||||
this.ldpVp = ldpVp;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
/*
|
||||
* 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.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
/**
|
||||
* Interface for proof types in OID4VCI Credential Request (Section 8.2.1.1).
|
||||
*
|
||||
* @see <a href="https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-credential-request">OID4VCI Credential Request</a>
|
||||
*/
|
||||
public interface Proof {
|
||||
@JsonProperty("proof_type")
|
||||
String getProofType();
|
||||
}
|
||||
@@ -26,6 +26,6 @@ package org.keycloak.protocol.oid4vc.model;
|
||||
public final class ProofType {
|
||||
|
||||
public static final String JWT = "jwt";
|
||||
public static final String LD_PROOF = "ldp_vp";
|
||||
public static final String DI_PROOF = "di_vp";
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2024 Red Hat, Inc. and/or its affiliates
|
||||
* Copyright 2025 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");
|
||||
@@ -19,10 +19,10 @@ package org.keycloak.protocol.oid4vc.model;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
|
||||
/**
|
||||
* See: https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-ldp_vp-proof-type
|
||||
* See: https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-di_vp-proof-type
|
||||
*
|
||||
* @author <a href="mailto:francis.pouatcha@adorsys.com">Francis Pouatcha</a>
|
||||
*/
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class ProofTypeLdpVp {
|
||||
public class ProofTypeDiVp {
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright 2025 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.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Proofs object for Credential Request in OID4VCI (Section 8.2).
|
||||
* Contains arrays of different proof types (jwt, di_vp, attestation).
|
||||
*
|
||||
* @see <a href="https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-16.html#name-credential-request">OID4VCI Credential Request</a>
|
||||
*/
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class Proofs {
|
||||
|
||||
@JsonProperty("jwt")
|
||||
private List<String> jwt;
|
||||
|
||||
@JsonProperty("di_vp")
|
||||
private List<DiVpProof> diVp;
|
||||
|
||||
@JsonProperty("attestation")
|
||||
private List<String> attestation;
|
||||
|
||||
public Proofs() {
|
||||
}
|
||||
|
||||
public Proofs(List<String> jwt, List<DiVpProof> diVp, List<String> attestation) {
|
||||
this.jwt = jwt;
|
||||
this.diVp = diVp;
|
||||
this.attestation = attestation;
|
||||
}
|
||||
|
||||
public List<String> getJwt() {
|
||||
return jwt;
|
||||
}
|
||||
|
||||
public Proofs setJwt(List<String> jwt) {
|
||||
this.jwt = jwt;
|
||||
return this;
|
||||
}
|
||||
|
||||
public List<DiVpProof> getDiVp() {
|
||||
return diVp;
|
||||
}
|
||||
|
||||
public Proofs setDiVp(List<DiVpProof> diVp) {
|
||||
this.diVp = diVp;
|
||||
return this;
|
||||
}
|
||||
|
||||
public List<String> getAttestation() {
|
||||
return attestation;
|
||||
}
|
||||
|
||||
public Proofs setAttestation(List<String> attestation) {
|
||||
this.attestation = attestation;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,9 @@ import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.Test;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -27,10 +30,12 @@ import static org.junit.Assert.assertEquals;
|
||||
*/
|
||||
public class ProofSerializationTest {
|
||||
@Test
|
||||
public void testSerializeProof() throws JsonProcessingException {
|
||||
public void testSerializeProofs() throws JsonProcessingException {
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
String proofStr = " { \"proof_type\": \"jwt\", \"jwt\": \"ewogICJhbGciOiAiRVMyNTYiLAogICJ0eXAiOiAib3BlbmlkNHZjaS1wcm9vZitqd3QiLAogICJqd2siOiB7CiAgICAia3R5IjogIkVDIiwKICAgICJjcnYiOiAiUC0yNTYiLAogICAgIngiOiAiWEdkNU9GU1pwc080VkRRTUZrR3Z0TDVHU2FXWWE3SzBrNGhxUUdLbFBjWSIsCiAgICAieSI6ICJiSXFDaGhoVDdfdnYtYmhuRmVuREljVzVSUjRKTS1nME5sUi1qZGlHemNFIgogIH0KfQo.ewogICJpc3MiOiAib2lkNHZjaS1jbGllbnQiLAogICJhdWQiOiAiaHR0cDovL2xvY2FsaG9zdDo4MDgwL3JlYWxtcy9tYXN0ZXIiLAogICJpYXQiOiAxNzE4OTU5MzY5LAogICJub25jZSI6ICJOODAxTEpVam1qQ1FDMUpzTm5lTllXWFpqZHQ2UEZSd01pNkpoTTU1OF9JIgp9Cg.mKKrkRkG1BfOzgsKwcZhop74EHflzHslO_NFOloKPnZ-ms6t0SnsTNDQjM_o4FBQAgtv_fnFEWRgnkNIa34gvQ\" } ";
|
||||
Proof proof = objectMapper.readValue(proofStr, JwtProof.class);
|
||||
assertEquals(ProofType.JWT, proof.getProofType());
|
||||
String proofsStr = " { \"jwt\": [\"ewogICJhbGciOiAiRVMyNTYiLAogICJ0eXAiOiAib3BlbmlkNHZjaS1wcm9vZitqd3QiLAogICJqd2siOiB7CiAgICAia3R5IjogIkVDIiwKICAgICJjcnYiOiAiUC0yNTYiLAogICAgIngiOiAiWEdkNU9GU1pwc080VkRRTUZrR3Z0TDVHU2FXWWE3SzBrNGhxUUdLbFBjWSIsCiAgICAieSI6ICJiSXFDaGhoVDdfdnYtYmhuRmVuREljVzVSUjRKTS1nME5sUi1qZGlHemNFIgogIH0KfQo.ewogICJpc3MiOiAib2lkNHZjaS1jbGllbnQiLAogICJhdWQiOiAiaHR0cDovL2xvY2FsaG9zdDo4MDgwL3JlYWxtcy9tYXN0ZXIiLAogICJpYXQiOiAxNzE4OTU5MzY5LAogICJub25jZSI6ICJOODAxTEpVam1qQ1FDMUpzTm5lTllXWFpqZHQ2UEZSd01pNkpoTTU1OF9JIgp9Cg.mKKrkRkG1BfOzgsKwcZhop74EHflzHslO_NFOloKPnZ-ms6t0SnsTNDQjM_o4FBQAgtv_fnFEWRgnkNIa34gvQ\"] } ";
|
||||
Proofs proofs = objectMapper.readValue(proofsStr, Proofs.class);
|
||||
assertNotNull("Proofs should not be null", proofs);
|
||||
assertNotNull("JWT proofs should not be null", proofs.getJwt());
|
||||
assertEquals("Should have one JWT proof", 1, proofs.getJwt().size());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,12 +51,11 @@ import org.keycloak.protocol.oid4vc.model.Claim;
|
||||
import org.keycloak.protocol.oid4vc.model.Claims;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialIssuer;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialOfferURI;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialRequest;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialResponse;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialsOffer;
|
||||
import org.keycloak.protocol.oid4vc.model.Format;
|
||||
import org.keycloak.protocol.oid4vc.model.JwtProof;
|
||||
import org.keycloak.protocol.oid4vc.model.Proof;
|
||||
import org.keycloak.protocol.oid4vc.model.Proofs;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialRequest;
|
||||
import org.keycloak.protocol.oid4vc.model.SupportedCredentialConfiguration;
|
||||
import org.keycloak.protocol.oidc.grants.PreAuthorizedCodeGrantTypeFactory;
|
||||
import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation;
|
||||
@@ -116,8 +115,8 @@ public class OID4VCSdJwtIssuingEndpointTest extends OID4VCIssuerEndpointTest {
|
||||
testingClient
|
||||
.server(TEST_REALM_NAME)
|
||||
.run((session -> {
|
||||
JwtProof proof = new JwtProof()
|
||||
.setJwt(generateJwtProof(getCredentialIssuer(session), cNonce));
|
||||
Proofs proof = new Proofs()
|
||||
.setJwt(List.of(generateJwtProof(getCredentialIssuer(session), cNonce)));
|
||||
|
||||
ClientScopeRepresentation clientScope = fromJsonString(clientScopeString,
|
||||
ClientScopeRepresentation.class);
|
||||
@@ -137,8 +136,8 @@ public class OID4VCSdJwtIssuingEndpointTest extends OID4VCIssuerEndpointTest {
|
||||
try {
|
||||
withCausePropagation(() -> {
|
||||
testingClient.server(TEST_REALM_NAME).run((session -> {
|
||||
JwtProof proof = new JwtProof()
|
||||
.setJwt(generateInvalidJwtProof(getCredentialIssuer(session), cNonce));
|
||||
Proofs proof = new Proofs()
|
||||
.setJwt(List.of(generateInvalidJwtProof(getCredentialIssuer(session), cNonce)));
|
||||
|
||||
ClientScopeRepresentation clientScope = fromJsonString(clientScopeString,
|
||||
ClientScopeRepresentation.class);
|
||||
@@ -148,7 +147,7 @@ public class OID4VCSdJwtIssuingEndpointTest extends OID4VCIssuerEndpointTest {
|
||||
});
|
||||
Assert.fail("Should have thrown an exception");
|
||||
} catch (BadRequestException ex) {
|
||||
Assert.assertEquals("Could not validate provided proof", ex.getMessage());
|
||||
Assert.assertEquals("Could not validate provided jwt proof", ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,7 +166,7 @@ public class OID4VCSdJwtIssuingEndpointTest extends OID4VCIssuerEndpointTest {
|
||||
String cNonce = cNonceHandler.buildCNonce(null,
|
||||
Map.of(JwtCNonceHandler.SOURCE_ENDPOINT,
|
||||
nonceEndpoint));
|
||||
Proof proof = new JwtProof().setJwt(generateJwtProof(getCredentialIssuer(session), cNonce));
|
||||
Proofs proof = new Proofs().setJwt(List.of(generateJwtProof(getCredentialIssuer(session), cNonce)));
|
||||
|
||||
ClientScopeRepresentation clientScope = fromJsonString(clientScopeString,
|
||||
ClientScopeRepresentation.class);
|
||||
@@ -197,7 +196,7 @@ public class OID4VCSdJwtIssuingEndpointTest extends OID4VCIssuerEndpointTest {
|
||||
OID4VCIssuerWellKnownProvider.getCredentialsEndpoint(session.getContext());
|
||||
// creates a cNonce with missing data
|
||||
String cNonce = cNonceHandler.buildCNonce(List.of(credentialsEndpoint), null);
|
||||
Proof proof = new JwtProof().setJwt(generateJwtProof(getCredentialIssuer(session), cNonce));
|
||||
Proofs proof = new Proofs().setJwt(List.of(generateJwtProof(getCredentialIssuer(session), cNonce)));
|
||||
|
||||
ClientScopeRepresentation clientScope = fromJsonString(clientScopeString,
|
||||
ClientScopeRepresentation.class);
|
||||
@@ -231,7 +230,7 @@ public class OID4VCSdJwtIssuingEndpointTest extends OID4VCIssuerEndpointTest {
|
||||
session.getContext().getRealm().setAttribute(Oid4VciConstants.C_NONCE_LIFETIME_IN_SECONDS, -1);
|
||||
String cNonce = cNonceHandler.buildCNonce(List.of(credentialsEndpoint),
|
||||
Map.of(JwtCNonceHandler.SOURCE_ENDPOINT, nonceEndpoint));
|
||||
Proof proof = new JwtProof().setJwt(generateJwtProof(getCredentialIssuer(session), cNonce));
|
||||
Proofs proof = new Proofs().setJwt(List.of(generateJwtProof(getCredentialIssuer(session), cNonce)));
|
||||
|
||||
ClientScopeRepresentation clientScope = fromJsonString(clientScopeString,
|
||||
ClientScopeRepresentation.class);
|
||||
@@ -254,7 +253,7 @@ public class OID4VCSdJwtIssuingEndpointTest extends OID4VCIssuerEndpointTest {
|
||||
}
|
||||
|
||||
private static SdJwtVP testRequestTestCredential(KeycloakSession session, ClientScopeRepresentation clientScope,
|
||||
String token, Proof proof)
|
||||
String token, Proofs proof)
|
||||
throws VerificationException {
|
||||
|
||||
AppAuthManager.BearerTokenAuthenticator authenticator = new AppAuthManager.BearerTokenAuthenticator(session);
|
||||
@@ -264,7 +263,7 @@ public class OID4VCSdJwtIssuingEndpointTest extends OID4VCIssuerEndpointTest {
|
||||
final String credentialConfigurationId = clientScope.getAttributes().get(CredentialScopeModel.CONFIGURATION_ID);
|
||||
CredentialRequest credentialRequest = new CredentialRequest()
|
||||
.setCredentialConfigurationId(credentialConfigurationId)
|
||||
.setProof(proof);
|
||||
.setProofs(proof);
|
||||
|
||||
Response credentialResponse = issuerEndpoint.requestCredential(credentialRequest);
|
||||
assertEquals("The credential request should be answered successfully.",
|
||||
|
||||
Reference in New Issue
Block a user