Skip to content

Fix parsing SupportedCtapOptions from empty JSON object #420

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jun 4, 2025

Conversation

emlun
Copy link
Member

@emlun emlun commented Jun 4, 2025

(This would merge into #419, not into main)

Out of curiosity I tried what happens if you add a test of parsing from an empty JSON object, and it turned out to have wider repercussions than I expected 😄 it also interacts with the builder annotations in subtle ways. I've documented the process in detail in the first two commit descriptions. The last two are just cleanup and gathering related things closer together in the source code.

emlun added 4 commits June 4, 2025 13:55
This test will be needed later for a more focused view on why the sibling test
of `MetadataBLOBPayload` serialization stability begins to fail.
This commit was constructed through a series of steps:

Step 1: Add the new test "SupportedCtapOptions can be parsed from an empty JSON
object". It initially fails with:

```
true was not false
ScalaTestFailureLocation: com.yubico.fido.metadata.MetadataBlobSpec at (MetadataBlobSpec.scala:68)
org.scalatest.exceptions.TestFailedException: true was not false
	at org.scalatest.matchers.MatchersHelper$.indicateFailure(MatchersHelper.scala:392)
	at org.scalatest.matchers.should.Matchers$ShouldMethodHelperClass.shouldMatcher(Matchers.scala:7304)
	at org.scalatest.matchers.should.Matchers$AnyShouldWrapper.should(Matchers.scala:7347)
	at com.yubico.fido.metadata.MetadataBlobSpec.$anonfun$new$5(MetadataBlobSpec.scala:68)
```

At the line:

```
options.isUv should be(false)
```

This is because `SupportedCtapOptions` is still annotated with `@Jacksonized`,
so Jackson uses the builder to construct the object, and the class has

```
@Builder.Default boolean uv = false;
```

so the builder initializes the builder field to the primitive `false` value,
which causes the constructor argument to be `Boolean.FALSE`, which is not null,
and therefore the constructor sets `this.uv` to `true`.

Step 2: Remove `@Jacksonized` from `SupportedCtapOptions`. This causes Jackson
to invoke the constructor directly instead of going through the builder, which
bypasses the builder field defaults.

The test "SupportedCtapOptions can be parsed from an empty JSON object" now
fails with a different cause:

```
Cannot construct instance of `com.yubico.fido.metadata.SupportedCtapOptions`, problem: `java.lang.NullPointerException`
 at [Source: REDACTED (`StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION` disabled); line: 1, column: 2]
com.fasterxml.jackson.databind.exc.ValueInstantiationException: Cannot construct instance of `com.yubico.fido.metadata.SupportedCtapOptions`, problem: `java.lang.NullPointerException`
 at [Source: REDACTED (`StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION` disabled); line: 1, column: 2]
	at com.fasterxml.jackson.databind.exc.ValueInstantiationException.from(ValueInstantiationException.java:46)
        [...]
Caused by: java.lang.NullPointerException
	at com.yubico.fido.metadata.SupportedCtapOptions.<init>(SupportedCtapOptions.java:180)
        [...]
```

which is the line:

```
this.plat = plat;
```

The test "FIDO Metadata Service 3 blob payloads are structurally identical after
multiple (de)serialization round-trips." also begins failing now.

Step 3: Use `Boolean.TRUE.equals(...)` to fix `NullPointerException`s while
setting `plat`, `rk` and `up` in `SupportedCtapOptions` constructor.

The test "SupportedCtapOptions can be parsed from an empty JSON object" now
passes. Instead the test "FIDO Metadata Service 3 blob payloads are structurally
identical after multiple (de)serialization round-trips" now fails. This is why
we added the test in the previous commit, to get a more focused view of this
failure. This failure is:

```
TestFailedException was thrown during property evaluation.
  Message: SupportedCtapOptions(plat=false, rk=true, clientPin=true, up=true, uv=true, pinUvAuthToken=false, noMcGaPermissionsWithClientPin=false, largeBlobs=false, ep=true, bioEnroll=true, userVerificationMgmtPreview=true, uvBioEnroll=false, authnrCfg=false, uvAcfg=false, credMgmt=false, credentialMgmtPreview=false, setMinPINLength=false, makeCredUvNotRqd=false, alwaysUv=true) did not equal SupportedCtapOptions(plat=false, rk=true, clientPin=true, up=true, uv=true, pinUvAuthToken=false, noMcGaPermissionsWithClientPin=false, largeBlobs=false, ep=false, bioEnroll=false, userVerificationMgmtPreview=false, uvBioEnroll=false, authnrCfg=false, uvAcfg=false, credMgmt=false, credentialMgmtPreview=false, setMinPINLength=false, makeCredUvNotRqd=false, alwaysUv=false)
  Location: (MetadataBlobSpec.scala:108)
  Occurred when passed generated values (
    arg0 = SupportedCtapOptions(plat=false, rk=true, clientPin=true, up=true, uv=true, pinUvAuthToken=false, noMcGaPermissionsWithClientPin=false, largeBlobs=false, ep=false, bioEnroll=false, userVerificationMgmtPreview=false, uvBioEnroll=false, authnrCfg=false, uvAcfg=false, credMgmt=false, credentialMgmtPreview=false, setMinPINLength=false, makeCredUvNotRqd=false, alwaysUv=false)
  )
```

This is because the constructor sets some of the the tri-state inputs to `true`
when non-null, including when `false`. This means that when the input is
absent (null), it is set to `false` and then emitted as `false` when serialized
back to JSON, which is then interpreted as non-null and converted to `true` when
deserializing again.

Step 4: Add `@JsonInclude` directives to omit the tri-state fields that are
true-when-non-null when their value is false.

All tests now pass!
@emlun emlun requested a review from fdennis June 4, 2025 12:48
Copy link

github-actions bot commented Jun 4, 2025

Test Results

2 271 tests  ±0   2 263 ✅ ±0   1m 1s ⏱️ ±0s
   46 suites ±0       8 💤 ±0 
   46 files   ±0       0 ❌ ±0 

Results for commit 0452830. ± Comparison against base commit f4981f2.

@emlun emlun merged commit 0452830 into SupportedCtapOptions-absent Jun 4, 2025
20 checks passed
@emlun emlun deleted the emil/SupportedCtapOptions-absent branch June 4, 2025 14:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

2 participants