[OID4VCI] Omit blank key attestation values from OID4VCI metadata - #51356
Conversation
There was a problem hiding this comment.
Pull request overview
Fixes OID4VCI metadata generation by omitting blank key-attestation constraints.
Changes:
- Trims and filters blank resistance-level values.
- Returns
nullwhen no valid values remain. - Adds six focused regression tests.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
CredentialScopeModel.java |
Normalizes key-attestation attributes. |
CredentialScopeModelKeyAttestationTest.java |
Tests absent, blank, mixed, and configured values. |
mposolda
left a comment
There was a problem hiding this comment.
Thanks for the PR. Adding a review comment. Can you please doublecheck?
| * previously yielded {@code [""]}, because {@code "".split(",")} returns a single empty | ||
| * element and so the null fallback never applied. | ||
| */ | ||
| public class CredentialScopeModelKeyAttestationTest { |
There was a problem hiding this comment.
Is it please possible to rather test with the integration test from the base testsuite? Maybe adding new test method to the OID4VCIssuerWellKnownProviderTest would work fine.
2296702 to
8193dcb
Compare
|
@mposolda thanks — moved to the base testsuite as you asked, and doing so surfaced a second bug that I think changes the shape of this fix. Flagging it up front because it widens the diff beyond what #51347 describes. The integration test found a setter bugAll four key-attestation setters drop their argument: // CredentialScopeModel:418, :437
// CredentialScopeRepresentation:291, :306
.map(list -> String.join(",")) // `list` is never passedThat is This is where the reported So my original patch was treating the symptom. It made the metadata spec-compliant, but a user configuring Why the unit test couldn't see it, and yours canYou were right to push this to the base testsuite, and for a stronger reason than the mocking shape. My unit test stubbed the attribute directly, so it exercised the getter in isolation and could never observe the setter. The existing integration coverage couldn't catch it either, for a different reason — expectedKeyAttestationsRequired.setKeyStorage(credScope.getRequiredKeyAttestationKeyStorage());That assertion holds whether the getter returns What changed
VerificationKeeping the tests and reverting only the production change: I want to be precise about that "1", because it is not 2. Only the first test fails without the fix. The second passes on The reason is worth your attention. On the reverted build the getter returned Which raises a question I could not answer from here: the DCO is signed off now as well. One open question: if you would rather the getters stayed strict and blank data were handled as a migration instead, say so and I will drop that half and keep only the setter fix. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.
Suppressed comments (1)
tests/base/src/test/java/org/keycloak/tests/oid4vc/OID4VCIssuerWellKnownProviderTest.java:606
- The PR’s Tests/Verification section names
CredentialScopeModelKeyAttestationTestand aserver-spi-privatecommand, but that class does not exist in this change and the added coverage is thistests/baseintegration test, so the documented command does not verify the submitted test. Please update the PR description with the actual test and command that were run (or add the described unit test).
@Test
public void testKeyAttestationsRequiredOmitsUnconfiguredResistanceLevels() throws IOException {
|
Good catch from the Copilot review — the description was stale. I rewrote the code and the tests but left the original Tests/Verification section in place, so it still named The description now describes what is actually here: the two (with the note that |
Closes keycloak#51347 Both key attestation setters silently discarded their argument: Optional.ofNullable(keyStorage) .map(list -> String.join(",")) .orElse(null); That is String.join(CharSequence, CharSequence...) with zero elements, so it returns "" and ignores list entirely. Configuring any resistance level wrote an empty attribute. This is the source of the reported metadata: the testsuite's key-attestation-credential scope is built with List.of(MODERATE), the value was dropped on write, and the getter then rendered the blank attribute as: "key_attestations_required": {"key_storage":[""],"user_authentication":[""]} The same defect is present in CredentialScopeRepresentation, so both classes are fixed to String.join(",", list). OID4VCI 12.2.4 requires key_storage and user_authentication to be non-empty arrays when present, and permits an empty key_attestations_required object when neither is constrained. The getters now filter blank entries and collapse to null when nothing remains, which is the contract their existing comment already describes. CredentialScopeRepresentation gains the same treatment so the two classes agree on what a blank attribute means. KeyAttestationsRequired is annotated @JsonInclude(NON_NULL), so null members are omitted and the bare "key_attestations_required": {} falls out without further change. Values are also trimmed, so " a , b " no longer yields entries with surrounding whitespace. Testing moves to the base testsuite per review. The unit test and its java.lang.reflect.Proxy stub are removed in favour of two methods on OID4VCIssuerWellKnownProviderTest, which exercise the real metadata endpoint: one asserting a configured resistance level reaches the metadata, and one walking the cases from the report. Asserting literal expected values matters here -- the existing coverage derives its expectation from the same getter under test, so it holds regardless of what that getter returns. Signed-off-by: Rohit Behera <126186063+r0h1tb@users.noreply.github.com>
8193dcb to
91ce00d
Compare
|
Rebased onto current main ( The four red checks were a stale base, not the change. The branch head was Aug 3 and main was Aug 11, so it was ~100 commits behind; the tell was Keycloak JavaScript CI failing on a Java-only OID4VCI change, with annotations that were bare Workflow runs are currently sitting at |
Closes keycloak#51347 Signed-off-by: Rohit Behera <126186063+r0h1tb@users.noreply.github.com> Signed-off-by: theohh0 <theo.hinton-hallows@ibm.com>
Closes #51347
Problem
With key attestation enabled, the credential issuer metadata contains empty-string entries:
OID4VCI 12.2.4 requires
key_storageanduser_authenticationto be non-empty arrays when present, and permits an emptykey_attestations_requiredobject when neither is constrained.Root cause
There are two defects, and the first one is why the values are blank in the first place.
1. The setters discard their argument. All four key-attestation setters:
That is
String.join(CharSequence, CharSequence...)with zero elements, so it returns""and ignoreslistentirely. Configuring any resistance level writes an empty attribute.This is the source of the reported metadata. The testsuite's
key-attestation-credentialscope is built withList.of(MODERATE)—iso_18045_moderatewas configured, silently dropped on write, and the getter then rendered the blank attribute as[""]. Introduced in #51261.2. The getters turn a blank attribute into
[""].The attribute is present and blank, not absent, so
ofNullablesees"","".split(",")yields a single empty element, and theorElse(null)fallback never runs. The existing comment already states that null is the intended result — the guard just does not cover the blank case.Fix
String.join(",", list), in bothCredentialScopeModelandCredentialScopeRepresentation.KeyAttestationsRequiredis already@JsonInclude(NON_NULL), so null members are omitted and the bare"key_attestations_required": {}falls out with no change there. Values are also trimmed, so" a , b "no longer yields entries with surrounding whitespace.That covers the four cases enumerated in the issue:
{}key_storageuser_authenticationTests
Per review, these live in the base testsuite rather than as unit tests. Two methods added to
OID4VCIssuerWellKnownProviderTest, both driving the real metadata endpoint:testKeyAttestationsRequiredAdvertisesConfiguredResistanceLevels— asserts the configurediso_18045_moderateactually reaches the metadata. This is the one that pins the setter bug.testKeyAttestationsRequiredOmitsUnconfiguredResistanceLevels— walks the four cases above plus separator-only and padded input, restoring the scope in afinally.Asserting literal expected values is deliberate. The existing coverage at
OID4VCIssuerWellKnownProviderTest:714derives its expectation from the same getter under test:so it holds whether that getter returns
[""]ornull— which is why this bug shipped with integration coverage already in place.Verification
Keeping the tests and reverting only the production change:
That is 1, not 2, and the difference matters. Only the first test fails without the fix. The second passes on
maintoo, so it is a spec-compliance guard rather than a regression test for the reported symptom.The reason is worth flagging: on the reverted build the getter returned
null, not[""]— writing""throughClientScopeResource.update()comes back as an absent attribute, so I could not reproduce{"key_storage":[""]}through the admin REST path at all. The[""]in the report must come from a write path that does persist a blank value. If you know which one, I will point the second test at it so it pins the getter half too. Until then the getter hardening is defensive — it protects realms that already hold blank attributes, but no test here proves it is required.Note that
tests/baseneeds a prior./mvnw install -DskipTestsbefore the command above will resolve.Scope
String.join(",")call sites are affected — the rest of both classes already pass the list.