mirror of
https://github.com/keycloak/keycloak.git
synced 2025-12-17 20:44:50 -06:00
[OID4VCI] Handle key_attestation_required in metadata endpoint (#44471)
fixes #43801 Signed-off-by: Pascal Knüppel <pascal.knueppel@governikus.de> Signed-off-by: Pascal Knüppel <captain.p.goldfish@gmx.de> Signed-off-by: Captain-P-Goldfish <captain.p.goldfish@gmx.de> Co-authored-by: Ingrid Kamga <xingridkamga@gmail.com>
This commit is contained in:
@@ -50,4 +50,35 @@ public class OID4VCConstants {
|
||||
|
||||
private OID4VCConstants() {
|
||||
}
|
||||
|
||||
/**
|
||||
* from the OID4VCI specification:
|
||||
*
|
||||
* <pre>
|
||||
* Appendix D.2. Attack Potential Resistance
|
||||
*
|
||||
* This specification defines the following values for key_storage and user_authentication:
|
||||
* iso_18045_high: It MUST be used when key storage or user authentication is resistant to attack with attack
|
||||
* potential "High", equivalent to VAN.5 according to [ISO.18045].
|
||||
* iso_18045_moderate: It MUST be used when key storage or user authentication is resistant to attack with attack
|
||||
* potential "Moderate", equivalent to VAN.4 according to [ISO.18045]. iso_18045_enhanced-basic: It MUST be used
|
||||
* when key storage or user authentication is resistant to attack with attack potential "Enhanced-Basic",
|
||||
* equivalent to VAN.3 according to [ISO.18045]. iso_18045_basic: It MUST be used when key storage or user
|
||||
* authentication is resistant to attack with attack potential "Basic", equivalent to VAN.2 according to
|
||||
* [ISO.18045]. Specifications that extend this list MUST choose collision-resistant values.
|
||||
* </pre>
|
||||
* <p>
|
||||
* this tells us that the KeyAttestationResistance is potentially extendable, and must therefore be handled with
|
||||
* strings
|
||||
*/
|
||||
public static class KeyAttestationResistanceLevels {
|
||||
|
||||
public static final String HIGH = "iso_18045_high"; // VAN.5
|
||||
|
||||
public static final String MODERATE = "iso_18045_moderate"; // VAN.4
|
||||
|
||||
public static final String ENHANCED_BASIC = "iso_18045_enhanced-basic"; // VAN.3
|
||||
|
||||
public static final String BASIC = "iso_18045_basic"; // VAN.2
|
||||
}
|
||||
}
|
||||
|
||||
@@ -407,6 +407,23 @@ _Default_: `31536000` (one year)
|
||||
| optional
|
||||
| If this claim should be listed in the credentials metadata. +
|
||||
_Default_: `true` but depends on the mapper-type. Claims like `jti`, `nbf`, `exp`, etc. are set to `false` by default.
|
||||
|
||||
| `vc.key_attestations_required`
|
||||
| optional
|
||||
| Indicates whether issuing this credential requires a key attestation. +
|
||||
_Default_: `false`.
|
||||
|
||||
| `vc.key_attestations_required.key_storage`
|
||||
| optional
|
||||
| Comma separated list of accepted key-storage attack potential levels (see ISO 18045 levels, e.g. `iso_18045_high`). +
|
||||
Only relevant if `vc.key_attestations_required` is present. +
|
||||
_Default_: none
|
||||
|
||||
| `vc.key_attestations_required.user_authentication`
|
||||
| optional
|
||||
| Comma separated list of accepted user-authentication attack potential levels (see ISO 18045 levels). +
|
||||
Only relevant if `vc.key_attestations_required` is present. +
|
||||
_Default_: none
|
||||
|===
|
||||
|
||||
==== Attribute Breakdown - ProtocolMappers
|
||||
|
||||
@@ -114,11 +114,30 @@ public class CredentialScopeModel implements ClientScopeModel {
|
||||
public static final String TOKEN_JWS_TYPE = "vc.credential_build_config.token_jws_type";
|
||||
|
||||
/**
|
||||
* this configuration property can be used to enforce specific claims to be included in the metadata, if they
|
||||
* would normally not and vice versa
|
||||
* this configuration property can be used to enforce specific claims to be included in the metadata, if they would
|
||||
* normally not and vice versa
|
||||
*/
|
||||
public static final String INCLUDE_IN_METADATA = "vc.include_in_metadata";
|
||||
|
||||
/**
|
||||
* OPTIONAL. Object that describes the requirement for key attestations as described in Appendix D, which the
|
||||
* Credential Issuer expects the Wallet to send within the proof(s) of the Credential Request. If the Credential
|
||||
* Issuer does not require a key attestation, this parameter MUST NOT be present in the metadata. If both
|
||||
* key_storage and user_authentication parameters are absent, the key_attestations_required parameter may be empty,
|
||||
* indicating a key attestation is needed without additional constraints.
|
||||
*/
|
||||
public static final String KEY_ATTESTATION_REQUIRED = "vc.key_attestations_required";
|
||||
|
||||
/**
|
||||
* OPTIONAL. A non-empty array defining values specified in Appendix D.2 accepted by the Credential Issuer.
|
||||
*/
|
||||
public static final String KEY_ATTESTATION_REQUIRED_KEY_STORAGE = "vc.key_attestations_required.key_storage";
|
||||
|
||||
/**
|
||||
* OPTIONAL. A non-empty array defining values specified in Appendix D.2 accepted by the Credential Issuer.
|
||||
*/
|
||||
public static final String KEY_ATTESTATION_REQUIRED_USER_AUTH = "vc.key_attestations_required.user_authentication";
|
||||
|
||||
|
||||
/**
|
||||
* the actual object that is represented by this scope
|
||||
@@ -307,6 +326,46 @@ public class CredentialScopeModel implements ClientScopeModel {
|
||||
clientScope.setAttribute(VC_DISPLAY, vcDisplay);
|
||||
}
|
||||
|
||||
public boolean isKeyAttestationRequired() {
|
||||
return Optional.ofNullable(clientScope.getAttribute(KEY_ATTESTATION_REQUIRED))
|
||||
.map(Boolean::parseBoolean)
|
||||
.orElse(false);
|
||||
}
|
||||
|
||||
public void setKeyAttestationRequired(boolean keyAttestationRequired) {
|
||||
clientScope.setAttribute(KEY_ATTESTATION_REQUIRED, String.valueOf(keyAttestationRequired));
|
||||
}
|
||||
|
||||
public List<String> getRequiredKeyAttestationKeyStorage() {
|
||||
return Optional.ofNullable(clientScope.getAttribute(KEY_ATTESTATION_REQUIRED_KEY_STORAGE))
|
||||
.map(s -> Arrays.asList(s.split(",")))
|
||||
// it is important to return null here instead of an empty list:
|
||||
// If both key_storage and user_authentication parameters are absent, the
|
||||
// key_attestations_required parameter may be empty, indicating a key attestation is needed
|
||||
// without additional constraints. Meaning we must not add empty objects to the metadata endpoint
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
public void setRequiredKeyAttestationKeyStorage(List<String> keyStorage) {
|
||||
clientScope.setAttribute(KEY_ATTESTATION_REQUIRED_KEY_STORAGE,
|
||||
Optional.ofNullable(keyStorage).map(list -> String.join(",")).orElse(null));
|
||||
}
|
||||
|
||||
public List<String> getRequiredKeyAttestationUserAuthentication() {
|
||||
return Optional.ofNullable(clientScope.getAttribute(KEY_ATTESTATION_REQUIRED_USER_AUTH))
|
||||
.map(s -> Arrays.asList(s.split(",")))
|
||||
// it is important to return null here instead of an empty list:
|
||||
// If both key_storage and user_authentication parameters are absent, the
|
||||
// key_attestations_required parameter may be empty, indicating a key attestation is needed
|
||||
// without additional constraints. Meaning we must not add empty objects to the metadata endpoint
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
public void getRequiredKeyAttestationUserAuthentication(List<String> userAuthentication) {
|
||||
clientScope.setAttribute(KEY_ATTESTATION_REQUIRED_USER_AUTH,
|
||||
Optional.ofNullable(userAuthentication).map(list -> String.join(",")).orElse(null));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return clientScope.getId();
|
||||
|
||||
@@ -41,7 +41,6 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.keycloak.common.VerificationException;
|
||||
import org.keycloak.crypto.KeyUse;
|
||||
@@ -61,7 +60,6 @@ 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.ErrorType;
|
||||
import org.keycloak.protocol.oid4vc.model.ISO18045ResistanceLevel;
|
||||
import org.keycloak.protocol.oid4vc.model.KeyAttestationJwtBody;
|
||||
import org.keycloak.protocol.oid4vc.model.KeyAttestationsRequired;
|
||||
import org.keycloak.protocol.oid4vc.model.SupportedProofTypeData;
|
||||
@@ -90,8 +88,8 @@ public class AttestationValidatorUtil {
|
||||
String attestationJwt,
|
||||
KeycloakSession keycloakSession,
|
||||
VCIssuanceContext vcIssuanceContext,
|
||||
AttestationKeyResolver keyResolver) throws IOException, JWSInputException,
|
||||
VerificationException{
|
||||
AttestationKeyResolver keyResolver)
|
||||
throws JWSInputException, VerificationException {
|
||||
|
||||
if (attestationJwt == null || attestationJwt.split("\\.").length != 3) {
|
||||
throw new VCIssuerException("Invalid JWT format");
|
||||
@@ -164,21 +162,7 @@ public class AttestationValidatorUtil {
|
||||
|
||||
// Get resistance level requirements from configuration
|
||||
KeyAttestationsRequired attestationRequirements = getAttestationRequirements(vcIssuanceContext);
|
||||
|
||||
// Validate key_storage if present in attestation and required by config
|
||||
if (attestationBody.getKeyStorage() != null) {
|
||||
validateResistanceLevel(
|
||||
attestationBody.getKeyStorage(),
|
||||
attestationRequirements != null ? attestationRequirements.getKeyStorage() : null,
|
||||
"key_storage");
|
||||
}
|
||||
// Validate user_authentication if present in attestation and required by config
|
||||
if (attestationBody.getUserAuthentication() != null) {
|
||||
validateResistanceLevel(
|
||||
attestationBody.getUserAuthentication(),
|
||||
attestationRequirements != null ? attestationRequirements.getUserAuthentication() : null,
|
||||
"user_authentication");
|
||||
}
|
||||
validateResistanceLevel(attestationBody, attestationRequirements);
|
||||
|
||||
KeycloakContext keycloakContext = keycloakSession.getContext();
|
||||
CNonceHandler cNonceHandler = keycloakSession.getProvider(CNonceHandler.class);
|
||||
@@ -230,39 +214,66 @@ public class AttestationValidatorUtil {
|
||||
return proofTypeData != null ? proofTypeData.getKeyAttestationsRequired() : null;
|
||||
}
|
||||
|
||||
private static void validateResistanceLevel(
|
||||
List<String> actualLevels,
|
||||
List<ISO18045ResistanceLevel> requiredLevels,
|
||||
String levelType) throws VCIssuerException {
|
||||
/**
|
||||
* validates the configured key_attestations_required attribute against the given attestationBody
|
||||
*
|
||||
* @param attestationBody the body to be validated
|
||||
* @param attestationRequirements the configuration object that is also displayed in the metadata endpoint
|
||||
*/
|
||||
private static void validateResistanceLevel(KeyAttestationJwtBody attestationBody,
|
||||
KeyAttestationsRequired attestationRequirements) {
|
||||
// if the KeyAttestationRequired object is null it is not necessary to validate it because the issuer does
|
||||
// not require it:
|
||||
// From the spec:
|
||||
// ----
|
||||
// If the Credential Issuer does not require a key attestation, this parameter MUST NOT be present in the
|
||||
// metadata.
|
||||
// ---
|
||||
// Meaning if the object is null we do not need to validate the resistance level
|
||||
if (attestationRequirements != null) {
|
||||
// Validate key_storage if present in attestation and required by config
|
||||
validateResistanceLevel(attestationBody.getKeyStorage(),
|
||||
attestationRequirements.getKeyStorage(),
|
||||
"key_storage");
|
||||
// Validate user_authentication if present in attestation and required by config
|
||||
validateResistanceLevel(attestationBody.getUserAuthentication(),
|
||||
attestationRequirements.getUserAuthentication(),
|
||||
"user_authentication");
|
||||
}
|
||||
}
|
||||
|
||||
if (requiredLevels == null || requiredLevels.isEmpty()) {
|
||||
for (String level : actualLevels) {
|
||||
try {
|
||||
ISO18045ResistanceLevel.fromValue(level);
|
||||
} catch (Exception e) {
|
||||
throw new VCIssuerException("Invalid " + levelType + " level: " + level);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Validates the given key_attestations (key_storage or user_authentication) against the current configuration as
|
||||
* provided by the metadata endpoint.
|
||||
*
|
||||
* @param providedLevels the attestation levels to be validated
|
||||
* @param acceptedLevels the attestation levels as exposed by the metadata endpoint
|
||||
* @param levelType either "key_storage" or "user_authentication"
|
||||
* @throws VCIssuerException if the required resistance level is not met
|
||||
*/
|
||||
private static void validateResistanceLevel(List<String> providedLevels,
|
||||
List<String> acceptedLevels,
|
||||
String levelType)
|
||||
throws VCIssuerException {
|
||||
|
||||
if (acceptedLevels == null || acceptedLevels.isEmpty()) {
|
||||
// We accept all provided levels
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert required levels to string values for comparison
|
||||
Set<String> requiredLevelValues = requiredLevels.stream()
|
||||
.map(ISO18045ResistanceLevel::getValue)
|
||||
.collect(Collectors.toSet());
|
||||
// If both key_storage and user_authentication parameters are absent, the key_attestations_required
|
||||
// parameter may be empty, indicating a key attestation is needed without additional constraints.
|
||||
// from: https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#section-12.2.4
|
||||
if (providedLevels == null || providedLevels.isEmpty()) {
|
||||
throw new VCIssuerException(levelType + " is required but was missing.");
|
||||
}
|
||||
|
||||
// Check each actual level against requirements
|
||||
for (String level : actualLevels) {
|
||||
try {
|
||||
ISO18045ResistanceLevel levelEnum = ISO18045ResistanceLevel.fromValue(level);
|
||||
if (!requiredLevelValues.contains(levelEnum.getValue())) {
|
||||
// Check each provided level against the accepted levels
|
||||
boolean foundMatch = providedLevels.stream().anyMatch(acceptedLevels::contains);
|
||||
if (!foundMatch) {
|
||||
throw new VCIssuerException(
|
||||
levelType + " level '" + level + "' is not accepted by credential issuer. " +
|
||||
"Allowed values: " + requiredLevelValues);
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new VCIssuerException("Invalid " + levelType + " level: " + level);
|
||||
}
|
||||
levelType + " none of the provided levels from '" + providedLevels + "' did match any of the " +
|
||||
"accepted levels: " + acceptedLevels);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
/*
|
||||
* 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.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonValue;
|
||||
|
||||
/**
|
||||
* Attack Potential Resistance. Defined values for `key_storage` and `user_authentication`
|
||||
* in {@link KeyAttestationsRequired} as per ISO 18045.
|
||||
*
|
||||
* @author <a href="mailto:Ingrid.Kamga@adorsys.com">Ingrid Kamga</a>
|
||||
* @see <a href="https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-15.html#name-attack-potential-resistance">
|
||||
* OpenID4VCI Attack Potential Resistance</a>
|
||||
*/
|
||||
public enum ISO18045ResistanceLevel {
|
||||
|
||||
HIGH("iso_18045_high"), // VAN.5
|
||||
MODERATE("iso_18045_moderate"), // VAN.4
|
||||
ENHANCED_BASIC("iso_18045_enhanced-basic"), // VAN.3
|
||||
BASIC("iso_18045_basic"); // VAN.2
|
||||
|
||||
private final String value;
|
||||
|
||||
ISO18045ResistanceLevel(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@JsonValue
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getValue();
|
||||
}
|
||||
|
||||
@JsonCreator
|
||||
public static ISO18045ResistanceLevel fromValue(String value) {
|
||||
for (ISO18045ResistanceLevel level : values()) {
|
||||
if (level.value.equals(value)) {
|
||||
return level;
|
||||
}
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("Unknown ISO18045ResistanceLevel: " + value);
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,8 @@ package org.keycloak.protocol.oid4vc.model;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.keycloak.models.oid4vci.CredentialScopeModel;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
@@ -34,10 +36,10 @@ import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
public class KeyAttestationsRequired {
|
||||
|
||||
@JsonProperty("key_storage")
|
||||
private List<ISO18045ResistanceLevel> keyStorage;
|
||||
private List<String> keyStorage;
|
||||
|
||||
@JsonProperty("user_authentication")
|
||||
private List<ISO18045ResistanceLevel> userAuthentication;
|
||||
private List<String> userAuthentication;
|
||||
|
||||
/**
|
||||
* Default constructor for Jackson deserialization
|
||||
@@ -46,20 +48,30 @@ public class KeyAttestationsRequired {
|
||||
// Default constructor for Jackson deserialization
|
||||
}
|
||||
|
||||
public List<ISO18045ResistanceLevel> getKeyStorage() {
|
||||
public static KeyAttestationsRequired parse(CredentialScopeModel credentialScope) {
|
||||
KeyAttestationsRequired keyAttestationsRequired = null;
|
||||
if (credentialScope.isKeyAttestationRequired()) {
|
||||
keyAttestationsRequired = new KeyAttestationsRequired();
|
||||
keyAttestationsRequired.setKeyStorage(credentialScope.getRequiredKeyAttestationKeyStorage());
|
||||
keyAttestationsRequired.setUserAuthentication(credentialScope.getRequiredKeyAttestationUserAuthentication());
|
||||
}
|
||||
return keyAttestationsRequired;
|
||||
}
|
||||
|
||||
public List<String> getKeyStorage() {
|
||||
return keyStorage;
|
||||
}
|
||||
|
||||
public KeyAttestationsRequired setKeyStorage(List<ISO18045ResistanceLevel> keyStorage) {
|
||||
public KeyAttestationsRequired setKeyStorage(List<String> keyStorage) {
|
||||
this.keyStorage = keyStorage;
|
||||
return this;
|
||||
}
|
||||
|
||||
public List<ISO18045ResistanceLevel> getUserAuthentication() {
|
||||
public List<String> getUserAuthentication() {
|
||||
return userAuthentication;
|
||||
}
|
||||
|
||||
public KeyAttestationsRequired setUserAuthentication(List<ISO18045ResistanceLevel> userAuthentication) {
|
||||
public KeyAttestationsRequired setUserAuthentication(List<String> userAuthentication) {
|
||||
this.userAuthentication = userAuthentication;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -41,11 +41,11 @@ public class ProofTypesSupported {
|
||||
protected Map<String, SupportedProofTypeData> supportedProofTypes = new HashMap<>();
|
||||
|
||||
public static ProofTypesSupported parse(KeycloakSession keycloakSession,
|
||||
KeyAttestationsRequired keyAttestationsRequired,
|
||||
List<String> globalSupportedSigningAlgorithms) {
|
||||
ProofTypesSupported proofTypesSupported = new ProofTypesSupported();
|
||||
keycloakSession.getAllProviders(ProofValidator.class).forEach(proofValidator -> {
|
||||
String type = proofValidator.getProofType();
|
||||
KeyAttestationsRequired keyAttestationsRequired = new KeyAttestationsRequired();
|
||||
SupportedProofTypeData supportedProofTypeData = new SupportedProofTypeData(globalSupportedSigningAlgorithms,
|
||||
keyAttestationsRequired);
|
||||
proofTypesSupported.getSupportedProofTypes().put(type, supportedProofTypeData);
|
||||
@@ -80,6 +80,11 @@ public class ProofTypesSupported {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return toJsonString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public final boolean equals(Object o) {
|
||||
if (!(o instanceof ProofTypesSupported that)) {
|
||||
|
||||
@@ -114,7 +114,9 @@ public class SupportedCredentialConfiguration {
|
||||
CredentialDefinition credentialDefinition = CredentialDefinition.parse(credentialScope);
|
||||
credentialConfiguration.setCredentialDefinition(credentialDefinition);
|
||||
|
||||
KeyAttestationsRequired keyAttestationsRequired = KeyAttestationsRequired.parse(credentialScope);
|
||||
ProofTypesSupported proofTypesSupported = ProofTypesSupported.parse(keycloakSession,
|
||||
keyAttestationsRequired,
|
||||
globalSupportedSigningAlgorithms);
|
||||
credentialConfiguration.setProofTypesSupported(proofTypesSupported);
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import java.util.List;
|
||||
|
||||
import jakarta.ws.rs.core.Response;
|
||||
|
||||
import org.keycloak.OID4VCConstants;
|
||||
import org.keycloak.TokenVerifier;
|
||||
import org.keycloak.common.VerificationException;
|
||||
import org.keycloak.constants.OID4VCIConstants;
|
||||
@@ -25,7 +26,6 @@ import org.keycloak.protocol.oid4vc.issuance.keybinding.AttestationValidatorUtil
|
||||
import org.keycloak.protocol.oid4vc.issuance.keybinding.ProofValidator;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialRequest;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialResponse;
|
||||
import org.keycloak.protocol.oid4vc.model.ISO18045ResistanceLevel;
|
||||
import org.keycloak.protocol.oid4vc.model.KeyAttestationJwtBody;
|
||||
import org.keycloak.protocol.oid4vc.model.Proofs;
|
||||
import org.keycloak.protocol.oid4vc.model.VerifiableCredential;
|
||||
@@ -271,8 +271,8 @@ public class OID4VCAttestationProofTest extends OID4VCIssuerEndpointTest {
|
||||
payload.setIat((long) TIME_PROVIDER.currentTimeSeconds());
|
||||
payload.setNonce(cNonce);
|
||||
payload.setAttestedKeys(List.of(proofJwk));
|
||||
payload.setKeyStorage(List.of(ISO18045ResistanceLevel.HIGH.getValue()));
|
||||
payload.setUserAuthentication(List.of(ISO18045ResistanceLevel.HIGH.getValue()));
|
||||
payload.setKeyStorage(List.of(OID4VCConstants.KeyAttestationResistanceLevels.HIGH));
|
||||
payload.setUserAuthentication(List.of(OID4VCConstants.KeyAttestationResistanceLevels.HIGH));
|
||||
|
||||
String attestationJwt = new JWSBuilder()
|
||||
.type(AttestationValidatorUtil.ATTESTATION_JWT_TYP)
|
||||
|
||||
@@ -28,6 +28,7 @@ import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.interfaces.RSAPublicKey;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -46,6 +47,7 @@ import jakarta.ws.rs.core.HttpHeaders;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.core.UriBuilder;
|
||||
|
||||
import org.keycloak.OID4VCConstants.KeyAttestationResistanceLevels;
|
||||
import org.keycloak.TokenVerifier;
|
||||
import org.keycloak.admin.client.resource.ClientResource;
|
||||
import org.keycloak.admin.client.resource.RealmResource;
|
||||
@@ -217,21 +219,24 @@ public abstract class OID4VCIssuerEndpointTest extends OID4VCTest {
|
||||
sdJwtTypeCredentialScopeName,
|
||||
sdJwtCredentialVct,
|
||||
Format.SD_JWT_VC,
|
||||
null);
|
||||
null,
|
||||
List.of(KeyAttestationResistanceLevels.HIGH,
|
||||
KeyAttestationResistanceLevels.MODERATE));
|
||||
jwtTypeCredentialClientScope = registerOptionalClientScope(jwtTypeCredentialScopeName,
|
||||
TEST_DID.toString(),
|
||||
jwtTypeCredentialConfigurationIdName,
|
||||
jwtTypeCredentialScopeName,
|
||||
null,
|
||||
Format.JWT_VC,
|
||||
TEST_CREDENTIAL_MAPPERS_FILE);
|
||||
TEST_CREDENTIAL_MAPPERS_FILE,
|
||||
Collections.emptyList());
|
||||
minimalJwtTypeCredentialClientScope = registerOptionalClientScope("vc-with-minimal-config",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null);
|
||||
null, null);
|
||||
|
||||
List.of(client, namedClient).forEach(client -> {
|
||||
String clientId = client.getClientId();
|
||||
@@ -267,7 +272,8 @@ public abstract class OID4VCIssuerEndpointTest extends OID4VCTest {
|
||||
String credentialIdentifier,
|
||||
String vct,
|
||||
String format,
|
||||
String protocolMapperReferenceFile) {
|
||||
String protocolMapperReferenceFile,
|
||||
List<String> acceptedKeyAttestationValues) {
|
||||
// Check if the client scope already exists
|
||||
List<ClientScopeRepresentation> existingScopes = testRealm().clientScopes().findAll();
|
||||
for (ClientScopeRepresentation existingScope : existingScopes) {
|
||||
@@ -305,6 +311,15 @@ public abstract class OID4VCIssuerEndpointTest extends OID4VCTest {
|
||||
}
|
||||
addAttribute.accept(CredentialScopeModel.VC_DISPLAY, vcDisplay);
|
||||
}
|
||||
if (acceptedKeyAttestationValues != null) {
|
||||
attributes.put(CredentialScopeModel.KEY_ATTESTATION_REQUIRED, "true");
|
||||
if (!acceptedKeyAttestationValues.isEmpty()) {
|
||||
attributes.put(CredentialScopeModel.KEY_ATTESTATION_REQUIRED_KEY_STORAGE,
|
||||
String.join(",", acceptedKeyAttestationValues));
|
||||
attributes.put(CredentialScopeModel.KEY_ATTESTATION_REQUIRED_USER_AUTH,
|
||||
String.join(",", acceptedKeyAttestationValues));
|
||||
}
|
||||
}
|
||||
clientScope.setAttributes(attributes);
|
||||
|
||||
Response res = testRealm().clientScopes().create(clientScope);
|
||||
|
||||
@@ -59,6 +59,7 @@ import org.keycloak.protocol.oid4vc.model.CredentialRequestEncryptionMetadata;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialResponseEncryptionMetadata;
|
||||
import org.keycloak.protocol.oid4vc.model.DisplayObject;
|
||||
import org.keycloak.protocol.oid4vc.model.Format;
|
||||
import org.keycloak.protocol.oid4vc.model.KeyAttestationsRequired;
|
||||
import org.keycloak.protocol.oid4vc.model.ProofTypesSupported;
|
||||
import org.keycloak.protocol.oid4vc.model.SupportedCredentialConfiguration;
|
||||
import org.keycloak.representations.idm.ClientScopeRepresentation;
|
||||
@@ -537,9 +538,30 @@ public class OID4VCIssuerWellKnownProviderTest extends OID4VCIssuerEndpointTest
|
||||
List<String> signingAlgsSupported = new ArrayList<>(supportedConfig.getCredentialSigningAlgValuesSupported());
|
||||
String proofTypesSupportedString = supportedConfig.getProofTypesSupported().toJsonString();
|
||||
|
||||
KeyAttestationsRequired expectedKeyAttestationsRequired = null;
|
||||
if (Boolean.parseBoolean(clientScope.getAttributes().get(CredentialScopeModel.KEY_ATTESTATION_REQUIRED))) {
|
||||
expectedKeyAttestationsRequired = new KeyAttestationsRequired();
|
||||
expectedKeyAttestationsRequired.setKeyStorage(
|
||||
Optional.ofNullable(clientScope.getAttributes()
|
||||
.get(CredentialScopeModel.KEY_ATTESTATION_REQUIRED_KEY_STORAGE))
|
||||
.map(s -> Arrays.asList(s.split(",")))
|
||||
.orElse(null));
|
||||
expectedKeyAttestationsRequired.setUserAuthentication(
|
||||
Optional.ofNullable(clientScope.getAttributes()
|
||||
.get(CredentialScopeModel.KEY_ATTESTATION_REQUIRED_USER_AUTH))
|
||||
.map(s -> Arrays.asList(s.split(",")))
|
||||
.orElse(null));
|
||||
}
|
||||
String expectedKeyAttestationsRequiredString = toJsonString(expectedKeyAttestationsRequired);
|
||||
|
||||
try {
|
||||
withCausePropagation(() -> testingClient.server(TEST_REALM_NAME).run((session -> {
|
||||
KeyAttestationsRequired keyAttestationsRequired = //
|
||||
Optional.ofNullable(expectedKeyAttestationsRequiredString)
|
||||
.map(s -> fromJsonString(s, KeyAttestationsRequired.class))
|
||||
.orElse(null);
|
||||
ProofTypesSupported expectedProofTypesSupported = ProofTypesSupported.parse(session,
|
||||
keyAttestationsRequired,
|
||||
List.of(Algorithm.RS256));
|
||||
assertEquals(expectedProofTypesSupported,
|
||||
ProofTypesSupported.fromJsonString(proofTypesSupportedString));
|
||||
|
||||
@@ -27,6 +27,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import javax.net.ssl.TrustManagerFactory;
|
||||
|
||||
import org.keycloak.OID4VCConstants.KeyAttestationResistanceLevels;
|
||||
import org.keycloak.common.util.CertificateUtils;
|
||||
import org.keycloak.crypto.ECDSASignatureSignerContext;
|
||||
import org.keycloak.crypto.KeyType;
|
||||
@@ -44,7 +45,6 @@ import org.keycloak.protocol.oid4vc.issuance.keybinding.AttestationValidatorUtil
|
||||
import org.keycloak.protocol.oid4vc.issuance.keybinding.JwtProofValidator;
|
||||
import org.keycloak.protocol.oid4vc.issuance.keybinding.ProofValidator;
|
||||
import org.keycloak.protocol.oid4vc.issuance.keybinding.StaticAttestationKeyResolver;
|
||||
import org.keycloak.protocol.oid4vc.model.ISO18045ResistanceLevel;
|
||||
import org.keycloak.protocol.oid4vc.model.KeyAttestationJwtBody;
|
||||
import org.keycloak.protocol.oid4vc.model.KeyAttestationsRequired;
|
||||
import org.keycloak.protocol.oid4vc.model.Proofs;
|
||||
@@ -210,12 +210,12 @@ public class OID4VCKeyAttestationTest extends OID4VCIssuerEndpointTest {
|
||||
payload.setNonce(cNonce);
|
||||
payload.setAttestedKeys(List.of(proofJwk));
|
||||
payload.setKeyStorage(List.of(
|
||||
ISO18045ResistanceLevel.HIGH.getValue(),
|
||||
ISO18045ResistanceLevel.MODERATE.getValue()
|
||||
KeyAttestationResistanceLevels.HIGH,
|
||||
KeyAttestationResistanceLevels.MODERATE
|
||||
));
|
||||
payload.setUserAuthentication(List.of(
|
||||
ISO18045ResistanceLevel.ENHANCED_BASIC.getValue(),
|
||||
ISO18045ResistanceLevel.BASIC.getValue()
|
||||
KeyAttestationResistanceLevels.ENHANCED_BASIC,
|
||||
KeyAttestationResistanceLevels.BASIC
|
||||
));
|
||||
|
||||
String attestationJwt = new JWSBuilder()
|
||||
@@ -228,13 +228,13 @@ public class OID4VCKeyAttestationTest extends OID4VCIssuerEndpointTest {
|
||||
// Set attestation requirements
|
||||
KeyAttestationsRequired attestationRequirements = new KeyAttestationsRequired();
|
||||
attestationRequirements.setKeyStorage(List.of(
|
||||
ISO18045ResistanceLevel.HIGH,
|
||||
ISO18045ResistanceLevel.MODERATE,
|
||||
ISO18045ResistanceLevel.ENHANCED_BASIC
|
||||
KeyAttestationResistanceLevels.HIGH,
|
||||
KeyAttestationResistanceLevels.MODERATE,
|
||||
KeyAttestationResistanceLevels.ENHANCED_BASIC
|
||||
));
|
||||
attestationRequirements.setUserAuthentication(List.of(
|
||||
ISO18045ResistanceLevel.BASIC,
|
||||
ISO18045ResistanceLevel.ENHANCED_BASIC
|
||||
KeyAttestationResistanceLevels.BASIC,
|
||||
KeyAttestationResistanceLevels.ENHANCED_BASIC
|
||||
));
|
||||
|
||||
vcIssuanceContext.getCredentialConfig()
|
||||
@@ -433,8 +433,8 @@ public class OID4VCKeyAttestationTest extends OID4VCIssuerEndpointTest {
|
||||
payload.setIat((long) TIME_PROVIDER.currentTimeSeconds());
|
||||
payload.setNonce(cNonce);
|
||||
payload.setAttestedKeys(List.of(proofJwk1, proofJwk2));
|
||||
payload.setKeyStorage(List.of(ISO18045ResistanceLevel.HIGH.getValue()));
|
||||
payload.setUserAuthentication(List.of(ISO18045ResistanceLevel.HIGH.getValue()));
|
||||
payload.setKeyStorage(List.of(KeyAttestationResistanceLevels.HIGH));
|
||||
payload.setUserAuthentication(List.of(KeyAttestationResistanceLevels.HIGH));
|
||||
|
||||
String attestationJwt = new JWSBuilder()
|
||||
.type(AttestationValidatorUtil.ATTESTATION_JWT_TYP)
|
||||
@@ -486,8 +486,8 @@ public class OID4VCKeyAttestationTest extends OID4VCIssuerEndpointTest {
|
||||
payload.setNonce(cNonce);
|
||||
payload.setIat((long) TIME_PROVIDER.currentTimeSeconds());
|
||||
payload.setAttestedKeys(List.of(proofJwk));
|
||||
payload.setKeyStorage(List.of(ISO18045ResistanceLevel.HIGH.getValue()));
|
||||
payload.setUserAuthentication(List.of(ISO18045ResistanceLevel.HIGH.getValue()));
|
||||
payload.setKeyStorage(List.of(KeyAttestationResistanceLevels.HIGH));
|
||||
payload.setUserAuthentication(List.of(KeyAttestationResistanceLevels.HIGH));
|
||||
|
||||
String attestationJwt = new JWSBuilder()
|
||||
.type(AttestationValidatorUtil.ATTESTATION_JWT_TYP)
|
||||
@@ -587,8 +587,9 @@ public class OID4VCKeyAttestationTest extends OID4VCIssuerEndpointTest {
|
||||
validator.validateProof(context);
|
||||
fail("Expected VCIssuerException for missing attested_keys");
|
||||
} catch (VCIssuerException e) {
|
||||
assertTrue("Expected error about missing keys but got: " + e.getMessage(),
|
||||
e.getMessage().contains("attested_keys"));
|
||||
assertEquals("Expected error about missing keys but got: " + e.getMessage(),
|
||||
"key_storage is required but was missing.",
|
||||
e.getMessage());
|
||||
} catch (Exception e) {
|
||||
fail("Unexpected exception: " + e.getMessage());
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.core.UriBuilder;
|
||||
|
||||
import org.keycloak.OID4VCConstants.KeyAttestationResistanceLevels;
|
||||
import org.keycloak.admin.client.resource.ClientScopeResource;
|
||||
import org.keycloak.admin.client.resource.ProtocolMappersResource;
|
||||
import org.keycloak.common.Profile;
|
||||
@@ -75,8 +76,8 @@ import org.keycloak.protocol.oid4vc.model.AuthorizationDetail;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialRequest;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialSubject;
|
||||
import org.keycloak.protocol.oid4vc.model.Format;
|
||||
import org.keycloak.protocol.oid4vc.model.ISO18045ResistanceLevel;
|
||||
import org.keycloak.protocol.oid4vc.model.KeyAttestationJwtBody;
|
||||
import org.keycloak.protocol.oid4vc.model.KeyAttestationsRequired;
|
||||
import org.keycloak.protocol.oid4vc.model.NonceResponse;
|
||||
import org.keycloak.protocol.oid4vc.model.ProofTypesSupported;
|
||||
import org.keycloak.protocol.oid4vc.model.SupportedCredentialConfiguration;
|
||||
@@ -652,8 +653,8 @@ public abstract class OID4VCTest extends AbstractTestRealmKeycloakTest {
|
||||
payload.setIat((long) TIME_PROVIDER.currentTimeSeconds());
|
||||
payload.setNonce(cNonce);
|
||||
payload.setAttestedKeys(proofJwks);
|
||||
payload.setKeyStorage(List.of(ISO18045ResistanceLevel.HIGH.getValue()));
|
||||
payload.setUserAuthentication(List.of(ISO18045ResistanceLevel.HIGH.getValue()));
|
||||
payload.setKeyStorage(List.of(KeyAttestationResistanceLevels.HIGH));
|
||||
payload.setUserAuthentication(List.of(KeyAttestationResistanceLevels.HIGH));
|
||||
|
||||
return new JWSBuilder()
|
||||
.type(AttestationValidatorUtil.ATTESTATION_JWT_TYP)
|
||||
@@ -667,11 +668,14 @@ public abstract class OID4VCTest extends AbstractTestRealmKeycloakTest {
|
||||
|
||||
protected static VCIssuanceContext createVCIssuanceContext(KeycloakSession session) {
|
||||
VCIssuanceContext context = new VCIssuanceContext();
|
||||
KeyAttestationsRequired keyAttestationsRequired = new KeyAttestationsRequired();
|
||||
keyAttestationsRequired.setKeyStorage(List.of(KeyAttestationResistanceLevels.HIGH,
|
||||
KeyAttestationResistanceLevels.MODERATE));
|
||||
SupportedCredentialConfiguration config = new SupportedCredentialConfiguration()
|
||||
.setFormat(Format.SD_JWT_VC)
|
||||
.setVct("https://credentials.example.com/test-credential")
|
||||
.setCryptographicBindingMethodsSupported(List.of("jwk"))
|
||||
.setProofTypesSupported(ProofTypesSupported.parse(session, List.of("ES256")));
|
||||
.setProofTypesSupported(ProofTypesSupported.parse(session, keyAttestationsRequired, List.of("ES256")));
|
||||
|
||||
context.setCredentialConfig(config)
|
||||
.setCredentialRequest(new CredentialRequest());
|
||||
@@ -718,8 +722,8 @@ public abstract class OID4VCTest extends AbstractTestRealmKeycloakTest {
|
||||
Map<String, Object> payload = new HashMap<>();
|
||||
payload.put("iat", TIME_PROVIDER.currentTimeSeconds());
|
||||
payload.put("attested_keys", List.of(proofJwk));
|
||||
payload.put("key_storage", List.of(ISO18045ResistanceLevel.HIGH.getValue()));
|
||||
payload.put("user_authentication", List.of(ISO18045ResistanceLevel.HIGH.getValue()));
|
||||
payload.put("key_storage", List.of(KeyAttestationResistanceLevels.HIGH));
|
||||
payload.put("user_authentication", List.of(KeyAttestationResistanceLevels.HIGH));
|
||||
payload.put("nonce", cNonce);
|
||||
|
||||
return payload;
|
||||
|
||||
Reference in New Issue
Block a user