Skip to content

Commit

Permalink
Validate backup flags in finish operations
Browse files Browse the repository at this point in the history
  • Loading branch information
emlun committed Nov 10, 2022
1 parent e43430a commit 350c784
Show file tree
Hide file tree
Showing 10 changed files with 632 additions and 40 deletions.
15 changes: 13 additions & 2 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,19 @@ Changes:

New features:

* (Experimental) Added `AuthenticatorDataFlags` properties for the new `BE`
(backup eligible) and `BS` (backup state) flags (bits 0x08 and 0x10).
* (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:

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 @@ -58,10 +58,12 @@ public class AssertionResult {
* 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 Down Expand Up @@ -141,6 +143,58 @@ 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -397,7 +397,7 @@ public Step17 nextStep() {
}

@Value
class Step17 implements Step<Step18> {
class Step17 implements Step<PendingStep16> {
private final String username;
private final RegisteredCredential credential;

Expand All @@ -413,6 +413,31 @@ public void validate() {
}
}

@Override
public PendingStep16 nextStep() {
return new PendingStep16(username, credential);
}
}

@Value
// Step 16 in editor's draft as of 2022-11-09 https://w3c.github.io/webauthn/
// TODO: Finalize this when spec matures
class PendingStep16 implements Step<Step18> {
private final String username;
private final RegisteredCredential credential;

@Override
public void validate() {
assure(
!credential.isBackupEligible().isPresent()
|| response.getResponse().getParsedAuthenticatorData().getFlags().BE
== credential.isBackupEligible().get(),
"Backup eligibility must not change; Stored: BE=%s, received: BE=%s for credential: %s",
credential.isBackupEligible(),
response.getResponse().getParsedAuthenticatorData().getFlags().BE,
credential.getCredentialId());
}

@Override
public Step18 nextStep() {
return new Step18(username, credential);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@
import com.yubico.webauthn.data.COSEAlgorithmIdentifier;
import com.yubico.webauthn.data.PublicKeyCredentialDescriptor;
import com.yubico.webauthn.data.UserIdentity;
import java.util.Optional;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NonNull;
import lombok.Value;

Expand Down Expand Up @@ -95,16 +98,116 @@ public final class RegisteredCredential {
*/
@Builder.Default private final long signatureCount = 0;

/**
* The state of the <a href="https://w3c.github.io/webauthn/#authdata-flags-be">BE flag</a> when
* this credential was registered, if known.
*
* <p>If absent, it is not known whether or not this credential is backup eligible.
*
* <p>If present and <code>true</code>, the credential is backup eligible: it can be backed up in
* some way, most commonly by syncing the private key to a cloud account.
*
* <p>If present and <code>false</code>, the credential is not backup eligible: it cannot be
* backed up in any way.
*
* <p>{@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 <code>null</code> 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 <a href="https://w3c.github.io/webauthn/#authdata-flags-bs">BS
* flag</a> for this credential, if known.
*
* <p>If absent, the backup state of the credential is not known.
*
* <p>If present and <code>true</code>, the credential is believed to be currently backed up.
*
* <p>If present and <code>false</code>, the credential is believed to not be currently backed up.
*
* <p>{@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 <code>null
* </code> 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 <a href="https://w3c.github.io/webauthn/#authdata-flags-be">BE flag</a> when
* this credential was registered, if known.
*
* <p>If absent, it is not known whether or not this credential is backup eligible.
*
* <p>If present and <code>true</code>, the credential is backup eligible: it can be backed up in
* some way, most commonly by syncing the private key to a cloud account.
*
* <p>If present and <code>false</code>, the credential is not backup eligible: it cannot be
* backed up in any way.
*
* <p>{@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 <code>null</code> 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 Optional<Boolean> isBackupEligible() {
return Optional.ofNullable(backupEligible);
}

/**
* The last known state of the <a href="https://w3c.github.io/webauthn/#authdata-flags-bs">BS
* flag</a> for this credential, if known.
*
* <p>If absent, the backup state of the credential is not known.
*
* <p>If present and <code>true</code>, the credential is believed to be currently backed up.
*
* <p>If present and <code>false</code>, the credential is believed to not be currently backed up.
*
* <p>{@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 <code>null
* </code> 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 Optional<Boolean> isBackedUp() {
return Optional.ofNullable(backupState);
}

public static RegisteredCredentialBuilder.MandatoryStages builder() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,57 @@ private static RegistrationResult fromJson(
.collect(Collectors.toList())));
}

/**
* Check whether the created 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 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}.
*
* @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 credential.getResponse().getParsedAuthenticatorData().getFlags().BE;
}

/**
* Get the current <a href="https://w3c.github.io/webauthn/#backup-state">backup state</a> of the
* created credential, using the <a href="https://w3c.github.io/webauthn/#authdata-flags-bs">BS
* flag</a> in the authenticator data.
*
* <p>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 <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 credential.getResponse().getParsedAuthenticatorData().getFlags().BS;
}

/**
* The signature count returned with the created credential.
*
Expand Down
Loading

0 comments on commit 350c784

Please sign in to comment.