Skip to content

Commit 19a51c5

Browse files
committed
Add support for new backup flags in authenticator data
2 parents e14123a + a6d89fd commit 19a51c5

18 files changed

+1012
-453
lines changed

NEWS

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,33 @@
1-
== Version 2.1.1 (unreleased) ==
1+
== Version 2.2.0 (unreleased) ==
22

33
`webauthn-server-core`:
44

5+
Changes:
6+
7+
* Changed internal structure of `RegistrationResult` and `AssertionResult`. This
8+
may affect you if you use Jackson or similar tools to serialize these values
9+
to JSON, for example. This is not an officially supported use case and thus
10+
does not warrant a major version bump.
11+
* Removed methods `RegistrationResult.toBuilder()` and
12+
`AssertionResult.toBuilder()`. Both had package-private return types, and thus
13+
were not usable by outside callers.
14+
15+
New features:
16+
17+
* (Experimental) Added support for the new `BE` (backup eligible) and `BS`
18+
(backup state) flags in authenticator data:
19+
** Added `BE` and `BS` properties to `AuthenticatorDataFlags`, reflecting the
20+
respective flags (bits 0x08 and 0x10).
21+
** Added methods `isBackupEligible()` and `isBackedUp()` to
22+
`RegistrationResult` and `AssertionResult`, reflecting respectively the `BE`
23+
and `BS` flags.
24+
** Added properties `backupEligible` and `backupState`, getters
25+
`isBackupEligible()` and `isBackedUp()`, and corresponding builder methods
26+
to `RegisteredCredential`. `RelyingParty.finishAssertion(...)` will now
27+
validate that if `RegisteredCredential.isBackupEligible()` is present, then
28+
the `BE` flag of any assertion of that credential must match the stored
29+
value.
30+
531
Fixes:
632

733
* Fixed TPM attestation verification rejecting attestation certificates with TPM

