Skip to content

Commit

Permalink
Implement experimental support for SPC response type
Browse files Browse the repository at this point in the history
  • Loading branch information
emlun committed Jul 5, 2023
1 parent 4a794e5 commit b62ee43
Show file tree
Hide file tree
Showing 4 changed files with 128 additions and 4 deletions.
6 changes: 6 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
== Version 2.6.0 (unreleased) ==

New features:

* Added method `getParsedPublicKey(): java.security.PublicKey` to
`RegistrationResult` and `RegisteredCredential`.
** Thanks to Jakob Heher (A-SIT) for the contribution, see
https://github.com/Yubico/java-webauthn-server/pull/299
* (Experimental) Added option `isSecurePaymentConfirmation(boolean)` to
`FinishAssertionOptions`. When set, `RelyingParty.finishAssertion()` will
adapt the validation logic for a Secure Payment Confirmation (SPC) response
instead of an ordinary WebAuthn response. See the JavaDoc for details.


== Version 2.5.0 ==
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,10 @@
import com.yubico.webauthn.data.AuthenticatorAssertionResponse;
import com.yubico.webauthn.data.ByteArray;
import com.yubico.webauthn.data.ClientAssertionExtensionOutputs;
import com.yubico.webauthn.data.CollectedClientData;
import com.yubico.webauthn.data.PublicKeyCredential;
import java.util.Optional;
import java.util.Set;
import lombok.Builder;
import lombok.NonNull;
import lombok.Value;
Expand Down Expand Up @@ -59,6 +61,41 @@ public class FinishAssertionOptions {
*/
private final ByteArray callerTokenBindingId;

/**
* EXPERIMENTAL FEATURE:
*
* <p>If set to <code>false</code> (the default), the <code>"type"</code> property in the <a
* href="https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#dictionary-client-data">collected
* client data</a> of the assertion will be verified to equal <code>"webauthn.get"</code>.
*
* <p>If set to <code>true</code>, it will instead be verified to equal <code>"payment.get"</code>
* .
*
* <p>NOTE: If you're using <a
* href="https://www.w3.org/TR/2023/CR-secure-payment-confirmation-20230615/">Secure Payment
* Confirmation</a> (SPC), you likely also need to relax the origin validation logic. Right now
* this library only supports matching against a finite {@link Set} of acceptable origins. If
* necessary, your application may validate the origin externally (see {@link
* PublicKeyCredential#getResponse()}, {@link AuthenticatorAssertionResponse#getClientData()} and
* {@link CollectedClientData#getOrigin()}) and construct a new {@link RelyingParty} instance for
* each SPC response, setting the {@link RelyingParty.RelyingPartyBuilder#origins(Set) origins}
* setting on that instance to contain the pre-validated origin value.
*
* <p>Better support for relaxing origin validation may be added as the feature matures.
*
* @deprecated EXPERIMENTAL: This is an experimental feature. It is likely to change or be deleted
* before reaching a mature release.
* @see <a href="https://www.w3.org/TR/2023/CR-secure-payment-confirmation-20230615/">Secure
* Payment Confirmation</a>
* @see <a
* href="https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#dictionary-client-data">5.8.1.
* Client Data Used in WebAuthn Signatures (dictionary CollectedClientData)</a>
* @see RelyingParty.RelyingPartyBuilder#origins(Set)
* @see CollectedClientData
* @see CollectedClientData#getOrigin()
*/
@Deprecated @Builder.Default private final boolean isSecurePaymentConfirmation = false;

/**
* The <a href="https://tools.ietf.org/html/rfc8471#section-3.2">token binding ID</a> of the
* connection to the client, if any.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
final class FinishAssertionSteps {

private static final String CLIENT_DATA_TYPE = "webauthn.get";
private static final String SPC_CLIENT_DATA_TYPE = "payment.get";

private final AssertionRequest request;
private final PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs>
Expand All @@ -61,6 +62,7 @@ final class FinishAssertionSteps {
private final boolean allowOriginPort;
private final boolean allowOriginSubdomain;
private final boolean validateSignatureCounter;
private final boolean isSecurePaymentConfirmation;

FinishAssertionSteps(RelyingParty rp, FinishAssertionOptions options) {
this.request = options.getRequest();
Expand All @@ -72,6 +74,7 @@ final class FinishAssertionSteps {
this.allowOriginPort = rp.isAllowOriginPort();
this.allowOriginSubdomain = rp.isAllowOriginSubdomain();
this.validateSignatureCounter = rp.isValidateSignatureCounter();
this.isSecurePaymentConfirmation = options.isSecurePaymentConfirmation();
}

public Step5 begin() {
Expand Down Expand Up @@ -288,10 +291,12 @@ class Step11 implements Step<Step12> {

@Override
public void validate() {
final String expectedType =
isSecurePaymentConfirmation ? SPC_CLIENT_DATA_TYPE : CLIENT_DATA_TYPE;
assertTrue(
CLIENT_DATA_TYPE.equals(clientData.getType()),
expectedType.equals(clientData.getType()),
"The \"type\" in the client data must be exactly \"%s\", was: %s",
CLIENT_DATA_TYPE,
expectedType,
clientData.getType());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ package com.yubico.webauthn
import com.fasterxml.jackson.core.`type`.TypeReference
import com.fasterxml.jackson.databind.node.JsonNodeFactory
import com.fasterxml.jackson.databind.node.ObjectNode
import com.fasterxml.jackson.databind.node.TextNode
import com.upokecenter.cbor.CBORObject
import com.yubico.internal.util.JacksonCodecs
import com.yubico.webauthn.data.AssertionExtensionInputs
Expand Down Expand Up @@ -179,6 +180,7 @@ class RelyingPartyAssertionSpec
credentialId: ByteArray = Defaults.credentialId,
credentialKey: KeyPair = Defaults.credentialKey,
credentialRepository: Option[CredentialRepository] = None,
isSecurePaymentConfirmation: Option[Boolean] = None,
origins: Option[Set[String]] = None,
requestedExtensions: AssertionExtensionInputs =
Defaults.requestedExtensions,
Expand Down Expand Up @@ -283,6 +285,10 @@ class RelyingPartyAssertionSpec
.response(response)
.callerTokenBindingId(callerTokenBindingId.toJava)

isSecurePaymentConfirmation foreach { isSpc =>
fao.isSecurePaymentConfirmation(isSpc)
}

builder
.build()
._finishAssertion(fao.build())
Expand Down Expand Up @@ -941,14 +947,18 @@ class RelyingPartyAssertionSpec
step.validations shouldBe a[Success[_]]
}

def assertFails(typeString: String): Unit = {
def assertFails(
typeString: String,
isSecurePaymentConfirmation: Option[Boolean] = None,
): Unit = {
val steps = finishAssertion(
clientDataJson = JacksonCodecs.json.writeValueAsString(
JacksonCodecs.json
.readTree(Defaults.clientDataJson)
.asInstanceOf[ObjectNode]
.set("type", jsonFactory.textNode(typeString))
)
),
isSecurePaymentConfirmation = isSecurePaymentConfirmation,
)
val step: FinishAssertionSteps#Step11 =
steps.begin.next.next.next.next.next
Expand All @@ -973,6 +983,72 @@ class RelyingPartyAssertionSpec
it("""The string "webauthn.create" fails.""") {
assertFails("webauthn.create")
}

it("""The string "payment.get" fails.""") {
assertFails("payment.get")
}

describe("If the isSecurePaymentConfirmation option is set,") {
it("the default test case fails.") {
val steps =
finishAssertion(isSecurePaymentConfirmation = Some(true))
val step: FinishAssertionSteps#Step11 =
steps.begin.next.next.next.next.next

step.validations shouldBe a[Failure[_]]
step.validations.failed.get shouldBe an[IllegalArgumentException]
}

it("""the default test case succeeds if type is overwritten with the value "payment.get".""") {
val json = JacksonCodecs.json()
val steps = finishAssertion(
isSecurePaymentConfirmation = Some(true),
clientDataJson = json.writeValueAsString(
json
.readTree(Defaults.clientDataJson)
.asInstanceOf[ObjectNode]
.set[ObjectNode]("type", new TextNode("payment.get"))
),
)
val step: FinishAssertionSteps#Step11 =
steps.begin.next.next.next.next.next

step.validations shouldBe a[Success[_]]
}

it("""any value other than "payment.get" fails.""") {
forAll { (typeString: String) =>
whenever(typeString != "payment.get") {
assertFails(
typeString,
isSecurePaymentConfirmation = Some(true),
)
}
}
forAll(Gen.alphaNumStr) { (typeString: String) =>
whenever(typeString != "payment.get") {
assertFails(
typeString,
isSecurePaymentConfirmation = Some(true),
)
}
}
}

it("""the string "webauthn.create" fails.""") {
assertFails(
"webauthn.create",
isSecurePaymentConfirmation = Some(true),
)
}

it("""the string "webauthn.get" fails.""") {
assertFails(
"webauthn.get",
isSecurePaymentConfirmation = Some(true),
)
}
}
}

it("12. Verify that the value of C.challenge equals the base64url encoding of options.challenge.") {
Expand Down

1 comment on commit b62ee43

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mutation test results

Package Coverage Stats Prev Prev
Overall 81 % 🟢 1289 🔺 / 1573 🔺 81 % 1279 / 1572
com.yubico.fido.metadata 69 % 🟢 226 🔺 / 323 🔹 69 % 223 / 323
com.yubico.internal.util 47 % 🔹 57 🔹 / 120 🔹 47 % 57 / 120
com.yubico.webauthn 88 % 🟢 572 🔺 / 646 🔺 87 % 565 / 645
com.yubico.webauthn.attestation 92 % 🔹 13 🔹 / 14 🔹 92 % 13 / 14
com.yubico.webauthn.data 93 % 🔹 396 🔹 / 423 🔹 93 % 396 / 423
com.yubico.webauthn.extension.appid 100 % 🏆 13 🔹 / 13 🔹 100 % 13 / 13
com.yubico.webauthn.extension.uvm 50 % 🔹 12 🔹 / 24 🔹 50 % 12 / 24
com.yubico.webauthn.meta 0 % 🔹 0 🔹 / 10 🔹 0 % 0 / 10

Previous run: 984109e - Diff

Detailed reports: workflow run #235

Please sign in to comment.