Skip to content

Commit

Permalink
Add support for new backup flags in authenticator data
Browse files Browse the repository at this point in the history
  • Loading branch information
emlun committed Nov 10, 2022
2 parents e14123a + a6d89fd commit 19a51c5
Show file tree
Hide file tree
Showing 18 changed files with 1,012 additions and 453 deletions.
28 changes: 27 additions & 1 deletion NEWS
Original file line number Diff line number Diff line change
@@ -1,7 +1,33 @@
== Version 2.1.1 (unreleased) ==
== Version 2.2.0 (unreleased) ==

`webauthn-server-core`:

Changes:

* Changed internal structure of `RegistrationResult` and `AssertionResult`. This
may affect you if you use Jackson or similar tools to serialize these values
to JSON, for example. This is not an officially supported use case and thus
does not warrant a major version bump.
* Removed methods `RegistrationResult.toBuilder()` and
`AssertionResult.toBuilder()`. Both had package-private return types, and thus
were not usable by outside callers.

New features:

* (Experimental) Added support for the new `BE` (backup eligible) and `BS`
(backup state) flags in authenticator data:
** Added `BE` and `BS` properties to `AuthenticatorDataFlags`, reflecting the
respective flags (bits 0x08 and 0x10).
** Added methods `isBackupEligible()` and `isBackedUp()` to
`RegistrationResult` and `AssertionResult`, reflecting respectively the `BE`
and `BS` flags.
** Added properties `backupEligible` and `backupState`, getters
`isBackupEligible()` and `isBackedUp()`, and corresponding builder methods
to `RegisteredCredential`. `RelyingParty.finishAssertion(...)` will now
validate that if `RegisteredCredential.isBackupEligible()` is present, then
the `BE` flag of any assertion of that credential must match the stored
value.

Fixes:

