diff --git a/NEWS b/NEWS index cb61dfcce..0652b7898 100644 --- a/NEWS +++ b/NEWS @@ -2,6 +2,7 @@ * Added overloaded setter `RelyingPartyBuilder.origins(Optional>)`. * Added support for the CTAP2 `credProtect` extension. +* Added support for the `prf` extension. * (Experimental) Added a new suite of interfaces, starting with `CredentialRepositoryV2`. `RelyingParty` can now be configured with a `CredentialRepositoryV2` instance instead of a `CredentialRepository` diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AssertionExtensionInputs.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AssertionExtensionInputs.java index 18d259de0..abc744a90 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AssertionExtensionInputs.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AssertionExtensionInputs.java @@ -31,6 +31,7 @@ import com.yubico.webauthn.StartAssertionOptions; import com.yubico.webauthn.extension.appid.AppId; import java.util.HashSet; +import java.util.Map; import java.util.Optional; import java.util.Set; import lombok.Builder; @@ -55,15 +56,18 @@ public class AssertionExtensionInputs implements ExtensionInputs { private final AppId appid; private final Extensions.LargeBlob.LargeBlobAuthenticationInput largeBlob; + private final Extensions.Prf.PrfAuthenticationInput prf; private final Boolean uvm; @JsonCreator private AssertionExtensionInputs( @JsonProperty("appid") AppId appid, @JsonProperty("largeBlob") Extensions.LargeBlob.LargeBlobAuthenticationInput largeBlob, + @JsonProperty("prf") Extensions.Prf.PrfAuthenticationInput prf, @JsonProperty("uvm") Boolean uvm) { this.appid = appid; this.largeBlob = largeBlob; + this.prf = prf; this.uvm = (uvm != null && uvm) ? true : null; } @@ -78,6 +82,7 @@ public AssertionExtensionInputs merge(AssertionExtensionInputs other) { return new AssertionExtensionInputs( this.appid != null ? this.appid : other.appid, this.largeBlob != null ? this.largeBlob : other.largeBlob, + this.prf != null ? this.prf : other.prf, this.uvm != null ? this.uvm : other.uvm); } @@ -95,6 +100,9 @@ public Set getExtensionIds() { if (largeBlob != null) { ids.add(Extensions.LargeBlob.EXTENSION_ID); } + if (prf != null) { + ids.add(Extensions.Prf.EXTENSION_ID); + } if (getUvm()) { ids.add(Extensions.Uvm.EXTENSION_ID); } @@ -172,6 +180,37 @@ public AssertionExtensionInputsBuilder largeBlob( return this; } + /** + * Enable the Pseudo-random function extension (prf). + * + *

This extension allows a Relying Party to evaluate outputs from a pseudo-random function + * (PRF) associated with a credential. + * + *

Use the {@link com.yubico.webauthn.data.Extensions.Prf.PrfAuthenticationInput} factory + * functions to construct the argument: + * + *

+ * + * @see Extensions.Prf.PrfAuthenticationInput#eval(Extensions.Prf.PrfValues) + * @see Extensions.Prf.PrfAuthenticationInput#evalByCredential(Map) + * @see Extensions.Prf.PrfAuthenticationInput#evalByCredentialWithFallback(Map, + * Extensions.Prf.PrfValues) + * @see §10.1.4. + * Pseudo-random function extension (prf) + */ + public AssertionExtensionInputsBuilder prf(Extensions.Prf.PrfAuthenticationInput prf) { + this.prf = prf; + return this; + } + /** * Enable the User Verification Method Extension (uvm). * @@ -233,6 +272,31 @@ private Extensions.LargeBlob.LargeBlobAuthenticationInput getLargeBlobJson() { : null; } + /** + * The input to the Pseudo-random function extension (prf), if any. + * + *

This extension allows a Relying Party to evaluate outputs from a pseudo-random function + * (PRF) associated with a credential. + * + * @see Extensions.Prf.PrfAuthenticationInput#eval(Extensions.Prf.PrfValues) + * @see Extensions.Prf.PrfAuthenticationInput#evalByCredential(Map) + * @see Extensions.Prf.PrfAuthenticationInput#evalByCredentialWithFallback(Map, + * Extensions.Prf.PrfValues) + * @see §10.1.4. + * Pseudo-random function extension (prf) + */ + public Optional getPrf() { + return Optional.ofNullable(prf); + } + + /** For JSON serialization, to omit false and null values. */ + @JsonProperty("prf") + private Extensions.Prf.PrfAuthenticationInput getPrfJson() { + return prf != null && (prf.getEval().isPresent() || prf.getEvalByCredential().isPresent()) + ? prf + : null; + } + /** * @return true if the User Verification Method Extension (uvm) is * enabled, false otherwise. diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorAssertionExtensionOutputs.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorAssertionExtensionOutputs.java index ea47dc454..5cc76b9c4 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorAssertionExtensionOutputs.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorAssertionExtensionOutputs.java @@ -41,7 +41,7 @@ /** * Contains authenticator - * extension outputs from a navigator.credentials.create() operation. + * extension outputs from a navigator.credentials.get() operation. * *

Note that there is no guarantee that any extension input present in {@link * RegistrationExtensionInputs} will have a corresponding output present here. diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/ClientAssertionExtensionOutputs.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/ClientAssertionExtensionOutputs.java index 3c6579d66..e10f4f92b 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/ClientAssertionExtensionOutputs.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/ClientAssertionExtensionOutputs.java @@ -66,12 +66,16 @@ public class ClientAssertionExtensionOutputs implements ClientExtensionOutputs { private final Extensions.LargeBlob.LargeBlobAuthenticationOutput largeBlob; + private final Extensions.Prf.PrfAuthenticationOutput prf; + @JsonCreator private ClientAssertionExtensionOutputs( @JsonProperty("appid") Boolean appid, - @JsonProperty("largeBlob") Extensions.LargeBlob.LargeBlobAuthenticationOutput largeBlob) { + @JsonProperty("largeBlob") Extensions.LargeBlob.LargeBlobAuthenticationOutput largeBlob, + @JsonProperty("prf") Extensions.Prf.PrfAuthenticationOutput prf) { this.appid = appid; this.largeBlob = largeBlob; + this.prf = prf; } @Override @@ -84,6 +88,9 @@ public Set getExtensionIds() { if (largeBlob != null) { ids.add(Extensions.LargeBlob.EXTENSION_ID); } + if (prf != null) { + ids.add(Extensions.Prf.EXTENSION_ID); + } return ids; } @@ -105,7 +112,7 @@ public Optional getAppid() { * href="https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#sctn-large-blob-extension">Large blob * storage (largeBlob) extension, if any. * - * @see com.yubico.webauthn.data.Extensions.LargeBlob.LargeBlobRegistrationOutput + * @see com.yubico.webauthn.data.Extensions.LargeBlob.LargeBlobAuthenticationOutput * @see §10.5.Large * blob storage extension (largeBlob) @@ -114,6 +121,19 @@ public Optional getLargeBlob return Optional.ofNullable(largeBlob); } + /** + * The extension output for the Pseudo-random function + * (prf) extension, if any. + * + * @see com.yubico.webauthn.data.Extensions.Prf.PrfAuthenticationOutput + * @see §10.1.4. + * Pseudo-random function extension (prf) + */ + public Optional getPrf() { + return Optional.ofNullable(prf); + } + public static class ClientAssertionExtensionOutputsBuilder { /** diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/ClientRegistrationExtensionOutputs.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/ClientRegistrationExtensionOutputs.java index 83525e942..364a41d70 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/ClientRegistrationExtensionOutputs.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/ClientRegistrationExtensionOutputs.java @@ -58,15 +58,19 @@ public class ClientRegistrationExtensionOutputs implements ClientExtensionOutput private final Extensions.LargeBlob.LargeBlobRegistrationOutput largeBlob; + private final Extensions.Prf.PrfRegistrationOutput prf; + @JsonCreator private ClientRegistrationExtensionOutputs( @JsonProperty("appidExclude") Boolean appidExclude, @JsonProperty("credProps") Extensions.CredentialProperties.CredentialPropertiesOutput credProps, - @JsonProperty("largeBlob") Extensions.LargeBlob.LargeBlobRegistrationOutput largeBlob) { + @JsonProperty("largeBlob") Extensions.LargeBlob.LargeBlobRegistrationOutput largeBlob, + @JsonProperty("prf") Extensions.Prf.PrfRegistrationOutput prf) { this.appidExclude = appidExclude; this.credProps = credProps; this.largeBlob = largeBlob; + this.prf = prf; } @Override @@ -82,6 +86,9 @@ public Set getExtensionIds() { if (largeBlob != null) { ids.add(Extensions.LargeBlob.EXTENSION_ID); } + if (prf != null) { + ids.add(Extensions.Prf.EXTENSION_ID); + } return ids; } @@ -127,4 +134,17 @@ public Optional getC public Optional getLargeBlob() { return Optional.ofNullable(largeBlob); } + + /** + * The extension output for the Pseudo-random function + * (prf) extension, if any. + * + * @see com.yubico.webauthn.data.Extensions.Prf.PrfRegistrationOutput + * @see §10.1.4. + * Pseudo-random function extension (prf) + */ + public Optional getPrf() { + return Optional.ofNullable(prf); + } } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/Extensions.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/Extensions.java index f954c0d8d..3ff16f1ad 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/Extensions.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/Extensions.java @@ -15,7 +15,10 @@ import com.yubico.webauthn.extension.uvm.MatcherProtectionType; import com.yubico.webauthn.extension.uvm.UserVerificationMethod; import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -745,6 +748,449 @@ public Optional getWritten() { } } + /** + * Definitions for the Pseudo-random function extension (prf). + * + * @see §10.1.4. + * Pseudo-random function extension (prf) + */ + public static class Prf { + static final String EXTENSION_ID = "prf"; + + /** + * One or two inputs to or outputs from the pseudo-random function (PRF) associated with a + * credential. + * + *

{@link #getFirst()} is always present, but {@link #getSecond()} is empty when only one + * input or output was given. + * + * @see #one(ByteArray) + * @see #two(ByteArray, ByteArray) + * @see #oneOrTwo(ByteArray, Optional) + * @see dictionary + * AuthenticationExtensionsPRFValues + * @see §10.1.4. + * Pseudo-random function extension (prf) + */ + @Value + public static class PrfValues { + /** + * The first PRF input to evaluate, or the result of that evaluation. + * + * @see AuthenticationExtensionsPRFValues.first + * @see §10.1.4. + * Pseudo-random function extension (prf) + */ + @JsonProperty @NonNull private final ByteArray first; + + /** + * The second PRF input to evaluate, if any, or the result of that evaluation. + * + * @see AuthenticationExtensionsPRFValues.second + * @see §10.1.4. + * Pseudo-random function extension (prf) + */ + @JsonProperty private final ByteArray second; + + @JsonCreator + private PrfValues( + @JsonProperty("first") @NonNull final ByteArray first, + @JsonProperty("second") final ByteArray second) { + this.first = first; + this.second = second; + } + + /** + * The second PRF input to evaluate, if any, or the result of that evaluation. + * + * @see AuthenticationExtensionsPRFValues.second + */ + public Optional getSecond() { + return Optional.ofNullable(second); + } + + /** + * Construct a {@link PrfValues} with a single PRF input or output. + * + * @param first the PRF input or output. Must not be null. + * @see dictionary + * AuthenticationExtensionsPRFValues + * @see §10.1.4. + * Pseudo-random function extension (prf) + */ + public static PrfValues one(@NonNull ByteArray first) { + return new PrfValues(first, null); + } + + /** + * Construct a {@link PrfValues} with two PRF inputs or outputs. + * + * @param first the first PRF input or output. Must not be null. + * @param second the second PRF input or output. Must not be null. + * @see dictionary + * AuthenticationExtensionsPRFValues + * @see §10.1.4. + * Pseudo-random function extension (prf) + */ + public static PrfValues two(@NonNull ByteArray first, @NonNull ByteArray second) { + return new PrfValues(first, second); + } + + /** + * Construct a {@link PrfValues} with two PRF inputs or outputs if second is + * present, otherwise a {@link PrfValues} with one inputs or output. + * + * @param first the first PRF input or output. Must not be null. + * @param second the second PRF input or output, if any. Must not be null, but may be empty. + * @see dictionary + * AuthenticationExtensionsPRFValues + * @see §10.1.4. + * Pseudo-random function extension (prf) + */ + public static PrfValues oneOrTwo( + @NonNull ByteArray first, @NonNull Optional second) { + return new PrfValues(first, second.orElse(null)); + } + } + + /** + * Inputs for the Pseudo-random function extension (prf) in authentication + * ceremonies. + * + * @see dictionary + * AuthenticationExtensionsPRFInputs + * @see §10.1.4. + * Pseudo-random function extension (prf) + */ + @Value + public static class PrfAuthenticationInput { + /** + * PRF inputs to use for any credential without a dedicated input listed in {@link + * #getEvalByCredential()}. + * + * @see #eval(PrfValues) + * @see #evalByCredentialWithFallback(Map, PrfValues) + * @see AuthenticationExtensionsPRFInputs.eval + * @see §10.1.4. + * Pseudo-random function extension (prf) + */ + @JsonProperty private final PrfValues eval; + + /** + * A map of credential IDs to PRF inputs to use for that credential. Credentials without a + * mapping here fall back to the inputs in {@link #getEval()} if present, otherwise no PRF is + * evaluated for those credentials. + * + * @see #evalByCredential(Map) + * @see #evalByCredentialWithFallback(Map, PrfValues) + * @see AuthenticationExtensionsPRFInputs.evalByCredential + * @see §10.1.4. + * Pseudo-random function extension (prf) + */ + @JsonProperty private final Map evalByCredential; + + @JsonCreator + private PrfAuthenticationInput( + @JsonProperty("eval") PrfValues eval, + @JsonProperty("evalByCredential") Map evalByCredential) { + this.eval = eval; + this.evalByCredential = + evalByCredential == null ? null : Collections.unmodifiableMap(evalByCredential); + } + + /** + * PRF inputs to use for any credential without a dedicated input listed in {@link + * #getEvalByCredential()}. + * + * @see #eval(PrfValues) + * @see #evalByCredentialWithFallback(Map, PrfValues) + * @see AuthenticationExtensionsPRFInputs.eval + * @see §10.1.4. + * Pseudo-random function extension (prf) + */ + public Optional getEval() { + return Optional.ofNullable(eval); + } + + /** + * A map of credential IDs to PRF inputs to use for that credential. Credentials without a + * mapping here fall back to the inputs in {@link #getEval()} if present, otherwise no PRF is + * evaluated for those credentials. + * + * @see #evalByCredential(Map) + * @see #evalByCredentialWithFallback(Map, PrfValues) + * @see AuthenticationExtensionsPRFInputs.evalByCredential + * @see §10.1.4. + * Pseudo-random function extension (prf) + */ + public Optional> getEvalByCredential() { + return Optional.ofNullable(evalByCredential); + } + + private static HashMap descriptorsToIds( + Map evalByCredential) { + return evalByCredential.entrySet().stream() + .reduce( + new HashMap<>(), + (ebc, entry) -> { + ebc.put(entry.getKey().getId(), entry.getValue()); + return ebc; + }, + (a, b) -> { + a.putAll(b); + return a; + }); + } + + /** + * Use the same PRF inputs for all credentials. + * + * @see #getEval() + * @see AuthenticationExtensionsPRFInputs.eval + * @see §10.1.4. + * Pseudo-random function extension (prf) + */ + public static PrfAuthenticationInput eval(@NonNull PrfValues eval) { + return new PrfAuthenticationInput(eval, null); + } + + /** + * Use different PRF inputs for different credentials, and skip PRF evaluation for any + * credentials not present in the map. + * + * @see #getEvalByCredential() + * @see AuthenticationExtensionsPRFInputs.evalByCredential + * @see §10.1.4. + * Pseudo-random function extension (prf) + */ + public static PrfAuthenticationInput evalByCredential( + @NonNull Map evalByCredential) { + return new PrfAuthenticationInput(null, descriptorsToIds(evalByCredential)); + } + + /** + * Use different PRF inputs for different credentials, and "fallback" inputs for any + * credentials not present in the map. + * + * @param evalByCredential a map of credential IDs to PRF inputs to use for that credential. + * @param eval "fallback" inputs to use for any credential not listed in + * evalByCredential. + * @see #getEvalByCredential() + * @see #getEval() () + * @see AuthenticationExtensionsPRFInputs.evalByCredential + * @see AuthenticationExtensionsPRFInputs.eval + * @see §10.1.4. + * Pseudo-random function extension (prf) + */ + public static PrfAuthenticationInput evalByCredentialWithFallback( + @NonNull Map evalByCredential, + @NonNull PrfValues eval) { + return new PrfAuthenticationInput(eval, descriptorsToIds(evalByCredential)); + } + } + + /** + * Inputs for the Pseudo-random function extension (prf) in registration + * ceremonies. + * + * @see dictionary + * AuthenticationExtensionsPRFInputs + * @see §10.1.4. + * Pseudo-random function extension (prf) + */ + @Value + public static class PrfRegistrationInput { + /** + * PRF inputs to evaluate immediately if possible. Note that not all authenticators support + * this, in which case a follow-up authentication ceremony may be needed in order to evaluate + * the PRF. + * + * @see #eval(PrfValues) + * @see AuthenticationExtensionsPRFInputs.eval + * @see §10.1.4. + * Pseudo-random function extension (prf) + */ + @JsonProperty private final PrfValues eval; + + @JsonCreator + private PrfRegistrationInput(@JsonProperty("eval") PrfValues eval) { + this.eval = eval; + } + + /** + * PRF inputs to evaluate immediately if possible. Note that not all authenticators support + * this, in which case a follow-up authentication ceremony may be needed in order to evaluate + * the PRF. + * + * @see #eval(PrfValues) + * @see AuthenticationExtensionsPRFInputs.eval + * @see §10.1.4. + * Pseudo-random function extension (prf) + */ + public Optional getEval() { + return Optional.ofNullable(eval); + } + + /** + * Enable PRF for the created credential, without evaluating the PRF at this time. + * + * @see #eval(PrfValues) + * @see §10.1.4. + * Pseudo-random function extension (prf) + */ + public static PrfRegistrationInput enable() { + return new PrfRegistrationInput(null); + } + + /** + * Enable PRF for the created credential, and attempt to immediately evaluate the PRF with the + * given inputs. Note that not all authenticators support this, in which case a follow-up + * authentication ceremony may be needed in order to evaluate the PRF. + * + * @see #enable() + * @see #getEval() + * @see AuthenticationExtensionsPRFInputs.eval + * @see §10.1.4. + * Pseudo-random function extension (prf) + */ + public static PrfRegistrationInput eval(@NonNull PrfValues eval) { + return new PrfRegistrationInput(eval); + } + } + + /** + * Outputs for the Pseudo-random function extension (prf) in registration + * ceremonies. + * + * @see dictionary + * AuthenticationExtensionsPRFOutputs + * @see §10.1.4. + * Pseudo-random function extension (prf) + */ + @Value + public static class PrfRegistrationOutput { + + /** + * true if, and only if, a PRF is available for use with the created credential. + * + * @see AuthenticationExtensionsPRFOutputs.enabled + * @see §10.1.4. + * Pseudo-random function extension (prf) + */ + @JsonProperty private final Boolean enabled; + + /** + * The results of evaluating the PRF for the inputs given in {@link + * PrfRegistrationInput#getEval() eval}, if any. + * + * @see AuthenticationExtensionsPRFOutputs.results + * @see §10.1.4. + * Pseudo-random function extension (prf) + */ + @JsonProperty private final PrfValues results; + + @JsonCreator + PrfRegistrationOutput( + @JsonProperty("enabled") Boolean enabled, @JsonProperty("results") PrfValues results) { + this.enabled = enabled; + this.results = results; + } + + /** + * true if, and only if, a PRF is available for use with the created credential. + * + * @see AuthenticationExtensionsPRFOutputs.enabled + * @see §10.1.4. + * Pseudo-random function extension (prf) + */ + public Optional getEnabled() { + return Optional.ofNullable(enabled); + } + + /** + * The results of evaluating the PRF for the inputs given in {@link + * PrfRegistrationInput#getEval() eval}, if any. + * + * @see AuthenticationExtensionsPRFOutputs.results + * @see §10.1.4. + * Pseudo-random function extension (prf) + */ + public Optional getResults() { + return Optional.ofNullable(results); + } + } + + /** + * Outputs for the Pseudo-random function extension (prf) in authentication + * ceremonies. + * + * @see dictionary + * AuthenticationExtensionsPRFOutputs + * @see §10.1.4. + * Pseudo-random function extension (prf) + */ + @Value + public static class PrfAuthenticationOutput { + + /** + * The results of evaluating the PRF for the inputs given in {@link + * PrfAuthenticationInput#getEval() eval} or {@link + * PrfAuthenticationInput#getEvalByCredential() evalByCredential}, if any. + * + * @see AuthenticationExtensionsPRFOutputs.results + * @see §10.1.4. + * Pseudo-random function extension (prf) + */ + @JsonProperty private final PrfValues results; + + @JsonCreator + PrfAuthenticationOutput(@JsonProperty("results") PrfValues results) { + this.results = results; + } + + /** + * The results of evaluating the PRF for the inputs given in {@link + * PrfAuthenticationInput#getEval() eval} or {@link + * PrfAuthenticationInput#getEvalByCredential() evalByCredential}, if any. + * + * @see AuthenticationExtensionsPRFOutputs.results + * @see §10.1.4. + * Pseudo-random function extension (prf) + */ + public Optional getResults() { + return Optional.ofNullable(results); + } + } + } + /** * Definitions for the User Verification Method (uvm) Extension. * 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 6cc724e95..5ab22c099 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 @@ -57,6 +57,7 @@ public final class RegistrationExtensionInputs implements ExtensionInputs { private final Boolean credProps; private final Extensions.CredentialProtection.CredentialProtectionInput credProtect; private final Extensions.LargeBlob.LargeBlobRegistrationInput largeBlob; + private final Extensions.Prf.PrfRegistrationInput prf; private final Boolean uvm; @JsonCreator @@ -66,11 +67,13 @@ private RegistrationExtensionInputs( @JsonProperty("credProtect") Extensions.CredentialProtection.CredentialProtectionInput credProtect, @JsonProperty("largeBlob") Extensions.LargeBlob.LargeBlobRegistrationInput largeBlob, + @JsonProperty("prf") Extensions.Prf.PrfRegistrationInput prf, @JsonProperty("uvm") Boolean uvm) { this.appidExclude = appidExclude; this.credProps = credProps; this.credProtect = credProtect; this.largeBlob = largeBlob; + this.prf = prf; this.uvm = uvm; } @@ -87,6 +90,7 @@ public RegistrationExtensionInputs merge(RegistrationExtensionInputs other) { this.credProps != null ? this.credProps : other.credProps, this.credProtect != null ? this.credProtect : other.credProtect, this.largeBlob != null ? this.largeBlob : other.largeBlob, + this.prf != null ? this.prf : other.prf, this.uvm != null ? this.uvm : other.uvm); } @@ -147,6 +151,21 @@ public Optional getLargeBlob() return Optional.ofNullable(largeBlob); } + /** + * The input to the Pseudo-random function extension (prf), if any. + * + *

This extension allows a Relying Party to evaluate outputs from a pseudo-random function + * (PRF) associated with a credential. + * + * @see Extensions.Prf.PrfRegistrationInput#enable() + * @see Extensions.Prf.PrfRegistrationInput#eval(Extensions.Prf.PrfValues) + * @see §10.1.4. + * Pseudo-random function extension (prf) + */ + public Optional getPrf() { + return Optional.ofNullable(prf); + } + /** * @return true if the User Verification Method Extension (uvm) is * enabled, false otherwise. @@ -184,6 +203,9 @@ public Set getExtensionIds() { if (largeBlob != null) { ids.add(Extensions.LargeBlob.EXTENSION_ID); } + if (prf != null) { + ids.add(Extensions.Prf.EXTENSION_ID); + } if (getUvm()) { ids.add(Extensions.Uvm.EXTENSION_ID); } @@ -330,6 +352,34 @@ public RegistrationExtensionInputsBuilder largeBlob( return this; } + /** + * Enable the Pseudo-random function extension (prf). + * + *

This extension allows a Relying Party to evaluate outputs from a pseudo-random function + * (PRF) associated with a credential. + * + *

Use the {@link com.yubico.webauthn.data.Extensions.Prf.PrfRegistrationInput} factory + * functions to construct the argument: + * + *

+ * + * @see Extensions.Prf.PrfRegistrationInput#enable() + * @see Extensions.Prf.PrfRegistrationInput#eval(Extensions.Prf.PrfValues) + * @see §10.1.4. + * Pseudo-random function extension (prf) + */ + public RegistrationExtensionInputsBuilder prf(Extensions.Prf.PrfRegistrationInput prf) { + this.prf = prf; + return this; + } + /** * Enable the User Verification Method Extension (uvm). * diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyAssertionSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyAssertionSpec.scala index 4d8d8fc8b..21eacef5d 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyAssertionSpec.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyAssertionSpec.scala @@ -41,7 +41,9 @@ import com.yubico.webauthn.data.ClientAssertionExtensionOutputs import com.yubico.webauthn.data.CollectedClientData import com.yubico.webauthn.data.Extensions.LargeBlob.LargeBlobAuthenticationInput import com.yubico.webauthn.data.Extensions.LargeBlob.LargeBlobAuthenticationOutput +import com.yubico.webauthn.data.Extensions.Prf.PrfAuthenticationOutput import com.yubico.webauthn.data.Extensions.Uvm.UvmEntry +import com.yubico.webauthn.data.Generators.Extensions.Prf.arbitraryPrfAuthenticationOutput import com.yubico.webauthn.data.Generators._ import com.yubico.webauthn.data.PublicKeyCredential import com.yubico.webauthn.data.PublicKeyCredentialCreationOptions @@ -2579,6 +2581,33 @@ class RelyingPartyAssertionSpec } } + it("pass through prf extension outputs when present.") { + forAll(minSuccessful(3)) { prfOutput: PrfAuthenticationOutput => + val result = rp.finishAssertion( + FinishAssertionOptions + .builder() + .request( + testDataBase.assertion.get.request + ) + .response( + testDataBase.assertion.get.response.toBuilder + .clientExtensionResults( + ClientAssertionExtensionOutputs + .builder() + .prf(prfOutput) + .build() + ) + .build() + ) + .build() + ) + + result.getClientExtensionOutputs.get().getPrf.toScala should equal( + Some(prfOutput) + ) + } + } + describe("support the uvm extension") { it("at authentication time.") { 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 a663a01f8..34c4ccec2 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 @@ -57,7 +57,9 @@ import com.yubico.webauthn.data.Extensions.CredentialProtection.CredentialProtec import com.yubico.webauthn.data.Extensions.CredentialProtection.CredentialProtectionPolicy import com.yubico.webauthn.data.Extensions.LargeBlob.LargeBlobRegistrationInput.LargeBlobSupport import com.yubico.webauthn.data.Extensions.LargeBlob.LargeBlobRegistrationOutput +import com.yubico.webauthn.data.Extensions.Prf.PrfRegistrationOutput import com.yubico.webauthn.data.Extensions.Uvm.UvmEntry +import com.yubico.webauthn.data.Generators.Extensions.Prf.arbitraryPrfRegistrationOutput import com.yubico.webauthn.data.Generators._ import com.yubico.webauthn.data.PublicKeyCredential import com.yubico.webauthn.data.PublicKeyCredentialCreationOptions @@ -4550,6 +4552,31 @@ class RelyingPartyRegistrationSpec } } + it("pass through prf extension outputs when present.") { + forAll(minSuccessful(3)) { prfOutput: PrfRegistrationOutput => + val testData = RegistrationTestData.Packed.BasicAttestation + val result = rp.finishRegistration( + FinishRegistrationOptions + .builder() + .request(testData.request) + .response( + testData.response.toBuilder + .clientExtensionResults( + ClientRegistrationExtensionOutputs + .builder() + .prf(prfOutput) + .build() + ) + .build() + ) + .build() + ) + result.getClientExtensionOutputs.get().getPrf.toScala should equal( + Some(prfOutput) + ) + } + } + describe("support the uvm extension") { it("at registration time.") { diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyStartOperationSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyStartOperationSpec.scala index aa552e978..61a48c921 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyStartOperationSpec.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyStartOperationSpec.scala @@ -34,6 +34,10 @@ import com.yubico.webauthn.data.AuthenticatorTransport import com.yubico.webauthn.data.ByteArray import com.yubico.webauthn.data.Extensions.CredentialProtection.CredentialProtectionInput import com.yubico.webauthn.data.Extensions.CredentialProtection.CredentialProtectionPolicy +import com.yubico.webauthn.data.Extensions.Prf.PrfAuthenticationInput +import com.yubico.webauthn.data.Extensions.Prf.PrfRegistrationInput +import com.yubico.webauthn.data.Generators.Extensions.Prf.arbitraryPrfAuthenticationInput +import com.yubico.webauthn.data.Generators.Extensions.Prf.arbitraryPrfRegistrationInput import com.yubico.webauthn.data.Generators.Extensions.registrationExtensionInputs import com.yubico.webauthn.data.Generators._ import com.yubico.webauthn.data.PublicKeyCredentialCreationOptions @@ -578,6 +582,40 @@ class RelyingPartyStartOperationSpec } } + it("by default does not set the prf extension.") { + val rp = relyingParty(userId = userId) + val result = rp.startRegistration( + StartRegistrationOptions + .builder() + .user(userId) + .build() + ) + result.getExtensions.getPrf.toScala should be( + None + ) + } + + it("sets the prf extension if enabled in StartRegistrationOptions.") { + forAll { + ( + extensions: RegistrationExtensionInputs, + prf: PrfRegistrationInput, + ) => + val rp = relyingParty(userId = userId) + val result = rp.startRegistration( + StartRegistrationOptions + .builder() + .user(userId) + .extensions(extensions.toBuilder.prf(prf).build()) + .build() + ) + + result.getExtensions.getPrf.toScala should equal( + Some(prf) + ) + } + } + it("respects the residentKey setting.") { val rp = relyingParty(userId = userId) @@ -1063,6 +1101,35 @@ class RelyingPartyStartOperationSpec ) } } + + it("by default does not set the prf extension.") { + val rp = relyingParty(userId = userId) + val result = rp.startAssertion( + StartAssertionOptions + .builder() + .build() + ) + result.getPublicKeyCredentialRequestOptions.getExtensions.getPrf.toScala should be( + None + ) + } + + it("sets the prf extension if enabled in StartAssertionOptions.") { + forAll { + (extensions: AssertionExtensionInputs, prf: PrfAuthenticationInput) => + val rp = relyingParty(userId = userId) + val result = rp.startAssertion( + StartAssertionOptions + .builder() + .extensions(extensions.toBuilder.prf(prf).build()) + .build() + ) + + result.getPublicKeyCredentialRequestOptions.getExtensions.getPrf.toScala should equal( + Some(prf) + ) + } + } } } @@ -1557,6 +1624,40 @@ class RelyingPartyStartOperationSpec } } + it("by default does not set the prf extension.") { + val rp = relyingParty(userId = userId) + val result = rp.startRegistration( + StartRegistrationOptions + .builder() + .user(userId) + .build() + ) + result.getExtensions.getPrf.toScala should be( + None + ) + } + + it("sets the prf extension if enabled in StartRegistrationOptions.") { + forAll { + ( + extensions: RegistrationExtensionInputs, + prf: PrfRegistrationInput, + ) => + val rp = relyingParty(userId = userId) + val result = rp.startRegistration( + StartRegistrationOptions + .builder() + .user(userId) + .extensions(extensions.toBuilder.prf(prf).build()) + .build() + ) + + result.getExtensions.getPrf.toScala should equal( + Some(prf) + ) + } + } + it("respects the residentKey setting.") { val rp = relyingParty(userId = userId) @@ -2060,6 +2161,35 @@ class RelyingPartyStartOperationSpec ) } } + + it("by default does not set the prf extension.") { + val rp = relyingParty(userId = userId) + val result = rp.startAssertion( + StartAssertionOptions + .builder() + .build() + ) + result.getPublicKeyCredentialRequestOptions.getExtensions.getPrf.toScala should be( + None + ) + } + + it("sets the prf extension if enabled in StartAssertionOptions.") { + forAll { + (extensions: AssertionExtensionInputs, prf: PrfAuthenticationInput) => + val rp = relyingParty(userId = userId) + val result = rp.startAssertion( + StartAssertionOptions + .builder() + .extensions(extensions.toBuilder.prf(prf).build()) + .build() + ) + + result.getPublicKeyCredentialRequestOptions.getExtensions.getPrf.toScala should equal( + Some(prf) + ) + } + } } } diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyV2AssertionSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyV2AssertionSpec.scala index ca7fa2aaf..c59005777 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyV2AssertionSpec.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyV2AssertionSpec.scala @@ -41,7 +41,9 @@ import com.yubico.webauthn.data.ClientAssertionExtensionOutputs import com.yubico.webauthn.data.CollectedClientData import com.yubico.webauthn.data.Extensions.LargeBlob.LargeBlobAuthenticationInput import com.yubico.webauthn.data.Extensions.LargeBlob.LargeBlobAuthenticationOutput +import com.yubico.webauthn.data.Extensions.Prf.PrfAuthenticationOutput import com.yubico.webauthn.data.Extensions.Uvm.UvmEntry +import com.yubico.webauthn.data.Generators.Extensions.Prf.arbitraryPrfAuthenticationOutput import com.yubico.webauthn.data.Generators._ import com.yubico.webauthn.data.PublicKeyCredential import com.yubico.webauthn.data.PublicKeyCredentialCreationOptions @@ -2657,6 +2659,35 @@ class RelyingPartyV2AssertionSpec } } + it("pass through prf extension outputs when present.") { + forAll(minSuccessful(3)) { prfOutput: PrfAuthenticationOutput => + val result = rp.finishAssertion( + FinishAssertionOptions + .builder() + .request( + testDataBase.assertion.get.request.toBuilder + .userHandle(testDataBase.userId.getId) + .build() + ) + .response( + testDataBase.assertion.get.response.toBuilder + .clientExtensionResults( + ClientAssertionExtensionOutputs + .builder() + .prf(prfOutput) + .build() + ) + .build() + ) + .build() + ) + + result.getClientExtensionOutputs.get().getPrf.toScala should equal( + Some(prfOutput) + ) + } + } + describe("support the uvm extension") { it("at authentication time.") { diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyV2RegistrationSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyV2RegistrationSpec.scala index d4a34e2f9..6cf73a2af 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyV2RegistrationSpec.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyV2RegistrationSpec.scala @@ -57,7 +57,9 @@ import com.yubico.webauthn.data.Extensions.CredentialProtection.CredentialProtec import com.yubico.webauthn.data.Extensions.CredentialProtection.CredentialProtectionPolicy import com.yubico.webauthn.data.Extensions.LargeBlob.LargeBlobRegistrationInput.LargeBlobSupport import com.yubico.webauthn.data.Extensions.LargeBlob.LargeBlobRegistrationOutput +import com.yubico.webauthn.data.Extensions.Prf.PrfRegistrationOutput import com.yubico.webauthn.data.Extensions.Uvm.UvmEntry +import com.yubico.webauthn.data.Generators.Extensions.Prf.arbitraryPrfRegistrationOutput import com.yubico.webauthn.data.Generators._ import com.yubico.webauthn.data.PublicKeyCredential import com.yubico.webauthn.data.PublicKeyCredentialCreationOptions @@ -4459,6 +4461,33 @@ class RelyingPartyV2RegistrationSpec } } + it("pass through prf extension outputs when present.") { + forAll(minSuccessful(3)) { prfOutput: PrfRegistrationOutput => + val testData = RegistrationTestData.Packed.BasicAttestation + val result = rp.finishRegistration( + FinishRegistrationOptions + .builder() + .request( + testData.request + ) + .response( + testData.response.toBuilder + .clientExtensionResults( + ClientRegistrationExtensionOutputs + .builder() + .prf(prfOutput) + .build() + ) + .build() + ) + .build() + ) + result.getClientExtensionOutputs.get().getPrf.toScala should equal( + Some(prfOutput) + ) + } + } + describe("support the uvm extension") { it("at registration time.") { diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/Generators.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/Generators.scala index 4c751b905..13533c6af 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/Generators.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/Generators.scala @@ -45,6 +45,11 @@ 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.PrfAuthenticationOutput +import com.yubico.webauthn.data.Extensions.Prf.PrfRegistrationInput +import com.yubico.webauthn.data.Extensions.Prf.PrfRegistrationOutput +import com.yubico.webauthn.data.Extensions.Prf.PrfValues import com.yubico.webauthn.data.Extensions.Uvm.UvmEntry import com.yubico.webauthn.extension.appid.AppId import com.yubico.webauthn.extension.appid.Generators._ @@ -407,9 +412,9 @@ object Generators { object Extensions { private val RegistrationExtensionIds: Set[String] = - Set("appidExclude", "credProps", "credProtect", "largeBlob", "uvm") + Set("appidExclude", "credProps", "credProtect", "largeBlob", "prf", "uvm") private val AuthenticationExtensionIds: Set[String] = - Set("appid", "largeBlob", "uvm") + Set("appid", "largeBlob", "prf", "uvm") private val ClientRegistrationExtensionOutputIds: Set[String] = RegistrationExtensionIds - "uvm" @@ -419,12 +424,18 @@ object Generators { "credProps", "credProtect", "largeBlob", + "prf", ) private val ClientAuthenticationExtensionOutputIds: Set[String] = AuthenticationExtensionIds - "uvm" private val AuthenticatorAuthenticationExtensionOutputIds: Set[String] = - AuthenticationExtensionIds -- Set("appid", "credProps", "largeBlob") + AuthenticationExtensionIds -- Set( + "appid", + "credProps", + "largeBlob", + "prf", + ) def registrationExtensionInputs( appidExcludeGen: Gen[Option[AppId]] = Gen.option(arbitrary[AppId]), @@ -621,6 +632,8 @@ object Generators { ) case "largeBlob" => resultBuilder.largeBlob(inputs.getLargeBlob orElse null) + case "prf" => + resultBuilder.prf(inputs.getPrf orElse null) case "uvm" => if (inputs.getUvm) { resultBuilder.uvm() @@ -640,6 +653,8 @@ object Generators { case "appid" => resultBuilder.appid(inputs.getAppid orElse null) case "largeBlob" => resultBuilder.largeBlob(inputs.getLargeBlob orElse null) + case "prf" => + resultBuilder.prf(inputs.getPrf orElse null) case "uvm" => if (inputs.getUvm) { resultBuilder.uvm() @@ -663,6 +678,8 @@ object Generators { case "credProtect" => // Skip case "largeBlob" => resultBuilder.largeBlob(clientOutputs.getLargeBlob orElse null) + case "prf" => + resultBuilder.prf(clientOutputs.getPrf orElse null) case "uvm" => // Skip } } @@ -679,6 +696,8 @@ object Generators { case "appid" => resultBuilder.appid(clientOutputs.getAppid) case "largeBlob" => resultBuilder.largeBlob(clientOutputs.getLargeBlob orElse null) + case "prf" => + resultBuilder.prf(clientOutputs.getPrf orElse null) case "uvm" => // Skip } } @@ -971,6 +990,67 @@ object Generators { } yield result) } + object Prf { + def prfValues: Gen[PrfValues] = + halfsized(for { + first <- arbitrary[ByteArray] + second <- Gen.option(arbitrary[ByteArray]) + } yield PrfValues.oneOrTwo(first, second.toJava)) + + def evalByCredential: Gen[Map[PublicKeyCredentialDescriptor, PrfValues]] = + halfsized(Gen.mapOf(for { + id <- arbitrary[PublicKeyCredentialDescriptor] + eval <- prfValues + } yield (id, eval))) + + def registrationInput: Gen[PrfRegistrationInput] = + Gen.oneOf( + Gen.const(PrfRegistrationInput.enable()), + prfValues.map(values => PrfRegistrationInput.eval(values)), + ) + implicit val arbitraryPrfRegistrationInput + : Arbitrary[PrfRegistrationInput] = Arbitrary(registrationInput) + + def registrationOutput: Gen[PrfRegistrationOutput] = + for { + enabled <- arbitrary[Option[java.lang.Boolean]] + results <- Gen.option(prfValues) + } yield new PrfRegistrationOutput(enabled.orNull, results.orNull) + + implicit val arbitraryPrfRegistrationOutput + : Arbitrary[PrfRegistrationOutput] = Arbitrary(registrationOutput) + + def authenticationInput: Gen[PrfAuthenticationInput] = + for { + eval <- prfValues + evalByCredential <- evalByCredential + (eval, evalByCredential) <- Gen.oneOf( + (Some(eval), None), + (None, Some(evalByCredential)), + (Some(eval), Some(evalByCredential)), + ) + } yield (eval, evalByCredential) match { + case (Some(eval), None) => PrfAuthenticationInput.eval(eval) + case (None, Some(evalByCredential)) => + PrfAuthenticationInput.evalByCredential(evalByCredential.asJava) + case (Some(eval), Some(evalByCredential)) => + PrfAuthenticationInput.evalByCredentialWithFallback( + evalByCredential.asJava, + eval, + ) + } + implicit val arbitraryPrfAuthenticationInput + : Arbitrary[PrfAuthenticationInput] = Arbitrary(authenticationInput) + + def authenticationOutput: Gen[PrfAuthenticationOutput] = + for { + results <- Gen.option(prfValues) + } yield new PrfAuthenticationOutput(results.orNull) + + implicit val arbitraryPrfAuthenticationOutput + : Arbitrary[PrfAuthenticationOutput] = Arbitrary(authenticationOutput) + } + object Uvm { def uvmEntry: Gen[UvmEntry] = for {