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 ee539ee58..22f3e2b34 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 @@ -57,6 +57,7 @@ public class AssertionExtensionInputs implements ExtensionInputs { private final AppId appid; private final Extensions.LargeBlob.LargeBlobAuthenticationInput largeBlob; private final Extensions.Prf.PrfAuthenticationInput prf; + private final Extensions.Spc.SpcAuthenticationInput spc; private final Boolean uvm; @JsonCreator @@ -64,10 +65,12 @@ private AssertionExtensionInputs( @JsonProperty("appid") AppId appid, @JsonProperty("largeBlob") Extensions.LargeBlob.LargeBlobAuthenticationInput largeBlob, @JsonProperty("prf") Extensions.Prf.PrfAuthenticationInput prf, + @JsonProperty("spc") Extensions.Spc.SpcAuthenticationInput spc, @JsonProperty("uvm") Boolean uvm) { this.appid = appid; this.largeBlob = largeBlob; this.prf = prf; + this.spc = spc; this.uvm = (uvm != null && uvm) ? true : null; } @@ -83,6 +86,7 @@ public AssertionExtensionInputs merge(AssertionExtensionInputs other) { this.appid != null ? this.appid : other.appid, this.largeBlob != null ? this.largeBlob : other.largeBlob, this.prf != null ? this.prf : other.prf, + this.spc != null ? this.spc : other.spc, this.uvm != null ? this.uvm : other.uvm); } @@ -103,6 +107,9 @@ public Set getExtensionIds() { if (prf != null) { ids.add(Extensions.Prf.EXTENSION_ID); } + if (spc != null) { + ids.add(Extensions.Spc.EXTENSION_ID); + } if (getUvm()) { ids.add(Extensions.Uvm.EXTENSION_ID); } @@ -212,6 +219,21 @@ public AssertionExtensionInputsBuilder prf(Extensions.Prf.PrfAuthenticationInput return this; } + /** + * Enable the Secure Payment Confirmation extension (spc). + * + *

This extension indicates that a credential is either being created for or used for Secure + * Payment Confirmation, respectively. + * + * @see §5. + * Secure Payment Confirmation extension (SPC) + */ + public AssertionExtensionInputsBuilder spc(Extensions.Spc.SpcAuthenticationInput spc) { + this.spc = spc; + return this; + } + /** * Enable the User Verification Method Extension (uvm). * @@ -299,6 +321,37 @@ private Extensions.Prf.PrfAuthenticationInput getPrfJson() { : null; } + /** + * The input to the Secure Payment Confirmation (spc) extension, if any. + * + *

This extension indicates that a credential is either being created for or used for Secure + * Payment Confirmation, respectively. + * + * @see §5. + * Secure Payment Confirmation extension (SPC) + */ + public Optional getSpc() { + return Optional.ofNullable(spc); + } + + /** For JSON serialization, to omit false and null values. */ + @JsonProperty("spc") + private Extensions.Spc.SpcAuthenticationInput getSpcJson() { + return spc != null + && (spc.getIsPayment() + || spc.getBrowserBoundPubKeyCredParams().isPresent() + || spc.getRpId().isPresent() + || spc.getTopOrigin().isPresent() + || spc.getPayeeName().isPresent() + || spc.getPayeeOrigin().isPresent() + || spc.getPaymentEntitiesLogos().isPresent() + || spc.getTotal().isPresent() + || spc.getInstrument().isPresent()) + ? spc + : 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/ClientAssertionExtensionOutputs.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/ClientAssertionExtensionOutputs.java index bba69646b..d23be1298 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 @@ -68,14 +68,18 @@ public class ClientAssertionExtensionOutputs implements ClientExtensionOutputs { private final Extensions.Prf.PrfAuthenticationOutput prf; + private final Extensions.Spc.SpcAuthenticationOutput spc; + @JsonCreator private ClientAssertionExtensionOutputs( @JsonProperty("appid") Boolean appid, @JsonProperty("largeBlob") Extensions.LargeBlob.LargeBlobAuthenticationOutput largeBlob, - @JsonProperty("prf") Extensions.Prf.PrfAuthenticationOutput prf) { + @JsonProperty("prf") Extensions.Prf.PrfAuthenticationOutput prf, + @JsonProperty("spc") Extensions.Spc.SpcAuthenticationOutput spc) { this.appid = appid; this.largeBlob = largeBlob; this.prf = prf; + this.spc = spc; } @Override @@ -91,6 +95,9 @@ public Set getExtensionIds() { if (prf != null) { ids.add(Extensions.Prf.EXTENSION_ID); } + if (spc != null) { + ids.add(Extensions.Spc.EXTENSION_ID); + } return ids; } @@ -135,6 +142,20 @@ public Optional getPrf() { return Optional.ofNullable(prf); } + /** + * The extension output for the Secure + * Payment Confirmation extension (spc), if any. + * + * @see com.yubico.webauthn.data.Extensions.Spc.SpcAuthenticationOutput + * @see §5. + * Secure Payment Confirmation extension (SPC) + */ + public Optional getSpc() { + return Optional.ofNullable(spc); + } + 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 7a661a198..721d85a5e 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 @@ -60,17 +60,21 @@ public class ClientRegistrationExtensionOutputs implements ClientExtensionOutput private final Extensions.Prf.PrfRegistrationOutput prf; + private final Extensions.Spc.SpcRegistrationOutput spc; + @JsonCreator private ClientRegistrationExtensionOutputs( @JsonProperty("appidExclude") Boolean appidExclude, @JsonProperty("credProps") Extensions.CredentialProperties.CredentialPropertiesOutput credProps, @JsonProperty("largeBlob") Extensions.LargeBlob.LargeBlobRegistrationOutput largeBlob, - @JsonProperty("prf") Extensions.Prf.PrfRegistrationOutput prf) { + @JsonProperty("prf") Extensions.Prf.PrfRegistrationOutput prf, + @JsonProperty("spc") Extensions.Spc.SpcRegistrationOutput spc) { this.appidExclude = appidExclude; this.credProps = credProps; this.largeBlob = largeBlob; this.prf = prf; + this.spc = spc; } @Override @@ -89,6 +93,9 @@ public Set getExtensionIds() { if (prf != null) { ids.add(Extensions.Prf.EXTENSION_ID); } + if (spc != null) { + ids.add(Extensions.Spc.EXTENSION_ID); + } return ids; } @@ -148,4 +155,18 @@ public Optional getLargeBlob() public Optional getPrf() { return Optional.ofNullable(prf); } + + /** + * The extension output for the Secure + * Payment Confirmation (spc) extension, if any. + * + * @see com.yubico.webauthn.data.Extensions.Spc.SpcRegistrationOutput + * @see §5. + * Secure Payment Confirmation extension (SPC) + */ + public Optional getSpc() { + return Optional.ofNullable(spc); + } } 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 3bd0eb6d8..0c9a00d7e 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 @@ -1231,6 +1231,476 @@ public Optional getResults() { } } + /** + * Definitions for the Secure Payment Confirmation extension (SPC). + * + * @see §5. + * Secure Payment Confirmation extension (SPC) + */ + public static class Spc { + static final String EXTENSION_ID = "payment"; + + /** + * Extension inputs for the Secure Payment Confirmation extension (SPC) in + * registration ceremonies. + * + * @see §5. + * Secure Payment Confirmation extension (SPC) + */ + @Value + @Builder(toBuilder = true) + public static class SpcRegistrationInput { + /** + * If true, indicates that the extension is active. + * + * @see §5. + * Secure Payment Confirmation extension (SPC) + */ + @JsonProperty private final boolean isPayment; + + /** + * The list of allowed types of credential restricting the types of cryptographic algorithms + * used for the browser bound key. + * + * @see §5. + * Secure Payment Confirmation extension (SPC) + */ + @JsonProperty private final List browserBoundPubKeyCredParams; + + @JsonCreator + private SpcRegistrationInput( + @JsonProperty("isPayment") final boolean isPayment, + @JsonProperty("browserBoundPubKeyCredParams") + final List browserBoundPubKeyCredParams) { + this.isPayment = isPayment; + this.browserBoundPubKeyCredParams = browserBoundPubKeyCredParams; + } + + /** + * If true, indicates that the extension is active. + * + * @see §5. + * Secure Payment Confirmation extension (SPC) + */ + public boolean getIsPayment() { + return isPayment; + } + + /** + * The list of allowed types of credential restricting the types of cryptographic algorithms + * used for the browser bound key. + * + * @see §5. + * Secure Payment Confirmation extension (SPC) + */ + public Optional> getBrowserBoundPubKeyCredParams() { + return Optional.ofNullable(browserBoundPubKeyCredParams); + } + } + + /** + * Extension inputs for the Secure Payment Confirmation extension (SPC) in + * authentication ceremonies. + * + * @see §5. + * Secure Payment Confirmation extension (SPC) + */ + @Value + @Builder(toBuilder = true) + public static class SpcAuthenticationInput { + /** + * If true, indicates that the extension is active. + * + * @see §5. + * Secure Payment Confirmation extension (SPC) + */ + @JsonProperty private final boolean isPayment; + + /** + * The list of allowed types of credential restricting the types of cryptographic algorithms + * used for the browser bound key. + * + * @see §5. + * Secure Payment Confirmation extension (SPC) + */ + @JsonProperty private final List browserBoundPubKeyCredParams; + + /** + * The Relying Party id of the credential(s) being used. + * + * @see §5. + * Secure Payment Confirmation extension (SPC) + */ + @JsonProperty private final String rpId; + + /** + * The origin of the top-level frame. + * + * @see §5. + * Secure Payment Confirmation extension (SPC) + */ + @JsonProperty private final String topOrigin; + + /** + * The payee name, if present, that was displayed to the user. + * + * @see §5. + * Secure Payment Confirmation extension (SPC) + */ + @JsonProperty private final String payeeName; + + /** + * The payee origin, if present, that was displayed to the user. + * + * @see §5. + * Secure Payment Confirmation extension (SPC) + */ + @JsonProperty private final String payeeOrigin; + + /** + * The logos, if any, that were displayed to the user in the transaction dialog. + * + * @see §5. + * Secure Payment Confirmation extension (SPC) + */ + @JsonProperty private final List paymentEntitiesLogos; + + /** + * The transaction amount that was displayed to the user. + * + * @see §5. + * Secure Payment Confirmation extension (SPC) + */ + @JsonProperty private final PaymentCurrencyAmount total; + + /** + * The instrument details that were displayed to the user. + * + * @see §5. + * Secure Payment Confirmation extension (SPC) + */ + @JsonProperty private final PaymentCredentialInstrument instrument; + + @JsonCreator + private SpcAuthenticationInput( + @JsonProperty("isPayment") final boolean isPayment, + @JsonProperty("browserBoundPubKeyCredParams") + final List browserBoundPubKeyCredParams, + @JsonProperty("rpId") final String rpId, + @JsonProperty("topOrigin") final String topOrigin, + @JsonProperty("payeeName") final String payeeName, + @JsonProperty("payeeOrigin") final String payeeOrigin, + @JsonProperty("paymentEntitiesLogos") final List paymentEntitiesLogos, + @JsonProperty("total") final PaymentCurrencyAmount total, + @JsonProperty("instrument") final PaymentCredentialInstrument instrument) { + this.isPayment = isPayment; + this.browserBoundPubKeyCredParams = browserBoundPubKeyCredParams; + this.rpId = rpId; + this.topOrigin = topOrigin; + this.payeeName = payeeName; + this.payeeOrigin = payeeOrigin; + this.paymentEntitiesLogos = paymentEntitiesLogos; + this.total = total; + this.instrument = instrument; + } + + /** + * If true, indicates that the extension is active. + * + * @see §5. + * Secure Payment Confirmation extension (SPC) + */ + public boolean getIsPayment() { + return isPayment; + } + + /** + * The list of allowed types of credential restricting the types of cryptographic algorithms + * used for the browser bound key. + * + * @see §5. + * Secure Payment Confirmation extension (SPC) + */ + public Optional> getBrowserBoundPubKeyCredParams() { + return Optional.ofNullable(browserBoundPubKeyCredParams); + } + + /** + * The Relying Party id of the credential(s) being used. + * + * @see §5. + * Secure Payment Confirmation extension (SPC) + */ + public Optional getRpId() { + return Optional.ofNullable(rpId); + } + + /** + * The origin of the top-level frame. + * + * @see §5. + * Secure Payment Confirmation extension (SPC) + */ + public Optional getTopOrigin() { + return Optional.ofNullable(topOrigin); + } + + /** + * The payee name, if present, that was displayed to the user. + * + * @see §5. + * Secure Payment Confirmation extension (SPC) + */ + public Optional getPayeeName() { + return Optional.ofNullable(payeeName); + } + + /** + * The payee origin, if present, that was displayed to the user. + * + * @see §5. + * Secure Payment Confirmation extension (SPC) + */ + public Optional getPayeeOrigin() { + return Optional.ofNullable(payeeOrigin); + } + + /** + * The logos, if any, that were displayed to the user in the transaction dialog. + * + * @see §5. + * Secure Payment Confirmation extension (SPC) + */ + public Optional> getPaymentEntitiesLogos() { + return Optional.ofNullable(paymentEntitiesLogos); + } + + /** + * The transaction amount that was displayed to the user. + * + * @see §5. + * Secure Payment Confirmation extension (SPC) + */ + public Optional getTotal() { + return Optional.ofNullable(total); + } + + /** + * The instrument details that were displayed to the user. + * + * @see §5. + * Secure Payment Confirmation extension (SPC) + */ + public Optional getInstrument() { + return Optional.ofNullable(instrument); + } + + /** + * Describes a logo for a payment entity that is facilitating the current transaction. + * + * @see §7.2 + * Secure Payment Confirmation extension (SPC) + */ + @Value + public static class PaymentEntityLogo { + + /** + * The URL of the logo. + * + * @see §7.2 + * Secure Payment Confirmation extension (SPC) + */ + @JsonProperty @NonNull private final String url; + + /** + * A label describing the logo. + * + * @see §7.2 + * Secure Payment Confirmation extension (SPC) + */ + @JsonProperty @NonNull private final String label; + } + + /** + * Represents a payment amount. + * + * @see §5. + * Payment Request API + */ + @Value + public static class PaymentCurrencyAmount { + + /** + * The 3-letter ISO 4217 currency code. + * + * @see §5. + * Payment Request API + */ + @JsonProperty @NonNull private final String currency; + + /** + * A valid decimal monetary value containing a monetary amount. + * + * @see §5. + * Payment Request API + */ + @JsonProperty @NonNull private final String value; + } + + /** + * Represents a payment instrument, containing the information to be displayed to the user and + * signed together with the transaction details. + * + * @see §7.1. + * Secure Payment Confirmation extension (SPC) + */ + @Value + public static class PaymentCredentialInstrument { + + /** + * The display name for the payment instrument to be displayed for the user. + * + * @see §7.1. + * Secure Payment Confirmation extension (SPC) + */ + @JsonProperty @NonNull private final String displayName; + + /** + * The URL of the icon of the payment instrument. + * + * @see §7.1. + * Secure Payment Confirmation extension (SPC) + */ + @JsonProperty @NonNull private final String icon; + + /** + * If true, indicates that the specified icon must be successfully fetched and + * shown for the request to succeed. + * + * @see §7.1. + * Secure Payment Confirmation extension (SPC) + */ + @JsonProperty private final boolean iconMustBeShown = true; + + /** + * Optional additional detail string for the payment instrument to be displayed to the user. + * + * @see §7.1. + * Secure Payment Confirmation extension (SPC) + */ + @JsonProperty private final String details; + } + } + + /** + * Outputs of the browser bound signature procedure. + * + * @see §5. + * Secure Payment Confirmation extension (SPC) + */ + @Value + public static class BrowserBoundSignature { + /** + * The output of the browser bound signing process. + * + * @see §5. + * Secure Payment Confirmation extension (SPC) + */ + @JsonProperty @NonNull private final ByteArray signature; + } + + /** + * Extension outputs for the Secure Payment Confirmation extension (SPC) in + * registration ceremonies. + * + * @see §5. + * Secure Payment Confirmation extension (SPC) + */ + @Value + public static class SpcRegistrationOutput { + /** + * Outputs of the browser bound signature procedure. + * + * @see §5. + * Secure Payment Confirmation extension (SPC) + */ + @JsonProperty private final BrowserBoundSignature browserBoundSignature; + + @JsonCreator + public SpcRegistrationOutput( + @JsonProperty("browserBoundSignature") + final BrowserBoundSignature browserBoundSignature) { + this.browserBoundSignature = browserBoundSignature; + } + } + + /** + * Extension outputs for the Secure Payment Confirmation extension (SPC) in + * authentication ceremonies. + * + * @see §5. + * Secure Payment Confirmation extension (SPC) + */ + @Value + public static class SpcAuthenticationOutput { + /** + * Outputs of the browser bound signature procedure. + * + * @see §5. + * Secure Payment Confirmation extension (SPC) + */ + @JsonProperty private final BrowserBoundSignature browserBoundSignature; + + @JsonCreator + public SpcAuthenticationOutput( + @JsonProperty("browserBoundSignature") + final BrowserBoundSignature browserBoundSignature) { + this.browserBoundSignature = browserBoundSignature; + } + } + } + /** * 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 00ebe1c0a..426403163 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 @@ -59,6 +59,7 @@ public final class RegistrationExtensionInputs implements ExtensionInputs { private final Extensions.CredentialProtection.CredentialProtectionInput credProtect; private final Extensions.LargeBlob.LargeBlobRegistrationInput largeBlob; private final Extensions.Prf.PrfRegistrationInput prf; + private final Extensions.Spc.SpcRegistrationInput spc; private final Boolean uvm; private RegistrationExtensionInputs( @@ -67,12 +68,14 @@ private RegistrationExtensionInputs( Extensions.CredentialProtection.CredentialProtectionInput credProtect, Extensions.LargeBlob.LargeBlobRegistrationInput largeBlob, Extensions.Prf.PrfRegistrationInput prf, + Extensions.Spc.SpcRegistrationInput spc, Boolean uvm) { this.appidExclude = appidExclude; this.credProps = credProps; this.credProtect = credProtect; this.largeBlob = largeBlob; this.prf = prf; + this.spc = spc; this.uvm = uvm; } @@ -85,6 +88,7 @@ private RegistrationExtensionInputs( @JsonProperty("enforceCredentialProtectionPolicy") Boolean enforceCredProtectPolicy, @JsonProperty("largeBlob") Extensions.LargeBlob.LargeBlobRegistrationInput largeBlob, @JsonProperty("prf") Extensions.Prf.PrfRegistrationInput prf, + @JsonProperty("spc") Extensions.Spc.SpcRegistrationInput spc, @JsonProperty("uvm") Boolean uvm) { this( appidExclude, @@ -99,6 +103,7 @@ private RegistrationExtensionInputs( .orElse(null), largeBlob, prf, + spc, uvm); } @@ -116,6 +121,7 @@ public RegistrationExtensionInputs merge(RegistrationExtensionInputs other) { this.credProtect != null ? this.credProtect : other.credProtect, this.largeBlob != null ? this.largeBlob : other.largeBlob, this.prf != null ? this.prf : other.prf, + this.spc != null ? this.spc : other.spc, this.uvm != null ? this.uvm : other.uvm); } @@ -219,6 +225,20 @@ public Optional getPrf() { return Optional.ofNullable(prf); } + /** + * The input to the Secure Payment Confirmation (spc) extension, if any. + * + *

This extension indicates that a credential is either being created for or used for Secure + * Payment Confirmation, respectively. + * + * @see §5. + * Secure Payment Confirmation extension (SPC) + */ + public Optional getSpc() { + return Optional.ofNullable(spc); + } + /** * @return true if the User Verification Method Extension (uvm) is * enabled, false otherwise. 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 21eacef5d..3278d9098 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 @@ -42,8 +42,10 @@ 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.Spc.SpcAuthenticationOutput import com.yubico.webauthn.data.Extensions.Uvm.UvmEntry import com.yubico.webauthn.data.Generators.Extensions.Prf.arbitraryPrfAuthenticationOutput +import com.yubico.webauthn.data.Generators.Extensions.Spc.arbitrarySpcAuthenticationOutput import com.yubico.webauthn.data.Generators._ import com.yubico.webauthn.data.PublicKeyCredential import com.yubico.webauthn.data.PublicKeyCredentialCreationOptions @@ -2608,6 +2610,33 @@ class RelyingPartyAssertionSpec } } + it("pass through spc extension outputs when present.") { + forAll(minSuccessful(3)) { spcOutput: SpcAuthenticationOutput => + val result = rp.finishAssertion( + FinishAssertionOptions + .builder() + .request( + testDataBase.assertion.get.request + ) + .response( + testDataBase.assertion.get.response.toBuilder + .clientExtensionResults( + ClientAssertionExtensionOutputs + .builder() + .spc(spcOutput) + .build() + ) + .build() + ) + .build() + ) + + result.getClientExtensionOutputs.get().getSpc.toScala should equal( + Some(spcOutput) + ) + } + } + 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 17e6f34b1..0ec8763ba 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 @@ -58,8 +58,10 @@ import com.yubico.webauthn.data.Extensions.CredentialProtection.CredentialProtec 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.Spc.SpcRegistrationOutput import com.yubico.webauthn.data.Extensions.Uvm.UvmEntry import com.yubico.webauthn.data.Generators.Extensions.Prf.arbitraryPrfRegistrationOutput +import com.yubico.webauthn.data.Generators.Extensions.Spc.arbitrarySpcRegistrationOutput import com.yubico.webauthn.data.Generators._ import com.yubico.webauthn.data.PublicKeyCredential import com.yubico.webauthn.data.PublicKeyCredentialCreationOptions @@ -4625,6 +4627,31 @@ class RelyingPartyRegistrationSpec } } + it("pass through spc extension outputs when present.") { + forAll(minSuccessful(3)) { spcOutput: SpcRegistrationOutput => + val testData = RegistrationTestData.Packed.BasicAttestation + val result = rp.finishRegistration( + FinishRegistrationOptions + .builder() + .request(testData.request) + .response( + testData.response.toBuilder + .clientExtensionResults( + ClientRegistrationExtensionOutputs + .builder() + .spc(spcOutput) + .build() + ) + .build() + ) + .build() + ) + result.getClientExtensionOutputs.get().getSpc.toScala should equal( + Some(spcOutput) + ) + } + } + 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 a52bb6f38..0ab3280da 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 @@ -36,8 +36,10 @@ import com.yubico.webauthn.data.Extensions.CredentialProtection.CredentialProtec 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.Extensions.Spc.SpcRegistrationInput 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.Spc.arbitrarySpcRegistrationInput import com.yubico.webauthn.data.Generators.Extensions.registrationExtensionInputs import com.yubico.webauthn.data.Generators._ import com.yubico.webauthn.data.PublicKeyCredentialCreationOptions @@ -616,6 +618,38 @@ class RelyingPartyStartOperationSpec } } + it("by default does not set the spc extension.") { + val rp = relyingParty(userId = userId) + val result = rp.startRegistration( + StartRegistrationOptions + .builder() + .user(userId) + .build() + ) + result.getExtensions.getSpc.toScala should be(None) + } + + it("sets the spc extension if enabled in StartRegistrationOptions.") { + forAll { + ( + extensions: RegistrationExtensionInputs, + spc: SpcRegistrationInput, + ) => + val rp = relyingParty(userId = userId) + val result = rp.startRegistration( + StartRegistrationOptions + .builder() + .user(userId) + .extensions(extensions.toBuilder.spc(spc).build()) + .build() + ) + + result.getExtensions.getSpc.toScala should equal( + Some(spc) + ) + } + } + it("respects the residentKey setting.") { val rp = relyingParty(userId = userId) 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 13533c6af..16f8f1aab 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 @@ -50,6 +50,14 @@ 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.Spc.BrowserBoundSignature +import com.yubico.webauthn.data.Extensions.Spc.SpcAuthenticationInput +import com.yubico.webauthn.data.Extensions.Spc.SpcAuthenticationInput.PaymentCredentialInstrument +import com.yubico.webauthn.data.Extensions.Spc.SpcAuthenticationInput.PaymentCurrencyAmount +import com.yubico.webauthn.data.Extensions.Spc.SpcAuthenticationInput.PaymentEntityLogo +import com.yubico.webauthn.data.Extensions.Spc.SpcAuthenticationOutput +import com.yubico.webauthn.data.Extensions.Spc.SpcRegistrationInput +import com.yubico.webauthn.data.Extensions.Spc.SpcRegistrationOutput import com.yubico.webauthn.data.Extensions.Uvm.UvmEntry import com.yubico.webauthn.extension.appid.AppId import com.yubico.webauthn.extension.appid.Generators._ @@ -412,9 +420,17 @@ object Generators { object Extensions { private val RegistrationExtensionIds: Set[String] = - Set("appidExclude", "credProps", "credProtect", "largeBlob", "prf", "uvm") + Set( + "appidExclude", + "credProps", + "credProtect", + "largeBlob", + "prf", + "spc", + "uvm", + ) private val AuthenticationExtensionIds: Set[String] = - Set("appid", "largeBlob", "prf", "uvm") + Set("appid", "largeBlob", "prf", "spc", "uvm") private val ClientRegistrationExtensionOutputIds: Set[String] = RegistrationExtensionIds - "uvm" @@ -425,6 +441,7 @@ object Generators { "credProtect", "largeBlob", "prf", + "spc", ) private val ClientAuthenticationExtensionOutputIds: Set[String] = @@ -435,6 +452,7 @@ object Generators { "credProps", "largeBlob", "prf", + "spc", ) def registrationExtensionInputs( @@ -634,6 +652,8 @@ object Generators { resultBuilder.largeBlob(inputs.getLargeBlob orElse null) case "prf" => resultBuilder.prf(inputs.getPrf orElse null) + case "spc" => + resultBuilder.spc(inputs.getSpc orElse null) case "uvm" => if (inputs.getUvm) { resultBuilder.uvm() @@ -655,6 +675,8 @@ object Generators { resultBuilder.largeBlob(inputs.getLargeBlob orElse null) case "prf" => resultBuilder.prf(inputs.getPrf orElse null) + case "spc" => + resultBuilder.spc(inputs.getSpc orElse null) case "uvm" => if (inputs.getUvm) { resultBuilder.uvm() @@ -680,6 +702,8 @@ object Generators { resultBuilder.largeBlob(clientOutputs.getLargeBlob orElse null) case "prf" => resultBuilder.prf(clientOutputs.getPrf orElse null) + case "spc" => + resultBuilder.spc(clientOutputs.getSpc orElse null) case "uvm" => // Skip } } @@ -698,6 +722,8 @@ object Generators { resultBuilder.largeBlob(clientOutputs.getLargeBlob orElse null) case "prf" => resultBuilder.prf(clientOutputs.getPrf orElse null) + case "spc" => + resultBuilder.spc(clientOutputs.getSpc orElse null) case "uvm" => // Skip } } @@ -1051,6 +1077,96 @@ object Generators { : Arbitrary[PrfAuthenticationOutput] = Arbitrary(authenticationOutput) } + object Spc { + def browserBoundPubKeyCredParams + : Gen[java.util.List[PublicKeyCredentialParameters]] = + Gen.listOf(arbitrary[PublicKeyCredentialParameters]).map(_.asJava) + + def registrationInput: Gen[SpcRegistrationInput] = + for { + isPayment <- arbitrary[Option[java.lang.Boolean]] + browserBoundParams <- browserBoundPubKeyCredParams + } yield SpcRegistrationInput + .builder() + .isPayment(isPayment.orNull) + .browserBoundPubKeyCredParams(browserBoundParams) + .build() + + implicit val arbitrarySpcRegistrationInput + : Arbitrary[SpcRegistrationInput] = Arbitrary(registrationInput) + + def browserBoundSignature: Gen[BrowserBoundSignature] = + for { + signature <- arbitrary[ByteArray] + } yield new BrowserBoundSignature(signature) + + def registrationOutput: Gen[SpcRegistrationOutput] = + for { + browserBoundSignature <- Gen.option(browserBoundSignature) + } yield new SpcRegistrationOutput(browserBoundSignature.orNull) + + implicit val arbitrarySpcRegistrationOutput + : Arbitrary[SpcRegistrationOutput] = Arbitrary(registrationOutput) + + def paymentEntityLogo: Gen[PaymentEntityLogo] = + for { + url <- arbitrary[String] + label <- arbitrary[String] + } yield new PaymentEntityLogo(url, label) + + def paymentCurrencyAmount: Gen[PaymentCurrencyAmount] = + for { + currency <- arbitrary[String] + value <- arbitrary[String] + } yield new PaymentCurrencyAmount(currency, value) + + def paymentCredentialInstrument: Gen[PaymentCredentialInstrument] = + for { + displayName <- arbitrary[String] + icon <- arbitrary[String] + details <- arbitrary[Option[String]] + } yield new PaymentCredentialInstrument( + displayName, + icon, + details.orNull, + ) + + def authenticationInput: Gen[SpcAuthenticationInput] = + for { + isPayment <- arbitrary[Option[java.lang.Boolean]] + browserBoundPubKeyCredParams <- browserBoundPubKeyCredParams + rpId <- arbitrary[Option[String]] + topOrigin <- arbitrary[Option[String]] + payeeName <- arbitrary[Option[String]] + payeeOrigin <- arbitrary[Option[String]] + paymentEntitiesLogos <- Gen.listOf(paymentEntityLogo).map(_.asJava) + total <- Gen.option(paymentCurrencyAmount) + instrument <- Gen.option(paymentCredentialInstrument) + } yield SpcAuthenticationInput + .builder() + .isPayment(isPayment.orNull) + .browserBoundPubKeyCredParams(browserBoundPubKeyCredParams) + .rpId(rpId.orNull) + .topOrigin(topOrigin.orNull) + .payeeName(payeeName.orNull) + .payeeOrigin(payeeOrigin.orNull) + .paymentEntitiesLogos(paymentEntitiesLogos) + .total(total.orNull) + .instrument(instrument.orNull) + .build() + + implicit val arbitrarySpcAuthenticationInput + : Arbitrary[SpcAuthenticationInput] = Arbitrary(authenticationInput) + + def authenticationOutput: Gen[SpcAuthenticationOutput] = + for { + browserBoundSignature <- Gen.option(browserBoundSignature) + } yield new SpcAuthenticationOutput(browserBoundSignature.orNull) + + implicit val arbitrarySpcAuthenticationOutput + : Arbitrary[SpcAuthenticationOutput] = Arbitrary(authenticationOutput) + } + object Uvm { def uvmEntry: Gen[UvmEntry] = for {