Skip to content

[OID4VCI-HAIP] Pass oid4vci-1_0-issuer-happy-flow#47405

Closed
tdiesler wants to merge 2 commits intokeycloak:mainfrom
tdiesler:ghi47316
Closed

[OID4VCI-HAIP] Pass oid4vci-1_0-issuer-happy-flow#47405
tdiesler wants to merge 2 commits intokeycloak:mainfrom
tdiesler:ghi47316

Conversation

@tdiesler
Copy link
Copy Markdown
Contributor

@tdiesler tdiesler commented Mar 24, 2026

@tdiesler
Copy link
Copy Markdown
Contributor Author

The list of dependencies is quite long. Next merge candidates are #47296 then #47150

@tdiesler tdiesler force-pushed the ghi47316 branch 15 times, most recently from 4ca0091 to 12ed169 Compare March 26, 2026 06:09
@tdiesler tdiesler marked this pull request as draft March 26, 2026 06:14
@tdiesler tdiesler force-pushed the ghi47316 branch 3 times, most recently from 309b796 to 38889bd Compare March 26, 2026 09:08
@tdiesler tdiesler force-pushed the ghi47316 branch 5 times, most recently from 5446980 to 5615979 Compare April 1, 2026 14:25
Copy link
Copy Markdown
Contributor

@IngridPuppet IngridPuppet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello @tdiesler - I only reviewed commit "[OID4VCI-HAIP] Pass oid4vci-1_0-issuer-happy-flow". I'm not sure if it is only commit relevant for this PR. As per my review, this PR enables ABCA for the HAIP test to pass. I left some comments, please could you check?

Comment on lines +251 to +253

