-
Notifications
You must be signed in to change notification settings - Fork 155
MDS: Adjust SupportedCtapOptions parsing logic #419
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
base: main
Are you sure you want to change the base?
Conversation
Test Results2 271 tests 2 263 ✅ 1m 0s ⏱️ Results for commit 6801734. ♻️ This comment has been updated with latest results. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The logic looks correct to spec, just needs a few small tweaks. 🙂
@JsonProperty("alwaysUv") Boolean alwaysUv) { | ||
this.plat = plat; | ||
this.rk = rk; | ||
this.clientPin = clientPin != null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We'll need to update the JavaDoc for clientPin
since the behaviour here is no longer the same as in the CTAP spec.
webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/SupportedCtapOptions.java
Outdated
Show resolved
Hide resolved
this.uv = uv != null; | ||
this.pinUvAuthToken = Boolean.TRUE.equals(pinUvAuthToken); | ||
this.noMcGaPermissionsWithClientPin = Boolean.TRUE.equals(noMcGaPermissionsWithClientPin); | ||
this.largeBlobs = Boolean.TRUE.equals(largeBlobs); | ||
this.ep = ep != null; | ||
this.bioEnroll = bioEnroll != null; | ||
this.userVerificationMgmtPreview = userVerificationMgmtPreview != null; | ||
this.uvBioEnroll = Boolean.TRUE.equals(uvBioEnroll); | ||
this.authnrCfg = Boolean.TRUE.equals(authnrCfg); | ||
this.uvAcfg = Boolean.TRUE.equals(uvAcfg); | ||
this.credMgmt = Boolean.TRUE.equals(credMgmt); | ||
this.credentialMgmtPreview = Boolean.TRUE.equals(credentialMgmtPreview); | ||
this.setMinPINLength = Boolean.TRUE.equals(setMinPINLength); | ||
this.makeCredUvNotRqd = Boolean.TRUE.equals(makeCredUvNotRqd); | ||
this.alwaysUv = alwaysUv != null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
uv
, ep
, bioEnroll
, userVerificationMgmtPreview
and alwaysUv
also need their JavaDoc updated.
this.setMinPINLength = Boolean.TRUE.equals(setMinPINLength); | ||
this.makeCredUvNotRqd = Boolean.TRUE.equals(makeCredUvNotRqd); | ||
this.alwaysUv = alwaysUv != null; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like an perCredMgmtRO
option has been added (between credMgmt
and credentialMgmtPreview
) since this class was written, we should add that too.
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!
Out of curiosity I tried what happens if you add the aforementioned 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'll post a meta-PR with what I found. |
No description provided.