From 5ab0e4dda72b164929de7478d8402ede1f3c899b Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 29 Apr 2025 18:33:55 +0200 Subject: [PATCH] Add experimental option FinishRegistrationOptions.isConditionalCreate --- NEWS | 5 ++ .../webauthn/FinishRegistrationOptions.java | 19 ++++++ .../webauthn/FinishRegistrationSteps.java | 11 ++-- .../RelyingPartyRegistrationSpec.scala | 58 +++++++++++++++++-- 4 files changed, 84 insertions(+), 9 deletions(-) diff --git a/NEWS b/NEWS index 0652b7898..95f2e0c18 100644 --- a/NEWS +++ b/NEWS @@ -32,6 +32,11 @@ * (Experimental) Added property `RegisteredCredential.transports`. ** NOTE: Experimental features may receive breaking changes without a major version increase. +* (Experimental) Added option `FinishRegistrationOptions.isConditionalCreate` to + allow UP=0 in registration response for registration ceremonies with + `mediation: "conditional"`. + ** NOTE: Experimental features may receive breaking changes without a major + version increase. == Version 2.6.0 == diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishRegistrationOptions.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishRegistrationOptions.java index 8dbc3cff2..6c734008a 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishRegistrationOptions.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishRegistrationOptions.java @@ -61,6 +61,25 @@ public class FinishRegistrationOptions { */ private final ByteArray callerTokenBindingId; + /** + * If true, then user presence (UP) will not be required during registration + * verification. This is needed for conditional + * create operations. + * + *

The default is false (UP is required). + * + * @deprecated EXPERIMENTAL: This feature is from a not yet mature standard; it could change as + * the standard matures. + * @since 2.7.0 + * @see + * conditionalCreate client capability. + * @see UP + * flag in authenticator data. + */ + @Deprecated @Builder.Default private final boolean isConditionalCreate = false; + /** * The token binding ID of the * connection to the client, if any. diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishRegistrationSteps.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishRegistrationSteps.java index a7ec85725..512274465 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishRegistrationSteps.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishRegistrationSteps.java @@ -89,6 +89,7 @@ final class FinishRegistrationSteps { private final Clock clock; private final boolean allowOriginPort; private final boolean allowOriginSubdomain; + private final boolean isConditionalCreate; static FinishRegistrationSteps fromV1(RelyingParty rp, FinishRegistrationOptions options) { return new FinishRegistrationSteps( @@ -102,7 +103,8 @@ static FinishRegistrationSteps fromV1(RelyingParty rp, FinishRegistrationOptions new CredentialRepositoryV1ToV2Adapter(rp.getCredentialRepository()), rp.getClock(), rp.isAllowOriginPort(), - rp.isAllowOriginSubdomain()); + rp.isAllowOriginSubdomain(), + options.isConditionalCreate()); } FinishRegistrationSteps(RelyingPartyV2 rp, FinishRegistrationOptions options) { @@ -117,7 +119,8 @@ static FinishRegistrationSteps fromV1(RelyingParty rp, FinishRegistrationOptions rp.getCredentialRepository(), rp.getClock(), rp.isAllowOriginPort(), - rp.isAllowOriginSubdomain()); + rp.isAllowOriginSubdomain(), + options.isConditionalCreate()); } public Step6 begin() { @@ -303,8 +306,8 @@ class Step14 implements Step { @Override public void validate() { assertTrue( - response.getResponse().getParsedAuthenticatorData().getFlags().UP, - "User Presence is required."); + isConditionalCreate || response.getResponse().getParsedAuthenticatorData().getFlags().UP, + "User Presence is required unless isConditionalCreate is true."); } @Override diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyRegistrationSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyRegistrationSpec.scala index 68c03ec9b..17e6f34b1 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyRegistrationSpec.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyRegistrationSpec.scala @@ -164,6 +164,7 @@ class RelyingPartyRegistrationSpec pubkeyCredParams: Option[List[PublicKeyCredentialParameters]] = None, testData: RegistrationTestData, clock: Clock = Clock.systemUTC(), + isConditionalCreate: Boolean = false, ): FinishRegistrationSteps = { var builder = RelyingParty .builder() @@ -191,6 +192,7 @@ class RelyingPartyRegistrationSpec ) .response(testData.response) .callerTokenBindingId(callerTokenBindingId.toJava) + .isConditionalCreate(isConditionalCreate) .build() builder @@ -873,6 +875,7 @@ class RelyingPartyRegistrationSpec )(chk: Step => B)( uvr: UserVerificationRequirement, authDataEdit: ByteArray => ByteArray, + isConditionalCreate: Boolean, ): B = { val steps = finishRegistration( testData = testData @@ -884,14 +887,19 @@ class RelyingPartyRegistrationSpec .build() ) ) - .editAuthenticatorData(authDataEdit) + .editAuthenticatorData(authDataEdit), + isConditionalCreate = isConditionalCreate, ) chk(stepsToStep(steps)) } def checkFailsWith( stepsToStep: FinishRegistrationSteps => Step - ): (UserVerificationRequirement, ByteArray => ByteArray) => Unit = + ): ( + UserVerificationRequirement, + ByteArray => ByteArray, + Boolean, + ) => Unit = check(stepsToStep) { step => step.validations shouldBe a[Failure[_]] step.validations.failed.get shouldBe an[ @@ -902,7 +910,11 @@ class RelyingPartyRegistrationSpec def checkSucceedsWith( stepsToStep: FinishRegistrationSteps => Step - ): (UserVerificationRequirement, ByteArray => ByteArray) => Unit = + ): ( + UserVerificationRequirement, + ByteArray => ByteArray, + Boolean, + ) => Unit = check(stepsToStep) { step => step.validations shouldBe a[Success[_]] step.tryNext shouldBe a[Success[_]] @@ -912,15 +924,41 @@ class RelyingPartyRegistrationSpec } describe("14. Verify that the User Present bit of the flags in authData is set.") { - val (checkFails, checkSucceeds) = checks[ + val (chkf, chks) = checks[ FinishRegistrationSteps#Step15, FinishRegistrationSteps#Step14, ](_.begin.next.next.next.next.next.next.next.next) + def checkFails( + uvr: UserVerificationRequirement, + authDataEdit: ByteArray => ByteArray, + isConditionalCreate: Boolean = false, + ): Unit = chkf(uvr, authDataEdit, isConditionalCreate) + def checkSucceeds( + uvr: UserVerificationRequirement, + authDataEdit: ByteArray => ByteArray, + isConditionalCreate: Boolean = false, + ): Unit = chks(uvr, authDataEdit, isConditionalCreate) it("Fails if UV is discouraged and flag is not set.") { checkFails(UserVerificationRequirement.DISCOURAGED, upOff) } + it("Fails if UV is discouraged, isConditionalCreate is false and flag is not set.") { + checkFails( + UserVerificationRequirement.DISCOURAGED, + upOff, + isConditionalCreate = false, + ) + } + + it("Succeeds if UV is discouraged, isConditionalCreate is true and flag is not set.") { + checkSucceeds( + UserVerificationRequirement.DISCOURAGED, + upOff, + isConditionalCreate = true, + ) + } + it("Succeeds if UV is discouraged and flag is set.") { checkSucceeds(UserVerificationRequirement.DISCOURAGED, upOn) } @@ -949,10 +987,20 @@ class RelyingPartyRegistrationSpec } describe("15. If user verification is required for this registration, verify that the User Verified bit of the flags in authData is set.") { - val (checkFails, checkSucceeds) = checks[ + val (chkf, chks) = checks[ FinishRegistrationSteps#Step16, FinishRegistrationSteps#Step15, ](_.begin.next.next.next.next.next.next.next.next.next) + def checkFails( + uvr: UserVerificationRequirement, + authDataEdit: ByteArray => ByteArray, + isConditionalCreate: Boolean = false, + ): Unit = chkf(uvr, authDataEdit, isConditionalCreate) + def checkSucceeds( + uvr: UserVerificationRequirement, + authDataEdit: ByteArray => ByteArray, + isConditionalCreate: Boolean = false, + ): Unit = chks(uvr, authDataEdit, isConditionalCreate) it("Succeeds if UV is discouraged and flag is not set.") { checkSucceeds(UserVerificationRequirement.DISCOURAGED, uvOff)