From ce28e0c4505ae03af8050d1fb1812e796ae2d985 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Fri, 2 May 2025 12:16:32 +0200 Subject: [PATCH 1/4] Add credProps assertion to RegistrationExtensionInputs deserialization test --- .../src/test/scala/com/yubico/webauthn/data/ExtensionsSpec.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/ExtensionsSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/ExtensionsSpec.scala index 30080c42c..a4cb7b9ab 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/ExtensionsSpec.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/ExtensionsSpec.scala @@ -90,6 +90,7 @@ class ExtensionsSpec decoded.getAppidExclude.toScala should equal( Some(new AppId("https://example.org")) ) + decoded.getCredProps should equal(true) decoded.getLargeBlob.toScala should equal( Some(new LargeBlobRegistrationInput(LargeBlobSupport.REQUIRED)) ) From ea137971fe2d129bfa948c164b72d16a1a0d30ab Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Fri, 2 May 2025 12:38:43 +0200 Subject: [PATCH 2/4] Add PRF to extension input deserialization tests --- .../yubico/webauthn/data/ExtensionsSpec.scala | 68 ++++++++++++++++++- 1 file changed, 66 insertions(+), 2 deletions(-) diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/ExtensionsSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/ExtensionsSpec.scala index a4cb7b9ab..2b33fe2a5 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/ExtensionsSpec.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/ExtensionsSpec.scala @@ -8,6 +8,9 @@ import com.yubico.webauthn.data.Extensions.LargeBlob.LargeBlobAuthenticationOutp import com.yubico.webauthn.data.Extensions.LargeBlob.LargeBlobRegistrationInput import com.yubico.webauthn.data.Extensions.LargeBlob.LargeBlobRegistrationInput.LargeBlobSupport import com.yubico.webauthn.data.Extensions.LargeBlob.LargeBlobRegistrationOutput +import com.yubico.webauthn.data.Extensions.Prf.PrfAuthenticationInput +import com.yubico.webauthn.data.Extensions.Prf.PrfRegistrationInput +import com.yubico.webauthn.data.Extensions.Prf.PrfValues import com.yubico.webauthn.data.Generators.arbitraryAssertionExtensionInputs import com.yubico.webauthn.data.Generators.arbitraryClientRegistrationExtensionOutputs import com.yubico.webauthn.data.Generators.arbitraryRegistrationExtensionInputs @@ -23,6 +26,7 @@ import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks import java.nio.charset.StandardCharsets import scala.jdk.CollectionConverters.IteratorHasAsScala +import scala.jdk.CollectionConverters.MapHasAsJava import scala.jdk.CollectionConverters.SetHasAsScala import scala.jdk.OptionConverters.RichOptional @@ -73,6 +77,12 @@ class ExtensionsSpec |"largeBlob": { | "support": "required" |}, + |"prf": { + | "eval": { + | "first": "AAAA", + | "second": "BBBB" + | } + |}, |"uvm": true |}""".stripMargin @@ -85,7 +95,13 @@ class ExtensionsSpec decoded should not be null decoded.getExtensionIds.asScala should equal( - Set("appidExclude", "credProps", "largeBlob", "uvm") + Set( + "appidExclude", + "credProps", + "largeBlob", + "prf", + "uvm", + ) ) decoded.getAppidExclude.toScala should equal( Some(new AppId("https://example.org")) @@ -94,6 +110,16 @@ class ExtensionsSpec decoded.getLargeBlob.toScala should equal( Some(new LargeBlobRegistrationInput(LargeBlobSupport.REQUIRED)) ) + decoded.getPrf.toScala should equal( + Some( + PrfRegistrationInput.eval( + PrfValues.two( + ByteArray.fromBase64Url("AAAA"), + ByteArray.fromBase64Url("BBBB"), + ) + ) + ) + ) decoded.getUvm should be(true) redecoded should equal(decoded) @@ -161,6 +187,21 @@ class ExtensionsSpec |"largeBlob": { | "read": true |}, + |"prf": { + | "eval": { + | "first": "AAAA", + | "second": "BBBB" + | }, + | "evalByCredential": { + | "CCCC": { + | "first": "DDDD" + | }, + | "EEEE": { + | "first": "FFFF", + | "second": "GGGG" + | } + | } + |}, |"uvm": true |}""".stripMargin @@ -173,7 +214,7 @@ class ExtensionsSpec decoded should not be null decoded.getExtensionIds.asScala should equal( - Set("appid", "largeBlob", "uvm") + Set("appid", "largeBlob", "prf", "uvm") ) decoded.getAppid.toScala should equal( Some(new AppId("https://example.org")) @@ -181,6 +222,29 @@ class ExtensionsSpec decoded.getLargeBlob.toScala should equal( Some(LargeBlobAuthenticationInput.read()) ) + decoded.getPrf.toScala should equal( + Some( + PrfAuthenticationInput.evalByCredentialWithFallback( + Map( + PublicKeyCredentialDescriptor + .builder() + .id(ByteArray.fromBase64Url("CCCC")) + .build() -> PrfValues.one(ByteArray.fromBase64Url("DDDD")), + PublicKeyCredentialDescriptor + .builder() + .id(ByteArray.fromBase64Url("EEEE")) + .build() -> PrfValues.two( + ByteArray.fromBase64Url("FFFF"), + ByteArray.fromBase64Url("GGGG"), + ), + ).asJava, + PrfValues.two( + ByteArray.fromBase64Url("AAAA"), + ByteArray.fromBase64Url("BBBB"), + ), + ) + ) + ) decoded.getUvm should be(true) redecoded should equal(decoded) From 37010cac12b1132501167dca3fd68bac9d31a313 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Fri, 2 May 2025 12:46:12 +0200 Subject: [PATCH 3/4] Reorder getCredProps methods together in RegistrationExtensionInputs --- .../webauthn/data/RegistrationExtensionInputs.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/RegistrationExtensionInputs.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/RegistrationExtensionInputs.java index 8d5c70145..b9d1464d4 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/RegistrationExtensionInputs.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/RegistrationExtensionInputs.java @@ -118,6 +118,12 @@ public boolean getCredProps() { return credProps != null && credProps; } + /** For JSON serialization, to omit false values. */ + @JsonProperty("credProps") + private Boolean getCredPropsJson() { + return getCredProps() ? true : null; + } + /** * @return The Credential Protection (credProtect) extension input, if set. * @since 2.7.0 @@ -131,12 +137,6 @@ public Optional getCr return Optional.ofNullable(credProtect); } - /** For JSON serialization, to omit false values. */ - @JsonProperty("credProps") - private Boolean getCredPropsJson() { - return getCredProps() ? true : null; - } - /** * @return The value of the Large blob storage extension (largeBlob) input if * configured, empty otherwise. From 0ae9fb19b01964672e3041c07dca958a091cec74 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Fri, 2 May 2025 12:26:52 +0200 Subject: [PATCH 4/4] Fix JSON encoding of credProtect extension inputs --- .../data/RegistrationExtensionInputs.java | 67 ++++++++++++++++--- .../yubico/webauthn/data/ExtensionsSpec.scala | 29 ++++++-- 2 files changed, 84 insertions(+), 12 deletions(-) diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/RegistrationExtensionInputs.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/RegistrationExtensionInputs.java index b9d1464d4..00ebe1c0a 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/RegistrationExtensionInputs.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/RegistrationExtensionInputs.java @@ -25,6 +25,7 @@ package com.yubico.webauthn.data; import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import com.yubico.webauthn.RelyingParty; @@ -60,15 +61,13 @@ public final class RegistrationExtensionInputs implements ExtensionInputs { private final Extensions.Prf.PrfRegistrationInput prf; private final Boolean uvm; - @JsonCreator private RegistrationExtensionInputs( - @JsonProperty("appidExclude") AppId appidExclude, - @JsonProperty("credProps") Boolean credProps, - @JsonProperty("credProtect") - Extensions.CredentialProtection.CredentialProtectionInput credProtect, - @JsonProperty("largeBlob") Extensions.LargeBlob.LargeBlobRegistrationInput largeBlob, - @JsonProperty("prf") Extensions.Prf.PrfRegistrationInput prf, - @JsonProperty("uvm") Boolean uvm) { + AppId appidExclude, + Boolean credProps, + Extensions.CredentialProtection.CredentialProtectionInput credProtect, + Extensions.LargeBlob.LargeBlobRegistrationInput largeBlob, + Extensions.Prf.PrfRegistrationInput prf, + Boolean uvm) { this.appidExclude = appidExclude; this.credProps = credProps; this.credProtect = credProtect; @@ -77,6 +76,32 @@ private RegistrationExtensionInputs( this.uvm = uvm; } + @JsonCreator + private RegistrationExtensionInputs( + @JsonProperty("appidExclude") AppId appidExclude, + @JsonProperty("credProps") Boolean credProps, + @JsonProperty("credentialProtectionPolicy") + Extensions.CredentialProtection.CredentialProtectionPolicy credProtectPolicy, + @JsonProperty("enforceCredentialProtectionPolicy") Boolean enforceCredProtectPolicy, + @JsonProperty("largeBlob") Extensions.LargeBlob.LargeBlobRegistrationInput largeBlob, + @JsonProperty("prf") Extensions.Prf.PrfRegistrationInput prf, + @JsonProperty("uvm") Boolean uvm) { + this( + appidExclude, + credProps, + Optional.ofNullable(credProtectPolicy) + .map( + policy -> { + return enforceCredProtectPolicy != null && enforceCredProtectPolicy + ? Extensions.CredentialProtection.CredentialProtectionInput.require(policy) + : Extensions.CredentialProtection.CredentialProtectionInput.prefer(policy); + }) + .orElse(null), + largeBlob, + prf, + uvm); + } + /** * Merge other into this. Non-null field values from this * take precedence. @@ -133,10 +158,36 @@ private Boolean getCredPropsJson() { * href="https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#sctn-authenticator-credential-properties-extension">ยง10.4. * Credential Properties Extension (credProps) */ + @JsonIgnore public Optional getCredProtect() { return Optional.ofNullable(credProtect); } + /** + * For JSON serialization, because credProtect does not group all inputs under the "credProtect" + * key. + */ + @JsonProperty("credentialProtectionPolicy") + private Optional + getCredProtectPolicy() { + return getCredProtect() + .map( + Extensions.CredentialProtection.CredentialProtectionInput + ::getCredentialProtectionPolicy); + } + + /** + * For JSON serialization, because credProtect does not group all inputs under the "credProtect" + * key. + */ + @JsonProperty("enforceCredentialProtectionPolicy") + private Optional getEnforceCredProtectPolicy() { + return getCredProtect() + .map( + Extensions.CredentialProtection.CredentialProtectionInput + ::isEnforceCredentialProtectionPolicy); + } + /** * @return The value of the Large blob storage extension (largeBlob) input if * configured, empty otherwise. diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/ExtensionsSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/ExtensionsSpec.scala index 2b33fe2a5..4840b9960 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/ExtensionsSpec.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/ExtensionsSpec.scala @@ -3,6 +3,8 @@ package com.yubico.webauthn.data import com.fasterxml.jackson.databind.node.ObjectNode import com.yubico.internal.util.JacksonCodecs import com.yubico.scalacheck.gen.JacksonGenerators.arbitraryObjectNode +import com.yubico.webauthn.data.Extensions.CredentialProtection.CredentialProtectionInput +import com.yubico.webauthn.data.Extensions.CredentialProtection.CredentialProtectionPolicy import com.yubico.webauthn.data.Extensions.LargeBlob.LargeBlobAuthenticationInput import com.yubico.webauthn.data.Extensions.LargeBlob.LargeBlobAuthenticationOutput import com.yubico.webauthn.data.Extensions.LargeBlob.LargeBlobRegistrationInput @@ -38,14 +40,23 @@ class ExtensionsSpec describe("RegistrationExtensionInputs") { describe("has a getExtensionIds() method which") { - it("contains exactly the names of contained extensions.") { + it("contains exactly the names of contained extensions, except for credProtect.") { forAll { input: RegistrationExtensionInputs => + val expectedJsonKeys = input.getExtensionIds.asScala.flatMap(id => { + if (id == "credProtect") { + // credProtect does not gather all inputs under the extension ID as a map key. + List( + "credentialProtectionPolicy", + "enforceCredentialProtectionPolicy", + ) + } else { + List(id) + } + }) val json = JacksonCodecs.json().valueToTree[ObjectNode](input) val jsonKeyNames = json.fieldNames.asScala.toList - val extensionIds = input.getExtensionIds - jsonKeyNames.length should equal(extensionIds.size) - jsonKeyNames.toSet should equal(extensionIds.asScala) + jsonKeyNames.toSet should equal(expectedJsonKeys) } } } @@ -74,6 +85,8 @@ class ExtensionsSpec """{ |"appidExclude": "https://example.org", |"credProps": true, + |"credentialProtectionPolicy": "userVerificationRequired", + |"enforceCredentialProtectionPolicy": true, |"largeBlob": { | "support": "required" |}, @@ -98,6 +111,7 @@ class ExtensionsSpec Set( "appidExclude", "credProps", + "credProtect", "largeBlob", "prf", "uvm", @@ -107,6 +121,13 @@ class ExtensionsSpec Some(new AppId("https://example.org")) ) decoded.getCredProps should equal(true) + decoded.getCredProtect.toScala should equal( + Some( + CredentialProtectionInput.require( + CredentialProtectionPolicy.UV_REQUIRED + ) + ) + ) decoded.getLargeBlob.toScala should equal( Some(new LargeBlobRegistrationInput(LargeBlobSupport.REQUIRED)) )