diff --git a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/SupportedCtapOptions.java b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/SupportedCtapOptions.java index cda8898e9..88fa473b0 100644 --- a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/SupportedCtapOptions.java +++ b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/SupportedCtapOptions.java @@ -1,160 +1,226 @@ package com.yubico.fido.metadata; import com.fasterxml.jackson.annotation.JsonAlias; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Builder; import lombok.Value; -import lombok.extern.jackson.Jacksonized; /** * A fixed-keys map of CTAP2 option names to Boolean values representing whether an authenticator * supports the respective option. * * @see Client + * href="https://fidoalliance.org/specs/fido-v2.2-ps-20250228/fido-client-to-authenticator-protocol-v2.2-ps-20250228.html#authenticatorGetInfo">Client * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) */ @Value @Builder -@Jacksonized -@AllArgsConstructor(access = AccessLevel.PRIVATE) public class SupportedCtapOptions { /** * @see Client + * href="https://fidoalliance.org/specs/fido-v2.2-ps-20250228/fido-client-to-authenticator-protocol-v2.2-ps-20250228.html#authenticatorGetInfo">Client * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) */ - @Builder.Default boolean plat = false; + boolean plat; /** * @see Client + * href="https://fidoalliance.org/specs/fido-v2.2-ps-20250228/fido-client-to-authenticator-protocol-v2.2-ps-20250228.html#authenticatorGetInfo">Client * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) */ - @Builder.Default boolean rk = false; + boolean rk; /** + * If set to true the device is capable of accepting PIN. + * * @see Client + * href="https://fidoalliance.org/specs/fido-v2.2-ps-20250228/fido-client-to-authenticator-protocol-v2.2-ps-20250228.html#authenticatorGetInfo">Client * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) */ - @Builder.Default boolean clientPin = false; + @JsonInclude(JsonInclude.Include.NON_DEFAULT) + boolean clientPin; /** * @see Client + * href="https://fidoalliance.org/specs/fido-v2.2-ps-20250228/fido-client-to-authenticator-protocol-v2.2-ps-20250228.html#authenticatorGetInfo">Client * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) */ - @Builder.Default boolean up = false; + boolean up; /** + * If set to true the device is capable of built-in user verification. + * * @see Client + * href="https://fidoalliance.org/specs/fido-v2.2-ps-20250228/fido-client-to-authenticator-protocol-v2.2-ps-20250228.html#authenticatorGetInfo">Client * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) */ - @Builder.Default boolean uv = false; + @JsonInclude(JsonInclude.Include.NON_DEFAULT) + boolean uv; /** * @see Client + * href="https://fidoalliance.org/specs/fido-v2.2-ps-20250228/fido-client-to-authenticator-protocol-v2.2-ps-20250228.html#authenticatorGetInfo">Client * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) */ - @JsonAlias("uvToken") - @Builder.Default - boolean pinUvAuthToken = false; + boolean pinUvAuthToken; /** * @see Client + * href="https://fidoalliance.org/specs/fido-v2.2-ps-20250228/fido-client-to-authenticator-protocol-v2.2-ps-20250228.html#authenticatorGetInfo">Client * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) */ - @Builder.Default boolean noMcGaPermissionsWithClientPin = false; + boolean noMcGaPermissionsWithClientPin; /** * @see Client + * href="https://fidoalliance.org/specs/fido-v2.2-ps-20250228/fido-client-to-authenticator-protocol-v2.2-ps-20250228.html#authenticatorGetInfo">Client * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) */ - @Builder.Default boolean largeBlobs = false; + boolean largeBlobs; /** + * If set to true the authenticator is enterprise attestation capable. + * * @see Client + * href="https://fidoalliance.org/specs/fido-v2.2-ps-20250228/fido-client-to-authenticator-protocol-v2.2-ps-20250228.html#authenticatorGetInfo">Client * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) */ - @Builder.Default boolean ep = false; + @JsonInclude(JsonInclude.Include.NON_DEFAULT) + boolean ep; /** + * If set to true the authenticator supports the authenticatorBioEnrollment commands. + * * @see Client + * href="https://fidoalliance.org/specs/fido-v2.2-ps-20250228/fido-client-to-authenticator-protocol-v2.2-ps-20250228.html#authenticatorGetInfo">Client * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) */ - @Builder.Default boolean bioEnroll = false; + @JsonInclude(JsonInclude.Include.NON_DEFAULT) + boolean bioEnroll; /** + * If set to true the authenticator supports the Prototype authenticatorBioEnrollment + * (0x40) commands. + * * @see Client + * href="https://fidoalliance.org/specs/fido-v2.2-ps-20250228/fido-client-to-authenticator-protocol-v2.2-ps-20250228.html#authenticatorGetInfo">Client * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) */ - @Builder.Default boolean userVerificationMgmtPreview = false; + @JsonInclude(JsonInclude.Include.NON_DEFAULT) + boolean userVerificationMgmtPreview; /** * @see Client + * href="https://fidoalliance.org/specs/fido-v2.2-ps-20250228/fido-client-to-authenticator-protocol-v2.2-ps-20250228.html#authenticatorGetInfo">Client * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) */ - @Builder.Default boolean uvBioEnroll = false; + boolean uvBioEnroll; /** * @see Client + * href="https://fidoalliance.org/specs/fido-v2.2-ps-20250228/fido-client-to-authenticator-protocol-v2.2-ps-20250228.html#authenticatorGetInfo">Client * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) */ - @JsonAlias("config") - @Builder.Default - boolean authnrCfg = false; + boolean authnrCfg; /** * @see Client + * href="https://fidoalliance.org/specs/fido-v2.2-ps-20250228/fido-client-to-authenticator-protocol-v2.2-ps-20250228.html#authenticatorGetInfo">Client * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) */ - @Builder.Default boolean uvAcfg = false; + boolean uvAcfg; /** * @see Client + * href="https://fidoalliance.org/specs/fido-v2.2-ps-20250228/fido-client-to-authenticator-protocol-v2.2-ps-20250228.html#authenticatorGetInfo">Client * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) */ - @Builder.Default boolean credMgmt = false; + boolean credMgmt; /** * @see Client + * href="https://fidoalliance.org/specs/fido-v2.2-ps-20250228/fido-client-to-authenticator-protocol-v2.2-ps-20250228.html#authenticatorGetInfo">Client * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) */ - @Builder.Default boolean credentialMgmtPreview = false; + boolean perCredMgmtRO; /** * @see Client + * href="https://fidoalliance.org/specs/fido-v2.2-ps-20250228/fido-client-to-authenticator-protocol-v2.2-ps-20250228.html#authenticatorGetInfo">Client * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) */ - @Builder.Default boolean setMinPINLength = false; + @JsonInclude(JsonInclude.Include.NON_DEFAULT) + boolean credentialMgmtPreview; /** * @see Client + * href="https://fidoalliance.org/specs/fido-v2.2-ps-20250228/fido-client-to-authenticator-protocol-v2.2-ps-20250228.html#authenticatorGetInfo">Client * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) */ - @Builder.Default boolean makeCredUvNotRqd = false; + boolean setMinPINLength; /** * @see Client + * href="https://fidoalliance.org/specs/fido-v2.2-ps-20250228/fido-client-to-authenticator-protocol-v2.2-ps-20250228.html#authenticatorGetInfo">Client * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) */ - @Builder.Default boolean alwaysUv = false; + boolean makeCredUvNotRqd; + + /** + * If set to true the authenticator supports the Always Require User Verification + * feature. + * + * @see Client + * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) + */ + @JsonInclude(JsonInclude.Include.NON_DEFAULT) + boolean alwaysUv; + + @JsonCreator + private SupportedCtapOptions( + @JsonProperty("plat") Boolean plat, + @JsonProperty("rk") Boolean rk, + @JsonProperty("clientPin") Boolean clientPin, + @JsonProperty("up") Boolean up, + @JsonProperty("uv") Boolean uv, + @JsonAlias("uvToken") @JsonProperty("pinUvAuthToken") Boolean pinUvAuthToken, + @JsonProperty("noMcGaPermissionsWithClientPin") Boolean noMcGaPermissionsWithClientPin, + @JsonProperty("largeBlobs") Boolean largeBlobs, + @JsonProperty("ep") Boolean ep, + @JsonProperty("bioEnroll") Boolean bioEnroll, + @JsonProperty("userVerificationMgmtPreview") Boolean userVerificationMgmtPreview, + @JsonProperty("uvBioEnroll") Boolean uvBioEnroll, + @JsonAlias("config") @JsonProperty("authnrCfg") Boolean authnrCfg, + @JsonProperty("uvAcfg") Boolean uvAcfg, + @JsonProperty("credMgmt") Boolean credMgmt, + @JsonProperty("perCredMgmtRO") Boolean perCredMgmtRO, + @JsonProperty("credentialMgmtPreview") Boolean credentialMgmtPreview, + @JsonProperty("setMinPINLength") Boolean setMinPINLength, + @JsonProperty("makeCredUvNotRqd") Boolean makeCredUvNotRqd, + @JsonProperty("alwaysUv") Boolean alwaysUv) { + this.plat = Boolean.TRUE.equals(plat); + this.rk = Boolean.TRUE.equals(rk); + this.clientPin = clientPin != null; + this.up = Boolean.TRUE.equals(up); + 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.perCredMgmtRO = Boolean.TRUE.equals(perCredMgmtRO); + this.credentialMgmtPreview = Boolean.TRUE.equals(credentialMgmtPreview); + this.setMinPINLength = Boolean.TRUE.equals(setMinPINLength); + this.makeCredUvNotRqd = Boolean.TRUE.equals(makeCredUvNotRqd); + this.alwaysUv = alwaysUv != null; + } } diff --git a/webauthn-server-attestation/src/test/scala/com/yubico/fido/metadata/MetadataBlobSpec.scala b/webauthn-server-attestation/src/test/scala/com/yubico/fido/metadata/MetadataBlobSpec.scala index ed0d126a9..3de744451 100644 --- a/webauthn-server-attestation/src/test/scala/com/yubico/fido/metadata/MetadataBlobSpec.scala +++ b/webauthn-server-attestation/src/test/scala/com/yubico/fido/metadata/MetadataBlobSpec.scala @@ -1,11 +1,17 @@ package com.yubico.fido.metadata +import com.yubico.fido.metadata.Generators.arbitrarySupportedCtapOptions import com.yubico.internal.util.JacksonCodecs import com.yubico.webauthn.data.ByteArray +import org.scalacheck.Arbitrary +import org.scalacheck.Gen import org.scalatest.funspec.AnyFunSpec import org.scalatest.matchers.should.Matchers import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks +import scala.jdk.CollectionConverters.SetHasAsScala +import scala.jdk.OptionConverters.RichOptional + class MetadataBlobSpec extends AnyFunSpec with Matchers @@ -50,4 +56,60 @@ class MetadataBlobSpec } } + describe("SupportedCtapOptions") { + it("can be parsed from an empty JSON object.") { + val options = JacksonCodecs + .json() + .readValue("{}", classOf[SupportedCtapOptions]) + options should not be null + options.isPlat should be(false) + options.isRk should be(false) + options.isUp should be(false) + options.isUv should be(false) + options.isPinUvAuthToken should be(false) + options.isNoMcGaPermissionsWithClientPin should be(false) + options.isLargeBlobs should be(false) + options.isEp should be(false) + options.isBioEnroll should be(false) + options.isUserVerificationMgmtPreview should be(false) + options.isUvBioEnroll should be(false) + options.isAuthnrCfg should be(false) + options.isUvAcfg should be(false) + options.isCredMgmt should be(false) + options.isPerCredMgmtRO should be(false) + options.isCredentialMgmtPreview should be(false) + options.isSetMinPINLength should be(false) + options.isMakeCredUvNotRqd should be(false) + options.isAlwaysUv should be(false) + } + + it( + "are structurally identical after multiple (de)serialization round-trips." + ) { + val json = JacksonCodecs.json() + val blob = json + .readValue( + ByteArray + .fromBase64Url(FidoMds3Examples.BlobPayloadBase64url) + .getBytes, + classOf[MetadataBLOBPayload], + ) + val blobOptions = blob.getEntries.asScala + .flatMap(entry => entry.getMetadataStatement.toScala) + .flatMap(statement => statement.getAuthenticatorGetInfo.toScala) + .flatMap(info => info.getOptions.toScala) + forAll(Gen.oneOf(Arbitrary.arbitrary, Gen.oneOf(blobOptions))) { + (options1: SupportedCtapOptions) => + val encoded1 = json.writeValueAsBytes(options1) + val options2 = json.readValue(encoded1, classOf[SupportedCtapOptions]) + val encoded2 = json.writeValueAsBytes(options2) + val options3 = json.readValue(encoded2, classOf[SupportedCtapOptions]) + + options2 should not be null + options2 should equal(options1) + options3 should not be null + options3 should equal(options1) + } + } + } }