webauthn-server-core/src/jmh/java/com/yubico/webauthn/benchmark/RelyingPartyBenchmark.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ public void finishRegistration(Blackhole bh, RegistrationState state)
7171
throws RegistrationFailedException {
7272
final RegistrationResult result = state.rp.finishRegistration(state.fro);
7373
bh.consume(result.getKeyId());
74+
bh.consume(result.isBackupEligible());
75+
bh.consume(result.isBackedUp());
7476
bh.consume(result.getSignatureCount());
7577
bh.consume(result.getAaguid());
7678
bh.consume(result.getPublicKeyCose());
@@ -80,6 +82,8 @@ public void finishRegistration(Blackhole bh, RegistrationState state)
8082
@Benchmark
8183
public void finishAssertion(Blackhole bh, AssertionState state) throws AssertionFailedException {
8284
final AssertionResult result = state.rp.finishAssertion(state.fao);
85+
bh.consume(result.isBackupEligible());
86+
bh.consume(result.isBackedUp());
8387
bh.consume(result.getSignatureCount());
8488
bh.consume(result.getAuthenticatorExtensionOutputs());
8589
bh.consume(result.getCredential().getCredentialId());

webauthn-server-core/src/main/java/com/yubico/webauthn/AssertionResult.java

Lines changed: 95 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -25,36 +25,45 @@
2525
package com.yubico.webauthn;
2626

2727
import com.fasterxml.jackson.annotation.JsonCreator;
28+
import com.fasterxml.jackson.annotation.JsonIgnore;
2829
import com.fasterxml.jackson.annotation.JsonProperty;
29-
import com.yubico.internal.util.ExceptionUtil;
3030
import com.yubico.webauthn.data.AuthenticatorAssertionExtensionOutputs;
31+
import com.yubico.webauthn.data.AuthenticatorAssertionResponse;
3132
import com.yubico.webauthn.data.AuthenticatorData;
3233
import com.yubico.webauthn.data.ByteArray;
3334
import com.yubico.webauthn.data.ClientAssertionExtensionOutputs;
35+
import com.yubico.webauthn.data.PublicKeyCredential;
3436
import com.yubico.webauthn.data.PublicKeyCredentialRequestOptions;
3537
import com.yubico.webauthn.data.UserIdentity;
3638
import java.util.Optional;
37-
import lombok.Builder;
39+
import lombok.AccessLevel;
40+
import lombok.Getter;
3841
import lombok.NonNull;
3942
import lombok.Value;
4043

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

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

51+
@JsonProperty
52+
@Getter(AccessLevel.NONE)
53+
private final PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs>
54+
credentialResponse;
55+
4956
/**
5057
* The {@link RegisteredCredential} that was returned by {@link
5158
* CredentialRepository#lookup(ByteArray, ByteArray)} and whose public key was used to
5259
* successfully verify the assertion signature.
5360
*
54-
* <p>NOTE: The {@link RegisteredCredential#getSignatureCount() signature count} in this object
55-
* will reflect the signature counter state <i>before</i> the assertion operation, not the new
56-
* counter value. When updating your database state, use the signature counter from {@link
57-
* #getSignatureCount()} instead.
61+
* <p>NOTE: The {@link RegisteredCredential#getSignatureCount() signature count}, {@link
62+
* RegisteredCredential#isBackupEligible() backup eligibility} and {@link
63+
* RegisteredCredential#isBackedUp() backup state} properties in this object will reflect the
64+
* state <i>before</i> the assertion operation, not the new state. When updating your database
65+
* state, use the signature counter and backup state from {@link #getSignatureCount()}, {@link
66+
* #isBackupEligible()} and {@link #isBackedUp()} instead.
5867
*/
5968
private final RegisteredCredential credential;
6069

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

68-
/**
69-
* The new <a href="https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#signcount">signature
70-
* count</a> of the credential used for the assertion.
71-
*
72-
* <p>You should update this value in your database.
73-
*
74-
* @see AuthenticatorData#getSignatureCounter()
75-
*/
76-
private final long signatureCount;
77-
7877
/**
7978
* <code>true</code> if and only if at least one of the following is true:
8079
*
@@ -96,65 +95,20 @@ public class AssertionResult {
9695
*/
9796
private final boolean signatureCounterValid;
9897

99-
private final ClientAssertionExtensionOutputs clientExtensionOutputs;
100-
101-
private final AuthenticatorAssertionExtensionOutputs authenticatorExtensionOutputs;
102-
103-
private AssertionResult(
104-
boolean success,
105-
@NonNull @JsonProperty("credential") RegisteredCredential credential,
106-
@NonNull String username,
107-
long signatureCount,
108-
boolean signatureCounterValid,
109-
ClientAssertionExtensionOutputs clientExtensionOutputs,
110-
AuthenticatorAssertionExtensionOutputs authenticatorExtensionOutputs) {
111-
this(
112-
success,
113-
credential,
114-
username,
115-
null,
116-
null,
117-
signatureCount,
118-
signatureCounterValid,
119-
clientExtensionOutputs,
120-
authenticatorExtensionOutputs);
121-
}
122-
12398
@JsonCreator
124-
private AssertionResult(
99+
AssertionResult(
125100
@JsonProperty("success") boolean success,
101+
@NonNull @JsonProperty("credentialResponse")
102+
PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs>
103+
credentialResponse,
126104
@NonNull @JsonProperty("credential") RegisteredCredential credential,
127105
@NonNull @JsonProperty("username") String username,
128-
@JsonProperty("credentialId") ByteArray credentialId, // TODO: Delete in next major release
129-
@JsonProperty("userHandle") ByteArray userHandle, // TODO: Delete in next major release
130-
@JsonProperty("signatureCount") long signatureCount,
131-
@JsonProperty("signatureCounterValid") boolean signatureCounterValid,
132-
@JsonProperty("clientExtensionOutputs")
133-
ClientAssertionExtensionOutputs clientExtensionOutputs,
134-
@JsonProperty("authenticatorExtensionOutputs")
135-
AuthenticatorAssertionExtensionOutputs authenticatorExtensionOutputs) {
106+
@JsonProperty("signatureCounterValid") boolean signatureCounterValid) {
136107
this.success = success;
108+
this.credentialResponse = credentialResponse;
137109
this.credential = credential;
138110
this.username = username;
139-
140-
if (credentialId != null) {
141-
ExceptionUtil.assure(
142-
credential.getCredentialId().equals(credentialId),
143-
"Legacy credentialId is present and does not equal credential.credentialId");
144-
}
145-
if (userHandle != null) {
146-
ExceptionUtil.assure(
147-
credential.getUserHandle().equals(userHandle),
148-
"Legacy userHandle is present and does not equal credential.userHandle");
149-
}
150-
151-
this.signatureCount = signatureCount;
152111
this.signatureCounterValid = signatureCounterValid;
153-
this.clientExtensionOutputs =
154-
clientExtensionOutputs == null || clientExtensionOutputs.getExtensionIds().isEmpty()
155-
? null
156-
: clientExtensionOutputs;
157-
this.authenticatorExtensionOutputs = authenticatorExtensionOutputs;
158112
}
159113

160114
/**
@@ -168,6 +122,7 @@ private AssertionResult(
168122
* getCredentialId()} instead.
169123
*/
170124
@Deprecated
125+
@JsonIgnore
171126
public ByteArray getCredentialId() {
172127
return credential.getCredentialId();
173128
}
@@ -183,10 +138,76 @@ public ByteArray getCredentialId() {
183138
* getUserHandle()} instead.
184139
*/
185140
@Deprecated
141+
@JsonIgnore
186142
public ByteArray getUserHandle() {
187143
return credential.getUserHandle();
188144
}
189145

146+
/**
147+
* Check whether the asserted credential is <a
148+
* href="https://w3c.github.io/webauthn/#backup-eligible">backup eligible</a>, using the <a
149+
* href="https://w3c.github.io/webauthn/#authdata-flags-be">BE flag</a> in the authenticator data.
150+
*
151+
* <p>You SHOULD store this value in your representation of the corresponding {@link
152+
* RegisteredCredential} if no value is stored yet. {@link CredentialRepository} implementations
153+
* SHOULD set this value as the {@link
154+
* RegisteredCredential.RegisteredCredentialBuilder#backupEligible(Boolean)
155+
* backupEligible(Boolean)} value when reconstructing that {@link RegisteredCredential}.
156+
*
157+
* @return <code>true</code> if and only if the created credential is backup eligible. NOTE that
158+
* this is only a hint and not a guarantee, unless backed by a trusted authenticator
159+
* attestation.
160+
* @see <a href="https://w3c.github.io/webauthn/#backup-eligible">Backup Eligible in §4.
161+
* Terminology</a>
162+
* @see <a href="https://w3c.github.io/webauthn/#authdata-flags-be">BE flag in §6.1. Authenticator
163+
* Data</a>
164+
* @deprecated EXPERIMENTAL: This feature is from a not yet mature standard; it could change as
165+
* the standard matures.
166+
*/
167+
@Deprecated
168+
@JsonIgnore
169+
public boolean isBackupEligible() {
170+
return credentialResponse.getResponse().getParsedAuthenticatorData().getFlags().BE;
171+
}
172+
173+
/**
174+
* Get the current <a href="https://w3c.github.io/webauthn/#backup-state">backup state</a> of the
175+
* asserted credential, using the <a href="https://w3c.github.io/webauthn/#authdata-flags-bs">BS
176+
* flag</a> in the authenticator data.
177+
*
178+
* <p>You SHOULD update this value in your representation of a {@link RegisteredCredential}.
179+
* {@link CredentialRepository} implementations SHOULD set this value as the {@link
180+
* RegisteredCredential.RegisteredCredentialBuilder#backupState(Boolean) backupState(Boolean)}
181+
* value when reconstructing that {@link RegisteredCredential}.
182+
*
183+
* @return <code>true</code> if and only if the created credential is believed to currently be
184+
* backed up. NOTE that this is only a hint and not a guarantee, unless backed by a trusted
185+
* authenticator attestation.
186+
* @see <a href="https://w3c.github.io/webauthn/#backup-state">Backup State in §4. Terminology</a>
187+
* @see <a href="https://w3c.github.io/webauthn/#authdata-flags-bs">BS flag in §6.1. Authenticator
188+
* Data</a>
189+
* @deprecated EXPERIMENTAL: This feature is from a not yet mature standard; it could change as
190+
* the standard matures.
191+
*/
192+
@Deprecated
193+
@JsonIgnore
194+
public boolean isBackedUp() {
195+
return credentialResponse.getResponse().getParsedAuthenticatorData().getFlags().BS;
196+
}
197+
198+
/**
199+
* The new <a href="https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#signcount">signature
200+
* count</a> of the credential used for the assertion.
201+
*
202+
* <p>You should update this value in your database.
203+
*
204+
* @see AuthenticatorData#getSignatureCounter()
205+
*/
206+
@JsonIgnore
207+
public long getSignatureCount() {
208+
return credentialResponse.getResponse().getParsedAuthenticatorData().getSignatureCounter();
209+
}
210+
190211
/**
191212
* The <a
192213
* href="https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#client-extension-output">client
@@ -200,8 +221,10 @@ public ByteArray getUserHandle() {
200221
* @see ClientAssertionExtensionOutputs
201222
* @see #getAuthenticatorExtensionOutputs() ()
202223
*/
224+
@JsonIgnore
203225
public Optional<ClientAssertionExtensionOutputs> getClientExtensionOutputs() {
204-
return Optional.ofNullable(clientExtensionOutputs);
226+
return Optional.of(credentialResponse.getClientExtensionResults())
227+
.filter(ceo -> !ceo.getExtensionIds().isEmpty());
205228
}
206229

207230
/**
@@ -217,65 +240,9 @@ public Optional<ClientAssertionExtensionOutputs> getClientExtensionOutputs() {
217240
* @see AuthenticatorAssertionExtensionOutputs
218241
* @see #getClientExtensionOutputs()
219242
*/
243+
@JsonIgnore
220244
public Optional<AuthenticatorAssertionExtensionOutputs> getAuthenticatorExtensionOutputs() {
221-
return Optional.ofNullable(authenticatorExtensionOutputs);
222-
}
223-
224-
static AssertionResultBuilder.MandatoryStages builder() {
225-
return new AssertionResultBuilder.MandatoryStages();
226-
}
227-
228-
static class AssertionResultBuilder {
229-
public static class MandatoryStages {
230-
private final AssertionResultBuilder builder = new AssertionResultBuilder();
231-
232-
public Step2 success(boolean success) {
233-
builder.success(success);
234-
return new Step2();
235-
}
236-
237-
public class Step2 {
238-
public Step3 credential(RegisteredCredential credential) {
239-
builder.credential(credential);
240-
return new Step3();
241-
}
242-
}
243-
244-
public class Step3 {
245-
public Step4 username(String username) {
246-
builder.username(username);
247-
return new Step4();
248-
}
249-
}
250-
251-
public class Step4 {
252-
public Step5 signatureCount(long signatureCount) {
253-
builder.signatureCount(signatureCount);
254-
return new Step5();
255-
}
256-
}
257-
258-
public class Step5 {
259-
public Step6 signatureCounterValid(boolean signatureCounterValid) {
260-
builder.signatureCounterValid(signatureCounterValid);
261-
return new Step6();
262-
}
263-
}
264-
265-
public class Step6 {
266-
public Step7 clientExtensionOutputs(
267-
ClientAssertionExtensionOutputs clientExtensionOutputs) {
268-
builder.clientExtensionOutputs(clientExtensionOutputs);
269-
return new Step7();
270-
}
271-
}
272-
273-
public class Step7 {
274-
public AssertionResultBuilder assertionExtensionOutputs(
275-
AuthenticatorAssertionExtensionOutputs authenticatorExtensionOutputs) {
276-
return builder.authenticatorExtensionOutputs(authenticatorExtensionOutputs);
277-
}
278-
}
279-
}
245+
return AuthenticatorAssertionExtensionOutputs.fromAuthenticatorData(
246+
credentialResponse.getResponse().getParsedAuthenticatorData());
280247
}
281248
}

0 commit comments

Comments
 (0)