true
if the assertion was verified successfully. */
private final boolean success;
+ @JsonProperty
+ @Getter(AccessLevel.NONE)
+ private final PublicKeyCredentialNOTE: The {@link RegisteredCredential#getSignatureCount() signature count} in this object - * will reflect the signature counter state before the assertion operation, not the new - * counter value. When updating your database state, use the signature counter from {@link - * #getSignatureCount()} instead. + *
NOTE: The {@link RegisteredCredential#getSignatureCount() signature count}, {@link + * RegisteredCredential#isBackupEligible() backup eligibility} and {@link + * RegisteredCredential#isBackedUp() backup state} properties in this object will reflect the + * state before the assertion operation, not the new state. When updating your database + * state, use the signature counter and backup state from {@link #getSignatureCount()}, {@link + * #isBackupEligible()} and {@link #isBackedUp()} instead. */ private final RegisteredCredential credential; @@ -65,16 +74,6 @@ public class AssertionResult { */ @NonNull private final String username; - /** - * The new signature - * count of the credential used for the assertion. - * - *
You should update this value in your database.
- *
- * @see AuthenticatorData#getSignatureCounter()
- */
- private final long signatureCount;
-
/**
* You SHOULD store this value in your representation of the corresponding {@link
+ * RegisteredCredential} if no value is stored yet. {@link CredentialRepository} implementations
+ * SHOULD set this value as the {@link
+ * RegisteredCredential.RegisteredCredentialBuilder#backupEligible(Boolean)
+ * backupEligible(Boolean)} value when reconstructing that {@link RegisteredCredential}.
+ *
+ * @return You SHOULD update this value in your representation of a {@link RegisteredCredential}.
+ * {@link CredentialRepository} implementations SHOULD set this value as the {@link
+ * RegisteredCredential.RegisteredCredentialBuilder#backupState(Boolean) backupState(Boolean)}
+ * value when reconstructing that {@link RegisteredCredential}.
+ *
+ * @return You should update this value in your database.
+ *
+ * @see AuthenticatorData#getSignatureCounter()
+ */
+ @JsonIgnore
+ public long getSignatureCount() {
+ return credentialResponse.getResponse().getParsedAuthenticatorData().getSignatureCounter();
+ }
+
/**
* The client
@@ -200,8 +221,10 @@ public ByteArray getUserHandle() {
* @see ClientAssertionExtensionOutputs
* @see #getAuthenticatorExtensionOutputs() ()
*/
+ @JsonIgnore
public Optional If absent, it is not known whether or not this credential is backup eligible.
+ *
+ * If present and If present and {@link CredentialRepository} implementations SHOULD set this to the first known value
+ * returned by {@link RegistrationResult#isBackupEligible()} or {@link
+ * AssertionResult#isBackupEligible()}, if known. If unknown, {@link CredentialRepository}
+ * implementations SHOULD set this to If absent, the backup state of the credential is not known.
+ *
+ * If present and If present and {@link CredentialRepository} implementations SHOULD set this to the most recent value
+ * returned by {@link AssertionResult#isBackedUp()} or {@link RegistrationResult#isBackedUp()}, if
+ * known. If unknown, {@link CredentialRepository} implementations SHOULD set this to If absent, it is not known whether or not this credential is backup eligible.
+ *
+ * If present and If present and {@link CredentialRepository} implementations SHOULD set this to the first known value
+ * returned by {@link RegistrationResult#isBackupEligible()} or {@link
+ * AssertionResult#isBackupEligible()}, if known. If unknown, {@link CredentialRepository}
+ * implementations SHOULD set this to If absent, the backup state of the credential is not known.
+ *
+ * If present and If present and {@link CredentialRepository} implementations SHOULD set this to the most recent value
+ * returned by {@link AssertionResult#isBackedUp()} or {@link RegistrationResult#isBackedUp()}, if
+ * known. If unknown, {@link CredentialRepository} implementations SHOULD set this to This MAY be an AAGUID consisting of only zeroes.
- */
- @NonNull private final ByteArray aaguid;
+ @JsonProperty
+ @Getter(AccessLevel.NONE)
+ private final PublicKeyCredential<
+ AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs>
+ credential;
/**
* This is used in {@link RelyingParty#finishAssertion(FinishAssertionOptions)} to verify the
- * authentication signatures.
- *
- * @see RegisteredCredential#getPublicKeyCose()
- */
- @NonNull private final ByteArray publicKeyCose;
-
- /**
- * The signature count returned with the created credential.
- *
- * This is used in {@link RelyingParty#finishAssertion(FinishAssertionOptions)} to verify the
- * validity of future signature counter values.
- *
- * @see RegisteredCredential#getSignatureCount()
- */
- private final long signatureCount;
-
- private final ClientRegistrationExtensionOutputs clientExtensionOutputs;
-
- private final AuthenticatorRegistrationExtensionOutputs authenticatorExtensionOutputs;
-
- private RegistrationResult(
- @NonNull PublicKeyCredentialDescriptor keyId,
- @NonNull ByteArray aaguid,
+ RegistrationResult(
+ PublicKeyCredential If present, this may be useful for looking up attestation metadata from external sources.
- * The attestation trust path has been successfully verified as trusted if and only if {@link
- * #isAttestationTrusted()} is You SHOULD store this value in your representation of a {@link RegisteredCredential}. {@link
+ * CredentialRepository} implementations SHOULD set this value as the {@link
+ * RegisteredCredential.RegisteredCredentialBuilder#backupEligible(Boolean)
+ * backupEligible(Boolean)} value when reconstructing that {@link RegisteredCredential}.
*
- * You can ignore this if authenticator attestation is not relevant to your application.
+ * @return You SHOULD store this value in your representation of a {@link RegisteredCredential}. {@link
+ * CredentialRepository} implementations SHOULD set this value as the {@link
+ * RegisteredCredential.RegisteredCredentialBuilder#backupState(Boolean) backupState(Boolean)}
+ * value when reconstructing that {@link RegisteredCredential}.
+ *
+ * @return This is used in {@link RelyingParty#finishAssertion(FinishAssertionOptions)} to verify the
+ * validity of future signature counter values.
*
+ * @see RegisteredCredential#getSignatureCount()
+ */
+ @JsonIgnore
+ public long getSignatureCount() {
+ return credential.getResponse().getParsedAuthenticatorData().getSignatureCounter();
+ }
+
+ /**
+ * The credential
+ * ID and transports
+ * of the created credential.
+ *
+ * @see Credential
+ * ID
* @see Attestation
- * trust path
+ * href="https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#dictionary-credential-descriptor">5.8.3.
+ * Credential Descriptor (dictionary PublicKeyCredentialDescriptor) This MAY be an AAGUID consisting of only zeroes.
+ */
+ @JsonIgnore
+ public ByteArray getAaguid() {
+ return credential
+ .getResponse()
+ .getAttestation()
+ .getAuthenticatorData()
+ .getAttestedCredentialData()
+ .get()
+ .getAaguid();
+ }
+
+ /**
+ * The public key of the created credential.
+ *
+ * This is used in {@link RelyingParty#finishAssertion(FinishAssertionOptions)} to verify the
+ * authentication signatures.
+ *
+ * @see RegisteredCredential#getPublicKeyCose()
+ */
+ @JsonIgnore
+ public ByteArray getPublicKeyCose() {
+ return credential
+ .getResponse()
+ .getAttestation()
+ .getAuthenticatorData()
+ .getAttestedCredentialData()
+ .get()
+ .getCredentialPublicKey();
}
/**
@@ -236,8 +261,10 @@ private Optional If present, this may be useful for looking up attestation metadata from external sources.
+ * The attestation trust path has been successfully verified as trusted if and only if {@link
+ * #isAttestationTrusted()} is You can ignore this if authenticator attestation is not relevant to your application.
+ *
+ * @see Attestation
+ * trust path
+ */
+ @JsonIgnore
+ public Optional NOTE that this is only a hint and not a guarantee, unless backed by a trusted authenticator
+ * attestation.
+ *
+ * @see §6.1. Authenticator Data
+ * @deprecated EXPERIMENTAL: This feature is from a not yet mature standard; it could change as
+ * the standard matures.
+ */
+ @Deprecated public final boolean BE;
+
+ /**
+ * Backup status: the credential is currently backed up.
+ *
+ * NOTE that this is only a hint and not a guarantee, unless backed by a trusted authenticator
+ * attestation.
+ *
+ * @see §6.1. Authenticator Data
+ * @deprecated EXPERIMENTAL: This feature is from a not yet mature standard; it could change as
+ * the standard matures.
+ */
+ @Deprecated public final boolean BS;
+
/**
* Attested credential data present.
*
@@ -68,20 +92,27 @@ public AuthenticatorDataFlags(@JsonProperty("value") byte value) {
UP = (value & Bitmasks.UP) != 0;
UV = (value & Bitmasks.UV) != 0;
+ BE = (value & Bitmasks.BE) != 0;
+ BS = (value & Bitmasks.BS) != 0;
AT = (value & Bitmasks.AT) != 0;
ED = (value & Bitmasks.ED) != 0;
+
+ if (BS && !BE) {
+ throw new IllegalArgumentException(
+ String.format("Flag combination is invalid: BE=0, BS=1 in flags: 0x%02x", value));
+ }
}
private static final class Bitmasks {
static final byte UP = 0x01;
static final byte UV = 0x04;
+ static final byte BE = 0x08;
+ static final byte BS = 0x10;
static final byte AT = 0x40;
static final byte ED = -0x80;
/* Reserved bits */
- // final boolean RFU1 = (value & 0x02) > 0;
- // final boolean RFU2_1 = (value & 0x08) > 0;
- // final boolean RFU2_2 = (value & 0x10) > 0;
- // static final boolean RFU2_3 = (value & 0x20) > 0;
+ // static final byte RFU1 = 0x02;
+ // static final byte RFU2 = 0x20;
}
}
diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/Generators.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/Generators.scala
index 15d346337..6053501c3 100644
--- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/Generators.scala
+++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/Generators.scala
@@ -2,13 +2,13 @@ package com.yubico.webauthn
import com.yubico.webauthn.data.AssertionExtensionInputs
import com.yubico.webauthn.data.AttestationType
-import com.yubico.webauthn.data.AuthenticatorAssertionExtensionOutputs
-import com.yubico.webauthn.data.AuthenticatorRegistrationExtensionOutputs
+import com.yubico.webauthn.data.AuthenticatorAssertionResponse
+import com.yubico.webauthn.data.AuthenticatorAttestationResponse
import com.yubico.webauthn.data.ByteArray
import com.yubico.webauthn.data.ClientAssertionExtensionOutputs
import com.yubico.webauthn.data.ClientRegistrationExtensionOutputs
import com.yubico.webauthn.data.Generators._
-import com.yubico.webauthn.data.PublicKeyCredentialDescriptor
+import com.yubico.webauthn.data.PublicKeyCredential
import com.yubico.webauthn.data.UserVerificationRequirement
import org.bouncycastle.asn1.x500.X500Name
import org.scalacheck.Arbitrary
@@ -17,56 +17,47 @@ import org.scalacheck.Gen
import java.security.cert.X509Certificate
import scala.jdk.CollectionConverters.SeqHasAsJava
+import scala.jdk.OptionConverters.RichOption
object Generators {
implicit val arbitraryAssertionResult: Arbitrary[AssertionResult] = Arbitrary(
for {
- authenticatorExtensionOutputs <-
- arbitrary[Option[AuthenticatorAssertionExtensionOutputs]]
- clientExtensionOutputs <- arbitrary[ClientAssertionExtensionOutputs]
+ credentialResponse <-
+ arbitrary[PublicKeyCredential[
+ AuthenticatorAssertionResponse,
+ ClientAssertionExtensionOutputs,
+ ]]
credential <- arbitrary[RegisteredCredential]
- signatureCount <- arbitrary[Long]
signatureCounterValid <- arbitrary[Boolean]
success <- arbitrary[Boolean]
username <- arbitrary[String]
- } yield AssertionResult
- .builder()
- .success(success)
- .credential(credential)
- .username(username)
- .signatureCount(signatureCount)
- .signatureCounterValid(signatureCounterValid)
- .clientExtensionOutputs(clientExtensionOutputs)
- .assertionExtensionOutputs(authenticatorExtensionOutputs.orNull)
- .build()
+ } yield new AssertionResult(
+ success,
+ credentialResponse,
+ credential,
+ username,
+ signatureCounterValid,
+ )
)
implicit val arbitraryRegistrationResult: Arbitrary[RegistrationResult] =
Arbitrary(
for {
- aaguid <- byteArray(16)
+ credential <-
+ arbitrary[PublicKeyCredential[
+ AuthenticatorAttestationResponse,
+ ClientRegistrationExtensionOutputs,
+ ]]
attestationTrusted <- arbitrary[Boolean]
attestationTrustPath <- generateAttestationCertificateChain
attestationType <- arbitrary[AttestationType]
- authenticatorExtensionOutputs <-
- arbitrary[Option[AuthenticatorRegistrationExtensionOutputs]]
- clientExtensionOutputs <- arbitrary[ClientRegistrationExtensionOutputs]
- keyId <- arbitrary[PublicKeyCredentialDescriptor]
- publicKeyCose <- arbitrary[ByteArray]
- signatureCount <- arbitrary[Long]
- } yield RegistrationResult
- .builder()
- .keyId(keyId)
- .aaguid(aaguid)
- .attestationTrusted(attestationTrusted)
- .attestationType(attestationType)
- .publicKeyCose(publicKeyCose)
- .signatureCount(signatureCount)
- .clientExtensionOutputs(clientExtensionOutputs)
- .authenticatorExtensionOutputs(authenticatorExtensionOutputs.orNull)
- .attestationTrustPath(attestationTrustPath.asJava)
- .build()
+ } yield new RegistrationResult(
+ credential,
+ attestationTrusted,
+ attestationType,
+ Some(attestationTrustPath.asJava).toJava,
+ )
)
implicit val arbitraryRegisteredCredential: Arbitrary[RegisteredCredential] =
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 604343faf..8f9e309c7 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
@@ -31,6 +31,7 @@ import com.upokecenter.cbor.CBORObject
import com.yubico.internal.util.JacksonCodecs
import com.yubico.webauthn.data.AssertionExtensionInputs
import com.yubico.webauthn.data.AuthenticatorAssertionResponse
+import com.yubico.webauthn.data.AuthenticatorDataFlags
import com.yubico.webauthn.data.AuthenticatorTransport
import com.yubico.webauthn.data.ByteArray
import com.yubico.webauthn.data.ClientAssertionExtensionOutputs
@@ -1468,7 +1469,10 @@ class RelyingPartyAssertionSpec
.toArray
)
val (checkFails, checkSucceeds) =
- checks[FinishAssertionSteps#Step18, FinishAssertionSteps#Step17](
+ checks[
+ FinishAssertionSteps#PendingStep16,
+ FinishAssertionSteps#Step17,
+ ](
_.begin.next.next.next.next.next.next.next.next.next.next.next
)
@@ -1498,6 +1502,80 @@ class RelyingPartyAssertionSpec
}
}
+ describe("(NOT YET MATURE) 16. If the credential backup state is used as part of Relying Party business logic or policy, let currentBe and currentBs be the values of the BE and BS bits, respectively, of the flags in authData. Compare currentBe and currentBs with credentialRecord.BE and credentialRecord.BS and apply Relying Party policy, if any.") {
+ it(
+ "Fails if BE=0 in the stored credential and BE=1 in the assertion."
+ ) {
+ forAll(
+ authenticatorDataBytes(
+ Gen.option(Extensions.authenticatorAssertionExtensionOutputs()),
+ rpIdHashGen = Gen.const(sha256(Defaults.rpId.getId)),
+ backupFlagsGen = arbitrary[Boolean].map(bs => (true, bs)),
+ )
+ ) { authData =>
+ val step: FinishAssertionSteps#PendingStep16 = finishAssertion(
+ authenticatorData = authData,
+ credentialRepository = Some(
+ Helpers.CredentialRepository.withUser(
+ Defaults.user,
+ RegisteredCredential
+ .builder()
+ .credentialId(Defaults.credentialId)
+ .userHandle(Defaults.userHandle)
+ .publicKeyCose(getPublicKeyBytes(Defaults.credentialKey))
+ .backupEligible(false)
+ .backupState(false)
+ .build(),
+ )
+ ),
+ ).begin.next.next.next.next.next.next.next.next.next.next.next.next
+
+ step.validations shouldBe a[Failure[_]]
+ step.validations.failed.get shouldBe an[IllegalArgumentException]
+ step.tryNext shouldBe a[Failure[_]]
+ }
+ }
+
+ it(
+ "Fails if BE=1 in the stored credential and BE=0 in the assertion."
+ ) {
+ forAll(
+ authenticatorDataBytes(
+ Gen.option(Extensions.authenticatorAssertionExtensionOutputs()),
+ rpIdHashGen = Gen.const(sha256(Defaults.rpId.getId)),
+ backupFlagsGen = Gen.const((false, false)),
+ ),
+ arbitrary[Boolean],
+ ) {
+ case (authData, storedBs) =>
+ val step: FinishAssertionSteps#PendingStep16 = finishAssertion(
+ authenticatorData = authData,
+ credentialRepository = Some(
+ Helpers.CredentialRepository.withUser(
+ Defaults.user,
+ RegisteredCredential
+ .builder()
+ .credentialId(Defaults.credentialId)
+ .userHandle(Defaults.userHandle)
+ .publicKeyCose(
+ getPublicKeyBytes(Defaults.credentialKey)
+ )
+ .backupEligible(true)
+ .backupState(storedBs)
+ .build(),
+ )
+ ),
+ ).begin.next.next.next.next.next.next.next.next.next.next.next.next
+
+ step.validations shouldBe a[Failure[_]]
+ step.validations.failed.get shouldBe an[
+ IllegalArgumentException
+ ]
+ step.tryNext shouldBe a[Failure[_]]
+ }
+ }
+ }
+
describe("18. Verify that the values of the client extension outputs in clientExtensionResults and the authenticator extension outputs in the extensions in authData are as expected, considering the client extension input values that were given in options.extensions and any specific policy of the Relying Party regarding unsolicited extensions, i.e., those that were not specified as part of options.extensions. In the general case, the meaning of \"are as expected\" is specific to the Relying Party and which extensions are in use.") {
it("Succeeds if clientExtensionResults is not a subset of the extensions requested by the Relying Party.") {
forAll(Extensions.unrequestedClientAssertionExtensions) {
@@ -1507,7 +1585,7 @@ class RelyingPartyAssertionSpec
clientExtensionResults = clientExtensionOutputs,
)
val step: FinishAssertionSteps#Step18 =
- steps.begin.next.next.next.next.next.next.next.next.next.next.next.next
+ steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next
step.validations shouldBe a[Success[_]]
step.tryNext shouldBe a[Success[_]]
@@ -1522,7 +1600,7 @@ class RelyingPartyAssertionSpec
clientExtensionResults = clientExtensionOutputs,
)
val step: FinishAssertionSteps#Step18 =
- steps.begin.next.next.next.next.next.next.next.next.next.next.next.next
+ steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next
step.validations shouldBe a[Success[_]]
step.tryNext shouldBe a[Success[_]]
@@ -1547,7 +1625,7 @@ class RelyingPartyAssertionSpec
),
)
val step: FinishAssertionSteps#Step18 =
- steps.begin.next.next.next.next.next.next.next.next.next.next.next.next
+ steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next
step.validations shouldBe a[Success[_]]
step.tryNext shouldBe a[Success[_]]
@@ -1572,7 +1650,7 @@ class RelyingPartyAssertionSpec
),
)
val step: FinishAssertionSteps#Step18 =
- steps.begin.next.next.next.next.next.next.next.next.next.next.next.next
+ steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next
step.validations shouldBe a[Success[_]]
step.tryNext shouldBe a[Success[_]]
@@ -1583,7 +1661,7 @@ class RelyingPartyAssertionSpec
it("19. Let hash be the result of computing a hash over the cData using SHA-256.") {
val steps = finishAssertion()
val step: FinishAssertionSteps#Step19 =
- steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next
+ steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next
step.validations shouldBe a[Success[_]]
step.tryNext shouldBe a[Success[_]]
@@ -1600,7 +1678,7 @@ class RelyingPartyAssertionSpec
it("The default test case succeeds.") {
val steps = finishAssertion()
val step: FinishAssertionSteps#Step20 =
- steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next
+ steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next
step.validations shouldBe a[Success[_]]
step.tryNext shouldBe a[Success[_]]
@@ -1617,7 +1695,7 @@ class RelyingPartyAssertionSpec
)
)
val step: FinishAssertionSteps#Step20 =
- steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next
+ steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next
step.validations shouldBe a[Failure[_]]
step.validations.failed.get shouldBe an[IllegalArgumentException]
@@ -1636,7 +1714,7 @@ class RelyingPartyAssertionSpec
origins = Some(Set("https://localhost")),
)
val step: FinishAssertionSteps#Step20 =
- steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next
+ steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next
step.validations shouldBe a[Failure[_]]
step.validations.failed.get shouldBe an[IllegalArgumentException]
@@ -1656,7 +1734,7 @@ class RelyingPartyAssertionSpec
)
)
val step: FinishAssertionSteps#Step20 =
- steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next
+ steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next
step.validations shouldBe a[Failure[_]]
step.validations.failed.get shouldBe an[IllegalArgumentException]
@@ -1672,7 +1750,7 @@ class RelyingPartyAssertionSpec
)
)
val step: FinishAssertionSteps#Step20 =
- steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next
+ steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next
step.validations shouldBe a[Failure[_]]
step.validations.failed.get shouldBe an[IllegalArgumentException]
@@ -1719,7 +1797,7 @@ class RelyingPartyAssertionSpec
validateSignatureCounter = true,
)
val step: FinishAssertionSteps#Step21 =
- steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next
+ steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next
step.validations shouldBe a[Success[_]]
step.tryNext shouldBe a[Success[_]]
@@ -1736,7 +1814,7 @@ class RelyingPartyAssertionSpec
validateSignatureCounter = true,
)
val step: FinishAssertionSteps#Step21 =
- steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next
+ steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next
step.validations shouldBe a[Failure[_]]
step.tryNext shouldBe a[Failure[_]]
@@ -1758,7 +1836,7 @@ class RelyingPartyAssertionSpec
validateSignatureCounter = true,
)
val step: FinishAssertionSteps#Step21 =
- steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next
+ steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next
step.validations shouldBe a[Success[_]]
step.tryNext shouldBe a[Success[_]]
@@ -1778,7 +1856,7 @@ class RelyingPartyAssertionSpec
validateSignatureCounter = false,
)
val step: FinishAssertionSteps#Step21 =
- steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next
+ steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next
step.validations shouldBe a[Success[_]]
step.tryNext shouldBe a[Success[_]]
@@ -1792,7 +1870,7 @@ class RelyingPartyAssertionSpec
validateSignatureCounter = true,
)
val step: FinishAssertionSteps#Step21 =
- steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next
+ steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next
val result = Try(step.run())
step.validations shouldBe a[Failure[_]]
@@ -1821,7 +1899,7 @@ class RelyingPartyAssertionSpec
it("22. If all the above steps are successful, continue with the authentication ceremony as appropriate. Otherwise, fail the authentication ceremony.") {
val steps = finishAssertion()
val step: FinishAssertionSteps#Finished =
- steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next
+ steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next
step.validations shouldBe a[Success[_]]
Try(steps.run) shouldBe a[Success[_]]
@@ -2377,6 +2455,145 @@ class RelyingPartyAssertionSpec
)
}
}
+
+ describe("returns AssertionResponse which") {
+ {
+ val user = UserIdentity.builder
+ .name("foo")
+ .displayName("Foo User")
+ .id(new ByteArray(Array(0, 1, 2, 3)))
+ .build()
+ val (credential, credentialKeypair, _) =
+ TestAuthenticator.createUnattestedCredential()
+ val rp = RelyingParty
+ .builder()
+ .identity(
+ RelyingPartyIdentity
+ .builder()
+ .id("localhost")
+ .name("Example RP")
+ .build()
+ )
+ .credentialRepository(
+ Helpers.CredentialRepository.withUser(
+ user,
+ RegisteredCredential
+ .builder()
+ .credentialId(credential.getId)
+ .userHandle(user.getId)
+ .publicKeyCose(
+ credential.getResponse.getParsedAuthenticatorData.getAttestedCredentialData.get.getCredentialPublicKey
+ )
+ .build(),
+ )
+ )
+ .build()
+
+ val request = AssertionRequest
+ .builder()
+ .publicKeyCredentialRequestOptions(
+ PublicKeyCredentialRequestOptions
+ .builder()
+ .challenge(ByteArray.fromBase64Url("Y2hhbGxlbmdl"))
+ .rpId("localhost")
+ .build()
+ )
+ .username(user.getName)
+ .build()
+
+ it("exposes isBackupEligible() with the BE flag value in authenticator data.") {
+ val pkcWithoutBackup =
+ TestAuthenticator.createAssertion(
+ flags = Some(new AuthenticatorDataFlags(0x00.toByte)),
+ challenge =
+ request.getPublicKeyCredentialRequestOptions.getChallenge,
+ credentialKey = credentialKeypair,
+ credentialId = credential.getId,
+ )
+ val pkcWithBackup =
+ TestAuthenticator.createAssertion(
+ flags = Some(new AuthenticatorDataFlags(0x08.toByte)),
+ challenge =
+ request.getPublicKeyCredentialRequestOptions.getChallenge,
+ credentialKey = credentialKeypair,
+ credentialId = credential.getId,
+ )
+
+ val resultWithoutBackup = rp.finishAssertion(
+ FinishAssertionOptions
+ .builder()
+ .request(request)
+ .response(pkcWithoutBackup)
+ .build()
+ )
+ val resultWithBackup = rp.finishAssertion(
+ FinishAssertionOptions
+ .builder()
+ .request(request)
+ .response(pkcWithBackup)
+ .build()
+ )
+
+ resultWithoutBackup.isBackupEligible should be(false)
+ resultWithBackup.isBackupEligible should be(true)
+ }
+
+ it(
+ "exposes isBackedUp() with the BS flag value in authenticator data."
+ ) {
+ val pkcWithoutBackup =
+ TestAuthenticator.createAssertion(
+ flags = Some(new AuthenticatorDataFlags(0x00.toByte)),
+ challenge =
+ request.getPublicKeyCredentialRequestOptions.getChallenge,
+ credentialKey = credentialKeypair,
+ credentialId = credential.getId,
+ )
+ val pkcWithBeOnly =
+ TestAuthenticator.createAssertion(
+ flags = Some(new AuthenticatorDataFlags(0x08.toByte)),
+ challenge =
+ request.getPublicKeyCredentialRequestOptions.getChallenge,
+ credentialKey = credentialKeypair,
+ credentialId = credential.getId,
+ )
+ val pkcWithBackup =
+ TestAuthenticator.createAssertion(
+ flags = Some(new AuthenticatorDataFlags(0x18.toByte)),
+ challenge =
+ request.getPublicKeyCredentialRequestOptions.getChallenge,
+ credentialKey = credentialKeypair,
+ credentialId = credential.getId,
+ )
+
+ val resultWithBackup = rp.finishAssertion(
+ FinishAssertionOptions
+ .builder()
+ .request(request)
+ .response(pkcWithBackup)
+ .build()
+ )
+ val resultWithBeOnly = rp.finishAssertion(
+ FinishAssertionOptions
+ .builder()
+ .request(request)
+ .response(pkcWithBeOnly)
+ .build()
+ )
+ val resultWithoutBackup = rp.finishAssertion(
+ FinishAssertionOptions
+ .builder()
+ .request(request)
+ .response(pkcWithoutBackup)
+ .build()
+ )
+
+ resultWithoutBackup.isBackedUp should be(false)
+ resultWithBeOnly.isBackedUp should be(false)
+ resultWithBackup.isBackedUp should be(true)
+ }
+ }
+ }
}
}
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 337e4aaeb..983cd281d 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
@@ -44,6 +44,7 @@ import com.yubico.webauthn.data.AttestationObject
import com.yubico.webauthn.data.AttestationType
import com.yubico.webauthn.data.AuthenticatorAttestationResponse
import com.yubico.webauthn.data.AuthenticatorData
+import com.yubico.webauthn.data.AuthenticatorDataFlags
import com.yubico.webauthn.data.AuthenticatorSelectionCriteria
import com.yubico.webauthn.data.AuthenticatorTransport
import com.yubico.webauthn.data.ByteArray
@@ -4515,6 +4516,110 @@ class RelyingPartyRegistrationSpec
testData.response.getResponse.getAttestation.getAuthenticatorData.getAttestedCredentialData.get.getAaguid
)
}
+
+ {
+ val rp = RelyingParty
+ .builder()
+ .identity(
+ RelyingPartyIdentity
+ .builder()
+ .id("localhost")
+ .name("Example RP")
+ .build()
+ )
+ .credentialRepository(Helpers.CredentialRepository.empty)
+ .build()
+ val user = UserIdentity.builder
+ .name("foo")
+ .displayName("Foo User")
+ .id(new ByteArray(Array(0, 1, 2, 3)))
+ .build()
+
+ val request = PublicKeyCredentialCreationOptions
+ .builder()
+ .rp(rp.getIdentity)
+ .user(user)
+ .challenge(ByteArray.fromBase64Url("Y2hhbGxlbmdl"))
+ .pubKeyCredParams(List(PublicKeyCredentialParameters.ES256).asJava)
+ .build()
+
+ it("exposes isBackupEligible() with the BE flag value in authenticator data.") {
+ val (pkcWithoutBackup, _, _) =
+ TestAuthenticator.createUnattestedCredential(
+ flags = Some(new AuthenticatorDataFlags(0x00.toByte)),
+ challenge = request.getChallenge,
+ )
+ val (pkcWithBackup, _, _) =
+ TestAuthenticator.createUnattestedCredential(
+ flags = Some(new AuthenticatorDataFlags(0x08.toByte)),
+ challenge = request.getChallenge,
+ )
+
+ val resultWithoutBackup = rp.finishRegistration(
+ FinishRegistrationOptions
+ .builder()
+ .request(request)
+ .response(pkcWithoutBackup)
+ .build()
+ )
+ val resultWithBackup = rp.finishRegistration(
+ FinishRegistrationOptions
+ .builder()
+ .request(request)
+ .response(pkcWithBackup)
+ .build()
+ )
+
+ resultWithoutBackup.isBackupEligible should be(false)
+ resultWithBackup.isBackupEligible should be(true)
+ }
+
+ it(
+ "exposes isBackedUp() with the BS flag value in authenticator data."
+ ) {
+ val (pkcWithoutBackup, _, _) =
+ TestAuthenticator.createUnattestedCredential(
+ flags = Some(new AuthenticatorDataFlags(0x00.toByte)),
+ challenge = request.getChallenge,
+ )
+ val (pkcWithBeOnly, _, _) =
+ TestAuthenticator.createUnattestedCredential(
+ flags = Some(new AuthenticatorDataFlags(0x08.toByte)),
+ challenge = request.getChallenge,
+ )
+ val (pkcWithBackup, _, _) =
+ TestAuthenticator.createUnattestedCredential(
+ flags = Some(new AuthenticatorDataFlags(0x18.toByte)),
+ challenge = request.getChallenge,
+ )
+
+ val resultWithBackup = rp.finishRegistration(
+ FinishRegistrationOptions
+ .builder()
+ .request(request)
+ .response(pkcWithBackup)
+ .build()
+ )
+ val resultWithBeOnly = rp.finishRegistration(
+ FinishRegistrationOptions
+ .builder()
+ .request(request)
+ .response(pkcWithBeOnly)
+ .build()
+ )
+ val resultWithoutBackup = rp.finishRegistration(
+ FinishRegistrationOptions
+ .builder()
+ .request(request)
+ .response(pkcWithoutBackup)
+ .build()
+ )
+
+ resultWithoutBackup.isBackedUp should be(false)
+ resultWithBeOnly.isBackedUp should be(false)
+ resultWithBackup.isBackedUp should be(true)
+ }
+ }
}
}
diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/TestAuthenticator.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/TestAuthenticator.scala
index 1fd23b828..3d70dfbc5 100644
--- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/TestAuthenticator.scala
+++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/TestAuthenticator.scala
@@ -38,6 +38,7 @@ import com.yubico.webauthn.TpmAttestationStatementVerifier.TpmRsaScheme
import com.yubico.webauthn.data.AuthenticatorAssertionResponse
import com.yubico.webauthn.data.AuthenticatorAttestationResponse
import com.yubico.webauthn.data.AuthenticatorData
+import com.yubico.webauthn.data.AuthenticatorDataFlags
import com.yubico.webauthn.data.ByteArray
import com.yubico.webauthn.data.COSEAlgorithmIdentifier
import com.yubico.webauthn.data.ClientAssertionExtensionOutputs
@@ -378,6 +379,7 @@ object TestAuthenticator {
authenticatorExtensions: Option[JsonNode] = None,
credentialKeypair: Option[KeyPair] = None,
keyAlgorithm: COSEAlgorithmIdentifier = Defaults.keyAlgorithm,
+ flags: Option[AuthenticatorDataFlags] = None,
): (
ByteArray,
KeyPair,
@@ -393,6 +395,7 @@ object TestAuthenticator {
val authDataBytes: ByteArray = makeAuthDataBytes(
rpId = Defaults.rpId,
+ flags = flags,
attestedCredentialDataBytes = Some(
makeAttestedCredentialDataBytes(
aaguid = aaguid,
@@ -538,6 +541,7 @@ object TestAuthenticator {
def createUnattestedCredential(
authenticatorExtensions: Option[JsonNode] = None,
challenge: ByteArray = Defaults.challenge,
+ flags: Option[AuthenticatorDataFlags] = None,
): (
PublicKeyCredential[
AuthenticatorAttestationResponse,
@@ -547,7 +551,8 @@ object TestAuthenticator {
List[(X509Certificate, PrivateKey)],
) = {
val (authData, keypair) = createAuthenticatorData(
- authenticatorExtensions = authenticatorExtensions
+ authenticatorExtensions = authenticatorExtensions,
+ flags = flags,
)
createCredential(
authDataBytes = authData,
@@ -593,6 +598,7 @@ object TestAuthenticator {
ClientAssertionExtensionOutputs.builder().build(),
credentialId: ByteArray = Defaults.credentialId,
credentialKey: KeyPair = Defaults.credentialKey,
+ flags: Option[AuthenticatorDataFlags] = None,
origin: String = Defaults.origin,
signatureCount: Option[Int] = None,
tokenBindingStatus: String = Defaults.TokenBinding.status,
@@ -632,6 +638,7 @@ object TestAuthenticator {
val authDataBytes: ByteArray =
makeAuthDataBytes(
+ flags = flags,
signatureCount = signatureCount,
rpId = Defaults.rpId,
extensionsCborBytes = authenticatorExtensions map (ext =>
@@ -1046,17 +1053,20 @@ object TestAuthenticator {
def makeAuthDataBytes(
rpId: String = Defaults.rpId,
+ flags: Option[AuthenticatorDataFlags] = None,
signatureCount: Option[Int] = None,
attestedCredentialDataBytes: Option[ByteArray] = None,
extensionsCborBytes: Option[ByteArray] = None,
- ): ByteArray =
+ ): ByteArray = {
+ val atFlag = if (attestedCredentialDataBytes.isDefined) 0x40 else 0x00
+ val edFlag = if (extensionsCborBytes.isDefined) 0x80 else 0x00
new ByteArray(
(Vector[Byte]()
++ sha256(rpId).getBytes.toVector
++ Some[Byte](
- (0x01 | (if (attestedCredentialDataBytes.isDefined) 0x40
- else 0x00) | (if (extensionsCborBytes.isDefined) 0x80
- else 0x00)).toByte
+ (flags
+ .map(_.value)
+ .getOrElse(0x00.toByte) | 0x01 | atFlag | edFlag).toByte
)
++ BinaryUtil
.encodeUint32(signatureCount.getOrElse(1337).toLong)
@@ -1068,6 +1078,7 @@ object TestAuthenticator {
_.getBytes.toVector
} getOrElse Nil)).toArray
)
+ }
def makeAttestedCredentialDataBytes(
publicKeyCose: ByteArray,
diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/AuthenticatorDataFlagsSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/AuthenticatorDataFlagsSpec.scala
index 33e7b16d6..9b67d11c7 100644
--- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/AuthenticatorDataFlagsSpec.scala
+++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/AuthenticatorDataFlagsSpec.scala
@@ -43,6 +43,8 @@ class AuthenticatorDataFlagsSpec extends AnyFunSpec with Matchers {
val flags = decode("01")
flags.UP should be(true)
flags.UV should be(false)
+ flags.BE should be(false)
+ flags.BS should be(false)
flags.AT should be(false)
flags.ED should be(false)
}
@@ -51,6 +53,28 @@ class AuthenticatorDataFlagsSpec extends AnyFunSpec with Matchers {
val flags = decode("04")
flags.UP should be(false)
flags.UV should be(true)
+ flags.BE should be(false)
+ flags.BS should be(false)
+ flags.AT should be(false)
+ flags.ED should be(false)
+ }
+
+ it("0x08 to BE.") {
+ val flags = decode("08")
+ flags.UP should be(false)
+ flags.UV should be(false)
+ flags.BE should be(true)
+ flags.BS should be(false)
+ flags.AT should be(false)
+ flags.ED should be(false)
+ }
+
+ it("0x10 to BS.") {
+ val flags = decode("18")
+ flags.UP should be(false)
+ flags.UV should be(false)
+ flags.BE should be(true)
+ flags.BS should be(true)
flags.AT should be(false)
flags.ED should be(false)
}
@@ -59,6 +83,8 @@ class AuthenticatorDataFlagsSpec extends AnyFunSpec with Matchers {
val flags = decode("40")
flags.UP should be(false)
flags.UV should be(false)
+ flags.BE should be(false)
+ flags.BS should be(false)
flags.AT should be(true)
flags.ED should be(false)
}
@@ -67,6 +93,8 @@ class AuthenticatorDataFlagsSpec extends AnyFunSpec with Matchers {
val flags = decode("80")
flags.UP should be(false)
flags.UV should be(false)
+ flags.BE should be(false)
+ flags.BS should be(false)
flags.AT should be(false)
flags.ED should be(true)
}
diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/AuthenticatorDataSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/AuthenticatorDataSpec.scala
index d3f5186bd..a405765f6 100644
--- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/AuthenticatorDataSpec.scala
+++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/AuthenticatorDataSpec.scala
@@ -27,6 +27,8 @@ package com.yubico.webauthn.data
import com.upokecenter.cbor.CBORObject
import com.yubico.internal.util.BinaryUtil
import com.yubico.webauthn.WebAuthnTestCodecs
+import com.yubico.webauthn.data.Generators.authenticatorDataBytes
+import com.yubico.webauthn.data.Generators.authenticatorDataFlagsByte
import com.yubico.webauthn.data.Generators.byteArray
import org.junit.runner.RunWith
import org.scalacheck.Arbitrary.arbitrary
@@ -61,10 +63,11 @@ class AuthenticatorDataSpec
}
it("with attested credential data must be at least 55 bytes.") {
- forAll(byteArray(37, 54)) { bytes =>
- val authData = new ByteArray(
- bytes.getBytes.updated(32, (bytes.getBytes.apply(32) | 0x40).toByte)
- )
+ forAll(
+ byteArray(37, 54),
+ authenticatorDataFlagsByte.map(flags => (flags | 0x40).toByte),
+ ) { (bytes, flags) =>
+ val authData = new ByteArray(bytes.getBytes.updated(32, flags))
val result = Try(new AuthenticatorData(authData))
result shouldBe a[Failure[_]]
result.failed.get.getMessage should include(
@@ -78,11 +81,12 @@ class AuthenticatorDataSpec
prefix <- Gen.infiniteLazyList(arbitrary[Byte]).map(_.take(53).toArray)
credIdLen <- Gen.chooseNum(1, 2048)
credId <- Gen.listOfN(credIdLen - 1, arbitrary[Byte])
- } yield (prefix, credIdLen, credId)) {
+ flags <- authenticatorDataFlagsByte.map(flags => (flags | 0x40).toByte)
+ } yield (prefix.updated(32, flags), credIdLen, credId)) {
case (prefix, credIdLen, credId) =>
- val bytes = prefix ++ BinaryUtil.encodeUint16(credIdLen) ++ credId
- val authData =
- new ByteArray(bytes.updated(32, (bytes(32) | 0x40).toByte))
+ val authData = new ByteArray(
+ prefix ++ BinaryUtil.encodeUint16(credIdLen) ++ credId
+ )
val result = Try(new AuthenticatorData(authData))
result shouldBe a[Failure[_]]
result.failed.get.getMessage should include(
@@ -93,6 +97,8 @@ class AuthenticatorDataSpec
def generateTests(
authDataHex: String,
+ backupEligible: Boolean = false,
+ backupState: Boolean = false,
hasAttestation: Boolean = false,
hasExtensions: Boolean = false,
): Unit = {
@@ -108,6 +114,8 @@ class AuthenticatorDataSpec
it("gets the correct flags from the raw bytes.") {
authData.getFlags.UP should be(true)
authData.getFlags.UV should be(false)
+ authData.getFlags.BE should be(backupEligible)
+ authData.getFlags.BS should be(backupState)
authData.getFlags.AT should equal(hasAttestation)
authData.getFlags.ED should equal(hasExtensions)
}
@@ -166,6 +174,63 @@ class AuthenticatorDataSpec
)
}
+ describe("with BE=1") {
+ generateTests(
+ "49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d9763" // RP ID hash
+ + "09" // Flags
+ + "00000539", // Signature count
+ backupEligible = true,
+ backupState = false,
+ )
+ }
+
+ describe("with BE=1 and BS=1") {
+ generateTests(
+ "49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d9763" // RP ID hash
+ + "19" // Flags
+ + "00000539", // Signature count
+ backupEligible = true,
+ backupState = true,
+ )
+ }
+
+ it("with BE=0 and BS=1 is invalid.") {
+ forAll(
+ authenticatorDataBytes(
+ Gen.option(Generators.Extensions.anyAssertionExtensions.map({
+ case (_, _, ext) => ext
+ })),
+ backupFlagsGen = Gen.const((false, true)),
+ )
+ ) { authDataBytes =>
+ val neitherFlag = new AuthenticatorData(
+ new ByteArray(
+ authDataBytes.getBytes
+ .updated(32, (authDataBytes.getBytes()(32) & ~0x10).toByte)
+ )
+ )
+ neitherFlag should not be null
+ neitherFlag.getFlags.BE should be(false)
+ neitherFlag.getFlags.BS should be(false)
+
+ val onlyBe = new AuthenticatorData(
+ new ByteArray(
+ authDataBytes.getBytes.updated(
+ 32,
+ ((authDataBytes.getBytes()(32) | 0x08) & ~0x10).toByte,
+ )
+ )
+ )
+ onlyBe should not be null
+ onlyBe.getFlags.BE should be(true)
+ onlyBe.getFlags.BS should be(false)
+
+ an[IllegalArgumentException] should be thrownBy {
+ new AuthenticatorData(authDataBytes)
+ }
+ }
+ }
+
describe("with only attestation data") {
generateTests(
"49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d9763" // RP ID hash
diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/BuildersSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/BuildersSpec.scala
index d0819af58..290c14ac8 100644
--- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/BuildersSpec.scala
+++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/BuildersSpec.scala
@@ -26,9 +26,6 @@ package com.yubico.webauthn.data
import com.fasterxml.jackson.core.`type`.TypeReference
import com.yubico.webauthn.AssertionRequest
-import com.yubico.webauthn.AssertionResult
-import com.yubico.webauthn.Generators._
-import com.yubico.webauthn.RegistrationResult
import com.yubico.webauthn.data.Generators._
import org.junit.runner.RunWith
import org.scalacheck.Arbitrary
@@ -65,7 +62,6 @@ class BuildersSpec
test(new TypeReference[AssertionExtensionInputs]() {})
test(new TypeReference[AssertionRequest]() {})
- test(new TypeReference[AssertionResult]() {})
test(new TypeReference[AttestedCredentialData]() {})
test(new TypeReference[AuthenticatorAssertionResponse]() {})
test(new TypeReference[AuthenticatorAttestationResponse]() {})
@@ -89,7 +85,6 @@ class BuildersSpec
test(new TypeReference[PublicKeyCredentialParameters]() {})
test(new TypeReference[PublicKeyCredentialRequestOptions]() {})
test(new TypeReference[RegistrationExtensionInputs]() {})
- test(new TypeReference[RegistrationResult]() {})
test(new TypeReference[RelyingPartyIdentity]() {})
test(new TypeReference[UserIdentity]() {})
}
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 493ef1a95..6a0ba9742 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
@@ -63,7 +63,7 @@ object Generators {
private def jsonFactory: JsonNodeFactory = JsonNodeFactory.instance
- private def setFlag(flags: Byte, mask: Byte, value: Boolean): Byte =
+ private def setFlag(mask: Byte, value: Boolean)(flags: Byte): Byte =
if (value)
(flags | (mask & (-0x01).toByte)).toByte
else
@@ -207,10 +207,15 @@ object Generators {
)
} yield new ByteArray(JacksonCodecs.cbor().writeValueAsBytes(attObj))
- implicit val arbitraryAuthenticatorDataFlags
- : Arbitrary[AuthenticatorDataFlags] = Arbitrary(for {
+ val authenticatorDataFlagsByte: Gen[Byte] = for {
value <- arbitrary[Byte]
- } yield new AuthenticatorDataFlags(value))
+ bsMask = (((value & 0x08) << 1) & 0xef).toByte // Bit 0x10 cannot be set unless 0x08 is
+ } yield (value & bsMask).toByte
+
+ implicit val arbitraryAuthenticatorDataFlags
+ : Arbitrary[AuthenticatorDataFlags] = Arbitrary(
+ authenticatorDataFlagsByte.map(new AuthenticatorDataFlags(_))
+ )
implicit val arbitraryAuthenticatorAssertionResponse
: Arbitrary[AuthenticatorAssertionResponse] = Arbitrary(
@@ -258,29 +263,47 @@ object Generators {
)
def authenticatorDataBytes(
- extensionsGen: Gen[Option[CBORObject]]
+ extensionsGen: Gen[Option[CBORObject]],
+ rpIdHashGen: Gen[ByteArray] = byteArray(32),
+ upFlagGen: Gen[Boolean] = Gen.const(true),
+ uvFlagGen: Gen[Boolean] = arbitrary[Boolean],
+ backupFlagsGen: Gen[(Boolean, Boolean)] =
+ arbitrary[(Boolean, Boolean)].map({ case (be, bs) => (be, be && bs) }),
+ signatureCountGen: Gen[ByteArray] = byteArray(4),
): Gen[ByteArray] =
for {
- fixedBytes <- byteArray(37)
+ rpIdHash <- rpIdHashGen
+ signatureCount <- signatureCountGen
attestedCredentialDataBytes <- Gen.option(attestedCredentialDataBytes)
- extensions <- extensionsGen
+ extensions <- extensionsGen
extensionsBytes = extensions map { exts =>
new ByteArray(
exts.EncodeToBytes(CBOREncodeOptions.DefaultCtap2Canonical)
)
}
+
+ flagsBase <- arbitrary[Byte]
+ upFlag <- upFlagGen
+ uvFlag <- uvFlagGen
+ (beFlag, bsFlag) <- backupFlagsGen
atFlag = attestedCredentialDataBytes.isDefined
edFlag = extensionsBytes.isDefined
- flagsByte: Byte = setFlag(
- setFlag(fixedBytes.getBytes()(32), 0x40, atFlag),
- BinaryUtil.singleFromHex("80"),
- edFlag,
+ flagsByte: Byte = setFlag(0x01, upFlag)(
+ setFlag(0x03, uvFlag)(
+ setFlag(0x40, atFlag)(
+ setFlag(BinaryUtil.singleFromHex("80"), edFlag)(
+ setFlag(0x08, beFlag)(setFlag(0x10, bsFlag)(flagsBase))
+ )
+ )
+ )
)
} yield new ByteArray(
- fixedBytes.getBytes.updated(32, flagsByte)
- ++ attestedCredentialDataBytes.map(_.getBytes).getOrElse(Array.empty)
- ++ extensionsBytes.map(_.getBytes).getOrElse(Array.empty)
+ rpIdHash.getBytes
+ :+ flagsByte
+ :++ signatureCount.getBytes
+ ++ attestedCredentialDataBytes.map(_.getBytes).getOrElse(Array.empty)
+ ++ extensionsBytes.map(_.getBytes).getOrElse(Array.empty)
)
implicit val arbitraryAuthenticatorSelectionCriteria
true
if and only if at least one of the following is true:
*
@@ -96,65 +95,20 @@ public class AssertionResult {
*/
private final boolean signatureCounterValid;
- private final ClientAssertionExtensionOutputs clientExtensionOutputs;
-
- private final AuthenticatorAssertionExtensionOutputs authenticatorExtensionOutputs;
-
- private AssertionResult(
- boolean success,
- @NonNull @JsonProperty("credential") RegisteredCredential credential,
- @NonNull String username,
- long signatureCount,
- boolean signatureCounterValid,
- ClientAssertionExtensionOutputs clientExtensionOutputs,
- AuthenticatorAssertionExtensionOutputs authenticatorExtensionOutputs) {
- this(
- success,
- credential,
- username,
- null,
- null,
- signatureCount,
- signatureCounterValid,
- clientExtensionOutputs,
- authenticatorExtensionOutputs);
- }
-
@JsonCreator
- private AssertionResult(
+ AssertionResult(
@JsonProperty("success") boolean success,
+ @NonNull @JsonProperty("credentialResponse")
+ PublicKeyCredentialtrue
if and only if the created credential is backup eligible. NOTE that
+ * this is only a hint and not a guarantee, unless backed by a trusted authenticator
+ * attestation.
+ * @see Backup Eligible in §4.
+ * Terminology
+ * @see BE flag in §6.1. Authenticator
+ * Data
+ * @deprecated EXPERIMENTAL: This feature is from a not yet mature standard; it could change as
+ * the standard matures.
+ */
+ @Deprecated
+ @JsonIgnore
+ public boolean isBackupEligible() {
+ return credentialResponse.getResponse().getParsedAuthenticatorData().getFlags().BE;
+ }
+
+ /**
+ * Get the current backup state of the
+ * asserted credential, using the BS
+ * flag in the authenticator data.
+ *
+ * true
if and only if the created credential is believed to currently be
+ * backed up. NOTE that this is only a hint and not a guarantee, unless backed by a trusted
+ * authenticator attestation.
+ * @see Backup State in §4. Terminology
+ * @see BS flag in §6.1. Authenticator
+ * Data
+ * @deprecated EXPERIMENTAL: This feature is from a not yet mature standard; it could change as
+ * the standard matures.
+ */
+ @Deprecated
+ @JsonIgnore
+ public boolean isBackedUp() {
+ return credentialResponse.getResponse().getParsedAuthenticatorData().getFlags().BS;
+ }
+
+ /**
+ * The new signature
+ * count of the credential used for the assertion.
+ *
+ * true
, the credential is backup eligible: it can be backed up in
+ * some way, most commonly by syncing the private key to a cloud account.
+ *
+ * false
, the credential is not backup eligible: it cannot be
+ * backed up in any way.
+ *
+ * null
or not set this value.
+ *
+ * @deprecated EXPERIMENTAL: This feature is from a not yet mature standard; it could change as
+ * the standard matures.
+ */
+ @Deprecated
+ @Getter(AccessLevel.NONE)
+ @Builder.Default
+ private final Boolean backupEligible = null;
+
+ /**
+ * The last known state of the BS
+ * flag for this credential, if known.
+ *
+ * true
, the credential is believed to be currently backed up.
+ *
+ * false
, the credential is believed to not be currently backed up.
+ *
+ * null
+ *
or not set this value.
+ *
+ * @deprecated EXPERIMENTAL: This feature is from a not yet mature standard; it could change as
+ * the standard matures.
+ */
+ @Deprecated
+ @Getter(AccessLevel.NONE)
+ @Builder.Default
+ private final Boolean backupState = null;
+
@JsonCreator
private RegisteredCredential(
@NonNull @JsonProperty("credentialId") ByteArray credentialId,
@NonNull @JsonProperty("userHandle") ByteArray userHandle,
@NonNull @JsonProperty("publicKeyCose") ByteArray publicKeyCose,
- @JsonProperty("signatureCount") long signatureCount) {
+ @JsonProperty("signatureCount") long signatureCount,
+ @JsonProperty("backupEligible") Boolean backupEligible,
+ @JsonProperty("backupState") Boolean backupState) {
this.credentialId = credentialId;
this.userHandle = userHandle;
this.publicKeyCose = publicKeyCose;
this.signatureCount = signatureCount;
+ this.backupEligible = backupEligible;
+ this.backupState = backupState;
+ }
+
+ /**
+ * The state of the BE flag when
+ * this credential was registered, if known.
+ *
+ * true
, the credential is backup eligible: it can be backed up in
+ * some way, most commonly by syncing the private key to a cloud account.
+ *
+ * false
, the credential is not backup eligible: it cannot be
+ * backed up in any way.
+ *
+ * null
or not set this value.
+ *
+ * @deprecated EXPERIMENTAL: This feature is from a not yet mature standard; it could change as
+ * the standard matures.
+ */
+ @Deprecated
+ public Optionaltrue
, the credential is believed to be currently backed up.
+ *
+ * false
, the credential is believed to not be currently backed up.
+ *
+ * null
+ *
or not set this value.
+ *
+ * @deprecated EXPERIMENTAL: This feature is from a not yet mature standard; it could change as
+ * the standard matures.
+ */
+ @Deprecated
+ public Optionalaaguid
- * reported in the of the
- * created credential.
- *
- * true
if and only if the attestation signature was successfully linked to a trusted
@@ -104,123 +86,166 @@ public class RegistrationResult {
// JavaDoc on getter
private final List> attestationTrustPath) {
+ this.credential = credential;
this.attestationTrusted = attestationTrusted;
this.attestationType = attestationType;
- this.attestationTrustPath = attestationTrustPath;
- this.publicKeyCose = publicKeyCose;
- this.signatureCount = signatureCount == null ? 0 : signatureCount;
- this.clientExtensionOutputs =
- clientExtensionOutputs == null || clientExtensionOutputs.getExtensionIds().isEmpty()
- ? null
- : clientExtensionOutputs;
- this.authenticatorExtensionOutputs = authenticatorExtensionOutputs;
+ this.attestationTrustPath = attestationTrustPath.orElse(null);
}
@JsonCreator
private static RegistrationResult fromJson(
- @NonNull @JsonProperty("keyId") PublicKeyCredentialDescriptor keyId,
- @NonNull @JsonProperty("aaguid") ByteArray aaguid,
+ @NonNull @JsonProperty("credential")
+ PublicKeyCredential
> attestationTrustPath) {
return new RegistrationResult(
- keyId,
- aaguid,
+ credential,
attestationTrusted,
attestationType,
- attestationTrustPath.stream()
- .map(
- pem -> {
- try {
- return CertificateParser.parsePem(pem);
- } catch (CertificateException e) {
- throw new RuntimeException(e);
- }
- })
- .collect(Collectors.toList()),
- publicKeyCose,
- signatureCount,
- clientExtensionOutputs,
- authenticatorExtensionOutputs);
+ attestationTrustPath.map(
+ atp ->
+ atp.stream()
+ .map(
+ pem -> {
+ try {
+ return CertificateParser.parsePem(pem);
+ } catch (CertificateException e) {
+ throw new RuntimeException(e);
+ }
+ })
+ .collect(Collectors.toList())));
}
/**
- * The attestation
- * trust path for the created credential, if any.
+ * Check whether the created credential is backup eligible, using the BE flag in the authenticator data.
*
- *
true
.
+ * true
if and only if the created credential is backup eligible. NOTE that
+ * this is only a hint and not a guarantee, unless backed by a trusted authenticator
+ * attestation.
+ * @see Backup Eligible in §4.
+ * Terminology
+ * @see BE flag in §6.1. Authenticator
+ * Data
+ * @deprecated EXPERIMENTAL: This feature is from a not yet mature standard; it could change as
+ * the standard matures.
+ */
+ @Deprecated
+ @JsonIgnore
+ public boolean isBackupEligible() {
+ return credential.getResponse().getParsedAuthenticatorData().getFlags().BE;
+ }
+
+ /**
+ * Get the current backup state of the
+ * created credential, using the BS
+ * flag in the authenticator data.
+ *
+ * true
if and only if the created credential is believed to currently be
+ * backed up. NOTE that this is only a hint and not a guarantee, unless backed by a trusted
+ * authenticator attestation.
+ * @see Backup State in §4. Terminology
+ * @see BS flag in §6.1. Authenticator
+ * Data
+ * @deprecated EXPERIMENTAL: This feature is from a not yet mature standard; it could change as
+ * the standard matures.
+ */
+ @Deprecated
+ @JsonIgnore
+ public boolean isBackedUp() {
+ return credential.getResponse().getParsedAuthenticatorData().getFlags().BS;
+ }
+
+ /**
+ * The signature count returned with the created credential.
+ *
+ * > getAttestationTrustPath() {
- return Optional.ofNullable(attestationTrustPath);
+ public PublicKeyCredentialDescriptor getKeyId() {
+ return PublicKeyCredentialDescriptor.builder()
+ .id(credential.getId())
+ .type(credential.getType())
+ .transports(credential.getResponse().getTransports())
+ .build();
}
- @JsonProperty("attestationTrustPath")
- private Optional
> getAttestationTrustPathJson() {
- return getAttestationTrustPath()
- .map(
- x5c ->
- x5c.stream()
- .map(
- cert -> {
- try {
- return new ByteArray(cert.getEncoded()).getBase64();
- } catch (CertificateEncodingException e) {
- throw new RuntimeException(e);
- }
- })
- .collect(Collectors.toList()));
+ /**
+ * The
aaguid
+ * reported in the of the
+ * created credential.
+ *
+ * > getAttestationTrustPathJson() {
* @see ClientRegistrationExtensionOutputs
* @see #getAuthenticatorExtensionOutputs() ()
*/
+ @JsonIgnore
public Optional
true
.
+ *
+ * > getAttestationTrustPath() {
+ return Optional.ofNullable(attestationTrustPath);
}
- static class RegistrationResultBuilder {
- static class MandatoryStages {
- private final RegistrationResultBuilder builder = new RegistrationResultBuilder();
-
- Step2 keyId(PublicKeyCredentialDescriptor keyId) {
- builder.keyId(keyId);
- return new Step2();
- }
-
- class Step2 {
- Step3 aaguid(ByteArray aaguid) {
- builder.aaguid(aaguid);
- return new Step3();
- }
- }
-
- class Step3 {
- Step4 attestationTrusted(boolean attestationTrusted) {
- builder.attestationTrusted(attestationTrusted);
- return new Step4();
- }
- }
-
- class Step4 {
- Step5 attestationType(AttestationType attestationType) {
- builder.attestationType(attestationType);
- return new Step5();
- }
- }
-
- class Step5 {
- Step6 publicKeyCose(ByteArray publicKeyCose) {
- builder.publicKeyCose(publicKeyCose);
- return new Step6();
- }
- }
-
- class Step6 {
- Step7 signatureCount(long signatureCount) {
- builder.signatureCount(signatureCount);
- return new Step7();
- }
- }
-
- class Step7 {
- Step8 clientExtensionOutputs(ClientRegistrationExtensionOutputs clientExtensionOutputs) {
- builder.clientExtensionOutputs(clientExtensionOutputs);
- return new Step8();
- }
- }
-
- class Step8 {
- RegistrationResultBuilder authenticatorExtensionOutputs(
- AuthenticatorRegistrationExtensionOutputs authenticatorExtensionOutputs) {
- return builder.authenticatorExtensionOutputs(authenticatorExtensionOutputs);
- }
- }
- }
+ @JsonProperty("attestationTrustPath")
+ private Optional
> getAttestationTrustPathJson() {
+ return getAttestationTrustPath()
+ .map(
+ x5c ->
+ x5c.stream()
+ .map(
+ cert -> {
+ try {
+ return new ByteArray(cert.getEncoded()).getBase64();
+ } catch (CertificateEncodingException e) {
+ throw new RuntimeException(e);
+ }
+ })
+ .collect(Collectors.toList()));
}
}
diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorAssertionResponse.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorAssertionResponse.java
index f4925bff6..f168f9a9b 100644
--- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorAssertionResponse.java
+++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorAssertionResponse.java
@@ -72,6 +72,11 @@ public class AuthenticatorAssertionResponse implements AuthenticatorResponse {
*/
private final ByteArray userHandle;
+ // This overrides the default getter in AuthenticatorResponse which re-parses the authenticator
+ // data on every invocation. This "optimization" has no measurable impact on performance, but it
+ // seems rude to obviously waste cycles for nothing.
+ private final transient AuthenticatorData parsedAuthenticatorData;
+
@NonNull
@Getter(onMethod = @__({@Override}))
private final transient CollectedClientData clientData;
@@ -85,6 +90,7 @@ private AuthenticatorAssertionResponse(
@JsonProperty("userHandle") final ByteArray userHandle)
throws IOException, Base64UrlException {
this.authenticatorData = authenticatorData;
+ this.parsedAuthenticatorData = new AuthenticatorData(authenticatorData);
this.clientDataJSON = clientDataJSON;
this.signature = signature;
this.userHandle = userHandle;
diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorAttestationResponse.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorAttestationResponse.java
index 918ca1f3a..22ba3bce3 100644
--- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorAttestationResponse.java
+++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorAttestationResponse.java
@@ -113,6 +113,14 @@ private AuthenticatorAttestationResponse(
this.clientData = new CollectedClientData(clientDataJSON);
}
+ // The default getter in AuthenticatorResponse re-parses the authenticator data on every
+ // invocation. This "optimization" override has no measurable impact on performance, but it seems
+ // rude to obviously waste cycles for nothing.
+ @Override
+ public AuthenticatorData getParsedAuthenticatorData() {
+ return attestation.getAuthenticatorData();
+ }
+
public static AuthenticatorAttestationResponseBuilder.MandatoryStages builder() {
return new AuthenticatorAttestationResponseBuilder.MandatoryStages();
}
diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorDataFlags.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorDataFlags.java
index 567ab2134..3d123010b 100644
--- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorDataFlags.java
+++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorDataFlags.java
@@ -45,6 +45,30 @@ public final class AuthenticatorDataFlags {
/** User verified */
public final boolean UV;
+ /**
+ * Backup eligible: the credential can and is allowed to be backed up.
+ *
+ *