if (kid == null || kid.equals(currentKid)) {
String kty = key.get("kty").asText();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In case kid is null, only the first JWK in the list will be considered. I'd rather either all are considered as potential verifier keys or null kids are not supported at all. WDYT?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doing this for now ...

        if (Strings.isEmpty(kid))
            throw new IllegalStateException("Invalid attester kid: " + kid);

        ABCAConfig attesterKeys = JsonSerialization.valueFromString(abcaConfigValue, ABCAConfig.class);
        for (JWK key : attesterKeys.keys) {
            if (kid.equals(key.getKeyId())) {
               ...
                return new JWKParser(key).toPublicKey();
            }
        }

i.e. we require the JWS header to provide a valid kid - this is lookup semantics rather than iterate-retry-on-failure - works for conformance tests.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fine for me. Thx.

Comment on lines +258 to +266

String n = key.get("n").asText();
String e = key.get("e").asText();

BigInteger modulus = new BigInteger(1, Base64Url.decode(n));
BigInteger exponent = new BigInteger(1, Base64Url.decode(e));

RSAPublicKeySpec spec = new RSAPublicKeySpec(modulus, exponent);
return KeyFactory.getInstance("RSA").generatePublic(spec);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider reusing this class for this related logic.

public class JwkParsingUtils {
public static SignatureVerifierContext convertJwkNodeToVerifierContext(JsonNode jwkNode) {
JWK jwk;
try {
jwk = SdJwtUtils.mapper.convertValue(jwkNode, JWK.class);
} catch (Exception e) {
throw new IllegalArgumentException("Malformed JWK");
}
return convertJwkToVerifierContext(jwk);
}
public static SignatureVerifierContext convertJwkToVerifierContext(JWK jwk) {
// Wrap JWK

Copy link
Copy Markdown
Contributor Author

@tdiesler tdiesler Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are doing this ...

        // The signature of the Client Attestation JWT verifies with the public key of a known and trusted Attester
        //
        PublicKey publicKey = findAttesterPublicKey(context, jws.getHeader().getKeyId());

        // Verification and Processing
        //
        TokenVerifier.create(headerValue, ClientAttestationJwt.class)
                .publicKey(publicKey)
                .withChecks(subCheck, issCheck, cnfCheck, TokenVerifier.IS_ACTIVE)
                .verify().getToken();

Is that ok?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the context. No strong opinion, that's fine for me.

HttpHeaders headers = httpRequest.getHttpHeaders();
String headerValue = headers.getHeaderString(OAUTH_CLIENT_ATTESTATION_HEADER);
if (headerValue == null)
throw new IllegalStateException("Required header " + OAUTH_CLIENT_ATTESTATION_HEADER + " for is missing");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks more like a BadRequestException. Same in the rest of the method.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed to IllegalArgumentException where appropriate. It doesn't matter much for these private methods - every Exception is caught and converted

        try {
            ClientAttestationJwt attesterJwt = validateClientAttestationJwt(context);
            validateClientAttestationPoPJwt(context, attesterJwt);

            context.success();

        } catch (Exception ex) {
            ServicesLogger.LOGGER.errorValidatingAssertion(ex);
            Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), OAuthErrorException.INVALID_CLIENT_ATTESTATION, ex.getMessage());
            context.failure(AuthenticationFlowError.INVALID_CLIENT_ATTESTATION, challengeResponse);
        }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the clarification.

Comment on lines +307 to +309
String clientIdParam = formParams.getFirst(CLIENT_ID);
if (clientIdParam != null && !clientIdParam.equals(jwt.getSubject()))
throw new IllegalStateException("The client attestation subject does not match the client_id parameter");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No error if clientIdParam == null. Does not seem intended.

Copy link
Copy Markdown
Contributor Author

@tdiesler tdiesler Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this is ok. The AttestationBasedClientAuthenticator is called twice, first on the PAR endpoint with a client_id param, then on the token endpoint without it.

We do ...

        // Get the client model from the JWT subject
        ClientAttestationJwt attesterJwt = jws.readJsonContent(ClientAttestationJwt.class);
        ClientModel clientModel = Optional.ofNullable(attesterJwt.getSubject())
                .map(realmModel::getClientByClientId)
                .orElseThrow(() -> new TokenVerificationException(attesterJwt, "The sub (subject) claim MUST identify a known client_id"));

            String clientIdParam = formParams.getFirst(CLIENT_ID);
            if (clientIdParam != null && !clientIdParam.equals(t.getSubject()))
                throw new TokenVerificationException(t, "The sub claim (subject) MUST match the client_id parameter");

Actually, I'm not sure whether it is correct that this is done twice. The spec says

(6) The Client Instance sends both the Client Attestation JWT and the Client Attestation PoP JWT to the authorization server, e.g. within a token request. The authorization server validates the Client Attestation and thus authenticates the Client Instance

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for clarification that clientIdParam is not required at the token endpoint. It's worth confirming whether the client needs to be authenticated twice, but I see nothing wrong with it.

Copy link
Copy Markdown
Contributor Author

@tdiesler tdiesler Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for clarification that clientIdParam is not required at the token endpoint. It's worth confirming whether the client needs to be authenticated twice, but I see nothing wrong with it.

We need to look into this nevertheless. With the TokenRequest (i.e. when there might be no client_id) the server should still be able to remember that it previously received an AuthorizationRequest

Doing this is fishy ...

ClientModel clientModel = Optional.ofNullable(attesterJwt.getSubject())

It should use the provided client_id from the authorization request, but I couldn't find an obvious way to get to it.

@tdiesler tdiesler changed the title [OID4VCI-HAIP] Pass oid4vci-1_0-issuer-happy-flow [OID4VCI-HAIP] Pass oid4vci-1_0-issuer-happy-flow (blocked) Apr 8, 2026
@tdiesler
Copy link
Copy Markdown
Contributor Author

tdiesler commented Apr 8, 2026

I suggest we focus on the other two PRs that this one depends on. Then, I'll bring the ABCA implementation (which is now spread over multiple PRs) forward to this one and then we can address all comments related to that (non-trivial) change. Indeed, I expect and welcome quite a bit of feedback on the ABCA implementation.

@tdiesler tdiesler force-pushed the ghi47316 branch 7 times, most recently from 3bc0bba to 337dfd7 Compare April 10, 2026 06:00
@VinodAnandan
Copy link
Copy Markdown
Contributor

@tdiesler @mposolda Just checking in on this one! If I understand correctly, the blockers have been cleared. Could you please confirm if it's ready for review?

@tdiesler
Copy link
Copy Markdown
Contributor Author

@tdiesler @mposolda Just checking in on this one! If I understand correctly, the blockers have been cleared. Could you please confirm if it's ready for review?

Now the actual work starts. I'll bring everything abca related forward to this PR - then we have a complete view on what is there already (and what isn't ;-)

@tdiesler tdiesler marked this pull request as draft April 10, 2026 13:34
@tdiesler tdiesler changed the title [OID4VCI-HAIP] Pass oid4vci-1_0-issuer-happy-flow (blocked) [OID4VCI-HAIP] Pass oid4vci-1_0-issuer-happy-flow Apr 10, 2026
@tdiesler tdiesler force-pushed the ghi47316 branch 2 times, most recently from 3b459be to 63f0df4 Compare April 10, 2026 14:38
@tdiesler tdiesler closed this Apr 11, 2026
@tdiesler tdiesler deleted the ghi47316 branch April 11, 2026 06:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[OID4VCI-HAIP] Pass oid4vci-1_0-issuer-happy-flow

3 participants