8000 [OID4VCI-HAIP] Pass oid4vci-1_0-issuer-happy-flow by tdiesler · Pull Request #49221 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
import java.util.Base64;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import javax.security.auth.x500.X500Principal;

import org.keycloak.crypto.SignatureSignerContext;
import org.keycloak.models.KeycloakSession;
Expand Down Expand Up @@ -80,18 +80,18 @@ public String signCredential(CredentialBody credentialBody, CredentialBuildConfi
*/
private void addX5cHeader(SdJwtCredentialBody sdJwtCredentialBody, SignatureSignerContext signer) {
List<X509Certificate> certificateChain = signer.getCertificateChain();

if (certificateChain != null && !certificateChain.isEmpty()) {
List<String> x5cList = certificateChain.stream()
.filter(Objects::nonNull)
.filter(cert -> !isTrustAnchor(cert))
.map(cert -> {
try {
return Base64.getEncoder().encodeToString(cert.getEncoded());
} catch (CertificateEncodingException e) {
throw new RuntimeException(e);
}
})
.collect(Collectors.toList());
.toList();

if (!x5cList.isEmpty()) {
sdJwtCredentialBody.getIssuerSignedJWT().getJwsHeader().setX5c(x5cList);
Expand All @@ -102,4 +102,17 @@ private void addX5cHeader(SdJwtCredentialBody sdJwtCredentialBody, SignatureSign
LOGGER.debugf("No certificate or certificate chain available for x5c header in SD-JWT credential.");
}
}

private boolean isTrustAnchor(X509Certificate cert) {
boolean isTrustAnchor = false;
try {
int basicConstraints = cert.getBasicConstraints();
X500Principal issuerPrincipal = cert.getIssuerX500Principal();
X500Principal subjectPrincipal = cert.getSubjectX500Principal();
isTrustAnchor = subjectPrincipal.equals(issuerPrincipal) && basicConstraints >= 0;
} catch (Exception e) {
// ignore
}
return isTrustAnchor;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,10 @@
import org.keycloak.crypto.KeyWrapper;
import org.keycloak.events.EventType;
import org.keycloak.keys.KeyProvider;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeyManager;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.oid4vc.clientpolicy.CredentialClientPolicyExecutorFactory;
Expand Down Expand Up @@ -122,6 +124,7 @@
import static org.keycloak.OID4VCConstants.CLAIM_NAME_SUBJECT_ID;
import static org.keycloak.OID4VCConstants.OID4VCI_ENABLED_ATTRIBUTE_KEY;
import static org.keycloak.OID4VCConstants.OPENID_CREDENTIAL;
import static org.keycloak.authentication.authenticators.client.AttestationBasedClientAuthenticator.OAUTH_CLIENT_ATTESTATION_CONFIG_TRUST_IDPS;
import static org.keycloak.constants.OID4VCIConstants.CREDENTIAL_OFFER_CREATE;
import static org.keycloak.models.Constants.CREATE_DEFAULT_CLIENT_SCOPES;
import static org.keycloak.models.oid4vci.CredentialScopeModel.CRYPTOGRAPHIC_BINDING_METHODS_DEFAULT;
Expand Down Expand Up @@ -266,7 +269,32 @@ void afterEachBase() {
driver.open("about:blank");
}

public static KeyWrapper getKeyFromSession(KeycloakSession keycloakSession) {
protected static void configureTrustIdentityProvider(RealmModel realm, String alias, String providerId, Map<String, String> config) {
IdentityProviderModel trustIdp = realm.getIdentityProviderByAlias(alias);
if (trustIdp == null) {
trustIdp = new IdentityProviderModel();
trustIdp.setAlias(alias);
trustIdp.setProviderId(providerId);
trustIdp.setEnabled(true);
trustIdp.setConfig(config);
realm.addIdentityProvider(trustIdp);
} else {
trustIdp.setProviderId(providerId);
trustIdp.setEnabled(true);
trustIdp.setConfig(config);
realm.updateIdentityProvider(trustIdp);
}
}

protected void setClientTrustSource(String alias) {
Map<String, String> attributes = new HashMap<>(Optional.ofNullable(abcaClient.getAttributes()).orElse(Map.of()));
attributes.put(OAUTH_CLIENT_ATTESTATION_CONFIG_TRUST_IDPS, alias);
abcaClient.setAttributes(attributes);
testRealm.admin().clients().get(abcaClient.getId()).update(abcaClient);
abcaClient = testRealm.admin().clients().get(abcaClient.getId()).toRepresentation();
}

protected static KeyWrapper getKeyFromSession(KeycloakSession keycloakSession) {
String realmName = keycloakSession.getContext().getRealm().getName();
Logger logger = Logger.getLogger(OID4VCIssuerTestBase.class);
KeyManager keyManager = keycloakSession.keys();
Expand All @@ -280,7 +308,7 @@ public static KeyWrapper getKeyFromSession(KeycloakSession keycloakSession) {
return kw;
}

public static String getKeyIdFromSession(KeycloakSession keycloakSession) {
protected static String getKeyIdFromSession(KeycloakSession keycloakSession) {
return getKeyFromSession(keycloakSession).getKid();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,8 @@
package org.keycloak.tests.oid4vc.abca;

import java.security.PublicKey;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

import org.keycloak.TokenVerifier;
import org.keycloak.authentication.authenticators.client.AttestationBasedClientAuthenticator.ClientAttestationJwt;
Expand All @@ -32,7 +30,6 @@
import org.keycloak.jose.jwk.JSONWebKeySet;
import org.keycloak.jose.jwk.JWK;
import org.keycloak.jose.jwk.JWKBuilder;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.RealmModel;
import org.keycloak.protocol.oid4vc.model.CredentialResponse;
import org.keycloak.protocol.oid4vc.model.Proofs;
Expand All @@ -50,7 +47,6 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import static org.keycloak.authentication.authenticators.client.AttestationBasedClientAuthenticator.OAUTH_CLIENT_ATTESTATION_CONFIG_TRUST_IDPS;
import static org.keycloak.authentication.authenticators.client.AttestationBasedClientAuthenticator.OAUTH_CLIENT_ATTESTATION_HEADER;
import static org.keycloak.authentication.authenticators.client.AttestationBasedClientAuthenticator.OAUTH_CLIENT_ATTESTATION_POP_HEADER;
import static org.keycloak.protocol.oidc.OIDCLoginProtocol.ATTEST_JWT_CLIENT_AUTH;
Expand Down Expand Up @@ -89,7 +85,6 @@ void beforeEach() {
String jwks = attesterJwks;
runOnServer.run(session -> {
RealmModel realm = session.getContext().getRealm();

configureTrustIdentityProvider(realm, ATTESTER_DEFAULT_TRUST_IDP_ALIAS,
DefaultTrustIdentityProviderFactory.PROVIDER_ID,
Map.of(DefaultTrustIdentityProviderConfig.TRUSTED_JWKS, jwks));
Expand All @@ -98,31 +93,6 @@ void beforeEach() {
oauth.client(abcaClient.getClientId(), null);
}

private static void configureTrustIdentityProvider(RealmModel realm, String alias, String providerId, Map<String, String> config) {
IdentityProviderModel trustIdp = realm.getIdentityProviderByAlias(alias);
if (trustIdp == null) {
trustIdp = new IdentityProviderModel();
trustIdp.setAlias(alias);
trustIdp.setProviderId(providerId);
trustIdp.setEnabled(true);
trustIdp.setConfig(config);
realm.addIdentityProvider(trustIdp);
} else {
trustIdp.setProviderId(providerId);
trustIdp.setEnabled(true);
trustIdp.setConfig(config);
realm.updateIdentityProvider(trustIdp);
}
}

private void setClientTrustSource(String alias) {
Map<String, String> attributes = new HashMap<>(Optional.ofNullable(abcaClient.getAttributes()).orElse(Map.of()));
attributes.put(OAUTH_CLIENT_ATTESTATION_CONFIG_TRUST_IDPS, alias);
abcaClient.setAttributes(attributes);
testRealm.admin().clients().get(abcaClient.getId()).update(abcaClient);
abcaClient = testRealm.admin().clients().get(abcaClient.getId()).toRepresentation();
}

@Test
public void testTokenEndpointAuthMethods() {
OIDCConfigurationRepresentation oidcConfiguration = oauth.doWellKnownRequest();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
package org.keycloak.tests.oid4vc.haip;


import java.util.Map;
import java.util.stream.Collectors;

import org.keycloak.TokenVerifier;
import org.keycloak.broker.trust.DefaultTrustIdentityProviderConfig;
import org.keycloak.broker.trust.DefaultTrustIdentityProviderFactory;
import org.keycloak.crypto.KeyWrapper;
import org.keycloak.jose.jwk.JSONWebKeySet;
import org.keycloak.jose.jwk.JWK;
import org.keycloak.jose.jwk.JWKBuilder;
import org.keycloak.models.RealmModel;
import org.keycloak.protocol.oid4vc.model.CredentialResponse;
import org.keycloak.protocol.oid4vc.model.CredentialScopeRepresentation;
import org.keycloak.protocol.oid4vc.model.Proofs;
import org.keycloak.representations.JsonWebToken;
import org.keycloak.sdjwt.IssuerSignedJWT;
import org.keycloak.sdjwt.vp.SdJwtVP;
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
import org.keycloak.testframework.annotations.TestSetup;
import org.keycloak.tests.oid4vc.OID4VCIssuerTestBase;
import org.keycloak.tests.oid4vc.OID4VCTestContext;
import org.keycloak.tests.oid4vc.abca.OIDCClientAttester;
import org.keycloak.tests.oid4vc.abca.OIDCMockClientAttester;
import org.keycloak.testsuite.util.oauth.AccessTokenResponse;
import org.keycloak.testsuite.util.oauth.PkceGenerator;
import org.keycloak.util.JsonSerialization;
import org.keycloak.util.TokenUtil;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import static org.keycloak.OID4VCConstants.CLAIM_NAME_VCT;
import static org.keycloak.authentication.authenticators.client.AttestationBasedClientAuthenticator.OAUTH_CLIENT_ATTESTATION_HEADER;
import static org.keycloak.authentication.authenticators.client.AttestationBasedClientAuthenticator.OAUTH_CLIENT_ATTESTATION_POP_HEADER;
import static org.keycloak.tests.oid4vc.OID4VCProofTestUtils.createRsaKeyPair;
import static org.keycloak.tests.oid4vc.OID4VCTestContext.CLIENT_ATTESTER_ATTACHMENT_KEY;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

/**
* Mirrors oid4vci-1_0-issuer-haip-test-plan:oid4vci-1_0-issuer-happy-flow
*/
@KeycloakIntegrationTest(config = OID4VCIssuerTestBase.VCTestServerWithABCAEnabled.class)
public class HAIPIssuerConformanceTest extends OID4VCIssuerTestBase {

private static final String ATTESTER_DEFAULT_TRUST_IDP_ALIAS = "abca-attester-default-trust";

private static OIDCClientAttester attester;
private static String attesterJwks;

@TestSetup
public void configure() throws Exception {
var kw = createRsaKeyPair("openid-abca-attester-key");
JWK jwk = JWKBuilder.create()
.kid(kw.getKid())
.algorithm(kw.getAlgorithm())
.rsa(kw.getPublicKey(), kw.getCertificate());
JSONWebKeySet jwks = new JSONWebKeySet();
jwks.setKeys(new JWK[] { jwk });
attesterJwks = JsonSerialization.writeValueAsString(jwks);
attester = new OIDCMockClientAttester(kw);
}

@BeforeEach
void beforeEach() {
String jwks = attesterJwks;
runOnServer.run(session -> {
RealmModel realm = session.getContext().getRealm();

configureTrustIdentityProvider(realm, ATTESTER_DEFAULT_TRUST_IDP_ALIAS,
DefaultTrustIdentityProviderFactory.PROVIDER_ID,
Map.of(DefaultTrustIdentityProviderConfig.TRUSTED_JWKS, jwks));
});
setClientTrustSource(ATTESTER_DEFAULT_TRUST_IDP_ALIAS);
setClientPolicyEnabled(VCI_CLIENT_POLICY_HAIP, true);
oauth.client(abcaClient.getClientId(), null);
}

@Test
public void testIssuerHappyFlow() throws Exception {

var ctx = new OID4VCTestContext(abcaClient, sdJwtTypeCredentialScope);
ctx.putAttachment(CLIENT_ATTESTER_ATTACHMENT_KEY, attester);

var pkce = PkceGenerator.s256();

// Generate ABCA Headers
//
KeyWrapper rsaKey = wallet.getRSAKeyPair(ctx);
String attestationJwt = wallet.buildClientAttestationJWT(ctx, rsaKey);
String attestationPoPJwt = wallet.buildClientAttestationPoPJWT(ctx, rsaKey);

// Send PAR Request
//
String requestUri = oauth.pushedAuthorizationRequest()
.header(OAUTH_CLIENT_ATTESTATION_HEADER, attestationJwt)
.header(OAUTH_CLIENT_ATTESTATION_POP_HEADER, attestationPoPJwt)
.scopeParam(ctx.getScope())
.codeChallenge(pkce)
.send().getRequestUri();
assertNotNull(requestUri, "No requestUri");

// Send Authorization Request
//
String authCode = wallet.authorizationRequest()
.requestUri(requestUri)
.codeChallenge(pkce)
.send(ctx.getHolder(), TEST_PASSWORD)
.getCode();
assertNotNull(authCode, "No auth code");

// Send Token Request
//
KeyWrapper ecKey = wallet.getECKeyPair(ctx);
String tokenEndpoint = oauth.getEndpoints().getToken();
String dpopProof = wallet.generateSignedDPoPProof(tokenEndpoint, ecKey, null);

AccessTokenResponse tokenResponse = wallet.accessTokenRequest(ctx, authCode)
.header(OAUTH_CLIENT_ATTESTATION_HEADER, attestationJwt)
.header(OAUTH_CLIENT_ATTESTATION_POP_HEADER, attestationPoPJwt)
.codeVerifier(pkce)
.dpopProof(dpopProof)
.send();
String tokenType = tokenResponse.getTokenType();
String accessToken = tokenResponse.getAccessToken();
assertEquals(TokenUtil.TOKEN_TYPE_DPOP, tokenType);
assertNotNull(accessToken, "No access token");

String credentialIdentifier = ctx.getAuthorizedCredentialIdentifier();
assertNotNull(credentialIdentifier, "No authorized credential identifier");

// Send Nonce Request
//
String nonce = wallet.nonceRequest().send().getNonce();
Proofs jwtProof = wallet.generateJwtProof(ctx, ecKey, nonce);

// Send Credential Request
// Note, we use the same EC key for DPoP and Holder identity
//
String credentialEndpoint = oauth.getEndpoints().getOid4vcCredential();
dpopProof = wallet.generateSignedDPoPProof(credentialEndpoint, ecKey, accessToken);

CredentialResponse credResponse = wallet.credentialRequest(ctx, tokenType, accessToken)
.credentialIdentifier(credentialIdentifier)
.dpopProof(dpopProof)
.proofs(jwtProof)
.send().getCredentialResponse();

// Verify Credential Response
//
verifyCredentialResponse(ctx, credResponse);
}

private void verifyCredentialResponse(OID4VCTestContext ctx, CredentialResponse credResponse) throws Exception {

CredentialScopeRepresentation credScope = ctx.getCredentialScope();
String issuer = wallet.getIssuerMetadata(ctx).getCredentialIssuer();
CredentialResponse.Credential credObj = credResponse.getCredentials().get(0);
assertNotNull(credObj, "The first credential in the array should not be null");

SdJwtVP sdJwtVP = SdJwtVP.of(credObj.getCredential().toString());
IssuerSignedJWT issuerSignedJWT = sdJwtVP.getIssuerSignedJWT();
JsonWebToken vcSdJwt = TokenVerifier.create(issuerSignedJWT.getJws(), JsonWebToken.class).getToken();
Map<String, Object> otherClaims = vcSdJwt.getOtherClaims();
assertEquals(issuer, vcSdJwt.getIssuer());
assertEquals(credScope.getVct(), otherClaims.get(CLAIM_NAME_VCT));

Map<String, String> claims = sdJwtVP.getClaims().values().stream().collect(Collectors.toMap(
arrayNode -> arrayNode.get(1).asText(),
arrayNode -> arrayNode.get(2).asText()
));
assertEquals("Alice", claims.get("firstName"));
assertEquals("Wonderland", claims.get("lastName"));
assertEquals("alice@email.cz", claims.get("email"));
}
}
Loading
0