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:
forkimenjeckayang
2025-09-03 15:33:43 +01:00
committed by GitHub
parent fb35439479
commit fc73537ba7
13 changed files with 383 additions and 217 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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.",