* Fixed TPM attestation verification rejecting attestation certificates with TPM
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ public void finishRegistration(Blackhole bh, RegistrationState state)
throws RegistrationFailedException {
final RegistrationResult result = state.rp.finishRegistration(state.fro);
bh.consume(result.getKeyId());
bh.consume(result.isBackupEligible());
bh.consume(result.isBackedUp());
bh.consume(result.getSignatureCount());
bh.consume(result.getAaguid());
bh.consume(result.getPublicKeyCose());
Expand All @@ -80,6 +82,8 @@ public void finishRegistration(Blackhole bh, RegistrationState state)
@Benchmark
public void finishAssertion(Blackhole bh, AssertionState state) throws AssertionFailedException {
final AssertionResult result = state.rp.finishAssertion(state.fao);
bh.consume(result.isBackupEligible());
bh.consume(result.isBackedUp());
bh.consume(result.getSignatureCount());
bh.consume(result.getAuthenticatorExtensionOutputs());
bh.consume(result.getCredential().getCredentialId());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,36 +25,45 @@
package com.yubico.webauthn;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.yubico.internal.util.ExceptionUtil;
import com.yubico.webauthn.data.AuthenticatorAssertionExtensionOutputs;
import com.yubico.webauthn.data.AuthenticatorAssertionResponse;
import com.yubico.webauthn.data.AuthenticatorData;
import com.yubico.webauthn.data.ByteArray;
import com.yubico.webauthn.data.ClientAssertionExtensionOutputs;
import com.yubico.webauthn.data.PublicKeyCredential;
import com.yubico.webauthn.data.PublicKeyCredentialRequestOptions;
import com.yubico.webauthn.data.UserIdentity;
import java.util.Optional;
import lombok.Builder;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NonNull;
import lombok.Value;

/** The result of a call to {@link RelyingParty#finishAssertion(FinishAssertionOptions)}. */
@Value
@Builder(toBuilder = true)
public class AssertionResult {

/** <code>true</code> if the assertion was verified successfully. */
private final boolean success;

@JsonProperty
@Getter(AccessLevel.NONE)
private final PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs>
credentialResponse;

/**
* The {@link RegisteredCredential} that was returned by {@link
* CredentialRepository#lookup(ByteArray, ByteArray)} and whose public key was used to
* successfully verify the assertion signature.
*
* <p>NOTE: The {@link RegisteredCredential#getSignatureCount() signature count} in this object
* will reflect the signature counter state <i>before</i> the assertion operation, not the new
* counter value. When updating your database state, use the signature counter from {@link
* #getSignatureCount()} instead.
* <p>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 <i>before</i> 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;

Expand All @@ -65,16 +74,6 @@ public class AssertionResult {
*/
@NonNull private final String username;

/**
* The new <a href="https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#signcount">signature
* count</a> of the credential used for the assertion.
*
* <p>You should update this value in your database.
*
* @see AuthenticatorData#getSignatureCounter()
*/
private final long signatureCount;

/**
* <code>true</code> if and only if at least one of the following is true:
*
Expand All @@ -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")
PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs>
credentialResponse,
@NonNull @JsonProperty("credential") RegisteredCredential credential,
@NonNull @JsonProperty("username") String username,
@JsonProperty("credentialId") ByteArray credentialId, // TODO: Delete in next major release
@JsonProperty("userHandle") ByteArray userHandle, // TODO: Delete in next major release
@JsonProperty("signatureCount") long signatureCount,
@JsonProperty("signatureCounterValid") boolean signatureCounterValid,
@JsonProperty("clientExtensionOutputs")
ClientAssertionExtensionOutputs clientExtensionOutputs,
@JsonProperty("authenticatorExtensionOutputs")
AuthenticatorAssertionExtensionOutputs authenticatorExtensionOutputs) {
@JsonProperty("signatureCounterValid") boolean signatureCounterValid) {
this.success = success;
this.credentialResponse = credentialResponse;
this.credential = credential;
this.username = username;

if (credentialId != null) {
ExceptionUtil.assure(
credential.getCredentialId().equals(credentialId),
"Legacy credentialId is present and does not equal credential.credentialId");
}
if (userHandle != null) {
ExceptionUtil.assure(
credential.getUserHandle().equals(userHandle),
"Legacy userHandle is present and does not equal credential.userHandle");
}

this.signatureCount = signatureCount;
this.signatureCounterValid = signatureCounterValid;
this.clientExtensionOutputs =
clientExtensionOutputs == null || clientExtensionOutputs.getExtensionIds().isEmpty()
? null
: clientExtensionOutputs;
this.authenticatorExtensionOutputs = authenticatorExtensionOutputs;
}

/**
Expand All @@ -168,6 +122,7 @@ private AssertionResult(
* getCredentialId()} instead.
*/
@Deprecated
@JsonIgnore
public ByteArray getCredentialId() {
return credential.getCredentialId();
}
Expand All @@ -183,10 +138,76 @@ public ByteArray getCredentialId() {
* getUserHandle()} instead.
*/
@Deprecated
@JsonIgnore
public ByteArray getUserHandle() {
return credential.getUserHandle();
}

/**
* Check whether the asserted credential is <a
* href="https://w3c.github.io/webauthn/#backup-eligible">backup eligible</a>, using the <a
* href="https://w3c.github.io/webauthn/#authdata-flags-be">BE flag</a> in the authenticator data.
*
* <p>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 <code>true</code> 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 <a href="https://w3c.github.io/webauthn/#backup-eligible">Backup Eligible in §4.
* Terminology</a>
* @see <a href="https://w3c.github.io/webauthn/#authdata-flags-be">BE flag in §6.1. Authenticator
* Data</a>
* @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 <a href="https://w3c.github.io/webauthn/#backup-state">backup state</a> of the
* asserted credential, using the <a href="https://w3c.github.io/webauthn/#authdata-flags-bs">BS
* flag</a> in the authenticator data.
*
* <p>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 <code>true</code> 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 <a href="https://w3c.github.io/webauthn/#backup-state">Backup State in §4. Terminology</a>
* @see <a href="https://w3c.github.io/webauthn/#authdata-flags-bs">BS flag in §6.1. Authenticator
* Data</a>
* @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 <a href="https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#signcount">signature
* count</a> of the credential used for the assertion.
*
* <p>You should update this value in your database.
*
* @see AuthenticatorData#getSignatureCounter()
*/
@JsonIgnore
public long getSignatureCount() {
return credentialResponse.getResponse().getParsedAuthenticatorData().getSignatureCounter();
}

/**
* The <a
* href="https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#client-extension-output">client
Expand All @@ -200,8 +221,10 @@ public ByteArray getUserHandle() {
* @see ClientAssertionExtensionOutputs
* @see #getAuthenticatorExtensionOutputs() ()
*/
@JsonIgnore
public Optional<ClientAssertionExtensionOutputs> getClientExtensionOutputs() {
return Optional.ofNullable(clientExtensionOutputs);
return Optional.of(credentialResponse.getClientExtensionResults())
.filter(ceo -> !ceo.getExtensionIds().isEmpty());
}

/**
Expand All @@ -217,65 +240,9 @@ public Optional<ClientAssertionExtensionOutputs> getClientExtensionOutputs() {
* @see AuthenticatorAssertionExtensionOutputs
* @see #getClientExtensionOutputs()
*/
@JsonIgnore
public Optional<AuthenticatorAssertionExtensionOutputs> getAuthenticatorExtensionOutputs() {
return Optional.ofNullable(authenticatorExtensionOutputs);
}

static AssertionResultBuilder.MandatoryStages builder() {
return new AssertionResultBuilder.MandatoryStages();
}

static class AssertionResultBuilder {
public static class MandatoryStages {
private final AssertionResultBuilder builder = new AssertionResultBuilder();

public Step2 success(boolean success) {
builder.success(success);
return new Step2();
}

public class Step2 {
public Step3 credential(RegisteredCredential credential) {
builder.credential(credential);
return new Step3();
}
}

public class Step3 {
public Step4 username(String username) {
builder.username(username);
return new Step4();
}
}

public class Step4 {
public Step5 signatureCount(long signatureCount) {
builder.signatureCount(signatureCount);
return new Step5();
}
}

public class Step5 {
public Step6 signatureCounterValid(boolean signatureCounterValid) {
builder.signatureCounterValid(signatureCounterValid);
return new Step6();
}
}

public class Step6 {
public Step7 clientExtensionOutputs(
ClientAssertionExtensionOutputs clientExtensionOutputs) {
builder.clientExtensionOutputs(clientExtensionOutputs);
return new Step7();
}
}

public class Step7 {
public AssertionResultBuilder assertionExtensionOutputs(
AuthenticatorAssertionExtensionOutputs authenticatorExtensionOutputs) {
return builder.authenticatorExtensionOutputs(authenticatorExtensionOutputs);
}
}
}
return AuthenticatorAssertionExtensionOutputs.fromAuthenticatorData(
credentialResponse.getResponse().getParsedAuthenticatorData());
}
}
Loading

1 comment on commit 19a51c5

@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 80 % 🔻 1222 🔻 / 1514 🔻 81 % 1234 / 1515
com.yubico.fido.metadata 68 % 🔹 217 🔺 / 316 🔹 68 % 216 / 316
com.yubico.internal.util 37 % 🔹 36 🔹 / 97 🔹 37 % 36 / 97
com.yubico.webauthn 87 % 🔹 540 🔻 / 620 🔻 87 % 549 / 628
com.yubico.webauthn.attestation 92 % 🔹 13 🔹 / 14 🔹 92 % 13 / 14
com.yubico.webauthn.data 93 % 🔹 391 🔺 / 420 🔺 93 % 386 / 413
com.yubico.webauthn.extension.appid 100 % 🏆 13 🔹 / 13 🔹 100 % 13 / 13
com.yubico.webauthn.extension.uvm 50 % 🔻 12 🔻 / 24 🔹 87 % 21 / 24
com.yubico.webauthn.meta 0 % 🔹 0 🔹 / 10 🔹 0 % 0 / 10

Previous run: 7cc725d

Detailed reports: workflow run #185

Please sign in to comment.