mirror of
https://github.com/keycloak/keycloak.git
synced 2025-12-16 20:15:46 -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:
@@ -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;
|
||||
@@ -212,26 +214,29 @@ public abstract class OID4VCIssuerEndpointTest extends OID4VCTest {
|
||||
|
||||
// Register the optional client scopes
|
||||
sdJwtTypeCredentialClientScope = registerOptionalClientScope(sdJwtTypeCredentialScopeName,
|
||||
null,
|
||||
sdJwtTypeCredentialConfigurationIdName,
|
||||
sdJwtTypeCredentialScopeName,
|
||||
sdJwtCredentialVct,
|
||||
Format.SD_JWT_VC,
|
||||
null);
|
||||
null,
|
||||
sdJwtTypeCredentialConfigurationIdName,
|
||||
sdJwtTypeCredentialScopeName,
|
||||
sdJwtCredentialVct,
|
||||
Format.SD_JWT_VC,
|
||||
null,
|
||||
List.of(KeyAttestationResistanceLevels.HIGH,
|
||||
KeyAttestationResistanceLevels.MODERATE));
|
||||
jwtTypeCredentialClientScope = registerOptionalClientScope(jwtTypeCredentialScopeName,
|
||||
TEST_DID.toString(),
|
||||
jwtTypeCredentialConfigurationIdName,
|
||||
jwtTypeCredentialScopeName,
|
||||
null,
|
||||
Format.JWT_VC,
|
||||
TEST_CREDENTIAL_MAPPERS_FILE);
|
||||
TEST_DID.toString(),
|
||||
jwtTypeCredentialConfigurationIdName,
|
||||
jwtTypeCredentialScopeName,
|
||||
null,
|
||||
Format.JWT_VC,
|
||||
TEST_CREDENTIAL_MAPPERS_FILE,
|
||||
Collections.emptyList());
|
||||
minimalJwtTypeCredentialClientScope = registerOptionalClientScope("vc-with-minimal-config",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
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,10 +538,31 @@ 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,
|
||||
List.of(Algorithm.RS256));
|
||||
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