From 1a3ad3f512aea894630de03cebfaf5fe700506ab Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Thu, 16 Jan 2025 14:24:28 +0100 Subject: [PATCH] Revert new experimental interfaces and classes These will be postponed to the next minor version instead. --- NEWS | 31 - .../yubico/webauthn/AssertionResultV2.java | 246 - .../com/yubico/webauthn/CredentialRecord.java | 225 - .../CredentialRepositoryV1ToV2Adapter.java | 43 - .../webauthn/CredentialRepositoryV2.java | 100 - .../yubico/webauthn/FinishAssertionSteps.java | 149 +- .../webauthn/FinishRegistrationSteps.java | 21 +- .../yubico/webauthn/RegisteredCredential.java | 75 +- .../com/yubico/webauthn/RelyingParty.java | 27 +- .../com/yubico/webauthn/RelyingPartyV2.java | 707 --- .../ToPublicKeyCredentialDescriptor.java | 34 - .../yubico/webauthn/UsernameRepository.java | 66 - .../data/PublicKeyCredentialDescriptor.java | 16 +- .../webauthn/RelyingPartyAssertionSpec.scala | 138 +- .../RelyingPartyStartOperationSpec.scala | 890 --- .../RelyingPartyUserIdentificationSpec.scala | 253 - .../RelyingPartyV2AssertionSpec.scala | 2930 ---------- .../RelyingPartyV2RegistrationSpec.scala | 4855 ----------------- .../com/yubico/webauthn/test/Helpers.scala | 198 - .../webauthn/InMemoryRegistrationStorage.java | 54 +- .../java/demo/webauthn/WebAuthnServer.java | 16 +- .../webauthn/data/CredentialRegistration.java | 35 +- 22 files changed, 162 insertions(+), 10947 deletions(-) delete mode 100644 webauthn-server-core/src/main/java/com/yubico/webauthn/AssertionResultV2.java delete mode 100644 webauthn-server-core/src/main/java/com/yubico/webauthn/CredentialRecord.java delete mode 100644 webauthn-server-core/src/main/java/com/yubico/webauthn/CredentialRepositoryV1ToV2Adapter.java delete mode 100644 webauthn-server-core/src/main/java/com/yubico/webauthn/CredentialRepositoryV2.java delete mode 100644 webauthn-server-core/src/main/java/com/yubico/webauthn/RelyingPartyV2.java delete mode 100644 webauthn-server-core/src/main/java/com/yubico/webauthn/ToPublicKeyCredentialDescriptor.java delete mode 100644 webauthn-server-core/src/main/java/com/yubico/webauthn/UsernameRepository.java delete mode 100644 webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyV2AssertionSpec.scala delete mode 100644 webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyV2RegistrationSpec.scala diff --git a/NEWS b/NEWS index df1d21692..9ac461368 100644 --- a/NEWS +++ b/NEWS @@ -29,37 +29,6 @@ New features: instead of an ordinary WebAuthn response. See the JavaDoc for details. ** NOTE: Experimental features may receive breaking changes without a major version increase. -* (Experimental) Added a new suite of interfaces, starting with - `CredentialRepositoryV2`. `RelyingParty` can now be configured with a - `CredentialRepositoryV2` instance instead of a `CredentialRepository` - instance. This changes the result of the `RelyingParty` builder to - `RelyingPartyV2`. `CredentialRepositoryV2` and `RelyingPartyV2` enable a suite - of new features: - ** `CredentialRepositoryV2` does not assume that the application has usernames, - instead username support is modular. In addition to the - `CredentialRepositoryV2`, `RelyingPartyV2` can be optionally configured with - a `UsernameRepository` as well. If a `UsernameRepository` is not set, then - `RelyingPartyV2.startAssertion(StartAssertionOptions)` will fail at runtime - if `StartAssertionOptions.username` is set. - ** `CredentialRepositoryV2` uses a new interface `CredentialRecord` to - represent registered credentials, instead of the concrete - `RegisteredCredential` class (although `RegisteredCredential` also - implements `CredentialRecord`). This provides implementations greater - flexibility while also automating the type conversion to - `PublicKeyCredentialDescriptor` needed in `startRegistration()` and - `startAssertion()`. - ** `RelyingPartyV2.finishAssertion()` returns a new type `AssertionResultV2` - with a new method `getCredential()`, which returns the `CredentialRecord` - that was verified. The return type of `getCredential()` is generic and - preserves the concrete type of `CredentialRecord` returned by the - `CredentialRepositoryV2` implementation. - ** NOTE: Experimental features may receive breaking changes without a major - version increase. -* (Experimental) Added property `RegisteredCredential.transports`. - ** NOTE: Experimental features may receive breaking changes without a major - version increase. - ** NOTE: Experimental features may receive breaking changes without a major - version increase. `webauthn-server-attestation`: diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/AssertionResultV2.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/AssertionResultV2.java deleted file mode 100644 index 5b027ffbc..000000000 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/AssertionResultV2.java +++ /dev/null @@ -1,246 +0,0 @@ -// Copyright (c) 2018, Yubico AB -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, this -// list of conditions and the following disclaimer. -// -// 2. Redistributions in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -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.webauthn.data.AuthenticatorAssertionExtensionOutputs; -import com.yubico.webauthn.data.AuthenticatorAssertionResponse; -import com.yubico.webauthn.data.AuthenticatorAttachment; -import com.yubico.webauthn.data.AuthenticatorData; -import com.yubico.webauthn.data.AuthenticatorDataFlags; -import com.yubico.webauthn.data.AuthenticatorResponse; -import com.yubico.webauthn.data.ByteArray; -import com.yubico.webauthn.data.ClientAssertionExtensionOutputs; -import com.yubico.webauthn.data.PublicKeyCredential; -import java.util.Optional; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.NonNull; -import lombok.Value; - -/** - * The result of a call to {@link RelyingPartyV2#finishAssertion(FinishAssertionOptions)}. - * - * @deprecated EXPERIMENTAL: This is an experimental feature. It is likely to change or be deleted - * before reaching a mature release. - */ -@Deprecated -@Value -public class AssertionResultV2 { - - /** true if the assertion was verified successfully. */ - private final boolean success; - - @JsonProperty - @Getter(AccessLevel.NONE) - private final PublicKeyCredential - credentialResponse; - - /** - * The {@link CredentialRecord} that was returned by {@link - * CredentialRepositoryV2#lookup(ByteArray, ByteArray)} and whose public key was used to - * successfully verify the assertion signature. - * - *

NOTE: The {@link CredentialRecord#getSignatureCount() signature count}, {@link - * CredentialRecord#isBackupEligible() backup eligibility} and {@link - * CredentialRecord#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. - * - * @deprecated EXPERIMENTAL: This is an experimental feature. It is likely to change or be deleted - * before reaching a mature release. - */ - @Deprecated private final C credential; - - /** - * true if and only if at least one of the following is true: - * - *

    - *
  • The {@link AuthenticatorData#getSignatureCounter() signature counter value} in the - * assertion was strictly greater than {@link CredentialRecord#getSignatureCount() the - * stored one}. - *
  • The {@link AuthenticatorData#getSignatureCounter() signature counter value} in the - * assertion and {@link CredentialRecord#getSignatureCount() the stored one} were both zero. - *
- * - * @see §6.1. - * Authenticator Data - * @see AuthenticatorData#getSignatureCounter() - * @see CredentialRecord#getSignatureCount() - * @see RelyingParty.RelyingPartyBuilder#validateSignatureCounter(boolean) - */ - private final boolean signatureCounterValid; - - @JsonCreator - AssertionResultV2( - @JsonProperty("success") boolean success, - @NonNull @JsonProperty("credentialResponse") - PublicKeyCredential - credentialResponse, - @NonNull @JsonProperty("credential") C credential, - @JsonProperty("signatureCounterValid") boolean signatureCounterValid) { - this.success = success; - this.credentialResponse = credentialResponse; - this.credential = credential; - this.signatureCounterValid = signatureCounterValid; - } - - /** - * Check whether the user - * verification as performed during the authentication ceremony. - * - *

This flag is also available via - * {@link PublicKeyCredential}.{@link PublicKeyCredential#getResponse() getResponse()}.{@link AuthenticatorResponse#getParsedAuthenticatorData() getParsedAuthenticatorData()}.{@link AuthenticatorData#getFlags() getFlags()}.{@link AuthenticatorDataFlags#UV UV} - * . - * - * @return true if and only if the authenticator claims to have performed user - * verification during the authentication ceremony. - * @see User Verification - * @see UV flag in §6.1. Authenticator - * Data - */ - @JsonIgnore - public boolean isUserVerified() { - return credentialResponse.getResponse().getParsedAuthenticatorData().getFlags().UV; - } - - /** - * Check whether the asserted credential is backup eligible, using the BE flag in the authenticator data. - * - *

You SHOULD store this value in your representation of the corresponding {@link - * CredentialRecord} if no value is stored yet. {@link CredentialRepository} implementations - * SHOULD set this value when reconstructing that {@link CredentialRecord}. - * - * @return 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 credentialResponse.getResponse().getParsedAuthenticatorData().getFlags().BE; - } - - /** - * Get the current backup state of the - * asserted credential, using the BS - * flag in the authenticator data. - * - *

You SHOULD update this value in your representation of a {@link CredentialRecord}. {@link - * CredentialRepository} implementations SHOULD set this value when reconstructing that {@link - * CredentialRecord}. - * - * @return 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 authenticator - * attachment modality in effect at the time the asserted credential was used. - * - * @see PublicKeyCredential#getAuthenticatorAttachment() - * @deprecated EXPERIMENTAL: This feature is from a not yet mature standard; it could change as - * the standard matures. - */ - @Deprecated - @JsonIgnore - public Optional getAuthenticatorAttachment() { - return credentialResponse.getAuthenticatorAttachment(); - } - - /** - * The new signature - * count of the credential used for the assertion. - * - *

You should update this value in your database. - * - * @see AuthenticatorData#getSignatureCounter() - */ - @JsonIgnore - public long getSignatureCount() { - return credentialResponse.getResponse().getParsedAuthenticatorData().getSignatureCounter(); - } - - /** - * The client - * extension outputs, if any. - * - *

This is present if and only if at least one extension output is present in the return value. - * - * @see §9.4. - * Client Extension Processing - * @see ClientAssertionExtensionOutputs - * @see #getAuthenticatorExtensionOutputs() () - */ - @JsonIgnore - public Optional getClientExtensionOutputs() { - return Optional.of(credentialResponse.getClientExtensionResults()) - .filter(ceo -> !ceo.getExtensionIds().isEmpty()); - } - - /** - * The authenticator - * extension outputs, if any. - * - *

This is present if and only if at least one extension output is present in the return value. - * - * @see §9.5. - * Authenticator Extension Processing - * @see AuthenticatorAssertionExtensionOutputs - * @see #getClientExtensionOutputs() - */ - @JsonIgnore - public Optional getAuthenticatorExtensionOutputs() { - return AuthenticatorAssertionExtensionOutputs.fromAuthenticatorData( - credentialResponse.getResponse().getParsedAuthenticatorData()); - } -} diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/CredentialRecord.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/CredentialRecord.java deleted file mode 100644 index 0b06f1b30..000000000 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/CredentialRecord.java +++ /dev/null @@ -1,225 +0,0 @@ -package com.yubico.webauthn; - -import com.yubico.webauthn.data.AttestedCredentialData; -import com.yubico.webauthn.data.AuthenticatorAssertionResponse; -import com.yubico.webauthn.data.AuthenticatorAttestationResponse; -import com.yubico.webauthn.data.AuthenticatorData; -import com.yubico.webauthn.data.AuthenticatorTransport; -import com.yubico.webauthn.data.ByteArray; -import com.yubico.webauthn.data.PublicKeyCredentialCreationOptions; -import com.yubico.webauthn.data.PublicKeyCredentialDescriptor; -import com.yubico.webauthn.data.PublicKeyCredentialRequestOptions; -import com.yubico.webauthn.data.UserIdentity; -import java.util.Optional; -import java.util.Set; -import lombok.NonNull; - -/** - * An abstraction of properties of a stored WebAuthn credential. - * - * @see Credential Record in Web - * Authentication Level 3 (Editor's Draft) - * @deprecated EXPERIMENTAL: This is an experimental feature. It is likely to change or be deleted - * before reaching a mature release. - */ -@Deprecated -public interface CredentialRecord extends ToPublicKeyCredentialDescriptor { - - /** - * The credential - * ID of the credential. - * - *

Implementations MUST NOT return null. - * - * @see Credential - * ID - * @see RegistrationResult#getKeyId() - * @see PublicKeyCredentialDescriptor#getId() - * @deprecated EXPERIMENTAL: This is an experimental feature. It is likely to change or be deleted - * before reaching a mature release. - */ - @Deprecated - @NonNull - ByteArray getCredentialId(); - - /** - * The user handle - * of the user the credential is registered to. - * - *

Implementations MUST NOT return null. - * - * @see User Handle - * @see UserIdentity#getId() - * @deprecated EXPERIMENTAL: This is an experimental feature. It is likely to change or be deleted - * before reaching a mature release. - */ - @Deprecated - @NonNull - ByteArray getUserHandle(); - - /** - * The credential public key encoded in COSE_Key format, as defined in Section 7 of RFC 8152. - * - *

This is used to verify the {@link AuthenticatorAssertionResponse#getSignature() signature} - * in authentication assertions. - * - *

If your database has credentials encoded in U2F (raw) format, you may need to use {@link - * #cosePublicKeyFromEs256Raw(ByteArray)} to convert them before returning them in this method. - * - *

Implementations MUST NOT return null. - * - * @see AttestedCredentialData#getCredentialPublicKey() - * @see RegistrationResult#getPublicKeyCose() - * @deprecated EXPERIMENTAL: This is an experimental feature. It is likely to change or be deleted - * before reaching a mature release. - */ - @Deprecated - @NonNull - ByteArray getPublicKeyCose(); - - /** - * The stored signature - * count of the credential. - * - *

This is used to validate the {@link AuthenticatorData#getSignatureCounter() signature - * counter} in authentication assertions. - * - * @see §6.1. - * Authenticator Data - * @see AuthenticatorData#getSignatureCounter() - * @see AssertionResult#getSignatureCount() - * @deprecated EXPERIMENTAL: This is an experimental feature. It is likely to change or be deleted - * before reaching a mature release. - */ - @Deprecated - long getSignatureCount(); - - /** - * Transport hints as to how the client might communicate with the authenticator this credential - * is bound to. - * - *

Implementations SHOULD return the value returned by {@link - * AuthenticatorAttestationResponse#getTransports()} when the credential was created. That value - * SHOULD NOT be modified. - * - *

Implementations MUST NOT return null. - * - *

This is used to set {@link PublicKeyCredentialDescriptor#getTransports()} in {@link - * PublicKeyCredentialCreationOptions#getExcludeCredentials() excludeCredentials} in {@link - * RelyingParty#startRegistration(StartRegistrationOptions)} and and {@link - * PublicKeyCredentialRequestOptions#getAllowCredentials() allowCredentials} in {@link - * RelyingParty#startAssertion(StartAssertionOptions)}. - * - * @see getTransports() - * in 5.2.1. Information About Public Key Credential (interface - * AuthenticatorAttestationResponse) - * @see transports - * in 5.8.3. Credential Descriptor (dictionary PublicKeyCredentialDescriptor) - * @see AuthenticatorAttestationResponse#getTransports() - * @see PublicKeyCredentialDescriptor#getTransports() - * @deprecated EXPERIMENTAL: This is an experimental feature. It is likely to change or be deleted - * before reaching a mature release. - */ - @Deprecated - Optional> getTransports(); - - // boolean isUvInitialized(); - - /** - * The state of the BE flag when - * this credential was registered, if known. - * - *

If absent, it is not known whether or not this credential is backup eligible. - * - *

If present and 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. - * - *

If present and false, the credential is not backup eligible: it cannot be - * backed up in any way. - * - *

{@link CredentialRecord} implementations SHOULD return the first known value returned by - * {@link RegistrationResult#isBackupEligible()} or {@link AssertionResult#isBackupEligible()}, if - * known. If unknown, {@link CredentialRecord} implementations SHOULD return - * Optional.empty(). - * - *

Implementations MUST NOT return null. - * - * @deprecated EXPERIMENTAL: This is an experimental feature. It is likely to change or be deleted - * before reaching a mature release. EXPERIMENTAL: This feature is from a not yet mature - * standard; it could change as the standard matures. - */ - @Deprecated - Optional isBackupEligible(); - - /** - * The last known state of the BS - * flag for this credential, if known. - * - *

If absent, the backup state of the credential is not known. - * - *

If present and true, the credential is believed to be currently backed up. - * - *

If present and false, the credential is believed to not be currently backed up. - * - *

{@link CredentialRecord} implementations SHOULD return the most recent value returned by - * {@link AssertionResult#isBackedUp()} or {@link RegistrationResult#isBackedUp()}, if known. If - * unknown, {@link CredentialRecord} implementations SHOULD return Optional.empty(). - * - *

Implementations MUST NOT return null. - * - * @deprecated EXPERIMENTAL: This is an experimental feature. It is likely to change or be deleted - * before reaching a mature release. EXPERIMENTAL: This feature is from a not yet mature - * standard; it could change as the standard matures. - */ - @Deprecated - Optional isBackedUp(); - - /** - * This default implementation of {@link - * ToPublicKeyCredentialDescriptor#toPublicKeyCredentialDescriptor()} sets the {@link - * PublicKeyCredentialDescriptor.PublicKeyCredentialDescriptorBuilder#id(ByteArray) id} field to - * the return value of {@link #getCredentialId()} and the {@link - * PublicKeyCredentialDescriptor.PublicKeyCredentialDescriptorBuilder#transports(Optional) - * transports} field to the return value of {@link #getTransports()}. - * - * @see credential - * descriptor for a credential record in Web Authentication Level 3 (Editor's Draft) - */ - @Override - default PublicKeyCredentialDescriptor toPublicKeyCredentialDescriptor() { - return PublicKeyCredentialDescriptor.builder() - .id(getCredentialId()) - .transports(getTransports()) - .build(); - } - - /** - * Convert a credential public key from U2F format to COSE_Key format. - * - *

The U2F JavaScript API encoded credential public keys in ALG_KEY_ECC_X962_RAW - * format as specified in FIDO - * Registry §3.6.2 Public Key Representation Formats. If your database has credential public - * keys stored in this format, those public keys need to be converted to COSE_Key format before - * they can be used by a {@link CredentialRecord} instance. This function performs the conversion. - * - *

If your application has only used the navigator.credentials.create() API to - * register credentials, you likely do not need this function. - * - * @param es256RawKey a credential public key in ALG_KEY_ECC_X962_RAW format as - * specified in FIDO - * Registry §3.6.2 Public Key Representation Formats. - * @return a credential public key in COSE_Key format, suitable to be returned by {@link - * CredentialRecord#getPublicKeyCose()}. - * @see RegisteredCredential.RegisteredCredentialBuilder#publicKeyEs256Raw(ByteArray) - */ - static ByteArray cosePublicKeyFromEs256Raw(final ByteArray es256RawKey) { - return WebAuthnCodecs.rawEcKeyToCose(es256RawKey); - } -} diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/CredentialRepositoryV1ToV2Adapter.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/CredentialRepositoryV1ToV2Adapter.java deleted file mode 100644 index 41e02e876..000000000 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/CredentialRepositoryV1ToV2Adapter.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.yubico.webauthn; - -import com.yubico.webauthn.data.ByteArray; -import java.util.Collections; -import java.util.Optional; -import java.util.Set; -import lombok.AllArgsConstructor; - -@AllArgsConstructor -class CredentialRepositoryV1ToV2Adapter - implements CredentialRepositoryV2, UsernameRepository { - - private final CredentialRepository inner; - - @Override - public Set getCredentialDescriptorsForUserHandle( - ByteArray userHandle) { - return inner - .getUsernameForUserHandle(userHandle) - .map(inner::getCredentialIdsForUsername) - .orElseGet(Collections::emptySet); - } - - @Override - public Optional lookup(ByteArray credentialId, ByteArray userHandle) { - return inner.lookup(credentialId, userHandle); - } - - @Override - public boolean credentialIdExists(ByteArray credentialId) { - return !inner.lookupAll(credentialId).isEmpty(); - } - - @Override - public Optional getUserHandleForUsername(String username) { - return inner.getUserHandleForUsername(username); - } - - @Override - public Optional getUsernameForUserHandle(ByteArray userHandle) { - return inner.getUsernameForUserHandle(userHandle); - } -} diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/CredentialRepositoryV2.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/CredentialRepositoryV2.java deleted file mode 100644 index f9a75e9cd..000000000 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/CredentialRepositoryV2.java +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright (c) 2018, Yubico AB -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, this -// list of conditions and the following disclaimer. -// -// 2. Redistributions in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -package com.yubico.webauthn; - -import com.yubico.webauthn.data.ByteArray; -import com.yubico.webauthn.data.PublicKeyCredentialDescriptor; -import java.util.Optional; -import java.util.Set; - -/** - * An abstraction of database lookups needed by this library. - * - *

This is used by {@link RelyingPartyV2} to look up credentials and credential IDs. - * - *

Unlike {@link CredentialRepository}, this interface does not require support for usernames. - * - * @deprecated EXPERIMENTAL: This is an experimental feature. It is likely to change or be deleted - * before reaching a mature release. - */ -@Deprecated -public interface CredentialRepositoryV2 { - - /** - * Get the credential IDs of all credentials registered to the user with the given user handle. - * - *

After a successful registration ceremony, the {@link RegistrationResult#getKeyId()} method - * returns a value suitable for inclusion in this set. - * - *

Note that the {@link CredentialRecord} interface extends from the expected {@link - * ToPublicKeyCredentialDescriptor} return type, so this method MAY return a {@link Set} of the - * same item type as the value returned by the {@link #lookup(ByteArray, ByteArray)} method. - * - *

Implementations MUST NOT return null. The returned {@link Set} MUST NOT contain null. - * - * @return a {@link Set} containing one {@link PublicKeyCredentialDescriptor} (or value that - * implements {@link ToPublicKeyCredentialDescriptor}, for example {@link CredentialRecord}) - * for each credential registered to the given user. The set MUST NOT be null, but MAY be - * empty if the user does not exist or has no credentials. - * @see ToPublicKeyCredentialDescriptor - * @see CredentialRecord - * @deprecated EXPERIMENTAL: This is an experimental feature. It is likely to change or be deleted - * before reaching a mature release. - */ - @Deprecated - Set getCredentialDescriptorsForUserHandle( - ByteArray userHandle); - - /** - * Look up the public key, backup flags and current signature count for the given credential - * registered to the given user. - * - *

The returned {@link CredentialRecord} is not expected to be long-lived. It may be read - * directly from a database or assembled from other components. - * - * @return a {@link CredentialRecord} describing the current state of the registered credential - * with credential ID credentialId, if any. If the credential does not exist or - * is registered to a different user handle than userHandle, return {@link - * Optional#empty()}. - * @deprecated EXPERIMENTAL: This is an experimental feature. It is likely to change or be deleted - * before reaching a mature release. - */ - @Deprecated - Optional lookup(ByteArray credentialId, ByteArray userHandle); - - /** - * Check whether any credential exists with the given credential ID, regardless of what user it is - * registered to. - * - *

This is used to refuse registration of duplicate credential IDs. - * - * @return true if and only if the credential database contains at least one - * credential with the given credential ID. - * @deprecated EXPERIMENTAL: This is an experimental feature. It is likely to change or be deleted - * before reaching a mature release. - */ - @Deprecated - boolean credentialIdExists(ByteArray credentialId); -} diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishAssertionSteps.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishAssertionSteps.java index 06bc78462..57d51c9be 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishAssertionSteps.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishAssertionSteps.java @@ -48,7 +48,7 @@ @Slf4j @AllArgsConstructor -final class FinishAssertionSteps { +final class FinishAssertionSteps { private static final String CLIENT_DATA_TYPE = "webauthn.get"; private static final String SPC_CLIENT_DATA_TYPE = "payment.get"; @@ -59,33 +59,13 @@ final class FinishAssertionSteps { private final Optional callerTokenBindingId; private final Set origins; private final String rpId; - private final CredentialRepositoryV2 credentialRepositoryV2; - private final Optional usernameRepository; + private final CredentialRepository credentialRepository; private final boolean allowOriginPort; private final boolean allowOriginSubdomain; private final boolean validateSignatureCounter; private final boolean isSecurePaymentConfirmation; - static FinishAssertionSteps fromV1( - RelyingParty rp, FinishAssertionOptions options) { - final CredentialRepository credRepo = rp.getCredentialRepository(); - final CredentialRepositoryV1ToV2Adapter credRepoV2 = - new CredentialRepositoryV1ToV2Adapter(credRepo); - return new FinishAssertionSteps<>( - options.getRequest(), - options.getResponse(), - options.getCallerTokenBindingId(), - rp.getOrigins(), - rp.getIdentity().getId(), - credRepoV2, - Optional.of(credRepoV2), - rp.isAllowOriginPort(), - rp.isAllowOriginSubdomain(), - rp.isValidateSignatureCounter(), - options.isSecurePaymentConfirmation()); - } - - FinishAssertionSteps(RelyingPartyV2 rp, FinishAssertionOptions options) { + FinishAssertionSteps(RelyingParty rp, FinishAssertionOptions options) { this( options.getRequest(), options.getResponse(), @@ -93,7 +73,6 @@ static FinishAssertionSteps fromV1( rp.getOrigins(), rp.getIdentity().getId(), rp.getCredentialRepository(), - Optional.ofNullable(rp.getUsernameRepository()), rp.isAllowOriginPort(), rp.isAllowOriginSubdomain(), rp.isValidateSignatureCounter(), @@ -101,7 +80,7 @@ static FinishAssertionSteps fromV1( } private Optional getUsernameForUserHandle(final ByteArray userHandle) { - return usernameRepository.flatMap(unameRepo -> unameRepo.getUsernameForUserHandle(userHandle)); + return credentialRepository.getUsernameForUserHandle(userHandle); } public Step5 begin() { @@ -112,11 +91,7 @@ public AssertionResult run() throws InvalidSignatureCountException { return begin().run(); } - public AssertionResultV2 runV2() throws InvalidSignatureCountException { - return begin().runV2(); - } - - interface Step> { + interface Step> { Next nextStep(); void validate() throws InvalidSignatureCountException; @@ -125,10 +100,6 @@ default Optional result() { return Optional.empty(); } - default Optional> resultV2() { - return Optional.empty(); - } - default Next next() throws InvalidSignatureCountException { validate(); return nextStep(); @@ -141,20 +112,12 @@ default AssertionResult run() throws InvalidSignatureCountException { return next().run(); } } - - default AssertionResultV2 runV2() throws InvalidSignatureCountException { - if (resultV2().isPresent()) { - return resultV2().get(); - } else { - return next().runV2(); - } - } } // Steps 1 through 4 are to create the request and run the client-side part @Value - class Step5 implements Step { + class Step5 implements Step { @Override public Step6 nextStep() { return new Step6(); @@ -177,7 +140,7 @@ public void validate() { } @Value - class Step6 implements Step { + class Step6 implements Step { private final Optional requestedUserHandle; private final Optional requestedUsername; @@ -189,7 +152,7 @@ class Step6 implements Step { private final Optional finalUserHandle; private final Optional finalUsername; - private final Optional registration; + private final Optional registration; public Step6() { requestedUserHandle = request.getUserHandle(); @@ -199,9 +162,7 @@ public Step6() { effectiveRequestUserHandle = OptionalUtil.orElseOptional( requestedUserHandle, - () -> - usernameRepository.flatMap( - unr -> requestedUsername.flatMap(unr::getUserHandleForUsername))); + () -> requestedUsername.flatMap(credentialRepository::getUserHandleForUsername)); effectiveRequestUsername = OptionalUtil.orElseOptional( @@ -219,7 +180,7 @@ public Step6() { () -> finalUserHandle.flatMap(FinishAssertionSteps.this::getUsernameForUserHandle)); registration = - finalUserHandle.flatMap(uh -> credentialRepositoryV2.lookup(response.getId(), uh)); + finalUserHandle.flatMap(uh -> credentialRepository.lookup(response.getId(), uh)); } @Override @@ -229,10 +190,6 @@ public Step7 nextStep() { @Override public void validate() { - assertTrue( - !(request.getUsername().isPresent() && !usernameRepository.isPresent()), - "Cannot set request username when usernameRepository is not configured."); - assertTrue( finalUserHandle.isPresent(), "Could not identify user to authenticate: none of requested username, requested user handle or response user handle are set."); @@ -262,20 +219,16 @@ public void validate() { finalUserHandle.get(), response.getId()); - if (usernameRepository.isPresent()) { - assertTrue( - finalUsername.isPresent(), - "Unknown username for user handle: %s", - finalUserHandle.get()); - } + assertTrue( + finalUsername.isPresent(), "Unknown username for user handle: %s", finalUserHandle.get()); } } @Value - class Step7 implements Step { + class Step7 implements Step { private final Optional username; private final ByteArray userHandle; - private final Optional credential; + private final Optional credential; @Override public Step8 nextStep() { @@ -293,10 +246,10 @@ public void validate() { } @Value - class Step8 implements Step { + class Step8 implements Step { private final Optional username; - private final C credential; + private final RegisteredCredential credential; @Override public void validate() { @@ -326,9 +279,9 @@ public ByteArray signature() { // Nothing to do for step 9 @Value - class Step10 implements Step { + class Step10 implements Step { private final Optional username; - private final C credential; + private final RegisteredCredential credential; @Override public void validate() { @@ -346,9 +299,9 @@ public CollectedClientData clientData() { } @Value - class Step11 implements Step { + class Step11 implements Step { private final Optional username; - private final C credential; + private final RegisteredCredential credential; private final CollectedClientData clientData; @Override @@ -369,9 +322,9 @@ public Step12 nextStep() { } @Value - class Step12 implements Step { + class Step12 implements Step { private final Optional username; - private final C credential; + private final RegisteredCredential credential; @Override public void validate() { @@ -390,9 +343,9 @@ public Step13 nextStep() { } @Value - class Step13 implements Step { + class Step13 implements Step { private final Optional username; - private final C credential; + private final RegisteredCredential credential; @Override public void validate() { @@ -410,9 +363,9 @@ public Step14 nextStep() { } @Value - class Step14 implements Step { + class Step14 implements Step { private final Optional username; - private final C credential; + private final RegisteredCredential credential; @Override public void validate() { @@ -427,9 +380,9 @@ public Step15 nextStep() { } @Value - class Step15 implements Step { + class Step15 implements Step { private final Optional username; - private final C credential; + private final RegisteredCredential credential; @Override public void validate() { @@ -459,9 +412,9 @@ public Step16 nextStep() { } @Value - class Step16 implements Step { + class Step16 implements Step { private final Optional username; - private final C credential; + private final RegisteredCredential credential; @Override public void validate() { @@ -477,9 +430,9 @@ public Step17 nextStep() { } @Value - class Step17 implements Step { + class Step17 implements Step { private final Optional username; - private final C credential; + private final RegisteredCredential credential; @Override public void validate() { @@ -502,9 +455,9 @@ public PendingStep16 nextStep() { @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 { + class PendingStep16 implements Step { private final Optional username; - private final C credential; + private final RegisteredCredential credential; @Override public void validate() { @@ -525,9 +478,9 @@ public Step18 nextStep() { } @Value - class Step18 implements Step { + class Step18 implements Step { private final Optional username; - private final C credential; + private final RegisteredCredential credential; @Override public void validate() {} @@ -539,9 +492,9 @@ public Step19 nextStep() { } @Value - class Step19 implements Step { + class Step19 implements Step { private final Optional username; - private final C credential; + private final RegisteredCredential credential; @Override public void validate() { @@ -559,9 +512,9 @@ public ByteArray clientDataJsonHash() { } @Value - class Step20 implements Step { + class Step20 implements Step { private final Optional username; - private final C credential; + private final RegisteredCredential credential; private final ByteArray clientDataJsonHash; @Override @@ -604,13 +557,13 @@ public ByteArray signedBytes() { } @Value - class Step21 implements Step { + class Step21 implements Step { private final Optional username; - private final C credential; + private final RegisteredCredential credential; private final long assertionSignatureCount; private final long storedSignatureCountBefore; - public Step21(Optional username, C credential) { + public Step21(Optional username, RegisteredCredential credential) { this.username = username; this.credential = credential; this.assertionSignatureCount = @@ -638,8 +591,8 @@ public Finished nextStep() { } @Value - class Finished implements Step { - private final C credential; + class Finished implements Step { + private final RegisteredCredential credential; private final Optional username; private final long assertionSignatureCount; private final boolean signatureCounterValid; @@ -657,17 +610,7 @@ public Finished nextStep() { @Override public Optional result() { return Optional.of( - new AssertionResult( - true, - response, - (RegisteredCredential) credential, - username.get(), - signatureCounterValid)); - } - - public Optional> resultV2() { - return Optional.of( - new AssertionResultV2(true, response, credential, signatureCounterValid)); + new AssertionResult(true, response, credential, username.get(), signatureCounterValid)); } } } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishRegistrationSteps.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishRegistrationSteps.java index 52caf8b4b..60d1350bf 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishRegistrationSteps.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishRegistrationSteps.java @@ -84,27 +84,12 @@ final class FinishRegistrationSteps { private final String rpId; private final boolean allowUntrustedAttestation; private final Optional attestationTrustSource; - private final CredentialRepositoryV2 credentialRepositoryV2; + private final CredentialRepository credentialRepository; private final Clock clock; private final boolean allowOriginPort; private final boolean allowOriginSubdomain; - static FinishRegistrationSteps fromV1(RelyingParty rp, FinishRegistrationOptions options) { - return new FinishRegistrationSteps( - options.getRequest(), - options.getResponse(), - options.getCallerTokenBindingId(), - rp.getOrigins(), - rp.getIdentity().getId(), - rp.isAllowUntrustedAttestation(), - rp.getAttestationTrustSource(), - new CredentialRepositoryV1ToV2Adapter(rp.getCredentialRepository()), - rp.getClock(), - rp.isAllowOriginPort(), - rp.isAllowOriginSubdomain()); - } - - FinishRegistrationSteps(RelyingPartyV2 rp, FinishRegistrationOptions options) { + FinishRegistrationSteps(RelyingParty rp, FinishRegistrationOptions options) { this( options.getRequest(), options.getResponse(), @@ -645,7 +630,7 @@ class Step22 implements Step { @Override public void validate() { assertTrue( - !credentialRepositoryV2.credentialIdExists(response.getId()), + credentialRepository.lookupAll(response.getId()).isEmpty(), "Credential ID is already registered: %s", response.getId()); } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/RegisteredCredential.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/RegisteredCredential.java index 0c1a5b15a..21246e5b2 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/RegisteredCredential.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/RegisteredCredential.java @@ -30,21 +30,16 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.yubico.webauthn.data.AttestedCredentialData; import com.yubico.webauthn.data.AuthenticatorAssertionResponse; -import com.yubico.webauthn.data.AuthenticatorAttestationResponse; import com.yubico.webauthn.data.AuthenticatorData; -import com.yubico.webauthn.data.AuthenticatorTransport; import com.yubico.webauthn.data.ByteArray; import com.yubico.webauthn.data.COSEAlgorithmIdentifier; -import com.yubico.webauthn.data.PublicKeyCredentialCreationOptions; import com.yubico.webauthn.data.PublicKeyCredentialDescriptor; -import com.yubico.webauthn.data.PublicKeyCredentialRequestOptions; import com.yubico.webauthn.data.UserIdentity; import java.io.IOException; import java.security.NoSuchAlgorithmException; import java.security.PublicKey; import java.security.spec.InvalidKeySpecException; import java.util.Optional; -import java.util.Set; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; @@ -60,7 +55,7 @@ */ @Value @Builder(toBuilder = true) -public final class RegisteredCredential implements CredentialRecord { +public final class RegisteredCredential { /** * The credential @@ -122,37 +117,6 @@ public PublicKey getParsedPublicKey() */ @Builder.Default private final long signatureCount = 0; - /** - * Transport hints as to how the client might communicate with the authenticator this credential - * is bound to. - * - *

This SHOULD be set to the value returned by {@link - * AuthenticatorAttestationResponse#getTransports()} when the credential was created. That value - * SHOULD NOT be modified. - * - *

This is only used if the {@link RelyingParty} is configured with a {@link - * CredentialRepositoryV2}, in which case this is used to set {@link - * PublicKeyCredentialDescriptor#getTransports()} in {@link - * PublicKeyCredentialCreationOptions#getExcludeCredentials() excludeCredentials} in {@link - * RelyingParty#startRegistration(StartRegistrationOptions)} and {@link - * PublicKeyCredentialRequestOptions#getAllowCredentials() allowCredentials} in {@link - * RelyingParty#startAssertion(StartAssertionOptions)}. This is not used if the {@link - * RelyingParty} is configured with a {@link CredentialRepository}. - * - * @see getTransports() - * in 5.2.1. Information About Public Key Credential (interface - * AuthenticatorAttestationResponse) - * @see transports - * in 5.8.3. Credential Descriptor (dictionary PublicKeyCredentialDescriptor) - * @see AuthenticatorAttestationResponse#getTransports() - * @see PublicKeyCredentialDescriptor#getTransports() - * @deprecated EXPERIMENTAL: This is an experimental feature. It is likely to change or be deleted - * before reaching a mature release. - */ - @Deprecated @Builder.Default private final Set transports = null; - /** * The state of the BE flag when * this credential was registered, if known. @@ -207,53 +171,16 @@ private RegisteredCredential( @NonNull @JsonProperty("userHandle") ByteArray userHandle, @NonNull @JsonProperty("publicKeyCose") ByteArray publicKeyCose, @JsonProperty("signatureCount") long signatureCount, - @JsonProperty("transports") Set transports, @JsonProperty("backupEligible") Boolean backupEligible, @JsonProperty("backupState") @JsonAlias("backedUp") Boolean backupState) { this.credentialId = credentialId; this.userHandle = userHandle; this.publicKeyCose = publicKeyCose; this.signatureCount = signatureCount; - this.transports = transports; this.backupEligible = backupEligible; this.backupState = backupState; } - /** - * Transport hints as to how the client might communicate with the authenticator this credential - * is bound to. - * - *

This SHOULD be set to the value returned by {@link - * AuthenticatorAttestationResponse#getTransports()} when the credential was created. That value - * SHOULD NOT be modified. - * - *

This is only used if the {@link RelyingParty} is configured with a {@link - * CredentialRepositoryV2}, in which case this is used to set {@link - * PublicKeyCredentialDescriptor#getTransports()} in {@link - * PublicKeyCredentialCreationOptions#getExcludeCredentials() excludeCredentials} in {@link - * RelyingParty#startRegistration(StartRegistrationOptions)} and {@link - * PublicKeyCredentialRequestOptions#getAllowCredentials() allowCredentials} in {@link - * RelyingParty#startAssertion(StartAssertionOptions)}. This is not used if the {@link - * RelyingParty} is configured with a {@link CredentialRepository}. - * - * @see getTransports() - * in 5.2.1. Information About Public Key Credential (interface - * AuthenticatorAttestationResponse) - * @see transports - * in 5.8.3. Credential Descriptor (dictionary PublicKeyCredentialDescriptor) - * @see AuthenticatorAttestationResponse#getTransports() - * @see PublicKeyCredentialDescriptor#getTransports() - * @deprecated EXPERIMENTAL: This is an experimental feature. It is likely to change or be deleted - * before reaching a mature release. - */ - @Deprecated - @Override - public Optional> getTransports() { - return Optional.ofNullable(transports); - } - /** * The state of the BE flag when * this credential was registered, if known. diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/RelyingParty.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/RelyingParty.java index b8f0bafcb..ba90555bd 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/RelyingParty.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/RelyingParty.java @@ -516,7 +516,7 @@ public RegistrationResult finishRegistration(FinishRegistrationOptions finishReg * #finishRegistration(FinishRegistrationOptions)} instead of this method. */ FinishRegistrationSteps _finishRegistration(FinishRegistrationOptions options) { - return FinishRegistrationSteps.fromV1(this, options); + return new FinishRegistrationSteps(this, options); } public AssertionRequest startAssertion(StartAssertionOptions startAssertionOptions) { @@ -574,8 +574,8 @@ public AssertionResult finishAssertion(FinishAssertionOptions finishAssertionOpt * a separate method to facilitate testing; users should call {@link * #finishAssertion(FinishAssertionOptions)} instead of this method. */ - FinishAssertionSteps _finishAssertion(FinishAssertionOptions options) { - return FinishAssertionSteps.fromV1(this, options); + FinishAssertionSteps _finishAssertion(FinishAssertionOptions options) { + return new FinishAssertionSteps(this, options); } public static RelyingPartyBuilder.MandatoryStages builder() { @@ -608,31 +608,10 @@ public class Step2 { * credentialRepository} is a required parameter. * * @see RelyingPartyBuilder#credentialRepository(CredentialRepository) - * @see #credentialRepositoryV2(CredentialRepositoryV2) */ public RelyingPartyBuilder credentialRepository(CredentialRepository credentialRepository) { return builder.credentialRepository(credentialRepository); } - - /** - * {@link RelyingPartyBuilder#credentialRepository(CredentialRepository) - * credentialRepository} is a required parameter. This setter differs from {@link - * #credentialRepository(CredentialRepository)} in that it takes an instance of {@link - * CredentialRepositoryV2} and converts the builder's return type to {@link RelyingPartyV2}. - * {@link CredentialRepositoryV2} does not require the application to support usernames, - * unless {@link RelyingPartyV2.RelyingPartyV2Builder#usernameRepository(UsernameRepository) - * usernameRepository} is also set in a subsequent builder step. - * - * @see #credentialRepository(CredentialRepository) - * @deprecated EXPERIMENTAL: This is an experimental feature. It is likely to change or be - * deleted before reaching a mature release. - */ - @Deprecated - public - RelyingPartyV2.RelyingPartyV2Builder credentialRepositoryV2( - CredentialRepositoryV2 credentialRepository) { - return RelyingPartyV2.builder(builder.identity, credentialRepository); - } } } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/RelyingPartyV2.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/RelyingPartyV2.java deleted file mode 100644 index 9ce2883c3..000000000 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/RelyingPartyV2.java +++ /dev/null @@ -1,707 +0,0 @@ -// Copyright (c) 2018, Yubico AB -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, this -// list of conditions and the following disclaimer. -// -// 2. Redistributions in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -package com.yubico.webauthn; - -import com.yubico.internal.util.CollectionUtil; -import com.yubico.internal.util.OptionalUtil; -import com.yubico.webauthn.attestation.AttestationTrustSource; -import com.yubico.webauthn.data.AssertionExtensionInputs; -import com.yubico.webauthn.data.AttestationConveyancePreference; -import com.yubico.webauthn.data.AuthenticatorData; -import com.yubico.webauthn.data.ByteArray; -import com.yubico.webauthn.data.CollectedClientData; -import com.yubico.webauthn.data.PublicKeyCredentialCreationOptions; -import com.yubico.webauthn.data.PublicKeyCredentialCreationOptions.PublicKeyCredentialCreationOptionsBuilder; -import com.yubico.webauthn.data.PublicKeyCredentialParameters; -import com.yubico.webauthn.data.PublicKeyCredentialRequestOptions; -import com.yubico.webauthn.data.PublicKeyCredentialRequestOptions.PublicKeyCredentialRequestOptionsBuilder; -import com.yubico.webauthn.data.RegistrationExtensionInputs; -import com.yubico.webauthn.data.RelyingPartyIdentity; -import com.yubico.webauthn.exception.AssertionFailedException; -import com.yubico.webauthn.exception.InvalidSignatureCountException; -import com.yubico.webauthn.exception.RegistrationFailedException; -import com.yubico.webauthn.extension.appid.AppId; -import java.net.MalformedURLException; -import java.net.URL; -import java.security.KeyFactory; -import java.security.SecureRandom; -import java.security.Signature; -import java.time.Clock; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Optional; -import java.util.Set; -import java.util.stream.Collectors; -import lombok.Builder; -import lombok.NonNull; -import lombok.Value; -import lombok.extern.slf4j.Slf4j; - -/** - * Encapsulates the four basic Web Authentication operations - start/finish registration, - * start/finish authentication - along with overall operational settings for them. - * - *

This class has no mutable state. An instance of this class may therefore be thought of as a - * container for specialized versions (function closures) of these four operations rather than a - * stateful object. - */ -@Slf4j -@Builder(toBuilder = true) -@Value -public class RelyingPartyV2 { - - private static final SecureRandom random = new SecureRandom(); - - /** - * The {@link RelyingPartyIdentity} that will be set as the {@link - * PublicKeyCredentialCreationOptions#getRp() rp} parameter when initiating registration - * operations, and which {@link AuthenticatorData#getRpIdHash()} will be compared against. This is - * a required parameter. - * - *

A successful registration or authentication operation requires {@link - * AuthenticatorData#getRpIdHash()} to exactly equal the SHA-256 hash of this member's {@link - * RelyingPartyIdentity#getId() id} member. Alternatively, it may instead equal the SHA-256 hash - * of {@link #getAppId() appId} if the latter is present. - * - * @see #startRegistration(StartRegistrationOptions) - * @see PublicKeyCredentialCreationOptions - */ - @NonNull private final RelyingPartyIdentity identity; - - /** - * The allowed origins that returned authenticator responses will be compared against. - * - *

The default is the set containing only the string - * "https://" + {@link #getIdentity()}.getId(). - * - *

If {@link RelyingPartyV2Builder#allowOriginPort(boolean) allowOriginPort} and {@link - * RelyingPartyV2Builder#allowOriginSubdomain(boolean) allowOriginSubdomain} are both false - * (the default), then a successful registration or authentication operation requires - * {@link CollectedClientData#getOrigin()} to exactly equal one of these values. - * - *

If {@link RelyingPartyV2Builder#allowOriginPort(boolean) allowOriginPort} is true - * , then the above rule is relaxed to allow any port number in {@link - * CollectedClientData#getOrigin()}, regardless of any port specified. - * - *

If {@link RelyingPartyV2Builder#allowOriginSubdomain(boolean) allowOriginSubdomain} is - * - * true, then the above rule is relaxed to allow any subdomain, of any depth, of any of - * these values. - * - *

For either of the above relaxations to take effect, both the allowed origin and the client - * data origin must be valid URLs. Origins that are not valid URLs are matched only by exact - * string equality. - * - * @see #getIdentity() - */ - @NonNull private final Set origins; - - /** - * An abstract database which can look up credentials, usernames and user handles from usernames, - * user handles and credential IDs. This is a required parameter. - * - *

This is used to look up: - * - *

    - *
  • the user handle for a user logging in via user name - *
  • the user name for a user logging in via user handle - *
  • the credential IDs to include in {@link - * PublicKeyCredentialCreationOptions#getExcludeCredentials()} - *
  • the credential IDs to include in {@link - * PublicKeyCredentialRequestOptions#getAllowCredentials()} - *
  • that the correct user owns the credential when verifying an assertion - *
  • the public key to use to verify an assertion - *
  • the stored signature counter when verifying an assertion - *
- */ - @NonNull private final CredentialRepositoryV2 credentialRepository; - - /** - * Enable support for identifying users by username. - * - *

If set, then {@link #startAssertion(StartAssertionOptions)} allows setting the {@link - * StartAssertionOptions.StartAssertionOptionsBuilder#username(String) username} parameter when - * starting an assertion. - * - *

By default, this is not set. - * - * @deprecated EXPERIMENTAL: This is an experimental feature. It is likely to change or be deleted - * before reaching a mature release. - */ - @Deprecated private final UsernameRepository usernameRepository; - - /** - * The extension input to set for the appid and appidExclude extensions. - * - *

You do not need this extension if you have not previously supported U2F. Its purpose is to - * make already-registered U2F credentials forward-compatible with the WebAuthn API. It is not - * needed for new registrations, even of U2F authenticators. - * - *

If this member is set, {@link #startAssertion(StartAssertionOptions) startAssertion} will - * automatically set the appid extension input, and {@link - * #finishAssertion(FinishAssertionOptions) finishAssertion} will adjust its verification logic to - * also accept this AppID as an alternative to the RP ID. Likewise, {@link - * #startRegistration(StartRegistrationOptions)} startRegistration} will automatically set the - * appidExclude extension input. - * - *

By default, this is not set. - * - * @see AssertionExtensionInputs#getAppid() - * @see RegistrationExtensionInputs#getAppidExclude() - * @see §10.1. - * FIDO AppID Extension (appid) - * @see §10.2. - * FIDO AppID Exclusion Extension (appidExclude) - */ - @NonNull private final Optional appId; - - /** - * The argument for the {@link PublicKeyCredentialCreationOptions#getAttestation() attestation} - * parameter in registration operations. - * - *

Unless your application has a concrete policy for authenticator attestation, it is - * recommended to leave this parameter undefined. - * - *

If you set this, you may want to explicitly set {@link - * RelyingPartyV2Builder#allowUntrustedAttestation(boolean) allowUntrustedAttestation} and {@link - * RelyingPartyV2Builder#attestationTrustSource(AttestationTrustSource) attestationTrustSource} - * too. - * - *

By default, this is not set. - * - * @see PublicKeyCredentialCreationOptions#getAttestation() - * @see §6.4. - * Attestation - */ - @NonNull private final Optional attestationConveyancePreference; - - /** - * An {@link AttestationTrustSource} instance to use for looking up trust roots for authenticator - * attestation. This matters only if {@link #getAttestationConveyancePreference()} is non-empty - * and not set to {@link AttestationConveyancePreference#NONE}. - * - *

By default, this is not set. - * - * @see PublicKeyCredentialCreationOptions#getAttestation() - * @see §6.4. - * Attestation - */ - @NonNull private final Optional attestationTrustSource; - - /** - * The argument for the {@link PublicKeyCredentialCreationOptions#getPubKeyCredParams() - * pubKeyCredParams} parameter in registration operations. - * - *

This is a list of acceptable public key algorithms and their parameters, ordered from most - * to least preferred. - * - *

The default is the following list, in order: - * - *

    - *
  1. {@link PublicKeyCredentialParameters#ES256 ES256} - *
  2. {@link PublicKeyCredentialParameters#EdDSA EdDSA} - *
  3. {@link PublicKeyCredentialParameters#ES256 ES384} - *
  4. {@link PublicKeyCredentialParameters#ES256 ES512} - *
  5. {@link PublicKeyCredentialParameters#RS256 RS256} - *
  6. {@link PublicKeyCredentialParameters#RS384 RS384} - *
  7. {@link PublicKeyCredentialParameters#RS512 RS512} - *
- * - * @see PublicKeyCredentialCreationOptions#getAttestation() - * @see §6.4. - * Attestation - */ - @Builder.Default @NonNull - private final List preferredPubkeyParams = - Collections.unmodifiableList( - Arrays.asList( - PublicKeyCredentialParameters.ES256, - PublicKeyCredentialParameters.EdDSA, - PublicKeyCredentialParameters.ES384, - PublicKeyCredentialParameters.ES512, - PublicKeyCredentialParameters.RS256, - PublicKeyCredentialParameters.RS384, - PublicKeyCredentialParameters.RS512)); - - /** - * If true, the origin matching rule is relaxed to allow any port number. - * - *

The default is false. - * - *

Examples with - * origins: ["https://example.org", "https://accounts.example.org", "https://acme.com:8443"] - * - * - *

    - *
  • - *

    allowOriginPort: false - *

    Accepted: - *

      - *
    • https://example.org - *
    • https://accounts.example.org - *
    • https://acme.com:8443 - *
    - *

    Rejected: - *

      - *
    • https://example.org:8443 - *
    • https://shop.example.org - *
    • https://acme.com - *
    • https://acme.com:9000 - *
    - *
  • - *

    allowOriginPort: true - *

    Accepted: - *

      - *
    • https://example.org - *
    • https://example.org:8443 - *
    • https://accounts.example.org - *
    • https://acme.com - *
    • https://acme.com:8443 - *
    • https://acme.com:9000 - *
    - *

    Rejected: - *

      - *
    • https://shop.example.org - *
    - *
- */ - @Builder.Default private final boolean allowOriginPort = false; - - /** - * If true, the origin matching rule is relaxed to allow any subdomain, of any depth, - * of the values of {@link RelyingPartyV2Builder#origins(Set) origins}. - * - *

The default is false. - * - *

Examples with origins: ["https://example.org", "https://acme.com:8443"] - * - *

    - *
  • - *

    allowOriginSubdomain: false - *

    Accepted: - *

      - *
    • https://example.org - *
    • https://acme.com:8443 - *
    - *

    Rejected: - *

      - *
    • https://example.org:8443 - *
    • https://accounts.example.org - *
    • https://acme.com - *
    • https://eu.shop.acme.com:8443 - *
    - *
  • - *

    allowOriginSubdomain: true - *

    Accepted: - *

      - *
    • https://example.org - *
    • https://accounts.example.org - *
    • https://acme.com:8443 - *
    • https://eu.shop.acme.com:8443 - *
    - *

    Rejected: - *

      - *
    • https://example.org:8443 - *
    • https://acme.com - *
    - *
- */ - @Builder.Default private final boolean allowOriginSubdomain = false; - - /** - * If false, {@link #finishRegistration(FinishRegistrationOptions) - * finishRegistration} will only allow registrations where the attestation signature can be linked - * to a trusted attestation root. This excludes none attestation, and self attestation unless the - * self attestation key is explicitly trusted. - * - *

Regardless of the value of this option, invalid attestation statements of supported formats - * will always be rejected. For example, a "packed" attestation statement with an invalid - * signature will be rejected even if this option is set to true. - * - *

The default is true. - */ - @Builder.Default private final boolean allowUntrustedAttestation = true; - - /** - * If true, {@link #finishAssertion(FinishAssertionOptions) finishAssertion} will - * succeed only if the {@link AuthenticatorData#getSignatureCounter() signature counter value} in - * the response is strictly greater than the {@link RegisteredCredential#getSignatureCount() - * stored signature counter value}, or if both counters are exactly zero. - * - *

The default is true. - */ - @Builder.Default private final boolean validateSignatureCounter = true; - - /** - * A {@link Clock} which will be used to tell the current time while verifying attestation - * certificate chains. - * - *

This is intended primarily for testing, and relevant only if {@link - * RelyingPartyV2Builder#attestationTrustSource(AttestationTrustSource)} is set. - * - *

The default is Clock.systemUTC(). - */ - @Builder.Default @NonNull private final Clock clock = Clock.systemUTC(); - - @Builder - private RelyingPartyV2( - @NonNull RelyingPartyIdentity identity, - Set origins, - @NonNull CredentialRepositoryV2 credentialRepository, - UsernameRepository usernameRepository, - @NonNull Optional appId, - @NonNull Optional attestationConveyancePreference, - @NonNull Optional attestationTrustSource, - List preferredPubkeyParams, - boolean allowOriginPort, - boolean allowOriginSubdomain, - boolean allowUntrustedAttestation, - boolean validateSignatureCounter, - Clock clock) { - this.identity = identity; - this.origins = - origins != null - ? CollectionUtil.immutableSet(origins) - : Collections.singleton("https://" + identity.getId()); - - for (String origin : this.origins) { - try { - new URL(origin); - } catch (MalformedURLException e) { - log.warn( - "Allowed origin is not a valid URL, it will match only by exact string equality: {}", - origin); - } - } - - this.credentialRepository = credentialRepository; - this.usernameRepository = usernameRepository; - this.appId = appId; - this.attestationConveyancePreference = attestationConveyancePreference; - this.attestationTrustSource = attestationTrustSource; - this.preferredPubkeyParams = filterAvailableAlgorithms(preferredPubkeyParams); - this.allowOriginPort = allowOriginPort; - this.allowOriginSubdomain = allowOriginSubdomain; - this.allowUntrustedAttestation = allowUntrustedAttestation; - this.validateSignatureCounter = validateSignatureCounter; - this.clock = clock; - } - - private static ByteArray generateChallenge() { - byte[] bytes = new byte[32]; - random.nextBytes(bytes); - return new ByteArray(bytes); - } - - /** - * Filter pubKeyCredParams to only contain algorithms with a {@link KeyFactory} and a - * {@link Signature} available, and log a warning for every unsupported algorithm. - * - * @return a new {@link List} containing only the algorithms supported in the current JCA context. - */ - private static List filterAvailableAlgorithms( - List pubKeyCredParams) { - return RelyingParty.filterAvailableAlgorithms(pubKeyCredParams); - } - - public PublicKeyCredentialCreationOptions startRegistration( - StartRegistrationOptions startRegistrationOptions) { - PublicKeyCredentialCreationOptionsBuilder builder = - PublicKeyCredentialCreationOptions.builder() - .rp(identity) - .user(startRegistrationOptions.getUser()) - .challenge(generateChallenge()) - .pubKeyCredParams(preferredPubkeyParams) - .excludeCredentials( - credentialRepository - .getCredentialDescriptorsForUserHandle( - startRegistrationOptions.getUser().getId()) - .stream() - .map(ToPublicKeyCredentialDescriptor::toPublicKeyCredentialDescriptor) - .collect(Collectors.toSet())) - .authenticatorSelection(startRegistrationOptions.getAuthenticatorSelection()) - .extensions( - startRegistrationOptions - .getExtensions() - .merge( - RegistrationExtensionInputs.builder() - .appidExclude(appId) - .credProps() - .build())) - .timeout(startRegistrationOptions.getTimeout()) - .hints(startRegistrationOptions.getHints()); - attestationConveyancePreference.ifPresent(builder::attestation); - return builder.build(); - } - - public RegistrationResult finishRegistration(FinishRegistrationOptions finishRegistrationOptions) - throws RegistrationFailedException { - try { - return _finishRegistration(finishRegistrationOptions).run(); - } catch (IllegalArgumentException e) { - throw new RegistrationFailedException(e); - } - } - - /** - * This method is NOT part of the public API. - * - *

This method is called internally by {@link #finishRegistration(FinishRegistrationOptions)}. - * It is a separate method to facilitate testing; users should call {@link - * #finishRegistration(FinishRegistrationOptions)} instead of this method. - */ - FinishRegistrationSteps _finishRegistration(FinishRegistrationOptions options) { - return new FinishRegistrationSteps(this, options); - } - - public AssertionRequest startAssertion(StartAssertionOptions startAssertionOptions) { - if (startAssertionOptions.getUsername().isPresent() && usernameRepository == null) { - throw new IllegalArgumentException( - "StartAssertionOptions.username must not be set when usernameRepository is not configured."); - } - - PublicKeyCredentialRequestOptionsBuilder pkcro = - PublicKeyCredentialRequestOptions.builder() - .challenge(generateChallenge()) - .rpId(identity.getId()) - .allowCredentials( - OptionalUtil.orElseOptional( - startAssertionOptions.getUserHandle(), - () -> - Optional.ofNullable(usernameRepository) - .flatMap( - unr -> - startAssertionOptions - .getUsername() - .flatMap(unr::getUserHandleForUsername))) - .map(credentialRepository::getCredentialDescriptorsForUserHandle) - .map( - descriptors -> - descriptors.stream() - .map( - ToPublicKeyCredentialDescriptor - ::toPublicKeyCredentialDescriptor) - .collect(Collectors.toList()))) - .extensions( - startAssertionOptions - .getExtensions() - .merge(startAssertionOptions.getExtensions().toBuilder().appid(appId).build())) - .timeout(startAssertionOptions.getTimeout()) - .hints(startAssertionOptions.getHints()); - - startAssertionOptions.getUserVerification().ifPresent(pkcro::userVerification); - - return AssertionRequest.builder() - .publicKeyCredentialRequestOptions(pkcro.build()) - .username(startAssertionOptions.getUsername()) - .userHandle(startAssertionOptions.getUserHandle()) - .build(); - } - - /** - * @throws InvalidSignatureCountException if {@link - * RelyingPartyV2Builder#validateSignatureCounter(boolean) validateSignatureCounter} is - * true, the {@link AuthenticatorData#getSignatureCounter() signature count} in the - * response is less than or equal to the {@link RegisteredCredential#getSignatureCount() - * stored signature count}, and at least one of the signature count values is nonzero. - * @throws AssertionFailedException if validation fails for any other reason. - */ - public AssertionResultV2 finishAssertion(FinishAssertionOptions finishAssertionOptions) - throws AssertionFailedException { - try { - return _finishAssertion(finishAssertionOptions).runV2(); - } catch (IllegalArgumentException e) { - throw new AssertionFailedException(e); - } - } - - /** - * This method is NOT part of the public API. - * - *

This method is called internally by {@link #finishAssertion(FinishAssertionOptions)}. It is - * a separate method to facilitate testing; users should call {@link - * #finishAssertion(FinishAssertionOptions)} instead of this method. - */ - FinishAssertionSteps _finishAssertion(FinishAssertionOptions options) { - return new FinishAssertionSteps(this, options); - } - - static RelyingPartyV2Builder builder( - RelyingPartyIdentity identity, CredentialRepositoryV2 credentialRepository) { - return new RelyingPartyV2Builder() - .identity(identity) - .credentialRepository(credentialRepository); - } - - public static class RelyingPartyV2Builder { - private @NonNull Optional appId = Optional.empty(); - private @NonNull Optional attestationConveyancePreference = - Optional.empty(); - private @NonNull Optional attestationTrustSource = Optional.empty(); - - /** - * The extension input to set for the appid and appidExclude - * extensions. - * - *

You do not need this extension if you have not previously supported U2F. Its purpose is to - * make already-registered U2F credentials forward-compatible with the WebAuthn API. It is not - * needed for new registrations, even of U2F authenticators. - * - *

If this member is set, {@link #startAssertion(StartAssertionOptions) startAssertion} will - * automatically set the appid extension input, and {@link - * #finishAssertion(FinishAssertionOptions) finishAssertion} will adjust its verification logic - * to also accept this AppID as an alternative to the RP ID. Likewise, {@link - * #startRegistration(StartRegistrationOptions)} startRegistration} will automatically set the - * appidExclude extension input. - * - *

By default, this is not set. - * - * @see AssertionExtensionInputs#getAppid() - * @see RegistrationExtensionInputs#getAppidExclude() - * @see §10.1. - * FIDO AppID Extension (appid) - * @see §10.2. - * FIDO AppID Exclusion Extension (appidExclude) - */ - public RelyingPartyV2Builder appId(@NonNull Optional appId) { - this.appId = appId; - return this; - } - - /** - * The extension input to set for the appid and appidExclude - * extensions. - * - *

You do not need this extension if you have not previously supported U2F. Its purpose is to - * make already-registered U2F credentials forward-compatible with the WebAuthn API. It is not - * needed for new registrations, even of U2F authenticators. - * - *

If this member is set, {@link #startAssertion(StartAssertionOptions) startAssertion} will - * automatically set the appid extension input, and {@link - * #finishAssertion(FinishAssertionOptions) finishAssertion} will adjust its verification logic - * to also accept this AppID as an alternative to the RP ID. Likewise, {@link - * #startRegistration(StartRegistrationOptions)} startRegistration} will automatically set the - * appidExclude extension input. - * - *

By default, this is not set. - * - * @see AssertionExtensionInputs#getAppid() - * @see RegistrationExtensionInputs#getAppidExclude() - * @see §10.1. - * FIDO AppID Extension (appid) - * @see §10.2. - * FIDO AppID Exclusion Extension (appidExclude) - */ - public RelyingPartyV2Builder appId(@NonNull AppId appId) { - return this.appId(Optional.of(appId)); - } - - /** - * The argument for the {@link PublicKeyCredentialCreationOptions#getAttestation() attestation} - * parameter in registration operations. - * - *

Unless your application has a concrete policy for authenticator attestation, it is - * recommended to leave this parameter undefined. - * - *

If you set this, you may want to explicitly set {@link - * RelyingPartyV2Builder#allowUntrustedAttestation(boolean) allowUntrustedAttestation} and - * {@link RelyingPartyV2Builder#attestationTrustSource(AttestationTrustSource) - * attestationTrustSource} too. - * - *

By default, this is not set. - * - * @see PublicKeyCredentialCreationOptions#getAttestation() - * @see §6.4. - * Attestation - */ - public RelyingPartyV2Builder attestationConveyancePreference( - @NonNull Optional attestationConveyancePreference) { - this.attestationConveyancePreference = attestationConveyancePreference; - return this; - } - - /** - * The argument for the {@link PublicKeyCredentialCreationOptions#getAttestation() attestation} - * parameter in registration operations. - * - *

Unless your application has a concrete policy for authenticator attestation, it is - * recommended to leave this parameter undefined. - * - *

If you set this, you may want to explicitly set {@link - * RelyingPartyV2Builder#allowUntrustedAttestation(boolean) allowUntrustedAttestation} and - * {@link RelyingPartyV2Builder#attestationTrustSource(AttestationTrustSource) - * attestationTrustSource} too. - * - *

By default, this is not set. - * - * @see PublicKeyCredentialCreationOptions#getAttestation() - * @see §6.4. - * Attestation - */ - public RelyingPartyV2Builder attestationConveyancePreference( - @NonNull AttestationConveyancePreference attestationConveyancePreference) { - return this.attestationConveyancePreference(Optional.of(attestationConveyancePreference)); - } - - /** - * An {@link AttestationTrustSource} instance to use for looking up trust roots for - * authenticator attestation. This matters only if {@link #getAttestationConveyancePreference()} - * is non-empty and not set to {@link AttestationConveyancePreference#NONE}. - * - *

By default, this is not set. - * - * @see PublicKeyCredentialCreationOptions#getAttestation() - * @see §6.4. - * Attestation - */ - public RelyingPartyV2Builder attestationTrustSource( - @NonNull Optional attestationTrustSource) { - this.attestationTrustSource = attestationTrustSource; - return this; - } - - /** - * An {@link AttestationTrustSource} instance to use for looking up trust roots for - * authenticator attestation. This matters only if {@link #getAttestationConveyancePreference()} - * is non-empty and not set to {@link AttestationConveyancePreference#NONE}. - * - *

By default, this is not set. - * - * @see PublicKeyCredentialCreationOptions#getAttestation() - * @see §6.4. - * Attestation - */ - public RelyingPartyV2Builder attestationTrustSource( - @NonNull AttestationTrustSource attestationTrustSource) { - return this.attestationTrustSource(Optional.of(attestationTrustSource)); - } - } -} diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/ToPublicKeyCredentialDescriptor.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/ToPublicKeyCredentialDescriptor.java deleted file mode 100644 index 92a6f2f35..000000000 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/ToPublicKeyCredentialDescriptor.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.yubico.webauthn; - -import com.yubico.webauthn.data.PublicKeyCredentialDescriptor; - -/** - * A type that can be converted into a {@link PublicKeyCredentialDescriptor} value. - * - * @see PublicKeyCredentialDescriptor - * @see §5.10.3. - * Credential Descriptor (dictionary PublicKeyCredentialDescriptor) - * @see CredentialRecord - * @deprecated EXPERIMENTAL: This is an experimental feature. It is likely to change or be deleted - * before reaching a mature release. - */ -@Deprecated -public interface ToPublicKeyCredentialDescriptor { - - /** - * Convert this value to a {@link PublicKeyCredentialDescriptor} value. - * - *

Implementations MUST NOT return null. - * - * @see PublicKeyCredentialDescriptor - * @see §5.10.3. - * Credential Descriptor (dictionary PublicKeyCredentialDescriptor) - * @see CredentialRecord - * @deprecated EXPERIMENTAL: This is an experimental feature. It is likely to change or be deleted - * before reaching a mature release. - */ - @Deprecated - PublicKeyCredentialDescriptor toPublicKeyCredentialDescriptor(); -} diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/UsernameRepository.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/UsernameRepository.java deleted file mode 100644 index 101937f69..000000000 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/UsernameRepository.java +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) 2018, Yubico AB -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, this -// list of conditions and the following disclaimer. -// -// 2. Redistributions in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -package com.yubico.webauthn; - -import com.yubico.webauthn.data.ByteArray; -import java.util.Optional; - -/** - * An abstraction of optional database lookups needed by this library. - * - *

This is used by {@link RelyingPartyV2} to look up usernames and user handles. - * - * @deprecated EXPERIMENTAL: This is an experimental feature. It is likely to change or be deleted - * before reaching a mature release. - */ -@Deprecated -public interface UsernameRepository { - - /** - * Get the user handle corresponding to the given username - the inverse of {@link - * #getUsernameForUserHandle(ByteArray)}. - * - *

Used to look up the user handle based on the username, for authentication ceremonies where - * the username is already given. - * - * @deprecated EXPERIMENTAL: This is an experimental feature. It is likely to change or be deleted - * before reaching a mature release. - */ - @Deprecated - Optional getUserHandleForUsername(String username); - - /** - * Get the username corresponding to the given user handle - the inverse of {@link - * #getUserHandleForUsername(String)}. - * - *

Used to look up the username based on the user handle, for username-less authentication - * ceremonies. - * - * @deprecated EXPERIMENTAL: This is an experimental feature. It is likely to change or be deleted - * before reaching a mature release. - */ - @Deprecated - Optional getUsernameForUserHandle(ByteArray userHandle); -} diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialDescriptor.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialDescriptor.java index be1810f2d..b2487b5c1 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialDescriptor.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialDescriptor.java @@ -29,7 +29,6 @@ import com.yubico.internal.util.CollectionUtil; import com.yubico.internal.util.ComparableUtil; import com.yubico.webauthn.RegistrationResult; -import com.yubico.webauthn.ToPublicKeyCredentialDescriptor; import java.util.Optional; import java.util.Set; import java.util.SortedSet; @@ -50,8 +49,7 @@ */ @Value @Builder(toBuilder = true) -public class PublicKeyCredentialDescriptor - implements Comparable, ToPublicKeyCredentialDescriptor { +public class PublicKeyCredentialDescriptor implements Comparable { /** The type of the credential the caller is referring to. */ @NonNull @Builder.Default @@ -110,18 +108,6 @@ public static PublicKeyCredentialDescriptorBuilder.MandatoryStages builder() { return new PublicKeyCredentialDescriptorBuilder.MandatoryStages(); } - /** - * This implementation of {@link - * ToPublicKeyCredentialDescriptor#toPublicKeyCredentialDescriptor()} is a no-op which returns - * this unchanged. - * - * @return this. - */ - @Override - public PublicKeyCredentialDescriptor toPublicKeyCredentialDescriptor() { - return this; - } - public static class PublicKeyCredentialDescriptorBuilder { private Set transports = null; 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 4d8d8fc8b..e6c57be86 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 @@ -195,7 +195,7 @@ class RelyingPartyAssertionSpec userVerificationRequirement: UserVerificationRequirement = UserVerificationRequirement.PREFERRED, validateSignatureCounter: Boolean = true, - ): FinishAssertionSteps[RegisteredCredential] = { + ): FinishAssertionSteps = { val clientDataJsonBytes: ByteArray = if (clientDataJson == null) null else new ByteArray(clientDataJson.getBytes("UTF-8")) @@ -577,7 +577,7 @@ class RelyingPartyAssertionSpec ), credentialId = new ByteArray(Array(0, 1, 2, 3)), ) - val step: FinishAssertionSteps[RegisteredCredential]#Step5 = + val step: FinishAssertionSteps#Step5 = steps.begin step.validations shouldBe a[Failure[_]] @@ -601,7 +601,7 @@ class RelyingPartyAssertionSpec ), credentialId = new ByteArray(Array(4, 5, 6, 7)), ) - val step: FinishAssertionSteps[RegisteredCredential]#Step5 = + val step: FinishAssertionSteps#Step5 = steps.begin step.validations shouldBe a[Success[_]] @@ -619,7 +619,7 @@ class RelyingPartyAssertionSpec allowCredentials = allowCredentials, credentialId = new ByteArray(Array(0, 1, 2, 3)), ) - val step: FinishAssertionSteps[RegisteredCredential]#Step5 = + val step: FinishAssertionSteps#Step5 = steps.begin step.validations shouldBe a[Success[_]] @@ -677,7 +677,7 @@ class RelyingPartyAssertionSpec userHandleForUser = owner.userHandle, usernameForRequest = Some(owner.username), ) - val step: FinishAssertionSteps[RegisteredCredential]#Step6 = + val step: FinishAssertionSteps#Step6 = steps.begin.next step.validations shouldBe a[Failure[_]] @@ -694,7 +694,7 @@ class RelyingPartyAssertionSpec userHandleForUser = owner.userHandle, userHandleForResponse = Some(owner.userHandle), ) - val step: FinishAssertionSteps[RegisteredCredential]#Step6 = + val step: FinishAssertionSteps#Step6 = steps.begin.next step.validations shouldBe a[Failure[_]] @@ -709,7 +709,7 @@ class RelyingPartyAssertionSpec userHandleForUser = owner.userHandle, usernameForRequest = Some(owner.username), ) - val step: FinishAssertionSteps[RegisteredCredential]#Step6 = + val step: FinishAssertionSteps#Step6 = steps.begin.next step.validations shouldBe a[Success[_]] @@ -727,7 +727,7 @@ class RelyingPartyAssertionSpec userHandleForUser = owner.userHandle, userHandleForResponse = None, ) - val step: FinishAssertionSteps[RegisteredCredential]#Step6 = + val step: FinishAssertionSteps#Step6 = steps.begin.next step.validations shouldBe a[Failure[_]] @@ -744,7 +744,7 @@ class RelyingPartyAssertionSpec userHandleForUser = owner.userHandle, usernameForRequest = None, ) - val step: FinishAssertionSteps[RegisteredCredential]#Step6 = + val step: FinishAssertionSteps#Step6 = steps.begin.next step.validations shouldBe a[Failure[_]] @@ -762,7 +762,7 @@ class RelyingPartyAssertionSpec userHandleForUser = owner.userHandle, usernameForRequest = None, ) - val step: FinishAssertionSteps[RegisteredCredential]#Step6 = + val step: FinishAssertionSteps#Step6 = steps.begin.next step.validations shouldBe a[Failure[_]] @@ -778,7 +778,7 @@ class RelyingPartyAssertionSpec userHandleForUser = owner.userHandle, usernameForRequest = None, ) - val step: FinishAssertionSteps[RegisteredCredential]#Step6 = + val step: FinishAssertionSteps#Step6 = steps.begin.next step.validations shouldBe a[Failure[_]] @@ -794,7 +794,7 @@ class RelyingPartyAssertionSpec userHandleForUser = owner.userHandle, usernameForRequest = None, ) - val step: FinishAssertionSteps[RegisteredCredential]#Step6 = + val step: FinishAssertionSteps#Step6 = steps.begin.next step.validations shouldBe a[Failure[_]] @@ -809,7 +809,7 @@ class RelyingPartyAssertionSpec userHandleForUser = owner.userHandle, usernameForRequest = None, ) - val step: FinishAssertionSteps[RegisteredCredential]#Step6 = + val step: FinishAssertionSteps#Step6 = steps.begin.next step.validations shouldBe a[Success[_]] @@ -824,7 +824,7 @@ class RelyingPartyAssertionSpec userHandleForUser = owner.userHandle, usernameForRequest = None, ) - val step: FinishAssertionSteps[RegisteredCredential]#Step6 = + val step: FinishAssertionSteps#Step6 = steps.begin.next step.validations shouldBe a[Success[_]] @@ -877,7 +877,7 @@ class RelyingPartyAssertionSpec ) ) ) - val step: FinishAssertionSteps[RegisteredCredential]#Step7 = + val step: FinishAssertionSteps#Step7 = steps.begin.next.next step.validations shouldBe a[Success[_]] @@ -888,7 +888,7 @@ class RelyingPartyAssertionSpec describe("8. Let cData, authData and sig denote the value of response’s clientDataJSON, authenticatorData, and signature respectively.") { it("Succeeds if all three are present.") { val steps = finishAssertion() - val step: FinishAssertionSteps[RegisteredCredential]#Step8 = + val step: FinishAssertionSteps#Step8 = steps.begin.next.next.next step.validations shouldBe a[Success[_]] @@ -943,7 +943,7 @@ class RelyingPartyAssertionSpec "type": "" }""" ) - val step: FinishAssertionSteps[RegisteredCredential]#Step10 = + val step: FinishAssertionSteps#Step10 = steps.begin.next.next.next.next step.validations shouldBe a[Success[_]] @@ -957,7 +957,7 @@ class RelyingPartyAssertionSpec ) { it("The default test case succeeds.") { val steps = finishAssertion() - val step: FinishAssertionSteps[RegisteredCredential]#Step11 = + val step: FinishAssertionSteps#Step11 = steps.begin.next.next.next.next.next step.validations shouldBe a[Success[_]] @@ -976,7 +976,7 @@ class RelyingPartyAssertionSpec ), isSecurePaymentConfirmation = isSecurePaymentConfirmation, ) - val step: FinishAssertionSteps[RegisteredCredential]#Step11 = + val step: FinishAssertionSteps#Step11 = steps.begin.next.next.next.next.next step.validations shouldBe a[Failure[_]] @@ -1008,7 +1008,7 @@ class RelyingPartyAssertionSpec it("the default test case fails.") { val steps = finishAssertion(isSecurePaymentConfirmation = Some(true)) - val step: FinishAssertionSteps[RegisteredCredential]#Step11 = + val step: FinishAssertionSteps#Step11 = steps.begin.next.next.next.next.next step.validations shouldBe a[Failure[_]] @@ -1026,7 +1026,7 @@ class RelyingPartyAssertionSpec .set[ObjectNode]("type", new TextNode("payment.get")) ), ) - val step: FinishAssertionSteps[RegisteredCredential]#Step11 = + val step: FinishAssertionSteps#Step11 = steps.begin.next.next.next.next.next step.validations shouldBe a[Success[_]] @@ -1070,7 +1070,7 @@ class RelyingPartyAssertionSpec it("12. Verify that the value of C.challenge equals the base64url encoding of options.challenge.") { val steps = finishAssertion(challenge = new ByteArray(Array.fill(16)(0))) - val step: FinishAssertionSteps[RegisteredCredential]#Step12 = + val step: FinishAssertionSteps#Step12 = steps.begin.next.next.next.next.next.next step.validations shouldBe a[Failure[_]] @@ -1095,7 +1095,7 @@ class RelyingPartyAssertionSpec allowOriginPort = allowOriginPort, allowOriginSubdomain = allowOriginSubdomain, ) - val step: FinishAssertionSteps[RegisteredCredential]#Step13 = + val step: FinishAssertionSteps#Step13 = steps.begin.next.next.next.next.next.next.next step.validations shouldBe a[Success[_]] @@ -1118,7 +1118,7 @@ class RelyingPartyAssertionSpec allowOriginPort = allowOriginPort, allowOriginSubdomain = allowOriginSubdomain, ) - val step: FinishAssertionSteps[RegisteredCredential]#Step13 = + val step: FinishAssertionSteps#Step13 = steps.begin.next.next.next.next.next.next.next step.validations shouldBe a[Failure[_]] @@ -1296,7 +1296,7 @@ class RelyingPartyAssertionSpec describe("14. Verify that the value of C.tokenBinding.status matches the state of Token Binding for the TLS connection over which the attestation was obtained.") { it("Verification succeeds if neither side uses token binding ID.") { val steps = finishAssertion() - val step: FinishAssertionSteps[RegisteredCredential]#Step14 = + val step: FinishAssertionSteps#Step14 = steps.begin.next.next.next.next.next.next.next.next step.validations shouldBe a[Success[_]] @@ -1307,7 +1307,7 @@ class RelyingPartyAssertionSpec val clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","hashAlgorithm":"SHA-256","type":"webauthn.get"}""" val steps = finishAssertion(clientDataJson = clientDataJson) - val step: FinishAssertionSteps[RegisteredCredential]#Step14 = + val step: FinishAssertionSteps#Step14 = steps.begin.next.next.next.next.next.next.next.next step.validations shouldBe a[Success[_]] @@ -1318,7 +1318,7 @@ class RelyingPartyAssertionSpec val clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","hashAlgorithm":"SHA-256","tokenBinding":{"status":"supported"},"type":"webauthn.get"}""" val steps = finishAssertion(clientDataJson = clientDataJson) - val step: FinishAssertionSteps[RegisteredCredential]#Step14 = + val step: FinishAssertionSteps#Step14 = steps.begin.next.next.next.next.next.next.next.next step.validations shouldBe a[Success[_]] @@ -1333,7 +1333,7 @@ class RelyingPartyAssertionSpec Some(ByteArray.fromBase64Url("YELLOWSUBMARINE")), clientDataJson = clientDataJson, ) - val step: FinishAssertionSteps[RegisteredCredential]#Step14 = + val step: FinishAssertionSteps#Step14 = steps.begin.next.next.next.next.next.next.next.next step.validations shouldBe a[Failure[_]] @@ -1348,7 +1348,7 @@ class RelyingPartyAssertionSpec callerTokenBindingId = None, clientDataJson = clientDataJson, ) - val step: FinishAssertionSteps[RegisteredCredential]#Step14 = + val step: FinishAssertionSteps#Step14 = steps.begin.next.next.next.next.next.next.next.next step.validations shouldBe a[Success[_]] @@ -1362,7 +1362,7 @@ class RelyingPartyAssertionSpec callerTokenBindingId = None, clientDataJson = clientDataJson, ) - val step: FinishAssertionSteps[RegisteredCredential]#Step14 = + val step: FinishAssertionSteps#Step14 = steps.begin.next.next.next.next.next.next.next.next step.validations shouldBe a[Failure[_]] @@ -1379,7 +1379,7 @@ class RelyingPartyAssertionSpec Some(ByteArray.fromBase64Url("YELLOWSUBMARINE")), clientDataJson = clientDataJson, ) - val step: FinishAssertionSteps[RegisteredCredential]#Step14 = + val step: FinishAssertionSteps#Step14 = steps.begin.next.next.next.next.next.next.next.next step.validations shouldBe a[Success[_]] @@ -1394,7 +1394,7 @@ class RelyingPartyAssertionSpec Some(ByteArray.fromBase64Url("YELLOWSUBMARINE")), clientDataJson = clientDataJson, ) - val step: FinishAssertionSteps[RegisteredCredential]#Step14 = + val step: FinishAssertionSteps#Step14 = steps.begin.next.next.next.next.next.next.next.next step.validations shouldBe a[Failure[_]] @@ -1410,7 +1410,7 @@ class RelyingPartyAssertionSpec Some(ByteArray.fromBase64Url("YELLOWSUBMARINE")), clientDataJson = clientDataJson, ) - val step: FinishAssertionSteps[RegisteredCredential]#Step14 = + val step: FinishAssertionSteps#Step14 = steps.begin.next.next.next.next.next.next.next.next step.validations shouldBe a[Failure[_]] @@ -1426,7 +1426,7 @@ class RelyingPartyAssertionSpec Some(ByteArray.fromBase64Url("YELLOWSUBMARINE")), clientDataJson = clientDataJson, ) - val step: FinishAssertionSteps[RegisteredCredential]#Step14 = + val step: FinishAssertionSteps#Step14 = steps.begin.next.next.next.next.next.next.next.next step.validations shouldBe a[Failure[_]] @@ -1442,7 +1442,7 @@ class RelyingPartyAssertionSpec Some(ByteArray.fromBase64Url("ORANGESUBMARINE")), clientDataJson = clientDataJson, ) - val step: FinishAssertionSteps[RegisteredCredential]#Step14 = + val step: FinishAssertionSteps#Step14 = steps.begin.next.next.next.next.next.next.next.next step.validations shouldBe a[Failure[_]] @@ -1458,7 +1458,7 @@ class RelyingPartyAssertionSpec rpId = Defaults.rpId.toBuilder.id("root.evil").build(), origins = Some(Set("https://localhost")), ) - val step: FinishAssertionSteps[RegisteredCredential]#Step15 = + val step: FinishAssertionSteps#Step15 = steps.begin.next.next.next.next.next.next.next.next.next step.validations shouldBe a[Failure[_]] @@ -1468,7 +1468,7 @@ class RelyingPartyAssertionSpec it("Succeeds if RP ID is the same.") { val steps = finishAssertion() - val step: FinishAssertionSteps[RegisteredCredential]#Step15 = + val step: FinishAssertionSteps#Step15 = steps.begin.next.next.next.next.next.next.next.next.next step.validations shouldBe a[Success[_]] @@ -1490,7 +1490,7 @@ class RelyingPartyAssertionSpec .drop(32) ), ) - val step: FinishAssertionSteps[RegisteredCredential]#Step15 = + val step: FinishAssertionSteps#Step15 = steps.begin.next.next.next.next.next.next.next.next.next step.validations shouldBe a[Failure[_]] @@ -1500,7 +1500,7 @@ class RelyingPartyAssertionSpec it("succeeds if RP ID is the SHA-256 hash of the standard RP ID.") { val steps = finishAssertion(requestedExtensions = extensions) - val step: FinishAssertionSteps[RegisteredCredential]#Step15 = + val step: FinishAssertionSteps#Step15 = steps.begin.next.next.next.next.next.next.next.next.next step.validations shouldBe a[Success[_]] @@ -1516,7 +1516,7 @@ class RelyingPartyAssertionSpec ).getBytes ++ Defaults.authenticatorData.getBytes.drop(32) ), ) - val step: FinishAssertionSteps[RegisteredCredential]#Step15 = + val step: FinishAssertionSteps#Step15 = steps.begin.next.next.next.next.next.next.next.next.next step.validations shouldBe a[Success[_]] @@ -1527,13 +1527,13 @@ class RelyingPartyAssertionSpec { def checks[ - Next <: FinishAssertionSteps.Step[RegisteredCredential, _], - Step <: FinishAssertionSteps.Step[RegisteredCredential, Next], + Next <: FinishAssertionSteps.Step[_], + Step <: FinishAssertionSteps.Step[Next], ]( - stepsToStep: FinishAssertionSteps[RegisteredCredential] => Step + stepsToStep: FinishAssertionSteps => Step ) = { def check[Ret]( - stepsToStep: FinishAssertionSteps[RegisteredCredential] => Step + stepsToStep: FinishAssertionSteps => Step )( chk: Step => Ret )(uvr: UserVerificationRequirement, authData: ByteArray): Ret = { @@ -1544,7 +1544,7 @@ class RelyingPartyAssertionSpec chk(stepsToStep(steps)) } def checkFailsWith( - stepsToStep: FinishAssertionSteps[RegisteredCredential] => Step + stepsToStep: FinishAssertionSteps => Step ): (UserVerificationRequirement, ByteArray) => Unit = check(stepsToStep) { step => step.validations shouldBe a[Failure[_]] @@ -1554,7 +1554,7 @@ class RelyingPartyAssertionSpec step.tryNext shouldBe a[Failure[_]] } def checkSucceedsWith( - stepsToStep: FinishAssertionSteps[RegisteredCredential] => Step + stepsToStep: FinishAssertionSteps => Step ): (UserVerificationRequirement, ByteArray) => Unit = check(stepsToStep) { step => step.validations shouldBe a[Success[_]] @@ -1584,9 +1584,7 @@ class RelyingPartyAssertionSpec .toArray ) val (checkFails, checkSucceeds) = - checks[FinishAssertionSteps[ - RegisteredCredential - ]#Step17, FinishAssertionSteps[RegisteredCredential]#Step16]( + checks[FinishAssertionSteps#Step17, FinishAssertionSteps#Step16]( _.begin.next.next.next.next.next.next.next.next.next.next ) @@ -1636,8 +1634,8 @@ class RelyingPartyAssertionSpec ) val (checkFails, checkSucceeds) = checks[ - FinishAssertionSteps[RegisteredCredential]#PendingStep16, - FinishAssertionSteps[RegisteredCredential]#Step17, + FinishAssertionSteps#PendingStep16, + FinishAssertionSteps#Step17, ]( _.begin.next.next.next.next.next.next.next.next.next.next.next ) @@ -1679,8 +1677,7 @@ class RelyingPartyAssertionSpec backupFlagsGen = arbitrary[Boolean].map(bs => (true, bs)), ) ) { authData => - val step - : FinishAssertionSteps[RegisteredCredential]#PendingStep16 = + val step: FinishAssertionSteps#PendingStep16 = finishAssertion( authenticatorData = authData, credentialRepository = Some( @@ -1718,8 +1715,7 @@ class RelyingPartyAssertionSpec arbitrary[Boolean], ) { case (authData, storedBs) => - val step - : FinishAssertionSteps[RegisteredCredential]#PendingStep16 = + val step: FinishAssertionSteps#PendingStep16 = finishAssertion( authenticatorData = authData, credentialRepository = Some( @@ -1756,7 +1752,7 @@ class RelyingPartyAssertionSpec requestedExtensions = extensionInputs, clientExtensionResults = clientExtensionOutputs, ) - val step: FinishAssertionSteps[RegisteredCredential]#Step18 = + val step: FinishAssertionSteps#Step18 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next step.validations shouldBe a[Success[_]] @@ -1771,7 +1767,7 @@ class RelyingPartyAssertionSpec requestedExtensions = extensionInputs, clientExtensionResults = clientExtensionOutputs, ) - val step: FinishAssertionSteps[RegisteredCredential]#Step18 = + val step: FinishAssertionSteps#Step18 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next step.validations shouldBe a[Success[_]] @@ -1796,7 +1792,7 @@ class RelyingPartyAssertionSpec ) ), ) - val step: FinishAssertionSteps[RegisteredCredential]#Step18 = + val step: FinishAssertionSteps#Step18 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next step.validations shouldBe a[Success[_]] @@ -1821,7 +1817,7 @@ class RelyingPartyAssertionSpec ) ), ) - val step: FinishAssertionSteps[RegisteredCredential]#Step18 = + val step: FinishAssertionSteps#Step18 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next step.validations shouldBe a[Success[_]] @@ -1832,7 +1828,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[RegisteredCredential]#Step19 = + val step: FinishAssertionSteps#Step19 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next step.validations shouldBe a[Success[_]] @@ -1849,7 +1845,7 @@ class RelyingPartyAssertionSpec describe("20. Using credentialPublicKey, verify that sig is a valid signature over the binary concatenation of authData and hash.") { it("The default test case succeeds.") { val steps = finishAssertion() - val step: FinishAssertionSteps[RegisteredCredential]#Step20 = + val step: FinishAssertionSteps#Step20 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next step.validations shouldBe a[Success[_]] @@ -1866,7 +1862,7 @@ class RelyingPartyAssertionSpec .set("foo", jsonFactory.textNode("bar")) ) ) - val step: FinishAssertionSteps[RegisteredCredential]#Step20 = + val step: FinishAssertionSteps#Step20 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next step.validations shouldBe a[Failure[_]] @@ -1885,7 +1881,7 @@ class RelyingPartyAssertionSpec rpId = Defaults.rpId.toBuilder.id(rpId).build(), origins = Some(Set("https://localhost")), ) - val step: FinishAssertionSteps[RegisteredCredential]#Step20 = + val step: FinishAssertionSteps#Step20 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next step.validations shouldBe a[Failure[_]] @@ -1905,7 +1901,7 @@ class RelyingPartyAssertionSpec .toArray ) ) - val step: FinishAssertionSteps[RegisteredCredential]#Step20 = + val step: FinishAssertionSteps#Step20 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next step.validations shouldBe a[Failure[_]] @@ -1921,7 +1917,7 @@ class RelyingPartyAssertionSpec .toArray ) ) - val step: FinishAssertionSteps[RegisteredCredential]#Step20 = + val step: FinishAssertionSteps#Step20 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next step.validations shouldBe a[Failure[_]] @@ -1968,7 +1964,7 @@ class RelyingPartyAssertionSpec credentialRepository = Some(cr), validateSignatureCounter = true, ) - val step: FinishAssertionSteps[RegisteredCredential]#Step21 = + val step: FinishAssertionSteps#Step21 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next step.validations shouldBe a[Success[_]] @@ -1985,7 +1981,7 @@ class RelyingPartyAssertionSpec credentialRepository = Some(cr), validateSignatureCounter = true, ) - val step: FinishAssertionSteps[RegisteredCredential]#Step21 = + val step: FinishAssertionSteps#Step21 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next step.validations shouldBe a[Failure[_]] @@ -2007,7 +2003,7 @@ class RelyingPartyAssertionSpec credentialRepository = Some(cr), validateSignatureCounter = true, ) - val step: FinishAssertionSteps[RegisteredCredential]#Step21 = + val step: FinishAssertionSteps#Step21 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next step.validations shouldBe a[Success[_]] @@ -2027,7 +2023,7 @@ class RelyingPartyAssertionSpec credentialRepository = Some(cr), validateSignatureCounter = false, ) - val step: FinishAssertionSteps[RegisteredCredential]#Step21 = + val step: FinishAssertionSteps#Step21 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next step.validations shouldBe a[Success[_]] @@ -2041,7 +2037,7 @@ class RelyingPartyAssertionSpec credentialRepository = Some(cr), validateSignatureCounter = true, ) - val step: FinishAssertionSteps[RegisteredCredential]#Step21 = + val step: FinishAssertionSteps#Step21 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next val result = Try(step.run()) @@ -2070,7 +2066,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[RegisteredCredential]#Finished = + val step: FinishAssertionSteps#Finished = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next step.validations shouldBe a[Success[_]] diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyStartOperationSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyStartOperationSpec.scala index 4afd9a8f6..a56688eb7 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyStartOperationSpec.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyStartOperationSpec.scala @@ -44,7 +44,6 @@ import com.yubico.webauthn.data.ResidentKeyRequirement import com.yubico.webauthn.data.UserIdentity import com.yubico.webauthn.extension.appid.AppId import com.yubico.webauthn.extension.appid.Generators._ -import com.yubico.webauthn.test.Helpers import org.junit.runner.RunWith import org.scalacheck.Arbitrary.arbitrary import org.scalacheck.Gen @@ -956,895 +955,6 @@ class RelyingPartyStartOperationSpec } } - describe("RelyingPartyV2") { - def relyingParty( - appId: Option[AppId] = None, - attestationConveyancePreference: Option[ - AttestationConveyancePreference - ] = None, - credentials: Set[PublicKeyCredentialDescriptor] = Set.empty, - userId: UserIdentity, - usernameRepository: Boolean = false, - ): RelyingPartyV2[CredentialRecord] = { - var builder = RelyingParty - .builder() - .identity(rpId) - .credentialRepositoryV2( - Helpers.CredentialRepositoryV2.withUsers( - credentials - .map(c => - ( - userId, - Helpers.credentialRecord( - credentialId = c.getId, - userHandle = userId.getId, - publicKeyCose = ByteArray.fromHex(""), - transports = c.getTransports.map(_.asScala.toSet).toScala, - ), - ) - ) - .toList: _* - ) - ) - .preferredPubkeyParams(List(PublicKeyCredentialParameters.ES256).asJava) - .origins(Set.empty.asJava) - if (usernameRepository) { - builder.usernameRepository(Helpers.UsernameRepository.withUsers(userId)) - } - appId.foreach { appid => builder = builder.appId(appid) } - attestationConveyancePreference.foreach { acp => - builder = builder.attestationConveyancePreference(acp) - } - builder.build() - } - - describe("startRegistration") { - - it("sets excludeCredentials automatically.") { - forAll { credentials: Set[PublicKeyCredentialDescriptor] => - val rp = relyingParty(credentials = credentials, userId = userId) - val result = rp.startRegistration( - StartRegistrationOptions - .builder() - .user(userId) - .build() - ) - - result.getExcludeCredentials.toScala.map(_.asScala) should equal( - Some(credentials) - ) - } - } - - it("sets challenge randomly.") { - val rp = relyingParty(userId = userId) - - val request1 = rp.startRegistration( - StartRegistrationOptions.builder().user(userId).build() - ) - val request2 = rp.startRegistration( - StartRegistrationOptions.builder().user(userId).build() - ) - - request1.getChallenge should not equal request2.getChallenge - request1.getChallenge.size should be >= 32 - request2.getChallenge.size should be >= 32 - } - - it("allows setting authenticatorSelection.") { - val authnrSel = AuthenticatorSelectionCriteria - .builder() - .authenticatorAttachment(AuthenticatorAttachment.CROSS_PLATFORM) - .residentKey(ResidentKeyRequirement.REQUIRED) - .build() - - val pkcco = relyingParty(userId = userId).startRegistration( - StartRegistrationOptions - .builder() - .user(userId) - .authenticatorSelection(authnrSel) - .build() - ) - pkcco.getAuthenticatorSelection.toScala should equal(Some(authnrSel)) - } - - it("allows setting authenticatorSelection with an Optional value.") { - val authnrSel = AuthenticatorSelectionCriteria - .builder() - .authenticatorAttachment(AuthenticatorAttachment.CROSS_PLATFORM) - .residentKey(ResidentKeyRequirement.REQUIRED) - .build() - - val pkccoWith = relyingParty(userId = userId).startRegistration( - StartRegistrationOptions - .builder() - .user(userId) - .authenticatorSelection(Optional.of(authnrSel)) - .build() - ) - val pkccoWithout = relyingParty(userId = userId).startRegistration( - StartRegistrationOptions - .builder() - .user(userId) - .authenticatorSelection( - Optional.empty[AuthenticatorSelectionCriteria] - ) - .build() - ) - pkccoWith.getAuthenticatorSelection.toScala should equal( - Some(authnrSel) - ) - pkccoWithout.getAuthenticatorSelection.toScala should equal(None) - } - - it("uses the RelyingParty setting for attestationConveyancePreference.") { - forAll { acp: Option[AttestationConveyancePreference] => - val pkcco = - relyingParty(attestationConveyancePreference = acp, userId = userId) - .startRegistration( - StartRegistrationOptions - .builder() - .user(userId) - .build() - ) - pkcco.getAttestation should equal( - acp getOrElse AttestationConveyancePreference.NONE - ) - } - } - - describe("allows setting the hints") { - val rp = relyingParty(userId = userId) - - it("to string values in the spec or not.") { - val pkcco = rp.startRegistration( - StartRegistrationOptions - .builder() - .user(userId) - .hints("hej", "security-key", "hoj", "client-device", "hybrid") - .build() - ) - pkcco.getHints.asScala should equal( - List( - "hej", - PublicKeyCredentialHint.SECURITY_KEY.getValue, - "hoj", - PublicKeyCredentialHint.CLIENT_DEVICE.getValue, - PublicKeyCredentialHint.HYBRID.getValue, - ) - ) - } - - it("to PublicKeyCredentialHint values in the spec or not.") { - val pkcco = rp.startRegistration( - StartRegistrationOptions - .builder() - .user(userId) - .hints( - PublicKeyCredentialHint.of("hej"), - PublicKeyCredentialHint.HYBRID, - PublicKeyCredentialHint.SECURITY_KEY, - PublicKeyCredentialHint.of("hoj"), - PublicKeyCredentialHint.CLIENT_DEVICE, - ) - .build() - ) - pkcco.getHints.asScala should equal( - List( - "hej", - PublicKeyCredentialHint.HYBRID.getValue, - PublicKeyCredentialHint.SECURITY_KEY.getValue, - "hoj", - PublicKeyCredentialHint.CLIENT_DEVICE.getValue, - ) - ) - } - - it("or not, defaulting to the empty list.") { - val pkcco = rp.startRegistration( - StartRegistrationOptions.builder().user(userId).build() - ) - pkcco.getHints.asScala should equal(List()) - } - } - - it("allows setting the timeout to empty.") { - val pkcco = relyingParty(userId = userId).startRegistration( - StartRegistrationOptions - .builder() - .user(userId) - .timeout(Optional.empty[java.lang.Long]) - .build() - ) - pkcco.getTimeout.toScala shouldBe empty - } - - it("allows setting the timeout to a positive value.") { - val rp = relyingParty(userId = userId) - - forAll(Gen.posNum[Long]) { timeout: Long => - val pkcco = rp.startRegistration( - StartRegistrationOptions - .builder() - .user(userId) - .timeout(timeout) - .build() - ) - - pkcco.getTimeout.toScala should equal(Some(timeout)) - } - } - - it("does not allow setting the timeout to zero or negative.") { - an[IllegalArgumentException] should be thrownBy { - StartRegistrationOptions - .builder() - .user(userId) - .timeout(0) - } - - an[IllegalArgumentException] should be thrownBy { - StartRegistrationOptions - .builder() - .user(userId) - .timeout(Optional.of[java.lang.Long](0L)) - } - - forAll(Gen.negNum[Long]) { timeout: Long => - an[IllegalArgumentException] should be thrownBy { - StartRegistrationOptions - .builder() - .user(userId) - .timeout(timeout) - } - - an[IllegalArgumentException] should be thrownBy { - StartRegistrationOptions - .builder() - .user(userId) - .timeout(Optional.of[java.lang.Long](timeout)) - } - } - } - - it( - "sets the appidExclude extension if the RP instance is given an AppId." - ) { - forAll { appId: AppId => - val rp = relyingParty(appId = Some(appId), userId = userId) - val result = rp.startRegistration( - StartRegistrationOptions - .builder() - .user(userId) - .build() - ) - - result.getExtensions.getAppidExclude.toScala should equal(Some(appId)) - } - } - - it("does not set the appidExclude extension if the RP instance is not given an AppId.") { - val rp = relyingParty(userId = userId) - val result = rp.startRegistration( - StartRegistrationOptions - .builder() - .user(userId) - .build() - ) - - result.getExtensions.getAppidExclude.toScala should equal(None) - } - - it("does not override the appidExclude extension with an empty value if already non-null in StartRegistrationOptions.") { - forAll { requestAppId: AppId => - val rp = relyingParty(appId = None, userId = userId) - val result = rp.startRegistration( - StartRegistrationOptions - .builder() - .user(userId) - .extensions( - RegistrationExtensionInputs - .builder() - .appidExclude(requestAppId) - .build() - ) - .build() - ) - - result.getExtensions.getAppidExclude.toScala should equal( - Some(requestAppId) - ) - } - } - - it("does not override the appidExclude extension if already non-null in StartRegistrationOptions.") { - forAll { (requestAppId: AppId, rpAppId: AppId) => - whenever(requestAppId != rpAppId) { - val rp = relyingParty(appId = Some(rpAppId), userId = userId) - val result = rp.startRegistration( - StartRegistrationOptions - .builder() - .user(userId) - .extensions( - RegistrationExtensionInputs - .builder() - .appidExclude(requestAppId) - .build() - ) - .build() - ) - - result.getExtensions.getAppidExclude.toScala should equal( - Some(requestAppId) - ) - } - } - } - - it("by default sets the credProps extension.") { - forAll(registrationExtensionInputs(credPropsGen = None)) { - extensions: RegistrationExtensionInputs => - val rp = relyingParty(userId = userId) - val result = rp.startRegistration( - StartRegistrationOptions - .builder() - .user(userId) - .extensions(extensions) - .build() - ) - - result.getExtensions.getCredProps should be(true) - } - } - - it("does not override the credProps extension if explicitly set to false in StartRegistrationOptions.") { - forAll(registrationExtensionInputs(credPropsGen = Some(false))) { - extensions: RegistrationExtensionInputs => - val rp = relyingParty(userId = userId) - val result = rp.startRegistration( - StartRegistrationOptions - .builder() - .user(userId) - .extensions(extensions) - .build() - ) - - result.getExtensions.getCredProps should be(false) - } - } - - it("by default does not set the uvm extension.") { - val rp = relyingParty(userId = userId) - val result = rp.startRegistration( - StartRegistrationOptions - .builder() - .user(userId) - .build() - ) - result.getExtensions.getUvm should be(false) - } - - it("sets the uvm extension if enabled in StartRegistrationOptions.") { - forAll { extensions: RegistrationExtensionInputs => - val rp = relyingParty(userId = userId) - val result = rp.startRegistration( - StartRegistrationOptions - .builder() - .user(userId) - .extensions(extensions.toBuilder.uvm().build()) - .build() - ) - - result.getExtensions.getUvm should be(true) - } - } - - it("respects the residentKey setting.") { - val rp = relyingParty(userId = userId) - - val pkccoDiscouraged = rp.startRegistration( - StartRegistrationOptions - .builder() - .user(userId) - .authenticatorSelection( - AuthenticatorSelectionCriteria - .builder() - .residentKey(ResidentKeyRequirement.DISCOURAGED) - .build() - ) - .build() - ) - - val pkccoPreferred = rp.startRegistration( - StartRegistrationOptions - .builder() - .user(userId) - .authenticatorSelection( - AuthenticatorSelectionCriteria - .builder() - .residentKey(ResidentKeyRequirement.PREFERRED) - .build() - ) - .build() - ) - - val pkccoRequired = rp.startRegistration( - StartRegistrationOptions - .builder() - .user(userId) - .authenticatorSelection( - AuthenticatorSelectionCriteria - .builder() - .residentKey(ResidentKeyRequirement.REQUIRED) - .build() - ) - .build() - ) - - val pkccoUnspecified = rp.startRegistration( - StartRegistrationOptions - .builder() - .user(userId) - .authenticatorSelection( - AuthenticatorSelectionCriteria.builder().build() - ) - .build() - ) - - def jsonRequireResidentKey( - pkcco: PublicKeyCredentialCreationOptions - ): Option[Boolean] = - Option( - JacksonCodecs - .json() - .readTree(pkcco.toCredentialsCreateJson) - .get("publicKey") - .get("authenticatorSelection") - .get("requireResidentKey") - ).map(_.booleanValue) - - pkccoDiscouraged.getAuthenticatorSelection.get.getResidentKey.toScala should be( - Some(ResidentKeyRequirement.DISCOURAGED) - ) - jsonRequireResidentKey(pkccoDiscouraged) should be(Some(false)) - - pkccoPreferred.getAuthenticatorSelection.get.getResidentKey.toScala should be( - Some(ResidentKeyRequirement.PREFERRED) - ) - jsonRequireResidentKey(pkccoPreferred) should be(Some(false)) - - pkccoRequired.getAuthenticatorSelection.get.getResidentKey.toScala should be( - Some(ResidentKeyRequirement.REQUIRED) - ) - jsonRequireResidentKey(pkccoRequired) should be(Some(true)) - - pkccoUnspecified.getAuthenticatorSelection.get.getResidentKey.toScala should be( - None - ) - jsonRequireResidentKey(pkccoUnspecified) should be(None) - } - - it("respects the authenticatorAttachment parameter.") { - val rp = relyingParty(userId = userId) - - val pkcco = rp.startRegistration( - StartRegistrationOptions - .builder() - .user(userId) - .authenticatorSelection( - AuthenticatorSelectionCriteria - .builder() - .authenticatorAttachment(AuthenticatorAttachment.CROSS_PLATFORM) - .build() - ) - .build() - ) - val pkccoWith = rp.startRegistration( - StartRegistrationOptions - .builder() - .user(userId) - .authenticatorSelection( - AuthenticatorSelectionCriteria - .builder() - .authenticatorAttachment( - Optional.of(AuthenticatorAttachment.PLATFORM) - ) - .build() - ) - .build() - ) - val pkccoWithout = rp.startRegistration( - StartRegistrationOptions - .builder() - .user(userId) - .authenticatorSelection( - AuthenticatorSelectionCriteria - .builder() - .authenticatorAttachment( - Optional.empty[AuthenticatorAttachment] - ) - .build() - ) - .build() - ) - - pkcco.getAuthenticatorSelection.get.getAuthenticatorAttachment.toScala should be( - Some(AuthenticatorAttachment.CROSS_PLATFORM) - ) - pkccoWith.getAuthenticatorSelection.get.getAuthenticatorAttachment.toScala should be( - Some(AuthenticatorAttachment.PLATFORM) - ) - pkccoWithout.getAuthenticatorSelection.get.getAuthenticatorAttachment.toScala should be( - None - ) - } - } - - describe("startAssertion") { - - it("sets allowCredentials to empty if not given a username nor a user handle.") { - forAll { credentials: Set[PublicKeyCredentialDescriptor] => - val rp = relyingParty(credentials = credentials, userId = userId) - val result = - rp.startAssertion(StartAssertionOptions.builder().build()) - - result.getPublicKeyCredentialRequestOptions.getAllowCredentials.toScala shouldBe empty - } - } - - it("sets allowCredentials automatically if given a username.") { - forAll { credentials: Set[PublicKeyCredentialDescriptor] => - val rp = relyingParty( - credentials = credentials, - userId = userId, - usernameRepository = true, - ) - val result = rp.startAssertion( - StartAssertionOptions - .builder() - .username(userId.getName) - .build() - ) - - result.getPublicKeyCredentialRequestOptions.getAllowCredentials.toScala - .map(_.asScala.toSet) should equal(Some(credentials)) - } - } - - it("sets allowCredentials automatically if given a user handle.") { - forAll { credentials: Set[PublicKeyCredentialDescriptor] => - val rp = relyingParty(credentials = credentials, userId = userId) - val result = rp.startAssertion( - StartAssertionOptions - .builder() - .userHandle(userId.getId) - .build() - ) - - result.getPublicKeyCredentialRequestOptions.getAllowCredentials.toScala - .map(_.asScala.toSet) should equal(Some(credentials)) - } - } - - it("passes username through to AssertionRequest.") { - forAll { username: String => - val testCaseUserId = userId.toBuilder.name(username).build() - val rp = - relyingParty(userId = testCaseUserId, usernameRepository = true) - val result = rp.startAssertion( - StartAssertionOptions - .builder() - .username(testCaseUserId.getName) - .build() - ) - result.getUsername.asScala should equal(Some(testCaseUserId.getName)) - } - } - - it("passes user handle through to AssertionRequest.") { - forAll { userHandle: ByteArray => - val testCaseUserId = userId.toBuilder.id(userHandle).build() - val rp = relyingParty(userId = testCaseUserId) - val result = rp.startAssertion( - StartAssertionOptions - .builder() - .userHandle(testCaseUserId.getId) - .build() - ) - result.getUserHandle.asScala should equal(Some(testCaseUserId.getId)) - } - } - - it("includes transports in allowCredentials when available.") { - forAll( - Gen.nonEmptyContainerOf[Set, AuthenticatorTransport]( - arbitrary[AuthenticatorTransport] - ), - arbitrary[PublicKeyCredentialDescriptor], - arbitrary[PublicKeyCredentialDescriptor], - arbitrary[PublicKeyCredentialDescriptor], - ) { - ( - cred1Transports: Set[AuthenticatorTransport], - cred1: PublicKeyCredentialDescriptor, - cred2: PublicKeyCredentialDescriptor, - cred3: PublicKeyCredentialDescriptor, - ) => - val rp = relyingParty( - credentials = Set( - cred1.toBuilder.transports(cred1Transports.asJava).build(), - cred2.toBuilder - .transports( - Optional.of(Set.empty[AuthenticatorTransport].asJava) - ) - .build(), - cred3.toBuilder - .transports( - Optional.empty[java.util.Set[AuthenticatorTransport]] - ) - .build(), - ), - userId = userId, - usernameRepository = true, - ) - val result = rp.startAssertion( - StartAssertionOptions - .builder() - .username(userId.getName) - .build() - ) - - val requestCreds = - result.getPublicKeyCredentialRequestOptions.getAllowCredentials.get.asScala - requestCreds.head.getTransports.toScala should equal( - Some(cred1Transports.asJava) - ) - requestCreds(1).getTransports.toScala should equal( - Some(Set.empty.asJava) - ) - requestCreds(2).getTransports.toScala should equal(None) - } - } - - it("sets challenge randomly.") { - val rp = relyingParty(userId = userId) - - val request1 = - rp.startAssertion(StartAssertionOptions.builder().build()) - val request2 = - rp.startAssertion(StartAssertionOptions.builder().build()) - - request1.getPublicKeyCredentialRequestOptions.getChallenge should not equal request2.getPublicKeyCredentialRequestOptions.getChallenge - request1.getPublicKeyCredentialRequestOptions.getChallenge.size should be >= 32 - request2.getPublicKeyCredentialRequestOptions.getChallenge.size should be >= 32 - } - - it("sets the appid extension if the RP instance is given an AppId.") { - forAll { appId: AppId => - val rp = relyingParty( - appId = Some(appId), - userId = userId, - usernameRepository = true, - ) - val result = rp.startAssertion( - StartAssertionOptions - .builder() - .username(userId.getName) - .build() - ) - - result.getPublicKeyCredentialRequestOptions.getExtensions.getAppid.toScala should equal( - Some(appId) - ) - } - } - - it("does not set the appid extension if the RP instance is not given an AppId.") { - val rp = relyingParty(userId = userId, usernameRepository = true) - val result = rp.startAssertion( - StartAssertionOptions - .builder() - .username(userId.getName) - .build() - ) - - result.getPublicKeyCredentialRequestOptions.getExtensions.getAppid.toScala should equal( - None - ) - } - - it("does not override the appid extension with an empty value if already non-null in StartAssertionOptions.") { - forAll { requestAppId: AppId => - val rp = relyingParty( - appId = None, - userId = userId, - usernameRepository = true, - ) - val result = rp.startAssertion( - StartAssertionOptions - .builder() - .username(userId.getName) - .extensions( - AssertionExtensionInputs - .builder() - .appid(requestAppId) - .build() - ) - .build() - ) - - result.getPublicKeyCredentialRequestOptions.getExtensions.getAppid.toScala should equal( - Some(requestAppId) - ) - } - } - - it("does not override the appid extension if already non-null in StartAssertionOptions.") { - forAll { (requestAppId: AppId, rpAppId: AppId) => - whenever(requestAppId != rpAppId) { - val rp = relyingParty( - appId = Some(rpAppId), - userId = userId, - usernameRepository = true, - ) - val result = rp.startAssertion( - StartAssertionOptions - .builder() - .username(userId.getName) - .extensions( - AssertionExtensionInputs - .builder() - .appid(requestAppId) - .build() - ) - .build() - ) - - result.getPublicKeyCredentialRequestOptions.getExtensions.getAppid.toScala should equal( - Some(requestAppId) - ) - } - } - } - - describe("allows setting the hints") { - val rp = relyingParty(userId = userId) - - it("to string values in the spec or not.") { - val pkcro = rp.startAssertion( - StartAssertionOptions - .builder() - .hints("hej", "security-key", "hoj", "client-device", "hybrid") - .build() - ) - pkcro.getPublicKeyCredentialRequestOptions.getHints.asScala should equal( - List( - "hej", - PublicKeyCredentialHint.SECURITY_KEY.getValue, - "hoj", - PublicKeyCredentialHint.CLIENT_DEVICE.getValue, - PublicKeyCredentialHint.HYBRID.getValue, - ) - ) - } - - it("to PublicKeyCredentialHint values in the spec or not.") { - val pkcro = rp.startAssertion( - StartAssertionOptions - .builder() - .hints( - PublicKeyCredentialHint.of("hej"), - PublicKeyCredentialHint.HYBRID, - PublicKeyCredentialHint.SECURITY_KEY, - PublicKeyCredentialHint.of("hoj"), - PublicKeyCredentialHint.CLIENT_DEVICE, - ) - .build() - ) - pkcro.getPublicKeyCredentialRequestOptions.getHints.asScala should equal( - List( - "hej", - PublicKeyCredentialHint.HYBRID.getValue, - PublicKeyCredentialHint.SECURITY_KEY.getValue, - "hoj", - PublicKeyCredentialHint.CLIENT_DEVICE.getValue, - ) - ) - } - - it("or not, defaulting to the empty list.") { - val pkcro = rp.startAssertion(StartAssertionOptions.builder().build()) - pkcro.getPublicKeyCredentialRequestOptions.getHints.asScala should equal( - List() - ) - } - } - - it("allows setting the timeout to empty.") { - val req = relyingParty(userId = userId).startAssertion( - StartAssertionOptions - .builder() - .timeout(Optional.empty[java.lang.Long]) - .build() - ) - req.getPublicKeyCredentialRequestOptions.getTimeout.toScala shouldBe empty - } - - it("allows setting the timeout to a positive value.") { - val rp = relyingParty(userId = userId) - - forAll(Gen.posNum[Long]) { timeout: Long => - val req = rp.startAssertion( - StartAssertionOptions - .builder() - .timeout(timeout) - .build() - ) - - req.getPublicKeyCredentialRequestOptions.getTimeout.toScala should equal( - Some(timeout) - ) - } - } - - it("does not allow setting the timeout to zero or negative.") { - an[IllegalArgumentException] should be thrownBy { - StartAssertionOptions - .builder() - .timeout(0) - } - - an[IllegalArgumentException] should be thrownBy { - StartAssertionOptions - .builder() - .timeout(Optional.of[java.lang.Long](0L)) - } - - forAll(Gen.negNum[Long]) { timeout: Long => - an[IllegalArgumentException] should be thrownBy { - StartAssertionOptions - .builder() - .timeout(timeout) - } - - an[IllegalArgumentException] should be thrownBy { - StartAssertionOptions - .builder() - .timeout(Optional.of[java.lang.Long](timeout)) - } - } - } - - it("by default does not set the uvm extension.") { - val rp = relyingParty(userId = userId) - val result = rp.startAssertion( - StartAssertionOptions - .builder() - .build() - ) - result.getPublicKeyCredentialRequestOptions.getExtensions.getUvm should be( - false - ) - } - - it("sets the uvm extension if enabled in StartRegistrationOptions.") { - forAll { extensions: AssertionExtensionInputs => - val rp = relyingParty(userId = userId) - val result = rp.startAssertion( - StartAssertionOptions - .builder() - .extensions(extensions.toBuilder.uvm().build()) - .build() - ) - - result.getPublicKeyCredentialRequestOptions.getExtensions.getUvm should be( - true - ) - } - } - } - } - describe("StartAssertionOptions") { it("resets username when userHandle is set.") { diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyUserIdentificationSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyUserIdentificationSpec.scala index 4fd6222d5..2d53b1ffb 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyUserIdentificationSpec.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyUserIdentificationSpec.scala @@ -32,8 +32,6 @@ import com.yubico.webauthn.data.CollectedClientData import com.yubico.webauthn.data.PublicKeyCredential import com.yubico.webauthn.data.PublicKeyCredentialDescriptor import com.yubico.webauthn.data.RelyingPartyIdentity -import com.yubico.webauthn.data.UserIdentity -import com.yubico.webauthn.test.Helpers import org.junit.runner.RunWith import org.scalatest.funspec.AnyFunSpec import org.scalatest.matchers.should.Matchers @@ -271,255 +269,4 @@ class RelyingPartyUserIdentificationSpec extends AnyFunSpec with Matchers { } } - - describe("The assertion ceremony with RelyingPartyV2") { - - describe("with usernameRepository set") { - val user = UserIdentity - .builder() - .name(Defaults.username) - .displayName("") - .id(Defaults.userHandle) - .build() - val rp = RelyingParty - .builder() - .identity(Defaults.rpId) - .credentialRepositoryV2( - Helpers.CredentialRepositoryV2.withUser( - user, - credentialId = Defaults.credentialId, - publicKeyCose = WebAuthnTestCodecs.ecPublicKeyToCose( - Defaults.credentialKey.getPublic.asInstanceOf[ECPublicKey] - ), - signatureCount = 0, - ) - ) - .usernameRepository(Helpers.UsernameRepository.withUsers(user)) - .preferredPubkeyParams(Nil.asJava) - .origins(Set(Defaults.rpId.getId).asJava) - .allowUntrustedAttestation(false) - .validateSignatureCounter(true) - .build() - - it("succeeds for the default test case if a username was given.") { - val request = rp.startAssertion( - StartAssertionOptions - .builder() - .username(Defaults.username) - .build() - ) - val deterministicRequest = - request.toBuilder - .publicKeyCredentialRequestOptions( - request.getPublicKeyCredentialRequestOptions.toBuilder - .challenge(Defaults.challenge) - .build() - ) - .build() - - val result = Try( - rp.finishAssertion( - FinishAssertionOptions - .builder() - .request(deterministicRequest) - .response(Defaults.publicKeyCredential) - .build() - ) - ) - - result shouldBe a[Success[_]] - } - - it("succeeds for the default test case if a user handle was given.") { - val request = rp.startAssertion( - StartAssertionOptions - .builder() - .userHandle(Defaults.userHandle) - .build() - ) - val deterministicRequest = - request.toBuilder - .publicKeyCredentialRequestOptions( - request.getPublicKeyCredentialRequestOptions.toBuilder - .challenge(Defaults.challenge) - .build() - ) - .build() - - val result = Try( - rp.finishAssertion( - FinishAssertionOptions - .builder() - .request(deterministicRequest) - .response(Defaults.publicKeyCredential) - .build() - ) - ) - - result shouldBe a[Success[_]] - } - - it("succeeds if username or user handle was not given but userHandle was returned.") { - val request = rp.startAssertion(StartAssertionOptions.builder().build()) - val deterministicRequest = - request.toBuilder - .publicKeyCredentialRequestOptions( - request.getPublicKeyCredentialRequestOptions.toBuilder - .challenge(Defaults.challenge) - .build() - ) - .build() - - val response: PublicKeyCredential[ - AuthenticatorAssertionResponse, - ClientAssertionExtensionOutputs, - ] = Defaults.defaultPublicKeyCredential( - userHandle = Some(Defaults.userHandle) - ) - - val result = rp.finishAssertion( - FinishAssertionOptions - .builder() - .request(deterministicRequest) - .response(response) - .build() - ) - result.isSuccess should be(true) - } - - it("fails for the default test case if no username or user handle was given and no userHandle returned.") { - val request = rp.startAssertion(StartAssertionOptions.builder().build()) - val deterministicRequest = - request.toBuilder - .publicKeyCredentialRequestOptions( - request.getPublicKeyCredentialRequestOptions.toBuilder - .challenge(Defaults.challenge) - .build() - ) - .build() - - val result = Try( - rp.finishAssertion( - FinishAssertionOptions - .builder() - .request(deterministicRequest) - .response(Defaults.publicKeyCredential) - .build() - ) - ) - - result shouldBe a[Failure[_]] - } - } - - describe("with no usernameRepository set") { - val user = UserIdentity - .builder() - .name(Defaults.username) - .displayName("") - .id(Defaults.userHandle) - .build() - val rp = RelyingParty - .builder() - .identity(Defaults.rpId) - .credentialRepositoryV2( - Helpers.CredentialRepositoryV2.withUser( - user, - credentialId = Defaults.credentialId, - publicKeyCose = WebAuthnTestCodecs.ecPublicKeyToCose( - Defaults.credentialKey.getPublic.asInstanceOf[ECPublicKey] - ), - signatureCount = 0, - ) - ) - .preferredPubkeyParams(Nil.asJava) - .origins(Set(Defaults.rpId.getId).asJava) - .allowUntrustedAttestation(false) - .validateSignatureCounter(true) - .build() - - it("succeeds for the default test case if a userhandle was given.") { - val request = rp.startAssertion( - StartAssertionOptions - .builder() - .userHandle(Defaults.userHandle) - .build() - ) - val deterministicRequest = - request.toBuilder - .publicKeyCredentialRequestOptions( - request.getPublicKeyCredentialRequestOptions.toBuilder - .challenge(Defaults.challenge) - .build() - ) - .build() - - val result = Try( - rp.finishAssertion( - FinishAssertionOptions - .builder() - .request(deterministicRequest) - .response(Defaults.publicKeyCredential) - .build() - ) - ) - - result shouldBe a[Success[_]] - } - - it("succeeds if user handle was not given but userHandle was returned.") { - val request = rp.startAssertion(StartAssertionOptions.builder().build()) - val deterministicRequest = - request.toBuilder - .publicKeyCredentialRequestOptions( - request.getPublicKeyCredentialRequestOptions.toBuilder - .challenge(Defaults.challenge) - .build() - ) - .build() - - val response: PublicKeyCredential[ - AuthenticatorAssertionResponse, - ClientAssertionExtensionOutputs, - ] = Defaults.defaultPublicKeyCredential( - userHandle = Some(Defaults.userHandle) - ) - - val result = rp.finishAssertion( - FinishAssertionOptions - .builder() - .request(deterministicRequest) - .response(response) - .build() - ) - result.isSuccess should be(true) - } - - it("fails for the default test case if no user handle was given and no userHandle returned.") { - val request = rp.startAssertion(StartAssertionOptions.builder().build()) - val deterministicRequest = - request.toBuilder - .publicKeyCredentialRequestOptions( - request.getPublicKeyCredentialRequestOptions.toBuilder - .challenge(Defaults.challenge) - .build() - ) - .build() - - val result = Try( - rp.finishAssertion( - FinishAssertionOptions - .builder() - .request(deterministicRequest) - .response(Defaults.publicKeyCredential) - .build() - ) - ) - - result shouldBe a[Failure[_]] - } - } - - } - } diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyV2AssertionSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyV2AssertionSpec.scala deleted file mode 100644 index ca7fa2aaf..000000000 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyV2AssertionSpec.scala +++ /dev/null @@ -1,2930 +0,0 @@ -// Copyright (c) 2023, Yubico AB -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, this -// list of conditions and the following disclaimer. -// -// 2. Redistributions in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -package com.yubico.webauthn - -import com.fasterxml.jackson.core.`type`.TypeReference -import com.fasterxml.jackson.databind.node.JsonNodeFactory -import com.fasterxml.jackson.databind.node.ObjectNode -import com.fasterxml.jackson.databind.node.TextNode -import com.upokecenter.cbor.CBORObject -import com.yubico.internal.util.BinaryUtil -import com.yubico.internal.util.JacksonCodecs -import com.yubico.webauthn.data.AssertionExtensionInputs -import com.yubico.webauthn.data.AuthenticatorAssertionResponse -import com.yubico.webauthn.data.AuthenticatorAttachment -import com.yubico.webauthn.data.AuthenticatorDataFlags -import com.yubico.webauthn.data.AuthenticatorTransport -import com.yubico.webauthn.data.ByteArray -import com.yubico.webauthn.data.ClientAssertionExtensionOutputs -import com.yubico.webauthn.data.CollectedClientData -import com.yubico.webauthn.data.Extensions.LargeBlob.LargeBlobAuthenticationInput -import com.yubico.webauthn.data.Extensions.LargeBlob.LargeBlobAuthenticationOutput -import com.yubico.webauthn.data.Extensions.Uvm.UvmEntry -import com.yubico.webauthn.data.Generators._ -import com.yubico.webauthn.data.PublicKeyCredential -import com.yubico.webauthn.data.PublicKeyCredentialCreationOptions -import com.yubico.webauthn.data.PublicKeyCredentialDescriptor -import com.yubico.webauthn.data.PublicKeyCredentialParameters -import com.yubico.webauthn.data.PublicKeyCredentialRequestOptions -import com.yubico.webauthn.data.RelyingPartyIdentity -import com.yubico.webauthn.data.UserIdentity -import com.yubico.webauthn.data.UserVerificationRequirement -import com.yubico.webauthn.exception.InvalidSignatureCountException -import com.yubico.webauthn.extension.appid.AppId -import com.yubico.webauthn.extension.uvm.KeyProtectionType -import com.yubico.webauthn.extension.uvm.MatcherProtectionType -import com.yubico.webauthn.extension.uvm.UserVerificationMethod -import com.yubico.webauthn.test.Helpers -import com.yubico.webauthn.test.RealExamples -import com.yubico.webauthn.test.Util.toStepWithUtilities -import org.junit.runner.RunWith -import org.scalacheck.Arbitrary.arbitrary -import org.scalacheck.Gen -import org.scalatest.funspec.AnyFunSpec -import org.scalatest.matchers.should.Matchers -import org.scalatestplus.junit.JUnitRunner -import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks - -import java.io.IOException -import java.nio.charset.Charset -import java.security.KeyPair -import java.security.MessageDigest -import java.util.Optional -import scala.jdk.CollectionConverters._ -import scala.jdk.OptionConverters.RichOption -import scala.jdk.OptionConverters.RichOptional -import scala.util.Failure -import scala.util.Success -import scala.util.Try - -@RunWith(classOf[JUnitRunner]) -class RelyingPartyV2AssertionSpec - extends AnyFunSpec - with Matchers - with ScalaCheckDrivenPropertyChecks - with TestWithEachProvider { - - private def jsonFactory: JsonNodeFactory = JsonNodeFactory.instance - - private def sha256(bytes: ByteArray): ByteArray = Crypto.sha256(bytes) - private def sha256(data: String): ByteArray = - sha256(new ByteArray(data.getBytes(Charset.forName("UTF-8")))) - - private object Defaults { - - val rpId = - RelyingPartyIdentity.builder().id("localhost").name("Test party").build() - - // These values were generated using TestAuthenticator.makeAssertionExample() - val authenticatorData: ByteArray = - ByteArray.fromHex("49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97630100000539") - val clientDataJson: String = - """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","type":"webauthn.get","tokenBinding":{"status":"supported"},"clientExtensions":{}}""" - val credentialId: ByteArray = - ByteArray.fromBase64Url("AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8") - val credentialKey: KeyPair = TestAuthenticator.importEcKeypair( - privateBytes = - ByteArray.fromHex("308193020100301306072a8648ce3d020106082a8648ce3d030107047930770201010420449d91b8a2a508b2927cd5cf4dde32db8e58f237fc155e395d3aad127e115f5aa00a06082a8648ce3d030107a1440342000446c68a2eb75057b1f19b6d06dd3733381063d021391b3637889b0b432c54aaa2b184b35e44d433c70e63a9dd82568dd1ec02c5daba3e66b90a3a881c0c1f4c1a"), - publicBytes = - ByteArray.fromHex("3059301306072a8648ce3d020106082a8648ce3d0301070342000446c68a2eb75057b1f19b6d06dd3733381063d021391b3637889b0b432c54aaa2b184b35e44d433c70e63a9dd82568dd1ec02c5daba3e66b90a3a881c0c1f4c1a"), - ) - val signature: ByteArray = - ByteArray.fromHex("304502201dfef99d44222410686605e23227853f19e9bf89cbab181fdb52b7f40d79f0d5022100c167309d699a03416887af363de0628d7d77f678a01d135da996f0ecbed7e8a5") - - // These values are not signed over - val username: String = "foo-user" - val userHandle: ByteArray = - ByteArray.fromHex("6d8972d9603ce4f3fa5d520ce6d024bf") - val user: UserIdentity = UserIdentity - .builder() - .name(username) - .displayName("Test user") - .id(userHandle) - .build() - - // These values are defined by the attestationObject and clientDataJson above - val credentialPublicKeyCose: ByteArray = - WebAuthnTestCodecs.publicKeyToCose(credentialKey.getPublic) - val clientDataJsonBytes: ByteArray = new ByteArray( - clientDataJson.getBytes("UTF-8") - ) - val clientData = new CollectedClientData(clientDataJsonBytes) - val challenge: ByteArray = clientData.getChallenge - val requestedExtensions = AssertionExtensionInputs.builder().build() - val clientExtensionResults: ClientAssertionExtensionOutputs = - ClientAssertionExtensionOutputs.builder().build() - - } - - def finishAssertion[C <: CredentialRecord]( - credentialRepository: CredentialRepositoryV2[C], - allowCredentials: Option[java.util.List[PublicKeyCredentialDescriptor]] = - Some( - List( - PublicKeyCredentialDescriptor - .builder() - .id(Defaults.credentialId) - .build() - ).asJava - ), - allowOriginPort: Boolean = false, - allowOriginSubdomain: Boolean = false, - authenticatorData: ByteArray = Defaults.authenticatorData, - callerTokenBindingId: Option[ByteArray] = None, - challenge: ByteArray = Defaults.challenge, - clientDataJson: String = Defaults.clientDataJson, - clientExtensionResults: ClientAssertionExtensionOutputs = - Defaults.clientExtensionResults, - credentialId: ByteArray = Defaults.credentialId, - isSecurePaymentConfirmation: Option[Boolean] = None, - origins: Option[Set[String]] = None, - requestedExtensions: AssertionExtensionInputs = - Defaults.requestedExtensions, - rpId: RelyingPartyIdentity = Defaults.rpId, - signature: ByteArray = Defaults.signature, - userHandleForResponse: Option[ByteArray] = Some(Defaults.userHandle), - userHandleForRequest: Option[ByteArray] = None, - usernameForRequest: Option[String] = None, - usernameRepository: Option[UsernameRepository] = None, - userVerificationRequirement: UserVerificationRequirement = - UserVerificationRequirement.PREFERRED, - validateSignatureCounter: Boolean = true, - ): FinishAssertionSteps[C] = { - val clientDataJsonBytes: ByteArray = - if (clientDataJson == null) null - else new ByteArray(clientDataJson.getBytes("UTF-8")) - - val request = AssertionRequest - .builder() - .publicKeyCredentialRequestOptions( - PublicKeyCredentialRequestOptions - .builder() - .challenge(challenge) - .rpId(rpId.getId) - .allowCredentials(allowCredentials.toJava) - .userVerification(userVerificationRequirement) - .extensions(requestedExtensions) - .build() - ) - .username(usernameForRequest.toJava) - .userHandle(userHandleForRequest.toJava) - .build() - - val response = PublicKeyCredential - .builder() - .id(credentialId) - .response( - AuthenticatorAssertionResponse - .builder() - .authenticatorData( - if (authenticatorData == null) null else authenticatorData - ) - .clientDataJSON( - if (clientDataJsonBytes == null) null else clientDataJsonBytes - ) - .signature(if (signature == null) null else signature) - .userHandle(userHandleForResponse.toJava) - .build() - ) - .clientExtensionResults(clientExtensionResults) - .build() - - val builder = RelyingParty - .builder() - .identity(rpId) - .credentialRepositoryV2(credentialRepository) - .preferredPubkeyParams(Nil.asJava) - .allowOriginPort(allowOriginPort) - .allowOriginSubdomain(allowOriginSubdomain) - .allowUntrustedAttestation(false) - .validateSignatureCounter(validateSignatureCounter) - - usernameRepository.foreach(builder.usernameRepository) - origins.map(_.asJava).foreach(builder.origins) - - val fao = FinishAssertionOptions - .builder() - .request(request) - .response(response) - .callerTokenBindingId(callerTokenBindingId.toJava) - - isSecurePaymentConfirmation foreach { isSpc => - fao.isSecurePaymentConfirmation(isSpc) - } - - builder - .build() - ._finishAssertion(fao.build()) - } - - testWithEachProvider { it => - describe("RelyingParty.startAssertion") { - - describe( - "respects the userVerification parameter in StartAssertionOptions." - ) { - it(s"If the parameter is not set, or set to empty, it is also empty in the result.") { - val rp = RelyingParty - .builder() - .identity(Defaults.rpId) - .credentialRepositoryV2(Helpers.CredentialRepositoryV2.empty) - .build() - val request1 = - rp.startAssertion(StartAssertionOptions.builder().build()) - val request2 = rp.startAssertion( - StartAssertionOptions - .builder() - .userVerification(Optional.empty[UserVerificationRequirement]) - .build() - ) - - request1.getPublicKeyCredentialRequestOptions.getUserVerification.toScala should be( - None - ) - request2.getPublicKeyCredentialRequestOptions.getUserVerification.toScala should be( - None - ) - } - - it(s"If the parameter is set, that value is used.") { - val rp = RelyingParty - .builder() - .identity(Defaults.rpId) - .credentialRepositoryV2(Helpers.CredentialRepositoryV2.empty) - .build() - - forAll { uv: Option[UserVerificationRequirement] => - val request = rp.startAssertion( - StartAssertionOptions - .builder() - .userVerification(uv.toJava) - .build() - ) - - request.getPublicKeyCredentialRequestOptions.getUserVerification.toScala should equal( - uv - ) - } - } - } - - } - - describe("RelyingParty.finishAssertion") { - - it("does not make redundant calls to CredentialRepositoryV2.lookup().") { - val registrationTestData = - RegistrationTestData.Packed.BasicAttestationEdDsa - val testData = registrationTestData.assertion.get - - val credRepo = new Helpers.CredentialRepositoryV2.CountingCalls( - Helpers.CredentialRepositoryV2.withUsers( - ( - registrationTestData.userId, - Helpers.toCredentialRecord(registrationTestData), - ) - ) - ) - val usernameRepo = - Helpers.UsernameRepository.withUsers(registrationTestData.userId) - val rp = RelyingParty - .builder() - .identity( - RelyingPartyIdentity.builder().id("localhost").name("Test RP").build() - ) - .credentialRepositoryV2(credRepo) - .usernameRepository(usernameRepo) - .build() - - val result = rp.finishAssertion( - FinishAssertionOptions - .builder() - .request(testData.request) - .response(testData.response) - .build() - ) - - result.isSuccess should be(true) - result.getCredential.getUserHandle should equal( - registrationTestData.userId.getId - ) - result.getCredential.getCredentialId should equal( - registrationTestData.response.getId - ) - result.getCredential.getCredentialId should equal( - testData.response.getId - ) - credRepo.lookupCount should equal(1) - } - - describe("§7.2. Verifying an authentication assertion: When verifying a given PublicKeyCredential structure (credential) and an AuthenticationExtensionsClientOutputs structure clientExtensionResults, as part of an authentication ceremony, the Relying Party MUST proceed as follows:") { - - describe("1. Let options be a new PublicKeyCredentialRequestOptions structure configured to the Relying Party's needs for the ceremony.") { - it("If options.allowCredentials is present, the transports member of each item SHOULD be set to the value returned by credential.response.getTransports() when the corresponding credential was registered.") { - forAll( - Gen.nonEmptyContainerOf[Set, AuthenticatorTransport]( - arbitrary[AuthenticatorTransport] - ), - arbitrary[PublicKeyCredentialDescriptor], - arbitrary[PublicKeyCredentialDescriptor], - arbitrary[PublicKeyCredentialDescriptor], - ) { - ( - cred1Transports: Set[AuthenticatorTransport], - cred1: PublicKeyCredentialDescriptor, - cred2: PublicKeyCredentialDescriptor, - cred3: PublicKeyCredentialDescriptor, - ) => - val credRepo = new CredentialRepositoryV2[CredentialRecord] { - override def getCredentialDescriptorsForUserHandle( - userHandle: ByteArray - ): java.util.Set[PublicKeyCredentialDescriptor] = - Set( - cred1.toBuilder - .transports(cred1Transports.asJava) - .build(), - cred2.toBuilder - .transports( - Optional.of( - Set.empty[AuthenticatorTransport].asJava - ) - ) - .build(), - cred3.toBuilder - .transports( - Optional - .empty[java.util.Set[AuthenticatorTransport]] - ) - .build(), - ).asJava - - override def lookup( - credentialId: ByteArray, - userHandle: ByteArray, - ): Optional[CredentialRecord] = ??? - - override def credentialIdExists( - credentialId: ByteArray - ): Boolean = ??? - } - - { - val rp = RelyingParty - .builder() - .identity(Defaults.rpId) - .credentialRepositoryV2( - credRepo - ) - .preferredPubkeyParams( - List(PublicKeyCredentialParameters.ES256).asJava - ) - .build() - - val result = rp.startAssertion( - StartAssertionOptions - .builder() - .userHandle(Defaults.userHandle) - .build() - ) - - val requestCreds = - result.getPublicKeyCredentialRequestOptions.getAllowCredentials.get.asScala - requestCreds.head.getTransports.toScala should equal( - Some(cred1Transports.asJava) - ) - requestCreds(1).getTransports.toScala should equal( - Some(Set.empty.asJava) - ) - requestCreds(2).getTransports.toScala should equal(None) - - } - - { - val usernameRepo = Helpers.UsernameRepository.withUsers( - UserIdentity - .builder() - .name(Defaults.username) - .displayName(Defaults.username) - .id(Defaults.userHandle) - .build() - ) - val rp = RelyingParty - .builder() - .identity(Defaults.rpId) - .credentialRepositoryV2( - credRepo - ) - .usernameRepository(usernameRepo) - .preferredPubkeyParams( - List(PublicKeyCredentialParameters.ES256).asJava - ) - .build() - - val result = rp.startAssertion( - StartAssertionOptions - .builder() - .username(Defaults.username) - .build() - ) - - val requestCreds = - result.getPublicKeyCredentialRequestOptions.getAllowCredentials.get.asScala - requestCreds.head.getTransports.toScala should equal( - Some(cred1Transports.asJava) - ) - requestCreds(1).getTransports.toScala should equal( - Some(Set.empty.asJava) - ) - requestCreds(2).getTransports.toScala should equal(None) - - } - } - } - } - - describe("2. Call navigator.credentials.get() and pass options as the publicKey option. Let credential be the result of the successfully resolved promise. If the promise is rejected, abort the ceremony with a user-visible error, or otherwise guide the user experience as might be determinable from the context available in the rejected promise. For information on different error contexts and the circumstances leading to them, see § 6.3.3 The authenticatorGetAssertion Operation.") { - it("Nothing to test: applicable only to client side.") {} - } - - it("3. Let response be credential.response. If response is not an instance of AuthenticatorAssertionResponse, abort the ceremony with a user-visible error.") { - val testData = - RegistrationTestData.Packed.BasicAttestationEdDsa.assertion.get - val faob = FinishAssertionOptions - .builder() - .request(testData.request) - "faob.response(testData.request)" shouldNot compile - faob.response(testData.response).build() should not be null - } - - describe("4. Let clientExtensionResults be the result of calling credential.getClientExtensionResults().") { - it( - "The PublicKeyCredential class has a clientExtensionResults field" - ) { - val pkc = PublicKeyCredential.parseAssertionResponseJson("""{ - "type": "public-key", - "id": "", - "response": { - "authenticatorData": "xGzvgq0bVGR3WR0Aiwh1nsPm0uy085R0v-ppaZJdA7cBAAAABQ", - "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiaHZGN1AxNGwxTjZUcEhnZXVBMjhDdnJaTE1yVjRSMjdZd2JrY2FSYlRPZyIsIm9yaWdpbiI6Imh0dHBzOi8vZGVtby55dWJpY28uY29tIiwiY3Jvc3NPcmlnaW4iOmZhbHNlfQ==", - "signature": "MEYCIQCi7u0ErVIGZIWOQbc_y7IYcNXBniczTgzHH_yE0WfzcQIhALDsITBJDPQMBFxB6pKd608lRVPcNeNnrX3olAxA3AmX" - }, - "clientExtensionResults": { - "appid": true, - "org.example.foo": "bar" - } - }""") - pkc.getClientExtensionResults.getExtensionIds should contain( - "appid" - ) - } - } - - describe("5. If options.allowCredentials is not empty, verify that credential.id identifies one of the public key credentials listed in options.allowCredentials.") { - it("Fails if returned credential ID is not a requested one.") { - val steps = finishAssertion[CredentialRecord]( - credentialRepository = - Helpers.CredentialRepositoryV2.unimplemented[CredentialRecord], - allowCredentials = Some( - List( - PublicKeyCredentialDescriptor - .builder() - .id(new ByteArray(Array(3, 2, 1, 0))) - .build() - ).asJava - ), - credentialId = new ByteArray(Array(0, 1, 2, 3)), - ) - val step: FinishAssertionSteps[CredentialRecord]#Step5 = - steps.begin - - step.validations shouldBe a[Failure[_]] - step.validations.failed.get shouldBe an[IllegalArgumentException] - step.tryNext shouldBe a[Failure[_]] - } - - it("Succeeds if returned credential ID is a requested one.") { - val steps = finishAssertion( - credentialRepository = - Helpers.CredentialRepositoryV2.unimplemented[CredentialRecord], - allowCredentials = Some( - List( - PublicKeyCredentialDescriptor - .builder() - .id(new ByteArray(Array(0, 1, 2, 3))) - .build(), - PublicKeyCredentialDescriptor - .builder() - .id(new ByteArray(Array(4, 5, 6, 7))) - .build(), - ).asJava - ), - credentialId = new ByteArray(Array(4, 5, 6, 7)), - ) - val step: FinishAssertionSteps[CredentialRecord]#Step5 = - steps.begin - - step.validations shouldBe a[Success[_]] - } - - it("Succeeds if no credential IDs were requested.") { - for { - allowCredentials <- List( - None, - Some(List.empty[PublicKeyCredentialDescriptor].asJava), - ) - } { - val steps = finishAssertion( - credentialRepository = Helpers.CredentialRepositoryV2 - .unimplemented[CredentialRecord], - allowCredentials = allowCredentials, - credentialId = new ByteArray(Array(0, 1, 2, 3)), - ) - val step: FinishAssertionSteps[CredentialRecord]#Step5 = - steps.begin - - step.validations shouldBe a[Success[_]] - } - } - } - - describe("6. Identify the user being authenticated and verify that this user is the owner of the public key credential source credentialSource identified by credential.id:") { - val owner = UserIdentity - .builder() - .name("owner") - .displayName("") - .id(new ByteArray(Array(4, 5, 6, 7))) - .build() - val nonOwner = UserIdentity - .builder() - .name("non-owner") - .displayName("") - .id(new ByteArray(Array(8, 9, 10, 11))) - .build() - - val credentialOwnedByOwner = Helpers.CredentialRepositoryV2.withUsers( - ( - owner, - Helpers.credentialRecord( - credentialId = Defaults.credentialId, - userHandle = owner.getId, - publicKeyCose = null, - ), - ) - ) - - val credentialOwnedByNonOwner = - Helpers.CredentialRepositoryV2.withUsers( - ( - nonOwner, - Helpers.credentialRecord( - credentialId = new ByteArray(Array(12, 13, 14, 15)), - userHandle = nonOwner.getId, - publicKeyCose = null, - ), - ) - ) - - describe("If the user was identified before the authentication ceremony was initiated, e.g., via a username or cookie, verify that the identified user is the owner of credentialSource. If response.userHandle is present, let userHandle be its value. Verify that userHandle also maps to the same user.") { - def checks(usernameRepository: Option[UsernameRepository]) = { - it( - "Fails if credential ID is not owned by the requested user handle." - ) { - val steps = finishAssertion( - credentialRepository = credentialOwnedByNonOwner, - usernameRepository = usernameRepository, - userHandleForRequest = Some(owner.getId), - userHandleForResponse = None, - ) - val step: FinishAssertionSteps[CredentialRecord]#Step6 = - steps.begin.next - - step.validations shouldBe a[Failure[_]] - step.validations.failed.get shouldBe an[ - IllegalArgumentException - ] - step.tryNext shouldBe a[Failure[_]] - } - - it( - "Fails if response.userHandle does not identify the same user as request.userHandle." - ) { - val steps = finishAssertion( - credentialRepository = credentialOwnedByOwner, - usernameRepository = usernameRepository, - userHandleForRequest = Some(nonOwner.getId), - userHandleForResponse = Some(owner.getId), - ) - val step: FinishAssertionSteps[CredentialRecord]#Step6 = - steps.begin.next - - step.validations shouldBe a[Failure[_]] - step.validations.failed.get shouldBe an[ - IllegalArgumentException - ] - step.tryNext shouldBe a[Failure[_]] - } - - it("Succeeds if credential ID is owned by the requested user handle.") { - val steps = finishAssertion( - credentialRepository = credentialOwnedByOwner, - usernameRepository = usernameRepository, - userHandleForRequest = Some(owner.getId), - userHandleForResponse = None, - ) - val step: FinishAssertionSteps[CredentialRecord]#Step6 = - steps.begin.next - - step.validations shouldBe a[Success[_]] - step.tryNext shouldBe a[Success[_]] - } - - it("Succeeds if credential ID is owned by the requested and returned user handle.") { - val steps = finishAssertion( - credentialRepository = credentialOwnedByOwner, - usernameRepository = usernameRepository, - userHandleForRequest = Some(owner.getId), - userHandleForResponse = Some(owner.getId), - ) - val step: FinishAssertionSteps[CredentialRecord]#Step6 = - steps.begin.next - - step.validations shouldBe a[Success[_]] - step.tryNext shouldBe a[Success[_]] - } - } - - describe("When a UsernameRepository is set:") { - val usernameRepository = - Some(Helpers.UsernameRepository.withUsers(owner, nonOwner)) - checks(usernameRepository) - - it( - "Fails if credential ID is not owned by the requested username." - ) { - val steps = finishAssertion( - credentialRepository = credentialOwnedByNonOwner, - usernameRepository = usernameRepository, - usernameForRequest = Some(owner.getName), - userHandleForResponse = None, - ) - val step: FinishAssertionSteps[CredentialRecord]#Step6 = - steps.begin.next - - step.validations shouldBe a[Failure[_]] - step.validations.failed.get shouldBe an[ - IllegalArgumentException - ] - step.tryNext shouldBe a[Failure[_]] - } - - it( - "Fails if response.userHandle does not identify the same user as request.username." - ) { - val steps = finishAssertion( - credentialRepository = credentialOwnedByOwner, - usernameRepository = usernameRepository, - usernameForRequest = Some(nonOwner.getName), - userHandleForResponse = Some(owner.getId), - ) - val step: FinishAssertionSteps[CredentialRecord]#Step6 = - steps.begin.next - - step.validations shouldBe a[Failure[_]] - step.validations.failed.get shouldBe an[ - IllegalArgumentException - ] - step.tryNext shouldBe a[Failure[_]] - } - - it( - "Succeeds if credential ID is owned by the requested username." - ) { - val steps = finishAssertion( - credentialRepository = credentialOwnedByOwner, - usernameRepository = usernameRepository, - usernameForRequest = Some(owner.getName), - userHandleForResponse = None, - ) - val step: FinishAssertionSteps[CredentialRecord]#Step6 = - steps.begin.next - - step.validations shouldBe a[Success[_]] - step.tryNext shouldBe a[Success[_]] - } - - it("Succeeds if credential ID is owned by the requested username and returned user handle.") { - val steps = finishAssertion( - credentialRepository = credentialOwnedByOwner, - usernameRepository = usernameRepository, - usernameForRequest = Some(owner.getName), - userHandleForResponse = Some(owner.getId), - ) - val step: FinishAssertionSteps[CredentialRecord]#Step6 = - steps.begin.next - - step.validations shouldBe a[Success[_]] - step.tryNext shouldBe a[Success[_]] - } - } - - describe("When a UsernameRepository is not set:") { - checks(None) - } - } - - describe("If the user was not identified before the authentication ceremony was initiated, verify that response.userHandle is present, and that the user identified by this value is the owner of credentialSource.") { - def checks(usernameRepository: Option[UsernameRepository]) = { - it( - "Fails if response.userHandle is not present." - ) { - val steps = finishAssertion( - credentialRepository = credentialOwnedByOwner, - usernameRepository = usernameRepository, - usernameForRequest = None, - userHandleForRequest = None, - userHandleForResponse = None, - ) - val step: FinishAssertionSteps[CredentialRecord]#Step6 = - steps.begin.next - - step.validations shouldBe a[Failure[_]] - step.validations.failed.get shouldBe an[ - IllegalArgumentException - ] - step.tryNext shouldBe a[Failure[_]] - } - - it( - "Fails if credential ID is not owned by the user handle in the response." - ) { - val steps = finishAssertion( - credentialRepository = credentialOwnedByNonOwner, - usernameRepository = usernameRepository, - usernameForRequest = None, - userHandleForRequest = None, - userHandleForResponse = Some(owner.getId), - ) - val step: FinishAssertionSteps[CredentialRecord]#Step6 = - steps.begin.next - - step.validations shouldBe a[Failure[_]] - step.validations.failed.get shouldBe an[ - IllegalArgumentException - ] - step.tryNext shouldBe a[Failure[_]] - } - - it("Succeeds if credential ID is owned by the user handle in the response.") { - val steps = finishAssertion( - credentialRepository = credentialOwnedByOwner, - usernameRepository = usernameRepository, - usernameForRequest = None, - userHandleForRequest = None, - userHandleForResponse = Some(owner.getId), - ) - val step: FinishAssertionSteps[CredentialRecord]#Step6 = - steps.begin.next - - step.validations shouldBe a[Success[_]] - step.tryNext shouldBe a[Success[_]] - } - } - - val usernameRepository = - Helpers.UsernameRepository.withUsers(owner, nonOwner) - describe("When a UsernameRepository is set:") { - checks(Some(usernameRepository)) - } - - describe("When a UsernameRepository is not set:") { - checks(None) - } - } - } - - describe("7. Using credential.id (or credential.rawId, if base64url encoding is inappropriate for your use case), look up the corresponding credential public key and let credentialPublicKey be that credential public key.") { - it("Fails if the credential ID is unknown.") { - val steps = finishAssertion( - credentialRepository = Helpers.CredentialRepositoryV2.withUser( - Defaults.user, - credentialId = Defaults.credentialId, - publicKeyCose = ByteArray.fromHex(""), - ) - ) - val step: steps.Step7 = new steps.Step7( - Some(Defaults.username).toJava, - Defaults.userHandle, - None.toJava, - ) - - step.validations shouldBe a[Failure[_]] - step.validations.failed.get shouldBe an[IllegalArgumentException] - step.tryNext shouldBe a[Failure[_]] - } - - it("Succeeds if the credential ID is known.") { - val steps = finishAssertion( - credentialRepository = Helpers.CredentialRepositoryV2.withUser( - Defaults.user, - credentialId = Defaults.credentialId, - publicKeyCose = ByteArray.fromHex(""), - ) - ) - val step: FinishAssertionSteps[CredentialRecord]#Step7 = - steps.begin.next.next - - step.validations shouldBe a[Success[_]] - step.tryNext shouldBe a[Success[_]] - } - } - - describe("8. Let cData, authData and sig denote the value of response’s clientDataJSON, authenticatorData, and signature respectively.") { - it("Succeeds if all three are present.") { - val steps = finishAssertion(credentialRepository = - Helpers.CredentialRepositoryV2.withUser( - Defaults.user, - credentialId = Defaults.credentialId, - publicKeyCose = ByteArray.fromHex(""), - ) - ) - val step: FinishAssertionSteps[CredentialRecord]#Step8 = - steps.begin.next.next.next - - step.validations shouldBe a[Success[_]] - step.clientData should not be null - step.authenticatorData should not be null - step.signature should not be null - step.tryNext shouldBe a[Success[_]] - } - - it("Fails if clientDataJSON is missing.") { - a[NullPointerException] should be thrownBy finishAssertion( - credentialRepository = - Helpers.CredentialRepositoryV2.unimplemented[CredentialRecord], - clientDataJson = null, - ) - } - - it("Fails if authenticatorData is missing.") { - a[NullPointerException] should be thrownBy finishAssertion( - credentialRepository = - Helpers.CredentialRepositoryV2.unimplemented[CredentialRecord], - authenticatorData = null, - ) - } - - it("Fails if signature is missing.") { - a[NullPointerException] should be thrownBy finishAssertion( - credentialRepository = - Helpers.CredentialRepositoryV2.unimplemented[CredentialRecord], - signature = null, - ) - } - } - - describe("9. Let JSONtext be the result of running UTF-8 decode on the value of cData.") { - it("Fails if clientDataJSON is not valid UTF-8.") { - an[IOException] should be thrownBy new CollectedClientData( - new ByteArray(Array(-128)) - ) - } - } - - describe("10. Let C, the client data claimed as used for the signature, be the result of running an implementation-specific JSON parser on JSONtext.") { - it("Fails if cData is not valid JSON.") { - an[IOException] should be thrownBy new CollectedClientData( - new ByteArray("{".getBytes(Charset.forName("UTF-8"))) - ) - an[IOException] should be thrownBy finishAssertion( - credentialRepository = - Helpers.CredentialRepositoryV2.unimplemented[CredentialRecord], - clientDataJson = "{", - ) - } - - it("Succeeds if cData is valid JSON.") { - val steps = finishAssertion( - credentialRepository = Helpers.CredentialRepositoryV2.withUser( - Defaults.user, - credentialId = Defaults.credentialId, - publicKeyCose = ByteArray.fromHex(""), - ), - clientDataJson = """{ - "challenge": "", - "origin": "", - "type": "" - }""", - ) - val step: FinishAssertionSteps[CredentialRecord]#Step10 = - steps.begin.next.next.next.next - - step.validations shouldBe a[Success[_]] - step.clientData should not be null - step.tryNext shouldBe a[Success[_]] - } - } - - describe( - "11. Verify that the value of C.type is the string webauthn.get." - ) { - it("The default test case succeeds.") { - val steps = finishAssertion( - credentialRepository = Helpers.CredentialRepositoryV2.withUser( - Defaults.user, - credentialId = Defaults.credentialId, - publicKeyCose = ByteArray.fromHex(""), - ) - ) - val step: FinishAssertionSteps[CredentialRecord]#Step11 = - steps.begin.next.next.next.next.next - - step.validations shouldBe a[Success[_]] - } - - def assertFails( - typeString: String, - isSecurePaymentConfirmation: Option[Boolean] = None, - ): Unit = { - val steps = finishAssertion( - credentialRepository = Helpers.CredentialRepositoryV2.withUser( - Defaults.user, - credentialId = Defaults.credentialId, - publicKeyCose = ByteArray.fromHex(""), - ), - clientDataJson = JacksonCodecs.json.writeValueAsString( - JacksonCodecs.json - .readTree(Defaults.clientDataJson) - .asInstanceOf[ObjectNode] - .set("type", jsonFactory.textNode(typeString)) - ), - isSecurePaymentConfirmation = isSecurePaymentConfirmation, - ) - val step: FinishAssertionSteps[CredentialRecord]#Step11 = - steps.begin.next.next.next.next.next - - step.validations shouldBe a[Failure[_]] - step.validations.failed.get shouldBe an[IllegalArgumentException] - } - - it("""Any value other than "webauthn.get" fails.""") { - forAll { (typeString: String) => - whenever(typeString != "webauthn.get") { - assertFails(typeString) - } - } - forAll(Gen.alphaNumStr) { (typeString: String) => - whenever(typeString != "webauthn.get") { - assertFails(typeString) - } - } - } - - it("""The string "webauthn.create" fails.""") { - assertFails("webauthn.create") - } - - it("""The string "payment.get" fails.""") { - assertFails("payment.get") - } - - describe("If the isSecurePaymentConfirmation option is set,") { - it("the default test case fails.") { - val steps = - finishAssertion( - credentialRepository = - Helpers.CredentialRepositoryV2.withUser( - Defaults.user, - credentialId = Defaults.credentialId, - publicKeyCose = ByteArray.fromHex(""), - ), - isSecurePaymentConfirmation = Some(true), - ) - val step: FinishAssertionSteps[CredentialRecord]#Step11 = - steps.begin.next.next.next.next.next - - step.validations shouldBe a[Failure[_]] - step.validations.failed.get shouldBe an[IllegalArgumentException] - } - - it("""the default test case succeeds if type is overwritten with the value "payment.get".""") { - val json = JacksonCodecs.json() - val steps = finishAssertion( - credentialRepository = Helpers.CredentialRepositoryV2.withUser( - Defaults.user, - credentialId = Defaults.credentialId, - publicKeyCose = ByteArray.fromHex(""), - ), - isSecurePaymentConfirmation = Some(true), - clientDataJson = json.writeValueAsString( - json - .readTree(Defaults.clientDataJson) - .asInstanceOf[ObjectNode] - .set[ObjectNode]("type", new TextNode("payment.get")) - ), - ) - val step: FinishAssertionSteps[CredentialRecord]#Step11 = - steps.begin.next.next.next.next.next - - step.validations shouldBe a[Success[_]] - } - - it("""any value other than "payment.get" fails.""") { - forAll { (typeString: String) => - whenever(typeString != "payment.get") { - assertFails( - typeString, - isSecurePaymentConfirmation = Some(true), - ) - } - } - forAll(Gen.alphaNumStr) { (typeString: String) => - whenever(typeString != "payment.get") { - assertFails( - typeString, - isSecurePaymentConfirmation = Some(true), - ) - } - } - } - - it("""the string "webauthn.create" fails.""") { - assertFails( - "webauthn.create", - isSecurePaymentConfirmation = Some(true), - ) - } - - it("""the string "webauthn.get" fails.""") { - assertFails( - "webauthn.get", - isSecurePaymentConfirmation = Some(true), - ) - } - } - } - - it("12. Verify that the value of C.challenge equals the base64url encoding of options.challenge.") { - val steps = - finishAssertion( - credentialRepository = Helpers.CredentialRepositoryV2.withUser( - Defaults.user, - credentialId = Defaults.credentialId, - publicKeyCose = ByteArray.fromHex(""), - ), - challenge = new ByteArray(Array.fill(16)(0)), - ) - val step: FinishAssertionSteps[CredentialRecord]#Step12 = - steps.begin.next.next.next.next.next.next - - step.validations shouldBe a[Failure[_]] - step.validations.failed.get shouldBe an[IllegalArgumentException] - step.tryNext shouldBe a[Failure[_]] - } - - describe("13. Verify that the value of C.origin matches the Relying Party's origin.") { - def checkAccepted( - origin: String, - origins: Option[Set[String]] = None, - allowOriginPort: Boolean = false, - allowOriginSubdomain: Boolean = false, - ): Unit = { - val clientDataJson: String = Defaults.clientDataJson.replace( - "\"https://localhost\"", - "\"" + origin + "\"", - ) - val steps = finishAssertion( - credentialRepository = Helpers.CredentialRepositoryV2.withUser( - Defaults.user, - credentialId = Defaults.credentialId, - publicKeyCose = ByteArray.fromHex(""), - ), - clientDataJson = clientDataJson, - origins = origins, - allowOriginPort = allowOriginPort, - allowOriginSubdomain = allowOriginSubdomain, - ) - val step: FinishAssertionSteps[CredentialRecord]#Step13 = - steps.begin.next.next.next.next.next.next.next - - step.validations shouldBe a[Success[_]] - step.tryNext shouldBe a[Success[_]] - } - - def checkRejected( - origin: String, - origins: Option[Set[String]] = None, - allowOriginPort: Boolean = false, - allowOriginSubdomain: Boolean = false, - ): Unit = { - val clientDataJson: String = Defaults.clientDataJson.replace( - "\"https://localhost\"", - "\"" + origin + "\"", - ) - val steps = finishAssertion( - credentialRepository = Helpers.CredentialRepositoryV2.withUser( - Defaults.user, - credentialId = Defaults.credentialId, - publicKeyCose = ByteArray.fromHex(""), - ), - clientDataJson = clientDataJson, - origins = origins, - allowOriginPort = allowOriginPort, - allowOriginSubdomain = allowOriginSubdomain, - ) - val step: FinishAssertionSteps[CredentialRecord]#Step13 = - steps.begin.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 origin is different.") { - checkRejected(origin = "https://root.evil") - } - - describe("Explicit ports are") { - val origin = "https://localhost:8080" - - it("by default not allowed.") { - checkRejected(origin = origin) - } - - it("allowed if RP opts in to it.") { - checkAccepted(origin = origin, allowOriginPort = true) - } - } - - describe("Subdomains are") { - val origin = "https://foo.localhost" - - it("by default not allowed.") { - checkRejected(origin = origin) - } - - it("allowed if RP opts in to it.") { - checkAccepted(origin = origin, allowOriginSubdomain = true) - } - } - - describe("Subdomains and explicit ports at the same time are") { - val origin = "https://foo.localhost:8080" - - it("by default not allowed.") { - checkRejected(origin = origin) - } - - it("not allowed if only subdomains are allowed.") { - checkRejected(origin = origin, allowOriginSubdomain = true) - } - - it("not allowed if only explicit ports are allowed.") { - checkRejected(origin = origin, allowOriginPort = true) - } - - it("allowed if RP opts in to both.") { - checkAccepted( - origin = origin, - allowOriginPort = true, - allowOriginSubdomain = true, - ) - } - } - - describe("The examples in JavaDoc are correct:") { - def check( - origins: Set[String], - acceptOrigins: Iterable[String], - rejectOrigins: Iterable[String], - allowOriginPort: Boolean = false, - allowOriginSubdomain: Boolean = false, - ): Unit = { - for { origin <- acceptOrigins } { - it(s"${origin} is accepted.") { - checkAccepted( - origin = origin, - origins = Some(origins), - allowOriginPort = allowOriginPort, - allowOriginSubdomain = allowOriginSubdomain, - ) - } - } - - for { origin <- rejectOrigins } { - it(s"${origin} is rejected.") { - checkRejected( - origin = origin, - origins = Some(origins), - allowOriginPort = allowOriginPort, - allowOriginSubdomain = allowOriginSubdomain, - ) - } - } - } - - describe("For allowOriginPort:") { - val origins = Set( - "https://example.org", - "https://accounts.example.org", - "https://acme.com:8443", - ) - - describe("false,") { - check( - origins = origins, - acceptOrigins = List( - "https://example.org", - "https://accounts.example.org", - "https://acme.com:8443", - ), - rejectOrigins = List( - "https://example.org:8443", - "https://shop.example.org", - "https://acme.com", - "https://acme.com:9000", - ), - allowOriginPort = false, - ) - } - - describe("true,") { - check( - origins = origins, - acceptOrigins = List( - "https://example.org", - "https://example.org:8443", - "https://accounts.example.org", - "https://acme.com", - "https://acme.com:8443", - "https://acme.com:9000", - ), - rejectOrigins = List( - "https://shop.example.org" - ), - allowOriginPort = true, - ) - } - } - - describe("For allowOriginSubdomain:") { - val origins = Set("https://example.org", "https://acme.com:8443") - - describe("false,") { - check( - origins = origins, - acceptOrigins = List( - "https://example.org", - "https://acme.com:8443", - ), - rejectOrigins = List( - "https://example.org:8443", - "https://accounts.example.org", - "https://acme.com", - "https://shop.acme.com:8443", - ), - allowOriginSubdomain = false, - ) - } - - describe("true,") { - check( - origins = origins, - acceptOrigins = List( - "https://example.org", - "https://accounts.example.org", - "https://acme.com:8443", - "https://shop.acme.com:8443", - ), - rejectOrigins = List( - "https://example.org:8443", - "https://acme.com", - ), - allowOriginSubdomain = true, - ) - } - } - } - } - - describe("14. Verify that the value of C.tokenBinding.status matches the state of Token Binding for the TLS connection over which the attestation was obtained.") { - val credentialRepository = Helpers.CredentialRepositoryV2.withUser( - Defaults.user, - credentialId = Defaults.credentialId, - publicKeyCose = ByteArray.fromHex(""), - ) - - it("Verification succeeds if neither side uses token binding ID.") { - val steps = finishAssertion( - credentialRepository = credentialRepository - ) - val step: FinishAssertionSteps[CredentialRecord]#Step14 = - steps.begin.next.next.next.next.next.next.next.next - - step.validations shouldBe a[Success[_]] - step.tryNext shouldBe a[Success[_]] - } - - it("Verification succeeds if client data specifies token binding is unsupported, and RP does not use it.") { - val clientDataJson = - """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","hashAlgorithm":"SHA-256","type":"webauthn.get"}""" - val steps = finishAssertion( - credentialRepository = credentialRepository, - clientDataJson = clientDataJson, - ) - val step: FinishAssertionSteps[CredentialRecord]#Step14 = - steps.begin.next.next.next.next.next.next.next.next - - step.validations shouldBe a[Success[_]] - step.tryNext shouldBe a[Success[_]] - } - - it("Verification succeeds if client data specifies token binding is supported, and RP does not use it.") { - val clientDataJson = - """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","hashAlgorithm":"SHA-256","tokenBinding":{"status":"supported"},"type":"webauthn.get"}""" - val steps = finishAssertion( - credentialRepository = credentialRepository, - clientDataJson = clientDataJson, - ) - val step: FinishAssertionSteps[CredentialRecord]#Step14 = - steps.begin.next.next.next.next.next.next.next.next - - step.validations shouldBe a[Success[_]] - step.tryNext shouldBe a[Success[_]] - } - - it("Verification fails if client data does not specify token binding status and RP specifies token binding ID.") { - val clientDataJson = - """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","hashAlgorithm":"SHA-256","type":"webauthn.get"}""" - val steps = finishAssertion( - credentialRepository = credentialRepository, - callerTokenBindingId = - Some(ByteArray.fromBase64Url("YELLOWSUBMARINE")), - clientDataJson = clientDataJson, - ) - val step: FinishAssertionSteps[CredentialRecord]#Step14 = - steps.begin.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("Verification succeeds if client data does not specify token binding status and RP does not specify token binding ID.") { - val clientDataJson = - """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","hashAlgorithm":"SHA-256","type":"webauthn.get"}""" - val steps = finishAssertion( - credentialRepository = credentialRepository, - callerTokenBindingId = None, - clientDataJson = clientDataJson, - ) - val step: FinishAssertionSteps[CredentialRecord]#Step14 = - steps.begin.next.next.next.next.next.next.next.next - - step.validations shouldBe a[Success[_]] - step.tryNext shouldBe a[Success[_]] - } - - it("Verification fails if client data specifies token binding ID but RP does not.") { - val clientDataJson = - """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","hashAlgorithm":"SHA-256","tokenBinding":{"status":"present","id":"YELLOWSUBMARINE"},"type":"webauthn.get"}""" - val steps = finishAssertion( - credentialRepository = credentialRepository, - callerTokenBindingId = None, - clientDataJson = clientDataJson, - ) - val step: FinishAssertionSteps[CredentialRecord]#Step14 = - steps.begin.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("If Token Binding was used on that TLS connection, also verify that C.tokenBinding.id matches the base64url encoding of the Token Binding ID for the connection.") { - val credentialRepository = Helpers.CredentialRepositoryV2.withUser( - Defaults.user, - credentialId = Defaults.credentialId, - publicKeyCose = ByteArray.fromHex(""), - ) - - it("Verification succeeds if both sides specify the same token binding ID.") { - val clientDataJson = - """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","hashAlgorithm":"SHA-256","tokenBinding":{"status":"present","id":"YELLOWSUBMARINE"},"type":"webauthn.get"}""" - val steps = finishAssertion( - credentialRepository = credentialRepository, - callerTokenBindingId = - Some(ByteArray.fromBase64Url("YELLOWSUBMARINE")), - clientDataJson = clientDataJson, - ) - val step: FinishAssertionSteps[CredentialRecord]#Step14 = - steps.begin.next.next.next.next.next.next.next.next - - step.validations shouldBe a[Success[_]] - step.tryNext shouldBe a[Success[_]] - } - - it("Verification fails if ID is missing from tokenBinding in client data.") { - val clientDataJson = - """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","hashAlgorithm":"SHA-256","tokenBinding":{"status":"present"},"type":"webauthn.get"}""" - val steps = finishAssertion( - credentialRepository = credentialRepository, - callerTokenBindingId = - Some(ByteArray.fromBase64Url("YELLOWSUBMARINE")), - clientDataJson = clientDataJson, - ) - val step: FinishAssertionSteps[CredentialRecord]#Step14 = - steps.begin.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("Verification fails if RP specifies token binding ID but client does not support it.") { - val clientDataJson = - """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","hashAlgorithm":"SHA-256","type":"webauthn.get"}""" - val steps = finishAssertion( - credentialRepository = credentialRepository, - callerTokenBindingId = - Some(ByteArray.fromBase64Url("YELLOWSUBMARINE")), - clientDataJson = clientDataJson, - ) - val step: FinishAssertionSteps[CredentialRecord]#Step14 = - steps.begin.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("Verification fails if RP specifies token binding ID but client does not use it.") { - val clientDataJson = - """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","hashAlgorithm":"SHA-256","tokenBinding":{"status":"supported"},"type":"webauthn.get"}""" - val steps = finishAssertion( - credentialRepository = credentialRepository, - callerTokenBindingId = - Some(ByteArray.fromBase64Url("YELLOWSUBMARINE")), - clientDataJson = clientDataJson, - ) - val step: FinishAssertionSteps[CredentialRecord]#Step14 = - steps.begin.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("Verification fails if client data and RP specify different token binding IDs.") { - val clientDataJson = - """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","hashAlgorithm":"SHA-256","tokenBinding":{"status":"present","id":"YELLOWSUBMARINE"},"type":"webauthn.get"}""" - val steps = finishAssertion( - credentialRepository = credentialRepository, - callerTokenBindingId = - Some(ByteArray.fromBase64Url("ORANGESUBMARINE")), - clientDataJson = clientDataJson, - ) - val step: FinishAssertionSteps[CredentialRecord]#Step14 = - steps.begin.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("15. Verify that the rpIdHash in authData is the SHA-256 hash of the RP ID expected by the Relying Party.") { - val credentialRepository = Helpers.CredentialRepositoryV2.withUser( - Defaults.user, - credentialId = Defaults.credentialId, - publicKeyCose = ByteArray.fromHex(""), - ) - - it("Fails if RP ID is different.") { - val steps = finishAssertion( - credentialRepository = credentialRepository, - rpId = Defaults.rpId.toBuilder.id("root.evil").build(), - origins = Some(Set("https://localhost")), - ) - val step: FinishAssertionSteps[CredentialRecord]#Step15 = - steps.begin.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("Succeeds if RP ID is the same.") { - val steps = finishAssertion( - credentialRepository = credentialRepository - ) - val step: FinishAssertionSteps[CredentialRecord]#Step15 = - steps.begin.next.next.next.next.next.next.next.next.next - - step.validations shouldBe a[Success[_]] - step.tryNext shouldBe a[Success[_]] - } - - describe("When using the appid extension, it") { - val appid = new AppId("https://test.example.org/foo") - val extensions = AssertionExtensionInputs - .builder() - .appid(Some(appid).toJava) - .build() - - it("fails if RP ID is different.") { - val steps = finishAssertion( - credentialRepository = credentialRepository, - requestedExtensions = extensions, - authenticatorData = new ByteArray( - Array.fill[Byte](32)(0) ++ Defaults.authenticatorData.getBytes - .drop(32) - ), - ) - val step: FinishAssertionSteps[CredentialRecord]#Step15 = - steps.begin.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("succeeds if RP ID is the SHA-256 hash of the standard RP ID.") { - val steps = finishAssertion( - credentialRepository = credentialRepository, - requestedExtensions = extensions, - ) - val step: FinishAssertionSteps[CredentialRecord]#Step15 = - steps.begin.next.next.next.next.next.next.next.next.next - - step.validations shouldBe a[Success[_]] - step.tryNext shouldBe a[Success[_]] - } - - it("succeeds if RP ID is the SHA-256 hash of the appid.") { - val steps = finishAssertion( - credentialRepository = credentialRepository, - requestedExtensions = extensions, - authenticatorData = new ByteArray( - sha256( - appid.getId - ).getBytes ++ Defaults.authenticatorData.getBytes.drop(32) - ), - ) - val step: FinishAssertionSteps[CredentialRecord]#Step15 = - steps.begin.next.next.next.next.next.next.next.next.next - - step.validations shouldBe a[Success[_]] - step.tryNext shouldBe a[Success[_]] - } - } - } - - { - val credentialRepository = Helpers.CredentialRepositoryV2.withUser( - Defaults.user, - credentialId = Defaults.credentialId, - publicKeyCose = ByteArray.fromHex(""), - ) - - def checks[ - Next <: FinishAssertionSteps.Step[CredentialRecord, _], - Step <: FinishAssertionSteps.Step[CredentialRecord, Next], - ]( - stepsToStep: FinishAssertionSteps[CredentialRecord] => Step - ) = { - def check[Ret]( - stepsToStep: FinishAssertionSteps[CredentialRecord] => Step - )( - chk: Step => Ret - )(uvr: UserVerificationRequirement, authData: ByteArray): Ret = { - val steps = finishAssertion( - credentialRepository = credentialRepository, - userVerificationRequirement = uvr, - authenticatorData = authData, - ) - chk(stepsToStep(steps)) - } - def checkFailsWith( - stepsToStep: FinishAssertionSteps[CredentialRecord] => Step - ): (UserVerificationRequirement, ByteArray) => Unit = - check(stepsToStep) { step => - step.validations shouldBe a[Failure[_]] - step.validations.failed.get shouldBe an[ - IllegalArgumentException - ] - step.tryNext shouldBe a[Failure[_]] - } - def checkSucceedsWith( - stepsToStep: FinishAssertionSteps[CredentialRecord] => Step - ): (UserVerificationRequirement, ByteArray) => Unit = - check(stepsToStep) { step => - step.validations shouldBe a[Success[_]] - step.tryNext shouldBe a[Success[_]] - } - - (checkFailsWith(stepsToStep), checkSucceedsWith(stepsToStep)) - } - - describe("16. Verify that the User Present bit of the flags in authData is set.") { - val flagOn: ByteArray = new ByteArray( - Defaults.authenticatorData.getBytes.toVector - .updated( - 32, - (Defaults.authenticatorData.getBytes - .toVector(32) | 0x04 | 0x01).toByte, - ) - .toArray - ) - val flagOff: ByteArray = new ByteArray( - Defaults.authenticatorData.getBytes.toVector - .updated( - 32, - ((Defaults.authenticatorData.getBytes - .toVector(32) | 0x04) & 0xfe).toByte, - ) - .toArray - ) - val (checkFails, checkSucceeds) = - checks[FinishAssertionSteps[ - CredentialRecord - ]#Step17, FinishAssertionSteps[CredentialRecord]#Step16]( - _.begin.next.next.next.next.next.next.next.next.next.next - ) - - it("Fails if UV is discouraged and flag is not set.") { - checkFails(UserVerificationRequirement.DISCOURAGED, flagOff) - } - - it("Succeeds if UV is discouraged and flag is set.") { - checkSucceeds(UserVerificationRequirement.DISCOURAGED, flagOn) - } - - it("Fails if UV is preferred and flag is not set.") { - checkFails(UserVerificationRequirement.PREFERRED, flagOff) - } - - it("Succeeds if UV is preferred and flag is set.") { - checkSucceeds(UserVerificationRequirement.PREFERRED, flagOn) - } - - it("Fails if UV is required and flag is not set.") { - checkFails(UserVerificationRequirement.REQUIRED, flagOff) - } - - it("Succeeds if UV is required and flag is set.") { - checkSucceeds(UserVerificationRequirement.REQUIRED, flagOn) - } - } - - describe("17. If user verification is required for this assertion, verify that the User Verified bit of the flags in authData is set.") { - val flagOn: ByteArray = new ByteArray( - Defaults.authenticatorData.getBytes.toVector - .updated( - 32, - (Defaults.authenticatorData.getBytes - .toVector(32) | 0x04).toByte, - ) - .toArray - ) - val flagOff: ByteArray = new ByteArray( - Defaults.authenticatorData.getBytes.toVector - .updated( - 32, - (Defaults.authenticatorData.getBytes - .toVector(32) & 0xfb).toByte, - ) - .toArray - ) - val (checkFails, checkSucceeds) = - checks[FinishAssertionSteps[ - CredentialRecord - ]#PendingStep16, FinishAssertionSteps[CredentialRecord]#Step17]( - _.begin.next.next.next.next.next.next.next.next.next.next.next - ) - - it("Succeeds if UV is discouraged and flag is not set.") { - checkSucceeds(UserVerificationRequirement.DISCOURAGED, flagOff) - } - - it("Succeeds if UV is discouraged and flag is set.") { - checkSucceeds(UserVerificationRequirement.DISCOURAGED, flagOn) - } - - it("Succeeds if UV is preferred and flag is not set.") { - checkSucceeds(UserVerificationRequirement.PREFERRED, flagOff) - } - - it("Succeeds if UV is preferred and flag is set.") { - checkSucceeds(UserVerificationRequirement.PREFERRED, flagOn) - } - - it("Fails if UV is required and flag is not set.") { - checkFails(UserVerificationRequirement.REQUIRED, flagOff) - } - - it("Succeeds if UV is required and flag is set.") { - checkSucceeds(UserVerificationRequirement.REQUIRED, flagOn) - } - } - } - - 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." - ) { - val credentialRepository = Helpers.CredentialRepositoryV2.withUser( - Defaults.user, - credentialId = Defaults.credentialId, - publicKeyCose = ByteArray.fromHex(""), - be = Some(false), - bs = Some(false), - ) - forAll( - authenticatorDataBytes( - Gen.option(Extensions.authenticatorAssertionExtensionOutputs()), - rpIdHashGen = Gen.const(sha256(Defaults.rpId.getId)), - backupFlagsGen = arbitrary[Boolean].map(bs => (true, bs)), - ) - ) { authData => - val step: FinishAssertionSteps[CredentialRecord]#PendingStep16 = - finishAssertion( - credentialRepository = credentialRepository, - authenticatorData = authData, - ).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[CredentialRecord]#PendingStep16 = - finishAssertion( - credentialRepository = - Helpers.CredentialRepositoryV2.withUser( - Defaults.user, - credentialId = Defaults.credentialId, - publicKeyCose = ByteArray.fromHex(""), - be = Some(true), - bs = Some(storedBs), - ), - authenticatorData = authData, - ).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.") { - val credentialRepository = Helpers.CredentialRepositoryV2.withUser( - Defaults.user, - credentialId = Defaults.credentialId, - publicKeyCose = ByteArray.fromHex(""), - ) - - it("Succeeds if clientExtensionResults is not a subset of the extensions requested by the Relying Party.") { - forAll(Extensions.unrequestedClientAssertionExtensions) { - case (extensionInputs, clientExtensionOutputs, _) => - val steps = finishAssertion( - credentialRepository = credentialRepository, - requestedExtensions = extensionInputs, - clientExtensionResults = clientExtensionOutputs, - ) - val step: FinishAssertionSteps[CredentialRecord]#Step18 = - 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[_]] - } - } - - it("Succeeds if clientExtensionResults is a subset of the extensions requested by the Relying Party.") { - forAll(Extensions.subsetAssertionExtensions) { - case (extensionInputs, clientExtensionOutputs, _) => - val steps = finishAssertion( - credentialRepository = credentialRepository, - requestedExtensions = extensionInputs, - clientExtensionResults = clientExtensionOutputs, - ) - val step: FinishAssertionSteps[CredentialRecord]#Step18 = - 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[_]] - } - } - - it("Succeeds if authenticator extensions is not a subset of the extensions requested by the Relying Party.") { - forAll(Extensions.unrequestedAuthenticatorAssertionExtensions) { - case ( - extensionInputs: AssertionExtensionInputs, - _, - authenticatorExtensionOutputs: CBORObject, - ) => - val steps = finishAssertion( - credentialRepository = credentialRepository, - requestedExtensions = extensionInputs, - authenticatorData = TestAuthenticator.makeAuthDataBytes( - extensionsCborBytes = Some( - new ByteArray( - authenticatorExtensionOutputs.EncodeToBytes() - ) - ) - ), - ) - val step: FinishAssertionSteps[CredentialRecord]#Step18 = - 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[_]] - } - } - - it("Succeeds if authenticator extensions is a subset of the extensions requested by the Relying Party.") { - forAll(Extensions.subsetAssertionExtensions) { - case ( - extensionInputs: AssertionExtensionInputs, - _, - authenticatorExtensionOutputs: CBORObject, - ) => - val steps = finishAssertion( - credentialRepository = credentialRepository, - requestedExtensions = extensionInputs, - authenticatorData = TestAuthenticator.makeAuthDataBytes( - extensionsCborBytes = Some( - new ByteArray( - authenticatorExtensionOutputs.EncodeToBytes() - ) - ) - ), - ) - val step: FinishAssertionSteps[CredentialRecord]#Step18 = - 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[_]] - } - } - } - - it("19. Let hash be the result of computing a hash over the cData using SHA-256.") { - val steps = finishAssertion( - credentialRepository = Helpers.CredentialRepositoryV2.withUser( - Defaults.user, - credentialId = Defaults.credentialId, - publicKeyCose = ByteArray.fromHex(""), - ) - ) - val step: FinishAssertionSteps[CredentialRecord]#Step19 = - 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[_]] - step.clientDataJsonHash should equal( - new ByteArray( - MessageDigest - .getInstance("SHA-256") - .digest(Defaults.clientDataJsonBytes.getBytes) - ) - ) - } - - describe("20. Using credentialPublicKey, verify that sig is a valid signature over the binary concatenation of authData and hash.") { - val credentialRepository = Helpers.CredentialRepositoryV2.withUser( - Defaults.user, - credentialId = Defaults.credentialId, - publicKeyCose = Defaults.credentialPublicKeyCose, - ) - - it("The default test case succeeds.") { - val steps = finishAssertion( - credentialRepository = credentialRepository - ) - val step: FinishAssertionSteps[CredentialRecord]#Step20 = - 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[_]] - step.signedBytes should not be null - } - - it("A mutated clientDataJSON fails verification.") { - val steps = finishAssertion( - credentialRepository = credentialRepository, - clientDataJson = JacksonCodecs.json.writeValueAsString( - JacksonCodecs.json - .readTree(Defaults.clientDataJson) - .asInstanceOf[ObjectNode] - .set("foo", jsonFactory.textNode("bar")) - ), - ) - val step: FinishAssertionSteps[CredentialRecord]#Step20 = - 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] - step.tryNext shouldBe a[Failure[_]] - } - - it("A test case with a different signed RP ID hash fails.") { - val rpId = "ARGHABLARGHLER" - val rpIdHash: ByteArray = Crypto.sha256(rpId) - val steps = finishAssertion( - credentialRepository = credentialRepository, - authenticatorData = new ByteArray( - (rpIdHash.getBytes.toVector ++ Defaults.authenticatorData.getBytes.toVector - .drop(32)).toArray - ), - rpId = Defaults.rpId.toBuilder.id(rpId).build(), - origins = Some(Set("https://localhost")), - ) - val step: FinishAssertionSteps[CredentialRecord]#Step20 = - 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] - step.tryNext shouldBe a[Failure[_]] - } - - it("A test case with a different signed flags field fails.") { - val steps = finishAssertion( - credentialRepository = credentialRepository, - authenticatorData = new ByteArray( - Defaults.authenticatorData.getBytes.toVector - .updated( - 32, - (Defaults.authenticatorData.getBytes - .toVector(32) | 0x02).toByte, - ) - .toArray - ), - ) - val step: FinishAssertionSteps[CredentialRecord]#Step20 = - 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] - step.tryNext shouldBe a[Failure[_]] - } - - it("A test case with a different signed signature counter fails.") { - val steps = finishAssertion( - credentialRepository = credentialRepository, - authenticatorData = new ByteArray( - Defaults.authenticatorData.getBytes.toVector - .updated(33, 42.toByte) - .toArray - ), - ) - val step: FinishAssertionSteps[CredentialRecord]#Step20 = - 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] - step.tryNext shouldBe a[Failure[_]] - } - } - - describe("21. Let storedSignCount be the stored signature counter value associated with credential.id. If authData.signCount is nonzero or storedSignCount is nonzero, then run the following sub-step:") { - describe("If authData.signCount is") { - def credentialRepository(signatureCount: Long) = - Helpers.CredentialRepositoryV2.withUser( - Defaults.user, - credentialId = Defaults.credentialId, - publicKeyCose = Defaults.credentialPublicKeyCose, - signatureCount = signatureCount, - ) - - describe( - "zero, then the stored signature counter value must also be zero." - ) { - val authenticatorData = new ByteArray( - Defaults.authenticatorData.getBytes - .updated(33, 0: Byte) - .updated(34, 0: Byte) - .updated(35, 0: Byte) - .updated(36, 0: Byte) - ) - val signature = TestAuthenticator.makeAssertionSignature( - authenticatorData, - Crypto.sha256(Defaults.clientDataJsonBytes), - Defaults.credentialKey.getPrivate, - ) - - it("Succeeds if the stored signature counter value is zero.") { - val cr = credentialRepository(0) - val steps = finishAssertion( - credentialRepository = cr, - authenticatorData = authenticatorData, - signature = signature, - validateSignatureCounter = true, - ) - val step: FinishAssertionSteps[CredentialRecord]#Step21 = - 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[_]] - step.next.resultV2.get.isSignatureCounterValid should be(true) - step.next.resultV2.get.getSignatureCount should be(0) - } - - it("Fails if the stored signature counter value is nonzero.") { - val cr = credentialRepository(1) - val steps = finishAssertion( - credentialRepository = cr, - authenticatorData = authenticatorData, - signature = signature, - validateSignatureCounter = true, - ) - val step: FinishAssertionSteps[CredentialRecord]#Step21 = - 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[_]] - step.tryNext.failed.get shouldBe an[ - InvalidSignatureCountException - ] - } - } - - describe("greater than storedSignCount:") { - val cr = credentialRepository(1336) - - describe( - "Update storedSignCount to be the value of authData.signCount." - ) { - it("An increasing signature counter always succeeds.") { - val steps = finishAssertion( - credentialRepository = cr, - validateSignatureCounter = true, - ) - val step: FinishAssertionSteps[CredentialRecord]#Step21 = - 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[_]] - step.next.resultV2.get.isSignatureCounterValid should be(true) - step.next.resultV2.get.getSignatureCount should be(1337) - } - } - } - - describe("less than or equal to storedSignCount:") { - val cr = credentialRepository(1337) - - describe("This is a signal that the authenticator may be cloned, i.e. at least two copies of the credential private key may exist and are being used in parallel. Relying Parties should incorporate this information into their risk scoring. Whether the Relying Party updates storedSignCount in this case, or not, or fails the authentication ceremony or not, is Relying Party-specific.") { - it("If signature counter validation is disabled, a nonincreasing signature counter succeeds.") { - val steps = finishAssertion( - credentialRepository = cr, - validateSignatureCounter = false, - ) - val step: FinishAssertionSteps[CredentialRecord]#Step21 = - 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[_]] - step.next.resultV2.get.isSignatureCounterValid should be( - false - ) - step.next.resultV2.get.getSignatureCount should be(1337) - } - - it("If signature counter validation is enabled, a nonincreasing signature counter fails.") { - val steps = finishAssertion( - credentialRepository = cr, - validateSignatureCounter = true, - ) - val step: FinishAssertionSteps[CredentialRecord]#Step21 = - 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[_]] - step.validations.failed.get shouldBe an[ - InvalidSignatureCountException - ] - step.tryNext shouldBe a[Failure[_]] - - result shouldBe a[Failure[_]] - result.failed.get shouldBe an[InvalidSignatureCountException] - result.failed.get - .asInstanceOf[InvalidSignatureCountException] - .getExpectedMinimum should equal(1338) - result.failed.get - .asInstanceOf[InvalidSignatureCountException] - .getReceived should equal(1337) - result.failed.get - .asInstanceOf[InvalidSignatureCountException] - .getCredentialId should equal(Defaults.credentialId) - } - } - } - } - } - - it("22. If all the above steps are successful, continue with the authentication ceremony as appropriate. Otherwise, fail the authentication ceremony.") { - val steps = finishAssertion( - credentialRepository = Helpers.CredentialRepositoryV2.withUser( - Defaults.user, - credentialId = Defaults.credentialId, - publicKeyCose = Defaults.credentialPublicKeyCose, - ) - ) - val step: FinishAssertionSteps[CredentialRecord]#Finished = - 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.runV2) shouldBe a[Success[_]] - - step.resultV2.get.isSuccess should be(true) - step.resultV2.get.getCredential.getCredentialId should equal( - Defaults.credentialId - ) - step.resultV2.get.getCredential.getUserHandle should equal( - Defaults.userHandle - ) - step.resultV2.get.getCredential.getCredentialId should equal( - step.resultV2.get.getCredential.getCredentialId - ) - step.resultV2.get.getCredential.getUserHandle should equal( - step.resultV2.get.getCredential.getUserHandle - ) - step.resultV2.get.getCredential.getPublicKeyCose should not be null - } - } - } - - describe("RelyingParty supports authenticating") { - it("a real RSA key.") { - val testData = RegistrationTestData.Packed.BasicAttestationRsaReal - - val credData = - testData.response.getResponse.getAttestation.getAuthenticatorData.getAttestedCredentialData.get - val credId: ByteArray = credData.getCredentialId - val publicKeyBytes: ByteArray = credData.getCredentialPublicKey - - val request: AssertionRequest = AssertionRequest - .builder() - .publicKeyCredentialRequestOptions( - JacksonCodecs.json.readValue( - """{ - "challenge": "drdVqKT0T-9PyQfkceSE94Q8ruW2I-w1gsamBisjuMw", - "rpId": "demo3.yubico.test", - "userVerification": "preferred", - "extensions": { - "appid": "https://demo3.yubico.test:8443" - } - }""", - classOf[PublicKeyCredentialRequestOptions], - ) - ) - .username(testData.userId.getName) - .build() - - val response: PublicKeyCredential[ - AuthenticatorAssertionResponse, - ClientAssertionExtensionOutputs, - ] = JacksonCodecs.json.readValue( - """{ - "type": "public-key", - "id": "ClvGfsNH8ulYnrKNd4fEgQ", - "response": { - "authenticatorData": "AU4Ai_91hLmkf2mxjxj_SJrA3qTIOjr6tw1rluqSp_4FAAAABA", - "clientDataJSON": "ew0KCSJ0eXBlIiA6ICJ3ZWJhdXRobi5nZXQiLA0KCSJjaGFsbGVuZ2UiIDogImRyZFZxS1QwVC05UHlRZmtjZVNFOTRROHJ1VzJJLXcxZ3NhbUJpc2p1TXciLA0KCSJvcmlnaW4iIDogImh0dHBzOi8vZGVtbzMueXViaWNvLnRlc3Q6ODQ0MyIsDQoJInRva2VuQmluZGluZyIgOiANCgl7DQoJCSJzdGF0dXMiIDogInN1cHBvcnRlZCINCgl9DQp9", - "signature": "1YYgnM1Nau6FQV2YK1qZDaoF6CHkFSxhaWac00dJNQemQueU_a1wE0hYy-g0O-ZwKn_MTtmfnwgjHxTRZx6v51eiuBpy-FlfkMmQHkz26MKKnQOK0Mc4kVjugvM0XlQ7E0hvsrdvVlmrwYc-U2IVfgRUw5rD-SbUctA_ZXc248LjyrgD_vhDWLR6I4nzmH_pe2tgKAQgohmzD4kVpVzS_T_M4Bn0Vcc5oUwNU4m57DiWDWCAR5BohKdajRgt8DUqBp9jvn9mgStIhEq1EIjhGdEE47WxVJaQb5IdHRaCNJ186x_ilsQvGT2Iy4s5C8IOkuffw07GesdpmJ8awtiA4A", - "userHandle": "NiBJtVMh4AmSpZYuJ--jnEWgFzZHHVbS6zx7HFgAjAc" - }, - "clientExtensionResults": { - "appid": false - } - }""", - new TypeReference[PublicKeyCredential[ - AuthenticatorAssertionResponse, - ClientAssertionExtensionOutputs, - ]]() {}, - ) - - val credRepo = Helpers.CredentialRepositoryV2.withUser( - testData.userId, - credentialId = testData.response.getId, - publicKeyCose = publicKeyBytes, - ) - val usernameRepo = Helpers.UsernameRepository.withUsers(testData.userId) - - val rp = RelyingParty - .builder() - .identity( - RelyingPartyIdentity - .builder() - .id("demo3.yubico.test") - .name("Yubico WebAuthn demo") - .build() - ) - .credentialRepositoryV2(credRepo) - .usernameRepository(usernameRepo) - .origins(Set("https://demo3.yubico.test:8443").asJava) - .build() - - val result = rp.finishAssertion( - FinishAssertionOptions - .builder() - .request(request) - .response(response) - .build() - ) - - result.isSuccess should be(true) - result.getCredential.getUserHandle should equal(testData.userId.getId) - result.getCredential.getCredentialId should equal(credId) - } - - it("an Ed25519 key.") { - val registrationRequest = JacksonCodecs - .json() - .readValue( - """ - |{ - | "rp": { - | "name": "Yubico WebAuthn demo", - | "id": "demo3.yubico.test" - | }, - | "user": { - | "name": "foo", - | "displayName": "Foo Bar", - | "id": "a2jHKZU9PDuGzwGaRQ5fVc8b_B3cfIOMZEiesm0Z-g0" - | }, - | "challenge": "FFDZDypegliApKZXF8XCHCn2SlMy4BVupeOFXDSr1uE", - | "pubKeyCredParams": [ - | { - | "alg": -8, - | "type": "public-key" - | } - | ], - | "excludeCredentials": [], - | "authenticatorSelection": { - | "requireResidentKey": false, - | "userVerification": "preferred" - | }, - | "attestation": "direct", - | "extensions": {} - |} - """.stripMargin, - classOf[PublicKeyCredentialCreationOptions], - ) - val registrationResponse = - PublicKeyCredential.parseRegistrationResponseJson(""" - |{ - | "type": "public-key", - | "id": "PMEuc5FHylmDzH9BgG0lf_YqsOKKspino-b5ybq8CD0mpwU3Q4S4oUMQd_CgQsJOR3qyv3HirclQM2lNIiyi3dytZ6p-zbfBxDCH637qWTTZTZfKPxKBsdEOVPMBPopU_9uNXKh9dTxqe4mpSuznjxV-cEMF3BU3CSnJDU1BOCM", - | "response": { - | "attestationObject": "o2NmbXRmcGFja2VkaGF1dGhEYXRhWOEBTgCL_3WEuaR_abGPGP9ImsDepMg6Ovq3DWuW6pKn_kUAAAAC-KAR84wKTRWABhcRH57cfQCAPMEuc5FHylmDzH9BgG0lf_YqsOKKspino-b5ybq8CD0mpwU3Q4S4oUMQd_CgQsJOR3qyv3HirclQM2lNIiyi3dytZ6p-zbfBxDCH637qWTTZTZfKPxKBsdEOVPMBPopU_9uNXKh9dTxqe4mpSuznjxV-cEMF3BU3CSnJDU1BOCOkAQEDJyAGIVggSRLgxGS7m40dHlC9RGF4pzIj4V03KEVLj1iZ8-4zpgFnYXR0U3RtdKNjYWxnJmNzaWdYRzBFAiA6fyJf8gJc5N0fUJtpKckvc6jg0SJitLYVbzA3bl5uBgIhAI11DQDK7c0nhJGh5ElJzhTOcvvTovCAd31CZ_6ZsdrJY3g1Y4FZAmgwggJkMIIBTKADAgECAgQHL7bPMA0GCSqGSIb3DQEBCwUAMA8xDTALBgNVBAMMBHRlc3QwHhcNMTkwNDI0MTExMDAyWhcNMjAwNDIzMTExMDAyWjBuMQswCQYDVQQGEwJTRTESMBAGA1UECgwJWXViaWNvIEFCMSIwIAYDVQQLDBlBdXRoZW50aWNhdG9yIEF0dGVzdGF0aW9uMScwJQYDVQQDDB5ZdWJpY28gVTJGIEVFIFNlcmlhbCAxMjA1Njc1MDMwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATFcdVF_m2S3VTnMBABD0ZO8b4dvbqdr7a9zxLi9VBkR5YPakd2coJoFiuEcEuRhNJwSXlJlDX8q3Y-dY_Qp1XYozQwMjAiBgkrBgEEAYLECgIEFTEuMy42LjEuNC4xLjQxNDgyLjEuMjAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4IBAQBm6U8jEfxKn5WqNe1r7LNlq80RVYQraj1V90Z-a1BFKEEDtRzmoNEGlaUVbmYrdv5u4lWd1abiSq7hWc4H7uTklC8wUt9F1qnSjDWkK45cYjwMpTtRavAQtX00R-8g1orIdSMAVsJ1RG-gqlvJhQWvlWQk8fHRBQ74MzVgUhutu74CgL8_-QjH1_2yEkAndj6slsTyNOCv2n60jJNzT9dk6oYE9HyvOuhYTc0IBAR5XsWQj1XXOof9CnARaC7C0P2Tn1yW0wjeP5St4i2aKuoL5tsaaSVk11hZ6XF2kjKjjqjow9uTyVIrn1NH-kwHf0cZSkPExkHLIl1JDtpMCE5R", - | "clientDataJSON": "ew0KCSJ0eXBlIiA6ICJ3ZWJhdXRobi5jcmVhdGUiLA0KCSJjaGFsbGVuZ2UiIDogIkZGRFpEeXBlZ2xpQXBLWlhGOFhDSENuMlNsTXk0QlZ1cGVPRlhEU3IxdUUiLA0KCSJvcmlnaW4iIDogImh0dHBzOi8vZGVtbzMueXViaWNvLnRlc3Q6ODQ0MyIsDQoJInRva2VuQmluZGluZyIgOiANCgl7DQoJCSJzdGF0dXMiIDogInN1cHBvcnRlZCINCgl9DQp9" - | }, - | "clientExtensionResults": {} - |} - | - """.stripMargin) - - val assertionRequest = JacksonCodecs - .json() - .readValue( - """{ - | "challenge": "YK17iD3fpOQKPSU6bxIU-TFBj1HNVSrX5bX5Pzj-SHQ", - | "rpId": "demo3.yubico.test", - | "allowCredentials": [ - | { - | "type": "public-key", - | "id": "PMEuc5FHylmDzH9BgG0lf_YqsOKKspino-b5ybq8CD0mpwU3Q4S4oUMQd_CgQsJOR3qyv3HirclQM2lNIiyi3dytZ6p-zbfBxDCH637qWTTZTZfKPxKBsdEOVPMBPopU_9uNXKh9dTxqe4mpSuznjxV-cEMF3BU3CSnJDU1BOCM" - | } - | ], - | "userVerification": "preferred", - | "extensions": { - | "appid": "https://demo3.yubico.test:8443" - | } - |} - |""".stripMargin, - classOf[PublicKeyCredentialRequestOptions], - ) - val assertionResponse = PublicKeyCredential.parseAssertionResponseJson( - """ - |{ - | "type": "public-key", - | "id": "PMEuc5FHylmDzH9BgG0lf_YqsOKKspino-b5ybq8CD0mpwU3Q4S4oUMQd_CgQsJOR3qyv3HirclQM2lNIiyi3dytZ6p-zbfBxDCH637qWTTZTZfKPxKBsdEOVPMBPopU_9uNXKh9dTxqe4mpSuznjxV-cEMF3BU3CSnJDU1BOCM", - | "response": { - | "authenticatorData": "AU4Ai_91hLmkf2mxjxj_SJrA3qTIOjr6tw1rluqSp_4FAAAACA", - | "clientDataJSON": "ew0KCSJ0eXBlIiA6ICJ3ZWJhdXRobi5nZXQiLA0KCSJjaGFsbGVuZ2UiIDogIllLMTdpRDNmcE9RS1BTVTZieElVLVRGQmoxSE5WU3JYNWJYNVB6ai1TSFEiLA0KCSJvcmlnaW4iIDogImh0dHBzOi8vZGVtbzMueXViaWNvLnRlc3Q6ODQ0MyIsDQoJInRva2VuQmluZGluZyIgOiANCgl7DQoJCSJzdGF0dXMiIDogInN1cHBvcnRlZCINCgl9DQp9", - | "signature": "YWVfTS-0-j6mRFG_fYBN9ApkhgjH89hyOVGaOuqxazXv1jA3YBQjoTurN43PebHPXDC6gNxjATUGxMvCq2t5Dg", - | "userHandle": null - | }, - | "clientExtensionResults": { - | "appid": false - | } - |} - """.stripMargin - ) - - val credData = - registrationResponse.getResponse.getAttestation.getAuthenticatorData.getAttestedCredentialData.get - val credId: ByteArray = credData.getCredentialId - val publicKeyBytes: ByteArray = credData.getCredentialPublicKey - - val credRepo = Helpers.CredentialRepositoryV2.withUser( - registrationRequest.getUser, - credentialId = registrationResponse.getId, - publicKeyCose = publicKeyBytes, - ) - val usernameRepo = - Helpers.UsernameRepository.withUsers(registrationRequest.getUser) - - val rp = RelyingParty - .builder() - .identity( - RelyingPartyIdentity - .builder() - .id("demo3.yubico.test") - .name("Yubico WebAuthn demo") - .build() - ) - .credentialRepositoryV2(credRepo) - .usernameRepository(usernameRepo) - .origins(Set("https://demo3.yubico.test:8443").asJava) - .build() - - val result = rp.finishAssertion( - FinishAssertionOptions - .builder() - .request( - AssertionRequest - .builder() - .publicKeyCredentialRequestOptions(assertionRequest) - .username(registrationRequest.getUser.getName) - .build() - ) - .response(assertionResponse) - .build() - ) - - result.isSuccess should be(true) - result.getCredential.getUserHandle should equal( - registrationRequest.getUser.getId - ) - result.getCredential.getCredentialId should equal(credId) - } - - it("a generated Ed25519 key.") { - val registrationTestData = - RegistrationTestData.Packed.BasicAttestationEdDsa - val testData = registrationTestData.assertion.get - - val rp = RelyingParty - .builder() - .identity( - RelyingPartyIdentity.builder().id("localhost").name("Test RP").build() - ) - .credentialRepositoryV2( - Helpers.CredentialRepositoryV2.withUser( - registrationTestData.userId, - credentialId = registrationTestData.response.getId, - publicKeyCose = - registrationTestData.response.getResponse.getParsedAuthenticatorData.getAttestedCredentialData.get.getCredentialPublicKey, - ) - ) - .usernameRepository( - Helpers.UsernameRepository.withUsers(registrationTestData.userId) - ) - .build() - - val result = rp.finishAssertion( - FinishAssertionOptions - .builder() - .request(testData.request) - .response(testData.response) - .build() - ) - - result.isSuccess should be(true) - result.getCredential.getUserHandle should equal( - registrationTestData.userId.getId - ) - result.getCredential.getCredentialId should equal( - registrationTestData.response.getId - ) - result.getCredential.getCredentialId should equal( - testData.response.getId - ) - } - - describe("an RS1 key") { - def test(registrationTestData: RegistrationTestData): Unit = { - val testData = registrationTestData.assertion.get - - val rp = RelyingParty - .builder() - .identity( - RelyingPartyIdentity - .builder() - .id("localhost") - .name("Test RP") - .build() - ) - .credentialRepositoryV2( - Helpers.CredentialRepositoryV2.withUser( - registrationTestData.userId, - credentialId = registrationTestData.response.getId, - publicKeyCose = - registrationTestData.response.getResponse.getParsedAuthenticatorData.getAttestedCredentialData.get.getCredentialPublicKey, - ) - ) - .usernameRepository( - Helpers.UsernameRepository.withUsers(registrationTestData.userId) - ) - .build() - - val result = rp.finishAssertion( - FinishAssertionOptions - .builder() - .request(testData.request) - .response(testData.response) - .build() - ) - - result.isSuccess should be(true) - result.getCredential.getUserHandle should equal( - registrationTestData.userId.getId - ) - result.getCredential.getCredentialId should equal( - registrationTestData.response.getId - ) - result.getCredential.getCredentialId should equal( - testData.response.getId - ) - } - - it("with basic attestation.") { - test(RegistrationTestData.Packed.BasicAttestationRs1) - } - it("with self attestation.") { - test(RegistrationTestData.Packed.SelfAttestationRs1) - } - } - - it("a U2F-formatted public key.") { - val testData = RealExamples.YubiKeyNeo.asRegistrationTestData - val x = BinaryUtil.fromHex( - "39C94FBBDDC694A925E6F8657C66916CFE84CD0222EDFCF281B21F5CDC347923" - ) - val y = BinaryUtil.fromHex( - "D6B0D2021CFE1724A6FE81E3568C4FFAE339298216A30AFC18C0B975F2E2A891" - ) - val u2fPubkey = - new ByteArray(BinaryUtil.concat(BinaryUtil.fromHex("04"), x, y)) - - val rp = RelyingParty - .builder() - .identity(testData.rpId) - .credentialRepositoryV2( - Helpers.CredentialRepositoryV2.withUser( - testData.userId, - credentialId = testData.assertion.get.response.getId, - publicKeyCose = - CredentialRecord.cosePublicKeyFromEs256Raw(u2fPubkey), - ) - ) - .usernameRepository( - Helpers.UsernameRepository.withUsers(testData.userId) - ) - .build() - - val result = rp.finishAssertion( - FinishAssertionOptions - .builder() - .request(testData.assertion.get.request) - .response(testData.assertion.get.response) - .build() - ) - - result.isSuccess should be(true) - result.getCredential.getUserHandle should equal(testData.userId.getId) - result.getCredential.getCredentialId should equal( - testData.response.getId - ) - } - } - - describe("The default RelyingParty settings") { - val testDataBase = RegistrationTestData.Packed.BasicAttestationEdDsa - val rp = RelyingParty - .builder() - .identity(testDataBase.rpId) - .credentialRepositoryV2( - Helpers.CredentialRepositoryV2.withUser( - testDataBase.userId, - credentialId = testDataBase.response.getId, - publicKeyCose = - testDataBase.response.getResponse.getParsedAuthenticatorData.getAttestedCredentialData.get.getCredentialPublicKey, - ) - ) - .build() - - describe("support the largeBlob extension") { - it("for writing a blob.") { - val result = rp.finishAssertion( - FinishAssertionOptions - .builder() - .request( - testDataBase.assertion.get.request.toBuilder - .publicKeyCredentialRequestOptions( - testDataBase.assertion.get.request.getPublicKeyCredentialRequestOptions.toBuilder - .extensions( - AssertionExtensionInputs - .builder() - .largeBlob( - LargeBlobAuthenticationInput - .write(ByteArray.fromHex("00010203")) - ) - .build() - ) - .build() - ) - .userHandle(testDataBase.userId.getId) - .build() - ) - .response( - testDataBase.assertion.get.response.toBuilder - .clientExtensionResults( - ClientAssertionExtensionOutputs - .builder() - .largeBlob( - LargeBlobAuthenticationOutput.write(true) - ) - .build() - ) - .build() - ) - .build() - ) - - result.getClientExtensionOutputs.get.getLargeBlob.get.getWritten.toScala should be( - Some(true) - ) - result.getClientExtensionOutputs.get.getLargeBlob.get.getBlob.toScala should be( - None - ) - } - - it("for reading a blob.") { - val result = rp.finishAssertion( - FinishAssertionOptions - .builder() - .request( - testDataBase.assertion.get.request.toBuilder - .publicKeyCredentialRequestOptions( - testDataBase.assertion.get.request.getPublicKeyCredentialRequestOptions.toBuilder - .extensions( - AssertionExtensionInputs - .builder() - .largeBlob(LargeBlobAuthenticationInput.read()) - .build() - ) - .build() - ) - .userHandle(testDataBase.userId.getId) - .build() - ) - .response( - testDataBase.assertion.get.response.toBuilder - .clientExtensionResults( - ClientAssertionExtensionOutputs - .builder() - .largeBlob( - LargeBlobAuthenticationOutput - .read(ByteArray.fromHex("00010203")) - ) - .build() - ) - .build() - ) - .build() - ) - - result.getClientExtensionOutputs.get.getLargeBlob.get.getBlob.toScala should be( - Some(ByteArray.fromHex("00010203")) - ) - result.getClientExtensionOutputs.get.getLargeBlob.get.getWritten.toScala should be( - None - ) - } - } - - describe("support the uvm extension") { - it("at authentication time.") { - - // Example from spec: https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#sctn-uvm-extension - // A1 -- extension: CBOR map of one element - // 63 -- Key 1: CBOR text string of 3 bytes - // 75 76 6d -- "uvm" [=UTF-8 encoded=] string - // 82 -- Value 1: CBOR array of length 2 indicating two factor usage - // 83 -- Item 1: CBOR array of length 3 - // 02 -- Subitem 1: CBOR integer for User Verification Method Fingerprint - // 04 -- Subitem 2: CBOR short for Key Protection Type TEE - // 02 -- Subitem 3: CBOR short for Matcher Protection Type TEE - // 83 -- Item 2: CBOR array of length 3 - // 04 -- Subitem 1: CBOR integer for User Verification Method Passcode - // 01 -- Subitem 2: CBOR short for Key Protection Type Software - // 01 -- Subitem 3: CBOR short for Matcher Protection Type Software - val uvmCborExample = ByteArray.fromHex("A16375766d828302040283040101") - - val cred = TestAuthenticator.createAssertionFromTestData( - testDataBase, - testDataBase.assertion.get.request.getPublicKeyCredentialRequestOptions, - authenticatorExtensions = - Some(JacksonCodecs.cbor().readTree(uvmCborExample.getBytes)), - ) - - val result = rp.finishAssertion( - FinishAssertionOptions - .builder() - .request( - testDataBase.assertion.get.request.toBuilder - .publicKeyCredentialRequestOptions( - testDataBase.assertion.get.request.getPublicKeyCredentialRequestOptions.toBuilder - .extensions( - AssertionExtensionInputs - .builder() - .uvm() - .build() - ) - .build() - ) - .userHandle(testDataBase.userId.getId) - .build() - ) - .response(cred) - .build() - ) - - result.getAuthenticatorExtensionOutputs.get.getUvm.toScala should equal( - Some( - List( - new UvmEntry( - UserVerificationMethod.USER_VERIFY_FINGERPRINT_INTERNAL, - KeyProtectionType.KEY_PROTECTION_TEE, - MatcherProtectionType.MATCHER_PROTECTION_TEE, - ), - new UvmEntry( - UserVerificationMethod.USER_VERIFY_PASSCODE_INTERNAL, - KeyProtectionType.KEY_PROTECTION_SOFTWARE, - MatcherProtectionType.MATCHER_PROTECTION_SOFTWARE, - ), - ).asJava - ) - ) - } - } - - 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() - ) - .credentialRepositoryV2( - Helpers.CredentialRepositoryV2.withUser( - user, - credentialId = credential.getId, - publicKeyCose = - credential.getResponse.getParsedAuthenticatorData.getAttestedCredentialData.get.getCredentialPublicKey, - ) - ) - .usernameRepository(Helpers.UsernameRepository.withUsers(user)) - .build() - - val request = AssertionRequest - .builder() - .publicKeyCredentialRequestOptions( - PublicKeyCredentialRequestOptions - .builder() - .challenge(ByteArray.fromBase64Url("Y2hhbGxlbmdl")) - .rpId("localhost") - .build() - ) - .username(user.getName) - .build() - - it("exposes isUserVerified() with the UV flag value in authenticator data.") { - val pkcWithoutUv = - TestAuthenticator.createAssertion( - flags = Some(new AuthenticatorDataFlags(0x00.toByte)), - challenge = - request.getPublicKeyCredentialRequestOptions.getChallenge, - credentialKey = credentialKeypair, - credentialId = credential.getId, - ) - val pkcWithUv = - TestAuthenticator.createAssertion( - flags = Some(new AuthenticatorDataFlags(0x04.toByte)), - challenge = - request.getPublicKeyCredentialRequestOptions.getChallenge, - credentialKey = credentialKeypair, - credentialId = credential.getId, - ) - - val resultWithoutUv = rp.finishAssertion( - FinishAssertionOptions - .builder() - .request(request) - .response(pkcWithoutUv) - .build() - ) - val resultWithUv = rp.finishAssertion( - FinishAssertionOptions - .builder() - .request(request) - .response(pkcWithUv) - .build() - ) - - resultWithoutUv.isUserVerified should be(false) - resultWithUv.isUserVerified should be(true) - } - - 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) - } - - it( - "exposes getAuthenticatorAttachment() with the authenticatorAttachment value from the PublicKeyCredential." - ) { - val pkcTemplate = - TestAuthenticator.createAssertion( - challenge = - request.getPublicKeyCredentialRequestOptions.getChallenge, - credentialKey = credentialKeypair, - credentialId = credential.getId, - ) - - forAll { authenticatorAttachment: Option[AuthenticatorAttachment] => - val pkc = pkcTemplate.toBuilder - .authenticatorAttachment(authenticatorAttachment.orNull) - .build() - - val result = rp.finishAssertion( - FinishAssertionOptions - .builder() - .request(request) - .response(pkc) - .build() - ) - - result.getAuthenticatorAttachment should equal( - pkc.getAuthenticatorAttachment - ) - } - } - } - } - } - } - -} diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyV2RegistrationSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyV2RegistrationSpec.scala deleted file mode 100644 index f623ac2ef..000000000 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyV2RegistrationSpec.scala +++ /dev/null @@ -1,4855 +0,0 @@ -// Copyright (c) 2023, Yubico AB -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, this -// list of conditions and the following disclaimer. -// -// 2. Redistributions in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -package com.yubico.webauthn - -import com.fasterxml.jackson.databind.JsonNode -import com.fasterxml.jackson.databind.node.ArrayNode -import com.fasterxml.jackson.databind.node.JsonNodeFactory -import com.fasterxml.jackson.databind.node.ObjectNode -import com.upokecenter.cbor.CBORObject -import com.yubico.internal.util.BinaryUtil -import com.yubico.internal.util.CertificateParser -import com.yubico.internal.util.JacksonCodecs -import com.yubico.webauthn.TestAuthenticator.AttestationCert -import com.yubico.webauthn.TestAuthenticator.AttestationMaker -import com.yubico.webauthn.TestAuthenticator.AttestationSigner -import com.yubico.webauthn.TpmAttestationStatementVerifier.Attributes -import com.yubico.webauthn.TpmAttestationStatementVerifier.TPM_ALG_NULL -import com.yubico.webauthn.TpmAttestationStatementVerifier.TpmRsaScheme -import com.yubico.webauthn.attestation.AttestationTrustSource -import com.yubico.webauthn.attestation.AttestationTrustSource.TrustRootsResult -import com.yubico.webauthn.data.AttestationObject -import com.yubico.webauthn.data.AttestationType -import com.yubico.webauthn.data.AuthenticatorAttachment -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 -import com.yubico.webauthn.data.COSEAlgorithmIdentifier -import com.yubico.webauthn.data.ClientRegistrationExtensionOutputs -import com.yubico.webauthn.data.CollectedClientData -import com.yubico.webauthn.data.Extensions.CredentialProperties.CredentialPropertiesOutput -import com.yubico.webauthn.data.Extensions.LargeBlob.LargeBlobRegistrationInput.LargeBlobSupport -import com.yubico.webauthn.data.Extensions.LargeBlob.LargeBlobRegistrationOutput -import com.yubico.webauthn.data.Extensions.Uvm.UvmEntry -import com.yubico.webauthn.data.Generators._ -import com.yubico.webauthn.data.PublicKeyCredential -import com.yubico.webauthn.data.PublicKeyCredentialCreationOptions -import com.yubico.webauthn.data.PublicKeyCredentialParameters -import com.yubico.webauthn.data.RegistrationExtensionInputs -import com.yubico.webauthn.data.RelyingPartyIdentity -import com.yubico.webauthn.data.UserIdentity -import com.yubico.webauthn.data.UserVerificationRequirement -import com.yubico.webauthn.exception.RegistrationFailedException -import com.yubico.webauthn.extension.uvm.KeyProtectionType -import com.yubico.webauthn.extension.uvm.MatcherProtectionType -import com.yubico.webauthn.extension.uvm.UserVerificationMethod -import com.yubico.webauthn.test.Helpers -import com.yubico.webauthn.test.RealExamples -import com.yubico.webauthn.test.Util.toStepWithUtilities -import org.bouncycastle.asn1.ASN1Encodable -import org.bouncycastle.asn1.ASN1ObjectIdentifier -import org.bouncycastle.asn1.DEROctetString -import org.bouncycastle.asn1.DERSequence -import org.bouncycastle.asn1.DERUTF8String -import org.bouncycastle.asn1.x500.AttributeTypeAndValue -import org.bouncycastle.asn1.x500.RDN -import org.bouncycastle.asn1.x500.X500Name -import org.bouncycastle.asn1.x509.Extension -import org.bouncycastle.asn1.x509.GeneralName -import org.bouncycastle.asn1.x509.GeneralNamesBuilder -import org.bouncycastle.cert.jcajce.JcaX500NameUtil -import org.bouncycastle.jce.provider.BouncyCastleProvider -import org.junit.runner.RunWith -import org.mockito.Mockito -import org.scalacheck.Arbitrary.arbitrary -import org.scalacheck.Gen -import org.scalatest.funspec.AnyFunSpec -import org.scalatest.matchers.should.Matchers -import org.scalatestplus.junit.JUnitRunner -import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks - -import java.io.IOException -import java.math.BigInteger -import java.nio.charset.Charset -import java.nio.charset.StandardCharsets -import java.security.KeyFactory -import java.security.KeyPair -import java.security.MessageDigest -import java.security.PrivateKey -import java.security.Security -import java.security.SignatureException -import java.security.cert.CRL -import java.security.cert.CertStore -import java.security.cert.CollectionCertStoreParameters -import java.security.cert.PolicyNode -import java.security.cert.X509Certificate -import java.security.interfaces.ECPublicKey -import java.security.interfaces.RSAPublicKey -import java.time.Clock -import java.time.Instant -import java.time.ZoneOffset -import java.util -import java.util.Collections -import java.util.Optional -import java.util.function.Predicate -import javax.security.auth.x500.X500Principal -import scala.jdk.CollectionConverters._ -import scala.jdk.OptionConverters.RichOption -import scala.jdk.OptionConverters.RichOptional -import scala.util.Failure -import scala.util.Success -import scala.util.Try - -@RunWith(classOf[JUnitRunner]) -class RelyingPartyV2RegistrationSpec - extends AnyFunSpec - with Matchers - with ScalaCheckDrivenPropertyChecks - with TestWithEachProvider { - - private def jsonFactory: JsonNodeFactory = JsonNodeFactory.instance - private def toJsonObject(obj: Map[String, JsonNode]): JsonNode = - jsonFactory.objectNode().setAll(obj.asJava) - private def toJson(obj: Map[String, String]): JsonNode = - toJsonObject(obj.view.mapValues(jsonFactory.textNode).toMap) - - private def sha256(bytes: ByteArray): ByteArray = Crypto.sha256(bytes) - - def flipByte(index: Int, bytes: ByteArray): ByteArray = - editByte(bytes, index, b => (0xff ^ b).toByte) - def editByte(bytes: ByteArray, index: Int, updater: Byte => Byte): ByteArray = - new ByteArray( - bytes.getBytes.updated(index, updater(bytes.getBytes()(index))) - ) - - private def finishRegistration[C <: CredentialRecord]( - allowOriginPort: Boolean = false, - allowOriginSubdomain: Boolean = false, - allowUntrustedAttestation: Boolean = false, - callerTokenBindingId: Option[ByteArray] = None, - credentialRepository: CredentialRepositoryV2[C] = - Helpers.CredentialRepositoryV2.unimplemented, - attestationTrustSource: Option[AttestationTrustSource] = None, - origins: Option[Set[String]] = None, - pubkeyCredParams: Option[List[PublicKeyCredentialParameters]] = None, - testData: RegistrationTestData, - clock: Clock = Clock.systemUTC(), - ): FinishRegistrationSteps = { - var builder = RelyingParty - .builder() - .identity(testData.rpId) - .credentialRepositoryV2(credentialRepository) - .allowOriginPort(allowOriginPort) - .allowOriginSubdomain(allowOriginSubdomain) - .allowUntrustedAttestation(allowUntrustedAttestation) - .clock(clock) - - attestationTrustSource.foreach { ats => - builder = builder.attestationTrustSource(ats) - } - - origins.map(_.asJava).foreach(builder.origins _) - - val fro = FinishRegistrationOptions - .builder() - .request( - pubkeyCredParams - .map(pkcp => - testData.request.toBuilder.pubKeyCredParams(pkcp.asJava).build() - ) - .getOrElse(testData.request) - ) - .response(testData.response) - .callerTokenBindingId(callerTokenBindingId.toJava) - .build() - - builder - .build() - ._finishRegistration(fro) - } - - val emptyTrustSource = new AttestationTrustSource { - override def findTrustRoots( - attestationCertificateChain: util.List[X509Certificate], - aaguid: Optional[ByteArray], - ): TrustRootsResult = - TrustRootsResult.builder().trustRoots(Collections.emptySet()).build() - } - def trustSourceWith( - trustedCert: X509Certificate, - crls: Option[Set[CRL]] = None, - enableRevocationChecking: Boolean = true, - policyTreeValidator: Option[Predicate[PolicyNode]] = None, - ): AttestationTrustSource = - (_: util.List[X509Certificate], _: Optional[ByteArray]) => { - TrustRootsResult - .builder() - .trustRoots(Collections.singleton(trustedCert)) - .certStore( - crls - .map(crls => - CertStore.getInstance( - "Collection", - new CollectionCertStoreParameters(crls.asJava), - ) - ) - .orNull - ) - .enableRevocationChecking(enableRevocationChecking) - .policyTreeValidator(policyTreeValidator.orNull) - .build() - } - - testWithEachProvider { it => - describe("§7.1. Registering a new credential") { - - describe("In order to perform a registration ceremony, the Relying Party MUST proceed as follows:") { - - describe("1. Let options be a new PublicKeyCredentialCreationOptions structure configured to the Relying Party's needs for the ceremony.") { - it("Nothing to test: applicable only to client side.") {} - } - - describe("2. Call navigator.credentials.create() and pass options as the publicKey option. Let credential be the result of the successfully resolved promise. If the promise is rejected, abort the ceremony with a user-visible error, or otherwise guide the user experience as might be determinable from the context available in the rejected promise. For example if the promise is rejected with an error code equivalent to \"InvalidStateError\", the user might be instructed to use a different authenticator. For information on different error contexts and the circumstances leading to them, see §6.3.2 The authenticatorMakeCredential Operation.") { - it("Nothing to test: applicable only to client side.") {} - } - - describe("3. Let response be credential.response.") { - it("If response is not an instance of AuthenticatorAttestationResponse, abort the ceremony with a user-visible error.") { - val testData = RegistrationTestData.Packed.BasicAttestationEdDsa - val frob = FinishRegistrationOptions - .builder() - .request(testData.request) - "frob.response(testData.response)" should compile - "frob.response(testData.assertion.get.response)" shouldNot compile - frob.response(testData.response).build() should not be null - } - } - - describe("4. Let clientExtensionResults be the result of calling credential.getClientExtensionResults().") { - it( - "The PublicKeyCredential class has a clientExtensionResults field" - ) { - val pkc = PublicKeyCredential.parseRegistrationResponseJson("""{ - "type": "public-key", - "id": "", - "response": { - "attestationObject": "o2NmbXRmcGFja2VkaGF1dGhEYXRhWQFXAU4Ai_91hLmkf2mxjxj_SJrA3qTIOjr6tw1rluqSp_5FAAAAAG1Eupv27C5JuTAMj-kgy3MAEApbxn7DR_LpWJ6yjXeHxIGkAQMDOQEAIFkBAPm_XOU-DioXdG6YXFo5gpHPNxJDimlbnXCro2D_hvzBsxoY4oEzNyRDgK_PoDedZ4tJyk12_I8qJ8g5HqbpT6YUekYegcP4ugL1Omr31gGqTwsF45fIITcSWXcoJbqPnwotbaM98Hu15mSIT8NeXDce0MVNYJ6PULRm6xiiWXHk1cxwrHd9xPCjww6CjRKDc06hP--noBbToW3xx43eh7kGlisWPeU1naIMe7CZAjIMhNlu_uxQssaPAhEXNzDENpK99ieUg290Ym4YNAGbWdW4irkeTt7h_yC-ARrJUu4ygwwGaqCTl9QIMrwZGuiQD11LC0uKraIA2YHaGa2UGKshQwEAAWdhdHRTdG10o2NhbGcmY3NpZ1hHMEUCIQDLKMt6O4aKJkl71VhyIcuI6lqyFTHMDuCO5Y4Jdq2_xQIgPm2_1GF0ivkR816opfVQMWq0s-Hx0uJjcX5l5tm9ZgFjeDVjgVkCwTCCAr0wggGloAMCAQICBCrnYmMwDQYJKoZIhvcNAQELBQAwLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290IENBIFNlcmlhbCA0NTcyMDA2MzEwIBcNMTQwODAxMDAwMDAwWhgPMjA1MDA5MDQwMDAwMDBaMG4xCzAJBgNVBAYTAlNFMRIwEAYDVQQKDAlZdWJpY28gQUIxIjAgBgNVBAsMGUF1dGhlbnRpY2F0b3IgQXR0ZXN0YXRpb24xJzAlBgNVBAMMHll1YmljbyBVMkYgRUUgU2VyaWFsIDcxOTgwNzA3NTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCoDhl5gQ9meEf8QqiVUV4S_Ca-Oax47MhcpIW9VEhqM2RDTmd3HaL3-SnvH49q8YubSRp_1Z1uP-okMynSGnj-jbDBqMCIGCSsGAQQBgsQKAgQVMS4zLjYuMS40LjEuNDE0ODIuMS4xMBMGCysGAQQBguUcAgEBBAQDAgQwMCEGCysGAQQBguUcAQEEBBIEEG1Eupv27C5JuTAMj-kgy3MwDAYDVR0TAQH_BAIwADANBgkqhkiG9w0BAQsFAAOCAQEAclfQPNzD4RVphJDW-A75W1MHI3PZ5kcyYysR3Nx3iuxr1ZJtB-F7nFQweI3jL05HtFh2_4xVIgKb6Th4eVcjMecncBaCinEbOcdP1sEli9Hk2eVm1XB5A0faUjXAPw_-QLFCjgXG6ReZ5HVUcWkB7riLsFeJNYitiKrTDXFPLy-sNtVNutcQnFsCerDKuM81TvEAigkIbKCGlq8M_NvBg5j83wIxbCYiyV7mIr3RwApHieShzLdJo1S6XydgQjC-_64G5r8C-8AVvNFR3zXXCpio5C3KRIj88HEEIYjf6h1fdLfqeIsq-cUUqbq5T-c4nNoZUZCysTB9v5EY4akp-A", - "clientDataJSON": "ew0KCSJ0eXBlIiA6ICJ3ZWJhdXRobi5jcmVhdGUiLA0KCSJjaGFsbGVuZ2UiIDogImxaMllKbUZ2YWkteGhYMElteG9fQlk1SkpVdmREa3JXd1ZGZllmcHQtNmciLA0KCSJvcmlnaW4iIDogImh0dHBzOi8vZGVtbzMueXViaWNvLnRlc3Q6ODQ0MyIsDQoJInRva2VuQmluZGluZyIgOiANCgl7DQoJCSJzdGF0dXMiIDogInN1cHBvcnRlZCINCgl9DQp9" - }, - "clientExtensionResults": { - "appidExclude": true, - "org.example.foo": "bar" - } - }""") - pkc.getClientExtensionResults.getExtensionIds should contain( - "appidExclude" - ) - } - } - - describe("5. Let JSONtext be the result of running UTF-8 decode on the value of response.clientDataJSON.") { - it("Fails if clientDataJSON is not valid UTF-8.") { - an[IOException] should be thrownBy new CollectedClientData( - new ByteArray(Array(-128)) - ) - } - } - - describe("6. Let C, the client data claimed as collected during the credential creation, be the result of running an implementation-specific JSON parser on JSONtext.") { - - it("Fails if clientDataJson is not valid JSON.") { - an[IOException] should be thrownBy new CollectedClientData( - new ByteArray("{".getBytes(Charset.forName("UTF-8"))) - ) - an[IOException] should be thrownBy finishRegistration( - testData = RegistrationTestData.FidoU2f.BasicAttestation - .copy(clientDataJson = "{") - ) - } - - it("Succeeds if clientDataJson is valid JSON.") { - val steps = finishRegistration( - testData = RegistrationTestData.FidoU2f.BasicAttestation.copy( - clientDataJson = """{ - "challenge": "", - "origin": "", - "type": "" - }""", - overrideRequest = - Some(RegistrationTestData.FidoU2f.BasicAttestation.request), - ) - ) - val step: FinishRegistrationSteps#Step6 = steps.begin - - step.validations shouldBe a[Success[_]] - step.clientData should not be null - step.tryNext shouldBe a[Success[_]] - } - } - - describe("7. Verify that the value of C.type is webauthn.create.") { - it("The default test case succeeds.") { - val steps = finishRegistration(testData = - RegistrationTestData.FidoU2f.BasicAttestation - ) - val step: FinishRegistrationSteps#Step7 = steps.begin.next - - step.validations shouldBe a[Success[_]] - } - - def assertFails(typeString: String): Unit = { - val steps = finishRegistration( - testData = RegistrationTestData.FidoU2f.BasicAttestation - .editClientData("type", typeString) - ) - val step: FinishRegistrationSteps#Step7 = steps.begin.next - - step.validations shouldBe a[Failure[_]] - step.validations.failed.get shouldBe an[IllegalArgumentException] - } - - it("""Any value other than "webauthn.create" fails.""") { - forAll { (typeString: String) => - whenever(typeString != "webauthn.create") { - assertFails(typeString) - } - } - forAll(Gen.alphaNumStr) { (typeString: String) => - whenever(typeString != "webauthn.create") { - assertFails(typeString) - } - } - } - - it("""The string "webauthn.get" fails.""") { - assertFails("webauthn.get") - } - } - - it("8. Verify that the value of C.challenge equals the base64url encoding of options.challenge.") { - val steps = finishRegistration( - testData = RegistrationTestData.FidoU2f.BasicAttestation.copy( - overrideRequest = Some( - RegistrationTestData.FidoU2f.BasicAttestation.request.toBuilder - .challenge(new ByteArray(Array.fill(16)(0))) - .build() - ) - ) - ) - val step: FinishRegistrationSteps#Step8 = steps.begin.next.next - - step.validations shouldBe a[Failure[_]] - step.validations.failed.get shouldBe an[IllegalArgumentException] - step.tryNext shouldBe a[Failure[_]] - } - - describe("9. Verify that the value of C.origin matches the Relying Party's origin.") { - - def checkAccepted( - origin: String, - origins: Option[Set[String]] = None, - allowOriginPort: Boolean = false, - allowOriginSubdomain: Boolean = false, - ): Unit = { - val steps = finishRegistration( - testData = RegistrationTestData.FidoU2f.BasicAttestation - .editClientData("origin", origin), - origins = origins, - allowOriginPort = allowOriginPort, - allowOriginSubdomain = allowOriginSubdomain, - ) - val step: FinishRegistrationSteps#Step9 = steps.begin.next.next.next - - step.validations shouldBe a[Success[_]] - step.tryNext shouldBe a[Success[_]] - } - - def checkRejected( - origin: String, - origins: Option[Set[String]] = None, - allowOriginPort: Boolean = false, - allowOriginSubdomain: Boolean = false, - ): Unit = { - val steps = finishRegistration( - testData = RegistrationTestData.FidoU2f.BasicAttestation - .editClientData("origin", origin), - origins = origins, - allowOriginPort = allowOriginPort, - allowOriginSubdomain = allowOriginSubdomain, - ) - val step: FinishRegistrationSteps#Step9 = steps.begin.next.next.next - - step.validations shouldBe a[Failure[_]] - step.validations.failed.get shouldBe an[IllegalArgumentException] - step.tryNext shouldBe a[Failure[_]] - } - - it("Fails if origin is different.") { - checkRejected(origin = "https://root.evil") - } - - describe("Explicit ports are") { - val origin = "https://localhost:8080" - it("by default not allowed.") { - checkRejected(origin = origin) - } - - it("allowed if RP opts in to it.") { - checkAccepted(origin = origin, allowOriginPort = true) - } - } - - describe("Subdomains are") { - val origin = "https://foo.localhost" - - it("by default not allowed.") { - checkRejected(origin = origin) - } - - it("allowed if RP opts in to it.") { - checkAccepted(origin = origin, allowOriginSubdomain = true) - } - } - - describe("Subdomains and explicit ports at the same time are") { - val origin = "https://foo.localhost:8080" - - it("by default not allowed.") { - checkRejected(origin = origin) - } - - it("not allowed if only subdomains are allowed.") { - checkRejected( - origin = origin, - allowOriginPort = false, - allowOriginSubdomain = true, - ) - } - - it("not allowed if only explicit ports are allowed.") { - checkRejected( - origin = origin, - allowOriginPort = true, - allowOriginSubdomain = false, - ) - } - - it("allowed if RP opts in to both.") { - checkAccepted( - origin = origin, - allowOriginPort = true, - allowOriginSubdomain = true, - ) - } - } - - describe("The examples in JavaDoc are correct:") { - def check( - origins: Set[String], - acceptOrigins: Iterable[String], - rejectOrigins: Iterable[String], - allowOriginPort: Boolean = false, - allowOriginSubdomain: Boolean = false, - ): Unit = { - for { origin <- acceptOrigins } { - it(s"${origin} is accepted.") { - checkAccepted( - origin = origin, - origins = Some(origins), - allowOriginPort = allowOriginPort, - allowOriginSubdomain = allowOriginSubdomain, - ) - } - } - - for { origin <- rejectOrigins } { - it(s"${origin} is rejected.") { - checkRejected( - origin = origin, - origins = Some(origins), - allowOriginPort = allowOriginPort, - allowOriginSubdomain = allowOriginSubdomain, - ) - } - } - } - - describe("For allowOriginPort:") { - val origins = Set( - "https://example.org", - "https://accounts.example.org", - "https://acme.com:8443", - ) - - describe("false,") { - check( - origins = origins, - acceptOrigins = List( - "https://example.org", - "https://accounts.example.org", - "https://acme.com:8443", - ), - rejectOrigins = List( - "https://example.org:8443", - "https://shop.example.org", - "https://acme.com", - "https://acme.com:9000", - ), - allowOriginPort = false, - ) - } - - describe("true,") { - check( - origins = origins, - acceptOrigins = List( - "https://example.org", - "https://example.org:8443", - "https://accounts.example.org", - "https://acme.com", - "https://acme.com:8443", - "https://acme.com:9000", - ), - rejectOrigins = List( - "https://shop.example.org" - ), - allowOriginPort = true, - ) - } - } - - describe("For allowOriginSubdomain:") { - val origins = Set("https://example.org", "https://acme.com:8443") - - describe("false,") { - check( - origins = origins, - acceptOrigins = List( - "https://example.org", - "https://acme.com:8443", - ), - rejectOrigins = List( - "https://example.org:8443", - "https://accounts.example.org", - "https://acme.com", - "https://shop.acme.com:8443", - ), - allowOriginSubdomain = false, - ) - } - - describe("true,") { - check( - origins = origins, - acceptOrigins = List( - "https://example.org", - "https://accounts.example.org", - "https://acme.com:8443", - "https://shop.acme.com:8443", - ), - rejectOrigins = List( - "https://example.org:8443", - "https://acme.com", - ), - allowOriginSubdomain = true, - ) - } - } - } - } - - describe("10. Verify that the value of C.tokenBinding.status matches the state of Token Binding for the TLS connection over which the assertion was obtained.") { - it("Verification succeeds if neither side uses token binding ID.") { - val steps = finishRegistration(testData = - RegistrationTestData.FidoU2f.BasicAttestation - ) - val step: FinishRegistrationSteps#Step10 = - steps.begin.next.next.next.next - - step.validations shouldBe a[Success[_]] - step.tryNext shouldBe a[Success[_]] - } - - it("Verification succeeds if client data specifies token binding is unsupported, and RP does not use it.") { - val steps = finishRegistration(testData = - RegistrationTestData.FidoU2f.BasicAttestation - .editClientData(_.without[ObjectNode]("tokenBinding")) - ) - val step: FinishRegistrationSteps#Step10 = - steps.begin.next.next.next.next - - step.validations shouldBe a[Success[_]] - step.tryNext shouldBe a[Success[_]] - } - - it("Verification succeeds if client data specifies token binding is supported, and RP does not use it.") { - val steps = finishRegistration(testData = - RegistrationTestData.FidoU2f.BasicAttestation - .editClientData( - "tokenBinding", - toJson(Map("status" -> "supported")), - ) - ) - val step: FinishRegistrationSteps#Step10 = - steps.begin.next.next.next.next - - step.validations shouldBe a[Success[_]] - step.tryNext shouldBe a[Success[_]] - } - - it("Verification fails if client data does not specify token binding status and RP specifies token binding ID.") { - val steps = finishRegistration( - callerTokenBindingId = - Some(ByteArray.fromBase64Url("YELLOWSUBMARINE")), - testData = RegistrationTestData.FidoU2f.BasicAttestation - .editClientData(_.without[ObjectNode]("tokenBinding")), - ) - val step: FinishRegistrationSteps#Step10 = - steps.begin.next.next.next.next - - step.validations shouldBe a[Failure[_]] - step.validations.failed.get shouldBe an[IllegalArgumentException] - step.tryNext shouldBe a[Failure[_]] - } - - it("Verification succeeds if client data does not specify token binding status and RP does not specify token binding ID.") { - val steps = finishRegistration( - callerTokenBindingId = None, - testData = RegistrationTestData.FidoU2f.BasicAttestation - .editClientData(_.without[ObjectNode]("tokenBinding")), - ) - val step: FinishRegistrationSteps#Step10 = - steps.begin.next.next.next.next - - step.validations shouldBe a[Success[_]] - step.tryNext shouldBe a[Success[_]] - } - - it("Verification fails if client data specifies token binding ID but RP does not.") { - val steps = finishRegistration( - callerTokenBindingId = None, - testData = - RegistrationTestData.FidoU2f.BasicAttestation.editClientData( - "tokenBinding", - toJson(Map("status" -> "present", "id" -> "YELLOWSUBMARINE")), - ), - ) - val step: FinishRegistrationSteps#Step10 = - steps.begin.next.next.next.next - - step.validations shouldBe a[Failure[_]] - step.validations.failed.get shouldBe an[IllegalArgumentException] - step.tryNext shouldBe a[Failure[_]] - } - - describe("If Token Binding was used on that TLS connection, also verify that C.tokenBinding.id matches the base64url encoding of the Token Binding ID for the connection.") { - it("Verification succeeds if both sides specify the same token binding ID.") { - val steps = finishRegistration( - callerTokenBindingId = - Some(ByteArray.fromBase64Url("YELLOWSUBMARINE")), - testData = - RegistrationTestData.FidoU2f.BasicAttestation.editClientData( - "tokenBinding", - toJson( - Map("status" -> "present", "id" -> "YELLOWSUBMARINE") - ), - ), - ) - val step: FinishRegistrationSteps#Step10 = - steps.begin.next.next.next.next - - step.validations shouldBe a[Success[_]] - step.tryNext shouldBe a[Success[_]] - } - - it("Verification fails if ID is missing from tokenBinding in client data.") { - val steps = finishRegistration( - callerTokenBindingId = - Some(ByteArray.fromBase64Url("YELLOWSUBMARINE")), - testData = - RegistrationTestData.FidoU2f.BasicAttestation.editClientData( - "tokenBinding", - toJson(Map("status" -> "present")), - ), - ) - val step: FinishRegistrationSteps#Step10 = - steps.begin.next.next.next.next - - step.validations shouldBe a[Failure[_]] - step.validations.failed.get shouldBe an[IllegalArgumentException] - step.tryNext shouldBe a[Failure[_]] - } - - it("Verification fails if RP specifies token binding ID but client does not support it.") { - val steps = finishRegistration( - callerTokenBindingId = - Some(ByteArray.fromBase64Url("YELLOWSUBMARINE")), - testData = RegistrationTestData.FidoU2f.BasicAttestation - .editClientData(_.without[ObjectNode]("tokenBinding")), - ) - val step: FinishRegistrationSteps#Step10 = - steps.begin.next.next.next.next - - step.validations shouldBe a[Failure[_]] - step.validations.failed.get shouldBe an[IllegalArgumentException] - step.tryNext shouldBe a[Failure[_]] - } - - it("Verification fails if RP specifies token binding ID but client does not use it.") { - val steps = finishRegistration( - callerTokenBindingId = - Some(ByteArray.fromBase64Url("YELLOWSUBMARINE")), - testData = - RegistrationTestData.FidoU2f.BasicAttestation.editClientData( - "tokenBinding", - toJson(Map("status" -> "supported")), - ), - ) - val step: FinishRegistrationSteps#Step10 = - steps.begin.next.next.next.next - - step.validations shouldBe a[Failure[_]] - step.validations.failed.get shouldBe an[IllegalArgumentException] - step.tryNext shouldBe a[Failure[_]] - } - - it("Verification fails if client data and RP specify different token binding IDs.") { - val steps = finishRegistration( - callerTokenBindingId = - Some(ByteArray.fromBase64Url("ORANGESUBMARINE")), - testData = - RegistrationTestData.FidoU2f.BasicAttestation.editClientData( - "tokenBinding", - toJson( - Map("status" -> "supported", "id" -> "YELLOWSUBMARINE") - ), - ), - ) - val step: FinishRegistrationSteps#Step10 = - steps.begin.next.next.next.next - - step.validations shouldBe a[Failure[_]] - step.validations.failed.get shouldBe an[IllegalArgumentException] - step.tryNext shouldBe a[Failure[_]] - } - } - } - - it("11. Let hash be the result of computing a hash over response.clientDataJSON using SHA-256.") { - val steps = finishRegistration(testData = - RegistrationTestData.FidoU2f.BasicAttestation - ) - val step: FinishRegistrationSteps#Step11 = - steps.begin.next.next.next.next.next - val digest = MessageDigest.getInstance("SHA-256") - - step.validations shouldBe a[Success[_]] - step.tryNext shouldBe a[Success[_]] - step.clientDataJsonHash should equal( - new ByteArray( - digest.digest( - RegistrationTestData.FidoU2f.BasicAttestation.clientDataJsonBytes.getBytes - ) - ) - ) - } - - it("12. Perform CBOR decoding on the attestationObject field of the AuthenticatorAttestationResponse structure to obtain the attestation statement format fmt, the authenticator data authData, and the attestation statement attStmt.") { - val steps = finishRegistration(testData = - RegistrationTestData.FidoU2f.BasicAttestation - ) - val step: FinishRegistrationSteps#Step12 = - steps.begin.next.next.next.next.next.next - - step.validations shouldBe a[Success[_]] - step.tryNext shouldBe a[Success[_]] - step.attestation.getFormat should equal("fido-u2f") - step.attestation.getAuthenticatorData should not be null - step.attestation.getAttestationStatement should not be null - } - - describe("13. Verify that the rpIdHash in authData is the SHA-256 hash of the RP ID expected by the Relying Party.") { - it("Fails if RP ID is different.") { - val steps = finishRegistration( - testData = RegistrationTestData.FidoU2f.BasicAttestation - .editAuthenticatorData { authData: ByteArray => - new ByteArray( - Array.fill[Byte](32)(0) ++ authData.getBytes.drop(32) - ) - } - ) - val step: FinishRegistrationSteps#Step13 = - steps.begin.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("Succeeds if RP ID is the same.") { - val steps = finishRegistration(testData = - RegistrationTestData.FidoU2f.BasicAttestation - ) - val step: FinishRegistrationSteps#Step13 = - steps.begin.next.next.next.next.next.next.next - - step.validations shouldBe a[Success[_]] - step.tryNext shouldBe a[Success[_]] - } - } - - { - val testData = RegistrationTestData.Packed.BasicAttestation - - def upOn(authData: ByteArray): ByteArray = - new ByteArray( - authData.getBytes - .updated(32, (authData.getBytes()(32) | 0x01).toByte) - ) - - def upOff(authData: ByteArray): ByteArray = - new ByteArray( - authData.getBytes - .updated(32, (authData.getBytes()(32) & 0xfe).toByte) - ) - - def uvOn(authData: ByteArray): ByteArray = - new ByteArray( - authData.getBytes - .updated(32, (authData.getBytes()(32) | 0x04).toByte) - ) - - def uvOff(authData: ByteArray): ByteArray = - new ByteArray( - authData.getBytes - .updated(32, (authData.getBytes()(32) & 0xfb).toByte) - ) - - def checks[Next <: FinishRegistrationSteps.Step[ - _ - ], Step <: FinishRegistrationSteps.Step[Next]]( - stepsToStep: FinishRegistrationSteps => Step - ) = { - def check[B]( - stepsToStep: FinishRegistrationSteps => Step - )(chk: Step => B)( - uvr: UserVerificationRequirement, - authDataEdit: ByteArray => ByteArray, - ): B = { - val steps = finishRegistration( - testData = testData - .copy( - authenticatorSelection = Some( - AuthenticatorSelectionCriteria - .builder() - .userVerification(uvr) - .build() - ) - ) - .editAuthenticatorData(authDataEdit) - ) - chk(stepsToStep(steps)) - } - - def checkFailsWith( - stepsToStep: FinishRegistrationSteps => Step - ): (UserVerificationRequirement, ByteArray => ByteArray) => Unit = - check(stepsToStep) { step => - step.validations shouldBe a[Failure[_]] - step.validations.failed.get shouldBe an[ - IllegalArgumentException - ] - step.tryNext shouldBe a[Failure[_]] - } - - def checkSucceedsWith( - stepsToStep: FinishRegistrationSteps => Step - ): (UserVerificationRequirement, ByteArray => ByteArray) => Unit = - check(stepsToStep) { step => - step.validations shouldBe a[Success[_]] - step.tryNext shouldBe a[Success[_]] - } - - (checkFailsWith(stepsToStep), checkSucceedsWith(stepsToStep)) - } - - describe("14. Verify that the User Present bit of the flags in authData is set.") { - val (checkFails, checkSucceeds) = checks[ - FinishRegistrationSteps#Step15, - FinishRegistrationSteps#Step14, - ](_.begin.next.next.next.next.next.next.next.next) - - it("Fails if UV is discouraged and flag is not set.") { - checkFails(UserVerificationRequirement.DISCOURAGED, upOff) - } - - it("Succeeds if UV is discouraged and flag is set.") { - checkSucceeds(UserVerificationRequirement.DISCOURAGED, upOn) - } - - it("Fails if UV is preferred and flag is not set.") { - checkFails(UserVerificationRequirement.PREFERRED, upOff) - } - - it("Succeeds if UV is preferred and flag is set.") { - checkSucceeds(UserVerificationRequirement.PREFERRED, upOn) - } - - it("Fails if UV is required and flag is not set.") { - checkFails( - UserVerificationRequirement.REQUIRED, - upOff _ andThen uvOn, - ) - } - - it("Succeeds if UV is required and flag is set.") { - checkSucceeds( - UserVerificationRequirement.REQUIRED, - upOn _ andThen uvOn, - ) - } - } - - describe("15. If user verification is required for this registration, verify that the User Verified bit of the flags in authData is set.") { - val (checkFails, checkSucceeds) = checks[ - FinishRegistrationSteps#Step16, - FinishRegistrationSteps#Step15, - ](_.begin.next.next.next.next.next.next.next.next.next) - - it("Succeeds if UV is discouraged and flag is not set.") { - checkSucceeds(UserVerificationRequirement.DISCOURAGED, uvOff) - } - - it("Succeeds if UV is discouraged and flag is set.") { - checkSucceeds(UserVerificationRequirement.DISCOURAGED, uvOn) - } - - it("Succeeds if UV is preferred and flag is not set.") { - checkSucceeds(UserVerificationRequirement.PREFERRED, uvOff) - } - - it("Succeeds if UV is preferred and flag is set.") { - checkSucceeds(UserVerificationRequirement.PREFERRED, uvOn) - } - - it("Fails if UV is required and flag is not set.") { - checkFails(UserVerificationRequirement.REQUIRED, uvOff) - } - - it("Succeeds if UV is required and flag is set.") { - checkSucceeds(UserVerificationRequirement.REQUIRED, uvOn) - } - } - } - - describe("16. Verify that the \"alg\" parameter in the credential public key in authData matches the alg attribute of one of the items in options.pubKeyCredParams.") { - it("An ES256 key succeeds if ES256 was a requested algorithm.") { - val testData = RegistrationTestData.FidoU2f.BasicAttestation - val result = finishRegistration( - testData = testData, - credentialRepository = Helpers.CredentialRepositoryV2.empty, - allowUntrustedAttestation = true, - ).run - - result should not be null - result.getPublicKeyCose should not be null - } - - it("An ES256 key fails if only RSA and EdDSA are allowed.") { - val testData = RegistrationTestData.FidoU2f.BasicAttestation - val result = Try( - finishRegistration( - testData = testData.copy( - overrideRequest = Some( - testData.request.toBuilder - .pubKeyCredParams( - List( - PublicKeyCredentialParameters.EdDSA, - PublicKeyCredentialParameters.RS256, - ).asJava - ) - .build() - ) - ), - credentialRepository = Helpers.CredentialRepositoryV2.empty, - allowUntrustedAttestation = true, - ).run - ) - - result shouldBe a[Failure[_]] - result.failed.get shouldBe an[IllegalArgumentException] - } - } - - describe("17. 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 a subset of the extensions requested by the Relying Party.") { - forAll(Extensions.subsetRegistrationExtensions) { - case (extensionInputs, clientExtensionOutputs, _) => - val steps = finishRegistration( - testData = RegistrationTestData.Packed.BasicAttestation.copy( - requestedExtensions = extensionInputs, - clientExtensionResults = clientExtensionOutputs, - ) - ) - val stepAfter: Try[FinishRegistrationSteps#Step18] = - steps.begin.next.next.next.next.next.next.next.next.next.next.tryNext - - stepAfter shouldBe a[Success[_]] - } - } - - it("Succeeds if clientExtensionResults is not a subset of the extensions requested by the Relying Party.") { - forAll(Extensions.unrequestedClientRegistrationExtensions) { - case (extensionInputs, clientExtensionOutputs, _) => - val steps = finishRegistration( - testData = RegistrationTestData.Packed.BasicAttestation.copy( - requestedExtensions = extensionInputs, - clientExtensionResults = clientExtensionOutputs, - ) - ) - val stepAfter: Try[FinishRegistrationSteps#Step18] = - steps.begin.next.next.next.next.next.next.next.next.next.next.tryNext - - stepAfter shouldBe a[Success[_]] - } - } - - it("Succeeds if authenticator extensions is a subset of the extensions requested by the Relying Party.") { - forAll(Extensions.subsetRegistrationExtensions) { - case ( - extensionInputs: RegistrationExtensionInputs, - _, - authenticatorExtensionOutputs: CBORObject, - ) => - val steps = finishRegistration( - testData = RegistrationTestData.Packed.BasicAttestation - .copy( - requestedExtensions = extensionInputs - ) - .editAuthenticatorData(authData => - new ByteArray( - authData.getBytes.updated( - 32, - (authData.getBytes()(32) | 0x80).toByte, - ) ++ authenticatorExtensionOutputs.EncodeToBytes() - ) - ) - ) - val stepAfter: Try[FinishRegistrationSteps#Step18] = - steps.begin.next.next.next.next.next.next.next.next.next.next.tryNext - - stepAfter shouldBe a[Success[_]] - } - } - - it("Succeeds if authenticator extensions is not a subset of the extensions requested by the Relying Party.") { - forAll( - Extensions.unrequestedAuthenticatorRegistrationExtensions - ) { - case ( - extensionInputs: RegistrationExtensionInputs, - _, - authenticatorExtensionOutputs: CBORObject, - ) => - val steps = finishRegistration( - testData = RegistrationTestData.Packed.BasicAttestation - .copy( - requestedExtensions = extensionInputs - ) - .editAuthenticatorData(authData => - new ByteArray( - authData.getBytes.updated( - 32, - (authData.getBytes()(32) | 0x80).toByte, - ) ++ authenticatorExtensionOutputs.EncodeToBytes() - ) - ) - ) - val stepAfter: Try[FinishRegistrationSteps#Step18] = - steps.begin.next.next.next.next.next.next.next.next.next.next.tryNext - - stepAfter shouldBe a[Success[_]] - } - } - } - - describe("18. Determine the attestation statement format by performing a USASCII case-sensitive match on fmt against the set of supported WebAuthn Attestation Statement Format Identifier values. An up-to-date list of registered WebAuthn Attestation Statement Format Identifier values is maintained in the IANA \"WebAuthn Attestation Statement Format Identifiers\" registry established by RFC8809.") { - def setup(format: String): FinishRegistrationSteps = { - finishRegistration( - testData = RegistrationTestData.FidoU2f.BasicAttestation - .setAttestationStatementFormat(format) - ) - } - - def checkUnknown(format: String): Unit = { - it(s"""Returns no known attestation statement verifier if fmt is "${format}".""") { - val steps = setup(format) - val step: FinishRegistrationSteps#Step18 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next - - step.validations shouldBe a[Success[_]] - step.tryNext shouldBe a[Success[_]] - step.format should equal(format) - step.attestationStatementVerifier.toScala shouldBe empty - } - } - - def checkKnown(format: String): Unit = { - it(s"""Returns a known attestation statement verifier if fmt is "${format}".""") { - val steps = setup(format) - val step: FinishRegistrationSteps#Step18 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next - - step.validations shouldBe a[Success[_]] - step.tryNext shouldBe a[Success[_]] - step.format should equal(format) - step.attestationStatementVerifier.toScala should not be empty - } - } - - checkKnown("android-safetynet") - checkKnown("fido-u2f") - checkKnown("none") - checkKnown("packed") - checkKnown("tpm") - - checkUnknown("android-key") - - checkUnknown("FIDO-U2F") - checkUnknown("Fido-U2F") - checkUnknown("bleurgh") - } - - describe("19. Verify that attStmt is a correct attestation statement, conveying a valid attestation signature, by using the attestation statement format fmt’s verification procedure given attStmt, authData and hash.") { - - describe("If allowUntrustedAttestation is set,") { - it("a fido-u2f attestation is still rejected if invalid.") { - val testData = RegistrationTestData.FidoU2f.BasicAttestation - .updateAttestationObject( - "attStmt", - { attStmtNode: JsonNode => - attStmtNode - .asInstanceOf[ObjectNode] - .set[ObjectNode]( - "sig", - jsonFactory.binaryNode(Array(0, 0, 0, 0)), - ) - }, - ) - val steps = finishRegistration( - testData = testData, - allowUntrustedAttestation = true, - ) - val step: FinishRegistrationSteps#Step19 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next - - step.validations shouldBe a[Failure[_]] - step.validations.failed.get.getCause shouldBe a[ - SignatureException - ] - step.tryNext shouldBe a[Failure[_]] - } - } - - describe("For the fido-u2f statement format,") { - it("the default test case is a valid basic attestation.") { - val steps = finishRegistration(testData = - RegistrationTestData.FidoU2f.BasicAttestation - ) - val step: FinishRegistrationSteps#Step19 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next - - step.validations shouldBe a[Success[_]] - step.attestationType should equal(AttestationType.BASIC) - step.tryNext shouldBe a[Success[_]] - } - - it("a test case with self attestation is valid.") { - val steps = finishRegistration(testData = - RegistrationTestData.FidoU2f.SelfAttestation - ) - val step: FinishRegistrationSteps#Step19 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next - - step.validations shouldBe a[Success[_]] - step.attestationType should equal( - AttestationType.SELF_ATTESTATION - ) - step.tryNext shouldBe a[Success[_]] - } - - it("a test case with different signed client data is not valid.") { - val testData = RegistrationTestData.FidoU2f.SelfAttestation - val steps = finishRegistration(testData = - RegistrationTestData.FidoU2f.BasicAttestation - ) - val step: FinishRegistrationSteps#Step19 = new steps.Step19( - Crypto.sha256( - new ByteArray( - testData.clientDataJsonBytes.getBytes.updated( - 20, - (testData.clientDataJsonBytes.getBytes()(20) + 1).toByte, - ) - ) - ), - new AttestationObject(testData.attestationObject), - Optional.of(new FidoU2fAttestationStatementVerifier), - ) - - step.validations shouldBe a[Failure[_]] - step.validations.failed.get shouldBe an[IllegalArgumentException] - step.tryNext shouldBe a[Failure[_]] - } - - def checkByteFlipFails(index: Int): Unit = { - val testData = RegistrationTestData.FidoU2f.BasicAttestation - .editAuthenticatorData { - flipByte(index, _) - } - - val steps = finishRegistration(testData = testData) - val step: FinishRegistrationSteps#Step19 = new steps.Step19( - Crypto.sha256(testData.clientDataJsonBytes), - new AttestationObject(testData.attestationObject), - Optional.of(new FidoU2fAttestationStatementVerifier), - ) - - step.validations shouldBe a[Failure[_]] - step.validations.failed.get shouldBe an[IllegalArgumentException] - step.tryNext shouldBe a[Failure[_]] - } - - it("a test case with a different signed RP ID hash is not valid.") { - checkByteFlipFails(0) - } - - it( - "a test case with a different signed credential ID is not valid." - ) { - checkByteFlipFails(32 + 1 + 4 + 16 + 2 + 1) - } - - it("a test case with a different signed credential public key is not valid.") { - val testData = RegistrationTestData.FidoU2f.BasicAttestation - .editAuthenticatorData { authenticatorData => - val decoded = new AuthenticatorData(authenticatorData) - val L = - decoded.getAttestedCredentialData.get.getCredentialId.getBytes.length - val evilPublicKey: ByteArray = - WebAuthnTestCodecs.publicKeyToCose( - TestAuthenticator - .generateKeypair( - COSEAlgorithmIdentifier - .fromPublicKey( - decoded.getAttestedCredentialData.get.getCredentialPublicKey - ) - .get - ) - .getPublic - ) - - new ByteArray( - authenticatorData.getBytes.take( - 32 + 1 + 4 + 16 + 2 + L - ) ++ evilPublicKey.getBytes - ) - } - val steps = finishRegistration(testData = testData) - val step: FinishRegistrationSteps#Step19 = new steps.Step19( - Crypto.sha256(testData.clientDataJsonBytes), - new AttestationObject(testData.attestationObject), - Optional.of(new FidoU2fAttestationStatementVerifier), - ) - - step.validations shouldBe a[Failure[_]] - step.validations.failed.get shouldBe an[IllegalArgumentException] - step.tryNext shouldBe a[Failure[_]] - } - - describe("if x5c is not a certificate for an ECDSA public key over the P-256 curve, stop verification and return an error.") { - val testAuthenticator = TestAuthenticator - - def checkRejected( - attestationAlg: COSEAlgorithmIdentifier, - keypair: KeyPair, - ): Unit = { - val (credential, _, _) = testAuthenticator - .createBasicAttestedCredential(attestationMaker = - AttestationMaker.fidoU2f( - new AttestationCert( - attestationAlg, - testAuthenticator.generateAttestationCertificate( - attestationAlg, - Some(keypair), - ), - ) - ) - ) - - val steps = finishRegistration( - testData = RegistrationTestData( - alg = COSEAlgorithmIdentifier.ES256, - attestationObject = - credential.getResponse.getAttestationObject, - clientDataJson = new String( - credential.getResponse.getClientDataJSON.getBytes, - "UTF-8", - ), - ) - ) - val step: FinishRegistrationSteps#Step19 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next - - val standaloneVerification = Try { - new FidoU2fAttestationStatementVerifier() - .verifyAttestationSignature( - credential.getResponse.getAttestation, - Crypto.sha256(credential.getResponse.getClientDataJSON), - ) - } - - step.validations shouldBe a[Failure[_]] - step.validations.failed.get shouldBe an[ - IllegalArgumentException - ] - step.tryNext shouldBe a[Failure[_]] - - standaloneVerification shouldBe a[Failure[_]] - standaloneVerification.failed.get shouldBe an[ - IllegalArgumentException - ] - } - - def checkAccepted( - attestationAlg: COSEAlgorithmIdentifier, - keypair: KeyPair, - ): Unit = { - val (credential, _, _) = testAuthenticator - .createBasicAttestedCredential(attestationMaker = - AttestationMaker.fidoU2f( - new AttestationCert( - attestationAlg, - testAuthenticator.generateAttestationCertificate( - attestationAlg, - Some(keypair), - ), - ) - ) - ) - - val steps = finishRegistration( - testData = RegistrationTestData( - alg = COSEAlgorithmIdentifier.ES256, - attestationObject = - credential.getResponse.getAttestationObject, - clientDataJson = new String( - credential.getResponse.getClientDataJSON.getBytes, - "UTF-8", - ), - ) - ) - val step: FinishRegistrationSteps#Step19 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next - - val standaloneVerification = Try { - new FidoU2fAttestationStatementVerifier() - .verifyAttestationSignature( - credential.getResponse.getAttestation, - Crypto.sha256(credential.getResponse.getClientDataJSON), - ) - } - - step.validations shouldBe a[Success[_]] - step.tryNext shouldBe a[Success[_]] - - standaloneVerification should equal(Success(true)) - } - - it("An RSA attestation certificate is rejected.") { - checkRejected( - COSEAlgorithmIdentifier.RS256, - testAuthenticator.generateRsaKeypair(), - ) - } - - it("A secp256r1 attestation certificate is accepted.") { - checkAccepted( - COSEAlgorithmIdentifier.ES256, - testAuthenticator.generateEcKeypair(curve = "secp256r1"), - ) - } - - it("A secp256k1 attestation certificate is rejected.") { - checkRejected( - COSEAlgorithmIdentifier.ES256, - testAuthenticator.generateEcKeypair(curve = "secp256k1"), - ) - } - } - } - - describe("For the none statement format,") { - def flipByte(index: Int, bytes: ByteArray): ByteArray = - new ByteArray( - bytes.getBytes - .updated(index, (0xff ^ bytes.getBytes()(index)).toByte) - ) - - def checkByteFlipSucceeds( - mutationDescription: String, - index: Int, - ): Unit = { - it(s"the default test case with mutated ${mutationDescription} is accepted.") { - val testData = RegistrationTestData.NoneAttestation.Default - .editAuthenticatorData { - flipByte(index, _) - } - - val steps = finishRegistration(testData = testData) - val step: FinishRegistrationSteps#Step19 = new steps.Step19( - Crypto.sha256(testData.clientDataJsonBytes), - new AttestationObject(testData.attestationObject), - Optional.of(new NoneAttestationStatementVerifier), - ) - - step.validations shouldBe a[Success[_]] - step.attestationType should equal(AttestationType.NONE) - step.tryNext shouldBe a[Success[_]] - } - } - - it("the default test case is accepted.") { - val steps = finishRegistration(testData = - RegistrationTestData.NoneAttestation.Default - ) - val step: FinishRegistrationSteps#Step19 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next - - step.validations shouldBe a[Success[_]] - step.attestationType should equal(AttestationType.NONE) - step.tryNext shouldBe a[Success[_]] - } - - checkByteFlipSucceeds("signature counter", 32 + 1) - checkByteFlipSucceeds("AAGUID", 32 + 1 + 4) - checkByteFlipSucceeds("credential ID", 32 + 1 + 4 + 16 + 2) - } - - describe("For the packed statement format") { - val verifier = new PackedAttestationStatementVerifier - - it("the attestation statement verifier implementation is PackedAttestationStatementVerifier.") { - val steps = finishRegistration(testData = - RegistrationTestData.Packed.BasicAttestation - ) - val step: FinishRegistrationSteps#Step19 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next - - step.getAttestationStatementVerifier.get shouldBe a[ - PackedAttestationStatementVerifier - ] - } - - describe("the verification procedure is:") { - describe("1. Verify that attStmt is valid CBOR conforming to the syntax defined above and perform CBOR decoding on it to extract the contained fields.") { - - it("Fails if attStmt.sig is a text value.") { - val testData = RegistrationTestData.Packed.BasicAttestation - .editAttestationObject( - "attStmt", - jsonFactory - .objectNode() - .set("sig", jsonFactory.textNode("foo")), - ) - - val result: Try[Boolean] = Try( - verifier.verifyAttestationSignature( - new AttestationObject(testData.attestationObject), - testData.clientDataJsonHash, - ) - ) - - result shouldBe a[Failure[_]] - result.failed.get shouldBe an[IllegalArgumentException] - } - - it("Fails if attStmt.sig is missing.") { - val testData = RegistrationTestData.Packed.BasicAttestation - .editAttestationObject( - "attStmt", - jsonFactory - .objectNode() - .set("x5c", jsonFactory.arrayNode()), - ) - - val result: Try[Boolean] = Try( - verifier.verifyAttestationSignature( - new AttestationObject(testData.attestationObject), - testData.clientDataJsonHash, - ) - ) - - result shouldBe a[Failure[_]] - result.failed.get shouldBe an[IllegalArgumentException] - } - } - - describe("2. If x5c is present:") { - it("The attestation type is identified as Basic.") { - val steps = finishRegistration(testData = - RegistrationTestData.Packed.BasicAttestation - ) - val step: FinishRegistrationSteps#Step19 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next - - step.validations shouldBe a[Success[_]] - step.tryNext shouldBe a[Success[_]] - step.attestationType should be(AttestationType.BASIC) - } - - describe("1. Verify that sig is a valid signature over the concatenation of authenticatorData and clientDataHash using the attestation public key in attestnCert with the algorithm specified in alg.") { - it("Succeeds for the default test case.") { - val testData = RegistrationTestData.Packed.BasicAttestation - val result: Try[Boolean] = Try( - verifier.verifyAttestationSignature( - new AttestationObject(testData.attestationObject), - testData.clientDataJsonHash, - ) - ) - result should equal(Success(true)) - } - - it("Succeeds for an RS1 test case.") { - val testData = - RegistrationTestData.Packed.BasicAttestationRs1 - - val result = verifier.verifyAttestationSignature( - new AttestationObject(testData.attestationObject), - testData.clientDataJsonHash, - ) - result should equal(true) - } - - it("Fail if the default test case is mutated.") { - val testData = RegistrationTestData.Packed.BasicAttestation - - val result: Try[Boolean] = Try( - verifier.verifyAttestationSignature( - new AttestationObject( - testData - .editAuthenticatorData({ authData: ByteArray => - new ByteArray( - authData.getBytes.updated( - 16, - if (authData.getBytes()(16) == 0) 1: Byte - else 0: Byte, - ) - ) - }) - .attestationObject - ), - testData.clientDataJsonHash, - ) - ) - result should equal(Success(false)) - } - } - - describe("2. Verify that attestnCert meets the requirements in § 8.2.1 Packed Attestation Statement Certificate Requirements.") { - it("Fails for an attestation signature with an invalid country code.") { - val authenticator = TestAuthenticator - val alg = COSEAlgorithmIdentifier.ES256 - val (badCert, key): (X509Certificate, PrivateKey) = - authenticator.generateAttestationCertificate( - alg = alg, - name = new X500Name( - "O=Yubico, C=AA, OU=Authenticator Attestation" - ), - ) - val (credential, _, _) = - authenticator.createBasicAttestedCredential( - attestationMaker = AttestationMaker.packed( - new AttestationCert(alg, (badCert, key)) - ) - ) - val result = Try( - verifier.verifyAttestationSignature( - credential.getResponse.getAttestation, - sha256(credential.getResponse.getClientDataJSON), - ) - ) - - result shouldBe a[Failure[_]] - result.failed.get shouldBe an[IllegalArgumentException] - } - - it("succeeds for the default test case.") { - val testData = RegistrationTestData.Packed.BasicAttestation - val result = verifier.verifyAttestationSignature( - new AttestationObject(testData.attestationObject), - testData.clientDataJsonHash, - ) - result should equal(true) - } - } - - describe("3. If attestnCert contains an extension with OID 1.3.6.1.4.1.45724.1.1.4 (id-fido-gen-ce-aaguid) verify that the value of this extension matches the aaguid in authenticatorData.") { - it("Succeeds for the default test case.") { - val testData = RegistrationTestData.Packed.BasicAttestation - val result = verifier.verifyAttestationSignature( - new AttestationObject(testData.attestationObject), - testData.clientDataJsonHash, - ) - - testData.packedAttestationCert.getNonCriticalExtensionOIDs.asScala should equal( - Set("1.3.6.1.4.1.45724.1.1.4") - ) - result should equal(true) - } - - it("Succeeds if the attestation certificate does not have the extension.") { - val testData = - RegistrationTestData.Packed.BasicAttestationWithoutAaguidExtension - - val result = verifier.verifyAttestationSignature( - new AttestationObject(testData.attestationObject), - testData.clientDataJsonHash, - ) - - testData.packedAttestationCert.getNonCriticalExtensionOIDs shouldBe null - result should equal(true) - } - - it("Fails if the attestation certificate has the extension and it does not match the AAGUID.") { - val testData = - RegistrationTestData.Packed.BasicAttestationWithWrongAaguidExtension - - val result = Try( - verifier.verifyAttestationSignature( - new AttestationObject(testData.attestationObject), - testData.clientDataJsonHash, - ) - ) - - testData.packedAttestationCert.getNonCriticalExtensionOIDs should not be empty - result shouldBe a[Failure[_]] - result.failed.get shouldBe an[IllegalArgumentException] - } - } - - describe("4. Optionally, inspect x5c and consult externally provided knowledge to determine whether attStmt conveys a Basic or AttCA attestation.") { - it("Nothing to test.") {} - } - - it("5. If successful, return implementation-specific values representing attestation type Basic, AttCA or uncertainty, and attestation trust path x5c.") { - val testData = RegistrationTestData.Packed.BasicAttestation - val steps = finishRegistration(testData = testData) - val step: FinishRegistrationSteps#Step19 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next - - step.validations shouldBe a[Success[_]] - step.tryNext shouldBe a[Success[_]] - step.attestationType should be(AttestationType.BASIC) - step.attestationTrustPath.toScala should not be empty - step.attestationTrustPath.get.asScala should be( - List(testData.packedAttestationCert) - ) - } - } - - describe( - "3. If x5c is not present, self attestation is in use." - ) { - val testDataBase = RegistrationTestData.Packed.SelfAttestation - - it("The attestation type is identified as SelfAttestation.") { - val steps = finishRegistration(testData = testDataBase) - val step: FinishRegistrationSteps#Step19 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next - - step.validations shouldBe a[Success[_]] - step.tryNext shouldBe a[Success[_]] - step.attestationType should be( - AttestationType.SELF_ATTESTATION - ) - } - - describe("1. Validate that alg matches the algorithm of the credentialPublicKey in authenticatorData.") { - it("Succeeds for the default test case.") { - val result = verifier.verifyAttestationSignature( - new AttestationObject(testDataBase.attestationObject), - testDataBase.clientDataJsonHash, - ) - - CBORObject - .DecodeFromBytes( - new AttestationObject( - testDataBase.attestationObject - ).getAuthenticatorData.getAttestedCredentialData.get.getCredentialPublicKey.getBytes - ) - .get(CBORObject.FromObject(3)) - .AsInt64Value should equal(-7) - new AttestationObject( - testDataBase.attestationObject - ).getAttestationStatement.get("alg").longValue should equal( - -7 - ) - result should equal(true) - } - - it("Fails if the alg is a different value.") { - def modifyAuthdataPubkeyAlg(authDataBytes: Array[Byte]) - : Array[Byte] = { - val authData = - new AuthenticatorData(new ByteArray(authDataBytes)) - val key = WebAuthnCodecs - .importCosePublicKey( - authData.getAttestedCredentialData.get.getCredentialPublicKey - ) - .asInstanceOf[RSAPublicKey] - val reencodedKey = WebAuthnTestCodecs.rsaPublicKeyToCose( - key, - COSEAlgorithmIdentifier.RS256, - ) - BinaryUtil.concat( - java.util.Arrays.copyOfRange( - authDataBytes, - 0, - 32 + 1 + 4 + 16 + 2, - ), - authData.getAttestedCredentialData.get.getCredentialId.getBytes, - reencodedKey.getBytes, - ) - } - - def modifyAttobjPubkeyAlg(attObjBytes: ByteArray) - : ByteArray = { - val attObj = - JacksonCodecs.cbor.readTree(attObjBytes.getBytes) - new ByteArray( - JacksonCodecs.cbor.writeValueAsBytes( - attObj - .asInstanceOf[ObjectNode] - .set( - "authData", - jsonFactory.binaryNode( - modifyAuthdataPubkeyAlg( - attObj.get("authData").binaryValue() - ) - ), - ) - ) - ) - } - - val testData = - RegistrationTestData.Packed.SelfAttestationRs1 - val attObj = new AttestationObject( - modifyAttobjPubkeyAlg( - testData.response.getResponse.getAttestationObject - ) - ) - - val result = Try( - verifier.verifyAttestationSignature( - attObj, - testData.clientDataJsonHash, - ) - ) - - CBORObject - .DecodeFromBytes( - attObj.getAuthenticatorData.getAttestedCredentialData.get.getCredentialPublicKey.getBytes - ) - .get(CBORObject.FromObject(3)) - .AsInt64Value should equal(-257) - attObj.getAttestationStatement - .get("alg") - .longValue should equal(-65535) - result shouldBe a[Failure[_]] - result.failed.get shouldBe an[IllegalArgumentException] - } - } - - describe("2. Verify that sig is a valid signature over the concatenation of authenticatorData and clientDataHash using the credential public key with alg.") { - it("Succeeds for the default test case.") { - val result = verifier.verifyAttestationSignature( - new AttestationObject(testDataBase.attestationObject), - testDataBase.clientDataJsonHash, - ) - result should equal(true) - } - - it("Succeeds for an RS1 test case.") { - val testData = - RegistrationTestData.Packed.SelfAttestationRs1 - val alg = COSEAlgorithmIdentifier - .fromPublicKey( - testData.response.getResponse.getParsedAuthenticatorData.getAttestedCredentialData.get.getCredentialPublicKey - ) - .get - alg should be(COSEAlgorithmIdentifier.RS1) - - val result = verifier.verifyAttestationSignature( - new AttestationObject(testData.attestationObject), - testData.clientDataJsonHash, - ) - result should equal(true) - } - - it("Fails if the attestation object is mutated.") { - val testData = testDataBase.editAuthenticatorData { - authData: ByteArray => - new ByteArray( - authData.getBytes.updated( - 16, - if (authData.getBytes()(16) == 0) 1: Byte - else 0: Byte, - ) - ) - } - val result = verifier.verifyAttestationSignature( - new AttestationObject(testData.attestationObject), - testData.clientDataJsonHash, - ) - result should equal(false) - } - - it("Fails if the client data is mutated.") { - val result = verifier.verifyAttestationSignature( - new AttestationObject(testDataBase.attestationObject), - sha256( - new ByteArray( - testDataBase.clientDataJson - .updated(4, 'ä') - .getBytes("UTF-8") - ) - ), - ) - result should equal(false) - } - - it("Fails if the client data hash is mutated.") { - val result = verifier.verifyAttestationSignature( - new AttestationObject(testDataBase.attestationObject), - new ByteArray( - testDataBase.clientDataJsonHash.getBytes.updated( - 7, - if ( - testDataBase.clientDataJsonHash.getBytes()(7) == 0 - ) 1: Byte - else 0: Byte, - ) - ), - ) - result should equal(false) - } - } - - it("3. If successful, return implementation-specific values representing attestation type Self and an empty attestation trust path.") { - val testData = RegistrationTestData.Packed.SelfAttestation - val steps = finishRegistration(testData = testData) - val step: FinishRegistrationSteps#Step19 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next - - step.validations shouldBe a[Success[_]] - step.tryNext shouldBe a[Success[_]] - step.attestationType should be( - AttestationType.SELF_ATTESTATION - ) - step.attestationTrustPath.toScala shouldBe empty - } - } - } - - describe( - "8.2.1. Packed Attestation Statement Certificate Requirements" - ) { - val testDataBase = RegistrationTestData.Packed.BasicAttestation - - describe("The attestation certificate MUST have the following fields/extensions:") { - it("Version MUST be set to 3 (which is indicated by an ASN.1 INTEGER with value 2).") { - val badCert = Mockito.mock(classOf[X509Certificate]) - val principal = new X500Principal( - "O=Yubico, C=SE, OU=Authenticator Attestation" - ) - Mockito.when(badCert.getVersion) thenReturn 2 - Mockito.when( - badCert.getSubjectX500Principal - ) thenReturn principal - Mockito.when(badCert.getBasicConstraints) thenReturn -1 - val result = Try( - verifier.verifyX5cRequirements(badCert, testDataBase.aaguid) - ) - - result shouldBe a[Failure[_]] - result.failed.get shouldBe an[IllegalArgumentException] - - verifier.verifyX5cRequirements( - testDataBase.packedAttestationCert, - testDataBase.aaguid, - ) should equal(true) - } - - describe("Subject field MUST be set to:") { - it("Subject-C: ISO 3166 code specifying the country where the Authenticator vendor is incorporated (PrintableString)") { - val badCert: X509Certificate = TestAuthenticator - .generateAttestationCertificate( - name = new X500Name( - "O=Yubico, C=AA, OU=Authenticator Attestation" - ) - ) - ._1 - val result = Try( - verifier.verifyX5cRequirements( - badCert, - testDataBase.aaguid, - ) - ) - - result shouldBe a[Failure[_]] - result.failed.get shouldBe an[IllegalArgumentException] - - verifier.verifyX5cRequirements( - testDataBase.packedAttestationCert, - testDataBase.aaguid, - ) should equal(true) - } - - it("Subject-O: Legal name of the Authenticator vendor (UTF8String)") { - val badCert: X509Certificate = TestAuthenticator - .generateAttestationCertificate( - name = - new X500Name("C=SE, OU=Authenticator Attestation") - ) - ._1 - val result = Try( - verifier.verifyX5cRequirements( - badCert, - testDataBase.aaguid, - ) - ) - - result shouldBe a[Failure[_]] - result.failed.get shouldBe an[IllegalArgumentException] - - verifier.verifyX5cRequirements( - testDataBase.packedAttestationCert, - testDataBase.aaguid, - ) should equal(true) - } - - it("""Subject-OU: Literal string "Authenticator Attestation" (UTF8String)""") { - val badCert: X509Certificate = TestAuthenticator - .generateAttestationCertificate( - name = new X500Name("O=Yubico, C=SE, OU=Foo") - ) - ._1 - val result = Try( - verifier.verifyX5cRequirements( - badCert, - testDataBase.aaguid, - ) - ) - - result shouldBe a[Failure[_]] - result.failed.get shouldBe an[IllegalArgumentException] - - verifier.verifyX5cRequirements( - testDataBase.packedAttestationCert, - testDataBase.aaguid, - ) should equal(true) - } - - describe( - "Subject-CN: A UTF8String of the vendor’s choosing" - ) { - it("Nothing to test") {} - } - } - - it("If the related attestation root certificate is used for multiple authenticator models, the Extension OID 1.3.6.1.4.1.45724.1.1.4 (id-fido-gen-ce-aaguid) MUST be present, containing the AAGUID as a 16-byte OCTET STRING. The extension MUST NOT be marked as critical.") { - val idFidoGenCeAaguid = "1.3.6.1.4.1.45724.1.1.4" - - val badCert: X509Certificate = TestAuthenticator - .generateAttestationCertificate( - name = new X500Name( - "O=Yubico, C=SE, OU=Authenticator Attestation" - ), - extensions = List( - ( - idFidoGenCeAaguid, - false, - new DEROctetString(Array[Byte](0, 1, 2, 3)), - ) - ), - ) - ._1 - val result = Try( - verifier.verifyX5cRequirements(badCert, testDataBase.aaguid) - ) - - result shouldBe a[Failure[_]] - result.failed.get shouldBe an[IllegalArgumentException] - - val badCertCritical: X509Certificate = TestAuthenticator - .generateAttestationCertificate( - name = new X500Name( - "O=Yubico, C=SE, OU=Authenticator Attestation" - ), - extensions = List( - ( - idFidoGenCeAaguid, - true, - new DEROctetString(testDataBase.aaguid.getBytes), - ) - ), - ) - ._1 - val resultCritical = Try( - verifier.verifyX5cRequirements( - badCertCritical, - testDataBase.aaguid, - ) - ) - - resultCritical shouldBe a[Failure[_]] - resultCritical.failed.get shouldBe an[ - IllegalArgumentException - ] - - val goodResult = Try( - verifier.verifyX5cRequirements(badCert, testDataBase.aaguid) - ) - - goodResult shouldBe a[Failure[_]] - goodResult.failed.get shouldBe an[IllegalArgumentException] - - verifier.verifyX5cRequirements( - testDataBase.packedAttestationCert, - testDataBase.aaguid, - ) should equal(true) - } - - it("The Basic Constraints extension MUST have the CA component set to false.") { - val result = Try( - verifier.verifyX5cRequirements( - testDataBase.attestationCertChain.last._1, - testDataBase.aaguid, - ) - ) - - result shouldBe a[Failure[_]] - result.failed.get shouldBe an[IllegalArgumentException] - - verifier.verifyX5cRequirements( - testDataBase.packedAttestationCert, - testDataBase.aaguid, - ) should equal(true) - } - - describe("An Authority Information Access (AIA) extension with entry id-ad-ocsp and a CRL Distribution Point extension [RFC5280] are both OPTIONAL as the status of many attestation certificates is available through authenticator metadata services. See, for example, the FIDO Metadata Service [FIDOMetadataService].") { - it("Nothing to test.") {} - } - } - } - } - - describe("The tpm statement format") { - - it("is supported.") { - val testData = RealExamples.WindowsHelloTpm.asRegistrationTestData - val steps = - finishRegistration( - testData = testData, - origins = - Some(Set("https://dev.d2urpypvrhb05x.amplifyapp.com")), - credentialRepository = Helpers.CredentialRepositoryV2.empty, - attestationTrustSource = Some( - trustSourceWith( - testData.attestationRootCertificate.get, - enableRevocationChecking = false, - policyTreeValidator = Some(_ => true), - ) - ), - ) - val step: FinishRegistrationSteps#Step19 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next - - step.validations shouldBe a[Success[_]] - step.tryNext shouldBe a[Success[_]] - step.run.getAttestationType should be( - AttestationType.ATTESTATION_CA - ) - } - - describe("is supported and accepts test-generated values:") { - - val emptySubject = new X500Name(Array.empty[RDN]) - val tcgAtTpmManufacturer = new AttributeTypeAndValue( - new ASN1ObjectIdentifier("2.23.133.2.1"), - new DERUTF8String("id:00000000"), - ) - val tcgAtTpmModel = new AttributeTypeAndValue( - new ASN1ObjectIdentifier("2.23.133.2.2"), - new DERUTF8String("TEST_Yubico_java-webauthn-server"), - ) - val tcgAtTpmVersion = new AttributeTypeAndValue( - new ASN1ObjectIdentifier("2.23.133.2.3"), - new DERUTF8String("id:00000000"), - ) - val tcgKpAikCertificate = new ASN1ObjectIdentifier("2.23.133.8.3") - - def makeCred( - authDataAndKeypair: Option[(ByteArray, KeyPair)] = None, - credKeyAlgorithm: COSEAlgorithmIdentifier = - TestAuthenticator.Defaults.keyAlgorithm, - clientDataJson: Option[String] = None, - subject: X500Name = emptySubject, - rdn: Array[AttributeTypeAndValue] = - Array(tcgAtTpmManufacturer, tcgAtTpmModel, tcgAtTpmVersion), - extendedKeyUsage: Array[ASN1Encodable] = - Array(tcgKpAikCertificate), - ver: Option[String] = Some("2.0"), - magic: ByteArray = - TpmAttestationStatementVerifier.TPM_GENERATED_VALUE, - `type`: ByteArray = - TpmAttestationStatementVerifier.TPM_ST_ATTEST_CERTIFY, - modifyAttestedName: ByteArray => ByteArray = an => an, - overrideCosePubkey: Option[ByteArray] = None, - aaguidInCert: Option[ByteArray] = None, - attributes: Option[Long] = None, - symmetric: Option[Int] = None, - scheme: Option[Int] = None, - ): ( - PublicKeyCredential[ - AuthenticatorAttestationResponse, - ClientRegistrationExtensionOutputs, - ], - KeyPair, - List[(X509Certificate, PrivateKey)], - ) = { - val (authData, credentialKeypair) = - authDataAndKeypair.getOrElse( - TestAuthenticator.createAuthenticatorData( - credentialKeypair = Some( - TestAuthenticator.Defaults.defaultKeypair( - credKeyAlgorithm - ) - ), - keyAlgorithm = credKeyAlgorithm, - ) - ) - - TestAuthenticator.createCredential( - authDataBytes = authData, - credentialKeypair = credentialKeypair, - clientDataJson = clientDataJson, - attestationMaker = AttestationMaker.tpm( - cert = AttestationSigner.ca( - alg = COSEAlgorithmIdentifier.ES256, - certSubject = subject, - aaguid = aaguidInCert, - certExtensions = List( - ( - Extension.subjectAlternativeName.getId, - true, - new GeneralNamesBuilder() - .addName( - new GeneralName(new X500Name(Array(new RDN(rdn)))) - ) - .build(), - ), - ( - Extension.extendedKeyUsage.getId, - true, - new DERSequence(extendedKeyUsage), - ), - ), - validFrom = Instant.now(), - validTo = Instant.now().plusSeconds(600), - ), - ver = ver, - magic = magic, - `type` = `type`, - modifyAttestedName = modifyAttestedName, - overrideCosePubkey = overrideCosePubkey, - attributes = attributes, - symmetric = symmetric, - scheme = scheme, - ), - ) - } - - def init( - testData: RegistrationTestData - ): FinishRegistrationSteps#Step19 = { - val steps = - finishRegistration( - credentialRepository = Helpers.CredentialRepositoryV2.empty, - testData = testData, - attestationTrustSource = Some( - trustSourceWith( - testData.attestationCertChain.last._1, - enableRevocationChecking = false, - ) - ), - ) - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next - } - - def check( - testData: RegistrationTestData, - pubKeyCredParams: Option[ - List[PublicKeyCredentialParameters] - ] = None, - ) = { - val steps = - finishRegistration( - testData = testData, - credentialRepository = Helpers.CredentialRepositoryV2.empty, - attestationTrustSource = Some( - trustSourceWith( - testData.attestationRootCertificate.getOrElse( - testData.attestationCertChain.last._1 - ), - enableRevocationChecking = false, - ) - ), - pubkeyCredParams = pubKeyCredParams, - clock = Clock.fixed( - TestAuthenticator.Defaults.certValidFrom, - ZoneOffset.UTC, - ), - ) - val step: FinishRegistrationSteps#Step19 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next - - step.validations shouldBe a[Success[_]] - step.tryNext shouldBe a[Success[_]] - step.run.getAttestationType should be( - AttestationType.ATTESTATION_CA - ) - } - - it("ES256.") { - check(RegistrationTestData.Tpm.ValidEs256) - } - it("ES384.") { - check(RegistrationTestData.Tpm.ValidEs384) - } - it("ES512.") { - check(RegistrationTestData.Tpm.ValidEs512) - } - it("RS256.") { - check(RegistrationTestData.Tpm.ValidRs256) - } - it("RS1.") { - check( - RegistrationTestData.Tpm.ValidRs1, - pubKeyCredParams = - Some(List(PublicKeyCredentialParameters.RS1)), - ) - } - - it("Default cert generator settings.") { - val testData = (RegistrationTestData.from _).tupled(makeCred()) - val step = init(testData) - - step.validations shouldBe a[Success[_]] - step.tryNext shouldBe a[Success[_]] - step.run.getAttestationType should be( - AttestationType.ATTESTATION_CA - ) - } - - describe("Verify that the public key specified by the parameters and unique fields of pubArea is identical to the credentialPublicKey in the attestedCredentialData in authenticatorData.") { - it("Fails when EC key is unrelated but on the same curve.") { - val testData = (RegistrationTestData.from _).tupled( - makeCred( - overrideCosePubkey = Some( - WebAuthnTestCodecs.ecPublicKeyToCose( - TestAuthenticator - .generateEcKeypair() - .getPublic - .asInstanceOf[ECPublicKey] - ) - ) - ) - ) - val step = init(testData) - - step.validations shouldBe a[Failure[_]] - step.tryNext shouldBe a[Failure[_]] - step.validations.failed.get.getMessage should include( - "EC X coordinate differs" - ) - } - - it("Fails when EC key is on a different curve.") { - val testData = (RegistrationTestData.from _).tupled( - makeCred( - overrideCosePubkey = Some( - WebAuthnTestCodecs.ecPublicKeyToCose( - TestAuthenticator - .generateEcKeypair("secp384r1") - .getPublic - .asInstanceOf[ECPublicKey] - ) - ) - ) - ) - val step = init(testData) - - step.validations shouldBe a[Failure[_]] - step.tryNext shouldBe a[Failure[_]] - step.validations.failed.get.getMessage should include( - "elliptic curve differs" - ) - } - - it("Fails when EC key has an inverted Y coordinate.") { - val (authData, keypair) = - TestAuthenticator.createAuthenticatorData(keyAlgorithm = - COSEAlgorithmIdentifier.ES256 - ) - - val cose = CBORObject.DecodeFromBytes( - WebAuthnTestCodecs - .ecPublicKeyToCose( - keypair.getPublic.asInstanceOf[ECPublicKey] - ) - .getBytes - ) - val yneg = TestAuthenticator.Es256PrimeModulus - .subtract( - new BigInteger(1, cose.get(-3).GetByteString()) - ) - val ynegBytes = yneg.toByteArray.dropWhile(_ == 0) - cose.Set( - -3, - Array.fill[Byte](32 - ynegBytes.length)(0) ++ ynegBytes, - ) - - val testData = (RegistrationTestData.from _).tupled( - makeCred( - authDataAndKeypair = Some((authData, keypair)), - overrideCosePubkey = - Some(new ByteArray(cose.EncodeToBytes())), - ) - ) - val step = init(testData) - - step.validations shouldBe a[Failure[_]] - step.tryNext shouldBe a[Failure[_]] - step.validations.failed.get.getMessage should include( - "EC Y coordinate differs" - ) - } - - it("Fails when RSA key is unrelated.") { - val (authData, keypair) = - TestAuthenticator.createAuthenticatorData(keyAlgorithm = - COSEAlgorithmIdentifier.RS256 - ) - val testData = (RegistrationTestData.from _).tupled( - makeCred( - authDataAndKeypair = Some((authData, keypair)), - overrideCosePubkey = Some( - WebAuthnTestCodecs.rsaPublicKeyToCose( - TestAuthenticator - .generateRsaKeypair() - .getPublic - .asInstanceOf[RSAPublicKey], - COSEAlgorithmIdentifier.RS256, - ) - ), - ) - ) - val step = init(testData) - - step.validations shouldBe a[Failure[_]] - step.tryNext shouldBe a[Failure[_]] - } - } - - it("""The "ver" property must equal "2.0".""") { - forAll( - Gen.option( - Gen.oneOf( - Gen.numStr, - for { - major <- arbitrary[Int] - minor <- arbitrary[Int] - } yield s"${major}.${minor}", - arbitrary[String], - ) - ) - ) { ver: Option[String] => - whenever(!ver.contains("2.0")) { - val testData = - (RegistrationTestData.from _).tupled(makeCred(ver = ver)) - val step = init(testData) - - step.validations shouldBe a[Failure[_]] - step.tryNext shouldBe a[Failure[_]] - } - } - } - - it("""Verify that magic is set to TPM_GENERATED_VALUE.""") { - forAll(byteArray(4)) { magic => - whenever( - magic != TpmAttestationStatementVerifier.TPM_GENERATED_VALUE - ) { - val testData = (RegistrationTestData.from _).tupled( - makeCred(magic = magic) - ) - val step = init(testData) - - step.validations shouldBe a[Failure[_]] - step.tryNext shouldBe a[Failure[_]] - } - } - } - - it("""Verify that type is set to TPM_ST_ATTEST_CERTIFY.""") { - forAll( - Gen.oneOf( - byteArray(2), - flipOneBit( - TpmAttestationStatementVerifier.TPM_ST_ATTEST_CERTIFY - ), - ) - ) { `type` => - whenever( - `type` != TpmAttestationStatementVerifier.TPM_ST_ATTEST_CERTIFY - ) { - val testData = (RegistrationTestData.from _).tupled( - makeCred(`type` = `type`) - ) - val step = init(testData) - - step.validations shouldBe a[Failure[_]] - step.tryNext shouldBe a[Failure[_]] - } - } - } - - it("""Verify that extraData is set to the hash of attToBeSigned using the hash algorithm employed in "alg".""") { - val testData = (RegistrationTestData.from _).tupled(makeCred()) - val json = JacksonCodecs.json() - val clientData = json - .readTree(testData.clientDataJson) - .asInstanceOf[ObjectNode] - clientData.set( - "challenge", - jsonFactory.textNode( - Crypto - .sha256( - ByteArray.fromBase64Url( - clientData.get("challenge").textValue - ) - ) - .getBase64Url - ), - ) - val mutatedTestData = testData.copy(clientDataJson = - json.writeValueAsString(clientData) - ) - val step = init(mutatedTestData) - - step.validations shouldBe a[Failure[_]] - step.tryNext shouldBe a[Failure[_]] - } - - it("Verify that attested contains a TPMS_CERTIFY_INFO structure as specified in [TPMv2-Part2] section 10.12.3, whose name field contains a valid Name for pubArea, as computed using the algorithm in the nameAlg field of pubArea using the procedure specified in [TPMv2-Part1] section 16.") { - forAll( - Gen.oneOf( - for { - flipBitIndex: Int <- - Gen.oneOf(Gen.const(0), Gen.posNum[Int]) - } yield (an: ByteArray) => - flipBit(flipBitIndex % (8 * an.size()))(an), - for { - attestedName <- arbitrary[ByteArray] - } yield (_: ByteArray) => attestedName, - ) - ) { (modifyAttestedName: ByteArray => ByteArray) => - val testData = (RegistrationTestData.from _).tupled( - makeCred(modifyAttestedName = modifyAttestedName) - ) - val step = init(testData) - - step.validations shouldBe a[Failure[_]] - step.tryNext shouldBe a[Failure[_]] - } - } - - it("Verify the sig is a valid signature over certInfo using the attestation public key in aikCert with the algorithm specified in alg.") { - val testData = (RegistrationTestData.from _).tupled(makeCred()) - forAll( - flipOneBit( - new ByteArray( - new AttestationObject( - testData.attestationObject - ).getAttestationStatement.get("sig").binaryValue() - ) - ) - ) { sig => - val mutatedTestData = testData.updateAttestationObject( - "attStmt", - attStmt => - attStmt - .asInstanceOf[ObjectNode] - .set[ObjectNode]( - "sig", - jsonFactory.binaryNode(sig.getBytes), - ), - ) - val step = init(mutatedTestData) - - step.validations shouldBe a[Failure[_]] - step.tryNext shouldBe a[Failure[_]] - } - } - - describe("Verify that aikCert meets the requirements in §8.3.1 TPM Attestation Statement Certificate Requirements.") { - it("Version MUST be set to 3.") { - val testData = - (RegistrationTestData.from _).tupled(makeCred()) - forAll(arbitrary[Byte] suchThat { _ != 2 }) { version => - val mutatedTestData = testData.updateAttestationObject( - "attStmt", - attStmt => { - val origAikCert = attStmt - .get("x5c") - .get(0) - .binaryValue - - val x509VerOffset = 12 - attStmt - .get("x5c") - .asInstanceOf[ArrayNode] - .set(0, origAikCert.updated(x509VerOffset, version)) - attStmt - }, - ) - val step = init(mutatedTestData) - - step.validations shouldBe a[Failure[_]] - step.tryNext shouldBe a[Failure[_]] - } - } - - describe("Subject field MUST be set to empty.") { - it("Fails if a subject is set.") { - val testData = (RegistrationTestData.from _).tupled( - makeCred(subject = - new X500Name( - Array( - new RDN( - Array( - tcgAtTpmManufacturer, - tcgAtTpmModel, - tcgAtTpmVersion, - ) - ) - ) - ) - ) - ) - val step = init(testData) - - step.validations shouldBe a[Failure[_]] - step.tryNext shouldBe a[Failure[_]] - } - } - - describe("The Subject Alternative Name extension MUST be set as defined in [TPMv2-EK-Profile] section 3.2.9.") { - it("Fails when manufacturer is absent.") { - val testData = (RegistrationTestData.from _).tupled( - makeCred(rdn = Array(tcgAtTpmModel, tcgAtTpmVersion)) - ) - val step = init(testData) - - step.validations shouldBe a[Failure[_]] - step.tryNext shouldBe a[Failure[_]] - } - - it("Fails when model is absent.") { - val testData = (RegistrationTestData.from _).tupled( - makeCred(rdn = - Array(tcgAtTpmManufacturer, tcgAtTpmVersion) - ) - ) - val step = init(testData) - - step.validations shouldBe a[Failure[_]] - step.tryNext shouldBe a[Failure[_]] - } - - it("Fails when version is absent.") { - val testData = (RegistrationTestData.from _).tupled( - makeCred(rdn = Array(tcgAtTpmManufacturer, tcgAtTpmModel)) - ) - val step = init(testData) - - step.validations shouldBe a[Failure[_]] - step.tryNext shouldBe a[Failure[_]] - } - } - - describe("The Extended Key Usage extension MUST contain the OID 2.23.133.8.3 (\"joint-iso-itu-t(2) internationalorganizations(23) 133 tcg-kp(8) tcg-kp-AIKCertificate(3)\").") { - it("Fails when extended key usage is empty.") { - val testData = (RegistrationTestData.from _).tupled( - makeCred(extendedKeyUsage = Array.empty) - ) - val step = init(testData) - - step.validations shouldBe a[Failure[_]] - step.tryNext shouldBe a[Failure[_]] - } - - it("""Fails when extended key usage contains only "serverAuth".""") { - val testData = (RegistrationTestData.from _).tupled( - makeCred(extendedKeyUsage = - Array(new ASN1ObjectIdentifier("1.3.6.1.5.5.7.3.1")) - ) - ) - val step = init(testData) - - step.validations shouldBe a[Failure[_]] - step.tryNext shouldBe a[Failure[_]] - } - } - - describe("The Basic Constraints extension MUST have the CA component set to false.") { - it( - "Fails when the attestation cert is a self-signed CA cert." - ) { - val testData = (RegistrationTestData.from _).tupled( - TestAuthenticator.createBasicAttestedCredential( - keyAlgorithm = COSEAlgorithmIdentifier.ES256, - attestationMaker = AttestationMaker.tpm( - AttestationSigner.selfsigned( - alg = COSEAlgorithmIdentifier.ES256, - certSubject = emptySubject, - issuerSubject = - Some(TestAuthenticator.Defaults.caCertSubject), - certExtensions = List( - ( - Extension.subjectAlternativeName.getId, - true, - new GeneralNamesBuilder() - .addName( - new GeneralName( - new X500Name( - Array( - new RDN( - Array( - tcgAtTpmManufacturer, - tcgAtTpmModel, - tcgAtTpmVersion, - ) - ) - ) - ) - ) - ) - .build(), - ), - ( - Extension.extendedKeyUsage.getId, - true, - new DERSequence(tcgKpAikCertificate), - ), - ), - validFrom = Instant.now(), - validTo = Instant.now().plusSeconds(600), - isCa = true, - ) - ), - ) - ) - val step = init(testData) - testData.attestationCertChain.head._1.getBasicConstraints should not be (-1) - - step.validations shouldBe a[Failure[_]] - step.tryNext shouldBe a[Failure[_]] - } - } - - describe("An Authority Information Access (AIA) extension with entry id-ad-ocsp and a CRL Distribution Point extension [RFC5280] are both OPTIONAL as the status of many attestation certificates is available through metadata services. See, for example, the FIDO Metadata Service [FIDOMetadataService].") { - it("Nothing to test.") {} - } - } - - describe("If aikCert contains an extension with OID 1.3.6.1.4.1.45724.1.1.4 (id-fido-gen-ce-aaguid) verify that the value of this extension matches the aaguid in authenticatorData.") { - it("Succeeds if the cert does not have the extension.") { - val testData = (RegistrationTestData.from _).tupled( - makeCred(aaguidInCert = None) - ) - val step = init(testData) - - step.validations shouldBe a[Success[_]] - step.tryNext shouldBe a[Success[_]] - } - - it( - "Succeeds if the cert has the extension with the right value." - ) { - forAll(byteArray(16)) { aaguid => - val (authData, keypair) = - TestAuthenticator.createAuthenticatorData( - aaguid = aaguid, - credentialKeypair = Some( - TestAuthenticator.Defaults.defaultKeypair( - COSEAlgorithmIdentifier.ES256 - ) - ), - ) - val testData = (RegistrationTestData.from _).tupled( - makeCred( - authDataAndKeypair = Some((authData, keypair)), - aaguidInCert = Some(aaguid), - ) - ) - val step = init(testData) - - step.validations shouldBe a[Success[_]] - step.tryNext shouldBe a[Success[_]] - } - } - - it( - "Fails if the cert has the extension with the wrong value." - ) { - forAll(byteArray(16), byteArray(16)) { - (aaguidInCred, aaguidInCert) => - whenever(aaguidInCred != aaguidInCert) { - val (authData, keypair) = - TestAuthenticator.createAuthenticatorData( - aaguid = aaguidInCred, - credentialKeypair = Some( - TestAuthenticator.Defaults.defaultKeypair( - COSEAlgorithmIdentifier.ES256 - ) - ), - ) - val testData = (RegistrationTestData.from _).tupled( - makeCred( - authDataAndKeypair = Some((authData, keypair)), - aaguidInCert = Some(aaguidInCert), - ) - ) - val step = init(testData) - - step.validations shouldBe a[Failure[_]] - step.tryNext shouldBe a[Failure[_]] - } - } - } - } - - describe("Other requirements:") { - it("RSA keys must have the SIGN_ENCRYPT attribute.") { - forAll( - Gen.chooseNum(0, Int.MaxValue.toLong * 2 + 1), - minSuccessful(5), - ) { attributes: Long => - val testData = (RegistrationTestData.from _).tupled( - makeCred( - credKeyAlgorithm = COSEAlgorithmIdentifier.RS256, - attributes = Some(attributes & ~Attributes.SIGN_ENCRYPT), - ) - ) - val step = init(testData) - testData.alg should be(COSEAlgorithmIdentifier.RS256) - - step.validations shouldBe a[Failure[_]] - step.tryNext shouldBe a[Failure[_]] - } - } - - it("""RSA keys must have "symmetric" set to TPM_ALG_NULL""") { - forAll( - Gen.chooseNum(0, Short.MaxValue * 2 + 1), - minSuccessful(5), - ) { symmetric: Int => - whenever(symmetric != TPM_ALG_NULL) { - val testData = (RegistrationTestData.from _).tupled( - makeCred( - credKeyAlgorithm = COSEAlgorithmIdentifier.RS256, - symmetric = Some(symmetric), - ) - ) - val step = init(testData) - testData.alg should be(COSEAlgorithmIdentifier.RS256) - - step.validations shouldBe a[Failure[_]] - step.tryNext shouldBe a[Failure[_]] - } - } - } - - it("""RSA keys must have "scheme" set to TPM_ALG_RSASSA or TPM_ALG_NULL""") { - forAll( - Gen.chooseNum(0, Short.MaxValue * 2 + 1), - minSuccessful(5), - ) { scheme: Int => - whenever( - scheme != TpmRsaScheme.RSASSA && scheme != TPM_ALG_NULL - ) { - val testData = (RegistrationTestData.from _).tupled( - makeCred( - credKeyAlgorithm = COSEAlgorithmIdentifier.RS256, - scheme = Some(scheme), - ) - ) - val step = init(testData) - testData.alg should be(COSEAlgorithmIdentifier.RS256) - - step.validations shouldBe a[Failure[_]] - step.tryNext shouldBe a[Failure[_]] - } - } - } - - it("ECC keys must have the SIGN_ENCRYPT attribute.") { - forAll( - Gen.chooseNum(0, Int.MaxValue.toLong * 2 + 1), - minSuccessful(5), - ) { attributes: Long => - val testData = (RegistrationTestData.from _).tupled( - makeCred( - credKeyAlgorithm = COSEAlgorithmIdentifier.ES256, - attributes = Some(attributes & ~Attributes.SIGN_ENCRYPT), - ) - ) - val step = init(testData) - testData.alg should be(COSEAlgorithmIdentifier.ES256) - - step.validations shouldBe a[Failure[_]] - step.tryNext shouldBe a[Failure[_]] - } - } - - it("""ECC keys must have "symmetric" set to TPM_ALG_NULL""") { - forAll( - Gen.chooseNum(0, Short.MaxValue * 2 + 1), - minSuccessful(5), - ) { symmetric: Int => - whenever(symmetric != TPM_ALG_NULL) { - val testData = (RegistrationTestData.from _).tupled( - makeCred( - credKeyAlgorithm = COSEAlgorithmIdentifier.ES256, - symmetric = Some(symmetric), - ) - ) - val step = init(testData) - testData.alg should be(COSEAlgorithmIdentifier.ES256) - - step.validations shouldBe a[Failure[_]] - step.tryNext shouldBe a[Failure[_]] - } - } - } - - it("""ECC keys must have "scheme" set to TPM_ALG_NULL""") { - forAll( - Gen.chooseNum(0, Short.MaxValue * 2 + 1), - minSuccessful(5), - ) { scheme: Int => - whenever(scheme != TPM_ALG_NULL) { - val testData = (RegistrationTestData.from _).tupled( - makeCred( - credKeyAlgorithm = COSEAlgorithmIdentifier.ES256, - scheme = Some(scheme), - ) - ) - val step = init(testData) - testData.alg should be(COSEAlgorithmIdentifier.ES256) - - step.validations shouldBe a[Failure[_]] - step.tryNext shouldBe a[Failure[_]] - } - } - } - } - } - } - - ignore("The android-key statement format is supported.") { - val steps = finishRegistration(testData = - RegistrationTestData.AndroidKey.BasicAttestation - ) - val step: FinishRegistrationSteps#Step19 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next - - step.validations shouldBe a[Success[_]] - step.tryNext shouldBe a[Success[_]] - } - - describe("For the android-safetynet attestation statement format") { - val verifier = new AndroidSafetynetAttestationStatementVerifier - val testDataContainer = RegistrationTestData.AndroidSafetynet - val defaultTestData = testDataContainer.BasicAttestation - - it("the attestation statement verifier implementation is AndroidSafetynetAttestationStatementVerifier.") { - val steps = finishRegistration( - testData = defaultTestData, - allowUntrustedAttestation = false, - ) - val step: FinishRegistrationSteps#Step19 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next - - step.getAttestationStatementVerifier.get shouldBe an[ - AndroidSafetynetAttestationStatementVerifier - ] - } - - describe("the verification procedure is:") { - def checkFails(testData: RegistrationTestData): Unit = { - val result: Try[Boolean] = Try( - verifier.verifyAttestationSignature( - new AttestationObject(testData.attestationObject), - testData.clientDataJsonHash, - ) - ) - - result shouldBe a[Failure[_]] - result.failed.get shouldBe an[IllegalArgumentException] - } - - describe("1. Verify that attStmt is valid CBOR conforming to the syntax defined above and perform CBOR decoding on it to extract the contained fields.") { - it("Fails if attStmt.ver is a number value.") { - val testData = defaultTestData - .updateAttestationObject( - "attStmt", - attStmt => - attStmt - .asInstanceOf[ObjectNode] - .set[ObjectNode]("ver", jsonFactory.numberNode(123)), - ) - checkFails(testData) - } - - it("Fails if attStmt.ver is missing.") { - val testData = defaultTestData - .updateAttestationObject( - "attStmt", - attStmt => - attStmt - .asInstanceOf[ObjectNode] - .without[ObjectNode]("ver"), - ) - checkFails(testData) - } - - it("Fails if attStmt.response is a text value.") { - val testData = defaultTestData - .updateAttestationObject( - "attStmt", - attStmt => - attStmt - .asInstanceOf[ObjectNode] - .set[ObjectNode]( - "response", - jsonFactory.textNode( - new ByteArray( - attStmt.get("response").binaryValue() - ).getBase64Url - ), - ), - ) - checkFails(testData) - } - - it("Fails if attStmt.response is missing.") { - val testData = defaultTestData - .updateAttestationObject( - "attStmt", - attStmt => - attStmt - .asInstanceOf[ObjectNode] - .without[ObjectNode]("response"), - ) - checkFails(testData) - } - } - - describe("2. Verify that response is a valid SafetyNet response of version ver by following the steps indicated by the SafetyNet online documentation. As of this writing, there is only one format of the SafetyNet response and ver is reserved for future use.") { - it("Fails if there's a difference in the signature.") { - val testData = defaultTestData - .updateAttestationObject( - "attStmt", - attStmt => - attStmt - .asInstanceOf[ObjectNode] - .set[ObjectNode]( - "response", - jsonFactory.binaryNode( - editByte( - new ByteArray( - attStmt.get("response").binaryValue() - ), - 2000, - b => ((b + 1) % 26 + 0x41).toByte, - ).getBytes - ), - ), - ) - - val result: Try[Boolean] = Try( - verifier.verifyAttestationSignature( - new AttestationObject(testData.attestationObject), - testData.clientDataJsonHash, - ) - ) - - result shouldBe a[Success[_]] - result.get should be(false) - } - } - - describe("3. Verify that the nonce attribute in the payload of response is identical to the Base64 encoding of the SHA-256 hash of the concatenation of authenticatorData and clientDataHash.") { - it( - "Fails if an additional property is added to the client data." - ) { - val testData = defaultTestData.editClientData("foo", "bar") - checkFails(testData) - } - } - - describe("4. Verify that the SafetyNet response actually came from the SafetyNet service by following the steps in the SafetyNet online documentation.") { - it("Verify that attestationCert is issued to the hostname \"attest.android.com\".") { - checkFails(testDataContainer.WrongHostname) - } - - it("Verify that the ctsProfileMatch attribute in the payload of response is true.") { - checkFails(testDataContainer.FalseCtsProfileMatch) - } - } - - describe("5. If successful, return implementation-specific values representing attestation type Basic and attestation trust path x5c.") { - it("The real example succeeds.") { - val steps = finishRegistration( - testData = testDataContainer.RealExample - ) - val step: FinishRegistrationSteps#Step19 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next - - step.validations shouldBe a[Success[_]] - step.tryNext shouldBe a[Success[_]] - step.attestationType() should be(AttestationType.BASIC) - step.attestationTrustPath().get should not be empty - step.attestationTrustPath().get.size should be(2) - } - - it("The default test case succeeds.") { - val steps = finishRegistration(testData = - testDataContainer.BasicAttestation - ) - val step: FinishRegistrationSteps#Step19 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next - - step.validations shouldBe a[Success[_]] - step.tryNext shouldBe a[Success[_]] - step.attestationType() should be(AttestationType.BASIC) - step.attestationTrustPath().get should not be empty - step.attestationTrustPath().get.size should be(1) - } - } - } - } - - it("The android-safetynet statement format is supported.") { - val steps = finishRegistration( - testData = RegistrationTestData.AndroidSafetynet.RealExample - ) - val step: FinishRegistrationSteps#Step19 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next - - step.validations shouldBe a[Success[_]] - step.tryNext shouldBe a[Success[_]] - } - - it("The apple statement format is supported.") { - val steps = finishRegistration( - testData = RealExamples.AppleAttestationIos.asRegistrationTestData - ) - val step: FinishRegistrationSteps#Step19 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next - - step.validations shouldBe a[Success[_]] - step.tryNext shouldBe a[Success[_]] - } - - it("Unknown attestation statement formats are identified as such.") { - val steps = finishRegistration(testData = - RegistrationTestData.FidoU2f.BasicAttestation - .setAttestationStatementFormat("urgel") - ) - val step: FinishRegistrationSteps#Step19 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next - - step.validations shouldBe a[Success[_]] - step.tryNext shouldBe a[Success[_]] - step.attestationType should be(AttestationType.UNKNOWN) - step.attestationTrustPath.toScala shouldBe empty - } - - it("(Deleted) If verification of the attestation statement failed, the Relying Party MUST fail the registration ceremony.") { - val steps = finishRegistration(testData = - RegistrationTestData.FidoU2f.BasicAttestation - .editClientData("foo", "bar") - ) - val step14: FinishRegistrationSteps#Step19 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next - - step14.validations shouldBe a[Failure[_]] - Try(step14.next) shouldBe a[Failure[_]] - - Try(steps.run) shouldBe a[Failure[_]] - Try(steps.run).failed.get shouldBe an[IllegalArgumentException] - } - } - - describe("20. If validation is successful, obtain a list of acceptable trust anchors (i.e. attestation root certificates) for that attestation type and attestation statement format fmt, from a trusted source or from policy. For example, the FIDO Metadata Service [FIDOMetadataService] provides one way to obtain such information, using the aaguid in the attestedCredentialData in authData.") { - - val testData = RegistrationTestData.Packed.BasicAttestation - val (attestationRootCert, _) = - TestAuthenticator.generateAttestationCertificate() - - it("If an attestation trust source is set, it is used to get trust anchors.") { - val attestationTrustSource = new AttestationTrustSource { - override def findTrustRoots( - attestationCertificateChain: util.List[X509Certificate], - aaguid: Optional[ByteArray], - ): TrustRootsResult = - TrustRootsResult - .builder() - .trustRoots( - if ( - attestationCertificateChain - .get(0) - .equals( - CertificateParser.parseDer( - new AttestationObject( - testData.attestationObject - ).getAttestationStatement - .get("x5c") - .get(0) - .binaryValue() - ) - ) - ) { - Set(attestationRootCert).asJava - } else { - Set.empty[X509Certificate].asJava - } - ) - .build() - } - val steps = finishRegistration( - testData = testData, - attestationTrustSource = Some(attestationTrustSource), - ) - val step: FinishRegistrationSteps#Step20 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next - - step.validations shouldBe a[Success[_]] - step.getTrustRoots.toScala.map( - _.getTrustRoots.asScala - ) should equal( - Some(Set(attestationRootCert)) - ) - step.tryNext shouldBe a[Success[_]] - } - - it("When the AAGUID in authenticator data is zero, the AAGUID in the attestation certificate is used instead, if possible.") { - val example = RealExamples.SecurityKeyNfc - val testData = example.asRegistrationTestData - testData.aaguid should equal( - ByteArray.fromHex("00000000000000000000000000000000") - ) - val certAaguid = new ByteArray( - CertificateParser - .parseFidoAaguidExtension( - CertificateParser.parseDer(example.attestationCert.getBytes) - ) - .get - ) - - val attestationTrustSource = new AttestationTrustSource { - override def findTrustRoots( - attestationCertificateChain: util.List[X509Certificate], - aaguid: Optional[ByteArray], - ): TrustRootsResult = { - TrustRootsResult - .builder() - .trustRoots( - if (aaguid == Optional.of(certAaguid)) { - Set(attestationRootCert).asJava - } else { - Set.empty[X509Certificate].asJava - } - ) - .build() - } - } - val steps = finishRegistration( - testData = testData, - attestationTrustSource = Some(attestationTrustSource), - ) - val step: FinishRegistrationSteps#Step20 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next - - step.validations shouldBe a[Success[_]] - step.getTrustRoots.toScala.map( - _.getTrustRoots.asScala - ) should equal( - Some(Set(attestationRootCert)) - ) - step.tryNext shouldBe a[Success[_]] - } - - it( - "If an attestation trust source is not set, no trust anchors are returned." - ) { - val steps = finishRegistration( - testData = testData, - attestationTrustSource = None, - ) - val step: FinishRegistrationSteps#Step20 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next - - step.validations shouldBe a[Success[_]] - step.getTrustRoots.toScala shouldBe empty - step.tryNext shouldBe a[Success[_]] - } - } - - describe("21. Assess the attestation trustworthiness using the outputs of the verification procedure in step 19, as follows:") { - - describe("If no attestation was provided, verify that None attestation is acceptable under Relying Party policy.") { - describe("The default test case") { - it("is rejected if untrusted attestation is not allowed.") { - val steps = finishRegistration( - testData = RegistrationTestData.NoneAttestation.Default, - allowUntrustedAttestation = false, - ) - val step: FinishRegistrationSteps#Step21 = - steps.begin.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 - ] - step.attestationTrusted should be(false) - step.tryNext shouldBe a[Failure[_]] - } - - it("is accepted if untrusted attestation is allowed.") { - val steps = finishRegistration( - testData = RegistrationTestData.NoneAttestation.Default, - allowUntrustedAttestation = true, - ) - val step: FinishRegistrationSteps#Step21 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next - - step.validations shouldBe a[Success[_]] - step.attestationTrusted should be(false) - step.tryNext shouldBe a[Success[_]] - } - } - } - - describe("(Not in spec:) If an unknown attestation statement format was used, check if no attestation is acceptable under Relying Party policy.") { - val testData = RegistrationTestData.FidoU2f.BasicAttestation - .setAttestationStatementFormat("urgel") - - describe("The default test case") { - it("is rejected if untrusted attestation is not allowed.") { - val steps = finishRegistration( - testData = testData, - allowUntrustedAttestation = false, - ) - val step: FinishRegistrationSteps#Step21 = - steps.begin.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 - ] - step.attestationTrusted should be(false) - step.tryNext shouldBe a[Failure[_]] - } - - it("is accepted if untrusted attestation is allowed.") { - val steps = finishRegistration( - testData = testData, - allowUntrustedAttestation = true, - ) - val step: FinishRegistrationSteps#Step21 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next - - step.validations shouldBe a[Success[_]] - step.attestationTrusted should be(false) - step.tryNext shouldBe a[Success[_]] - } - } - } - - describe("If self attestation was used, verify that self attestation is acceptable under Relying Party policy.") { - - describe("The default test case, with self attestation,") { - it("is rejected if untrusted attestation is not allowed.") { - val steps = finishRegistration( - testData = RegistrationTestData.FidoU2f.SelfAttestation, - allowUntrustedAttestation = false, - ) - val step: FinishRegistrationSteps#Step21 = - steps.begin.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 - ] - step.attestationTrusted should be(false) - step.tryNext shouldBe a[Failure[_]] - } - - it("is accepted if untrusted attestation is allowed.") { - val steps = finishRegistration( - testData = RegistrationTestData.FidoU2f.SelfAttestation, - allowUntrustedAttestation = true, - ) - val step: FinishRegistrationSteps#Step21 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next - - step.validations shouldBe a[Success[_]] - step.attestationTrusted should be(false) - step.tryNext shouldBe a[Success[_]] - } - - it("is accepted if untrusted attestation is not allowed, but the self attestation key is a trust anchor.") { - val testData = RegistrationTestData.FidoU2f.SelfAttestation - val selfAttestationCert = CertificateParser.parseDer( - new AttestationObject( - testData.attestationObject - ).getAttestationStatement.get("x5c").get(0).binaryValue() - ) - val steps = finishRegistration( - testData = testData, - attestationTrustSource = Some( - trustSourceWith( - selfAttestationCert, - crls = Some( - Set( - TestAuthenticator.buildCrl( - JcaX500NameUtil.getX500Name( - selfAttestationCert.getSubjectX500Principal - ), - WebAuthnTestCodecs.importPrivateKey( - testData.privateKey.get, - testData.alg, - ), - "SHA256withECDSA", - currentTime = - TestAuthenticator.Defaults.certValidFrom, - nextUpdate = TestAuthenticator.Defaults.certValidTo, - ) - ) - ), - ) - ), - allowUntrustedAttestation = false, - clock = Clock.fixed( - TestAuthenticator.Defaults.certValidFrom, - ZoneOffset.UTC, - ), - ) - val step: FinishRegistrationSteps#Step21 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next - - step.validations shouldBe a[Success[_]] - step.attestationTrusted should be(true) - step.tryNext shouldBe a[Success[_]] - } - } - } - - describe("Otherwise, use the X.509 certificates returned as the attestation trust path from the verification procedure to verify that the attestation public key either correctly chains up to an acceptable root certificate, or is itself an acceptable certificate (i.e., it and the root certificate obtained in Step 20 may be the same).") { - - def generateTests( - testData: RegistrationTestData, - clock: Clock, - trustedRootCert: Option[X509Certificate] = None, - enableRevocationChecking: Boolean = true, - origins: Option[Set[String]] = None, - policyTreeValidator: Option[Predicate[PolicyNode]] = None, - ): Unit = { - it("is rejected if untrusted attestation is not allowed and the trust source does not trust it.") { - val steps = finishRegistration( - allowUntrustedAttestation = false, - testData = testData, - attestationTrustSource = Some(emptyTrustSource), - clock = clock, - origins = origins, - ) - val step: FinishRegistrationSteps#Step21 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next - - step.validations shouldBe a[Failure[_]] - step.attestationTrusted should be(false) - step.tryNext shouldBe a[Failure[_]] - } - - it("is accepted if untrusted attestation is allowed and the trust source does not trust it.") { - val steps = finishRegistration( - allowUntrustedAttestation = true, - testData = testData, - attestationTrustSource = Some(emptyTrustSource), - clock = clock, - origins = origins, - ) - val step: FinishRegistrationSteps#Step21 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next - - step.validations shouldBe a[Success[_]] - step.attestationTrusted should be(false) - step.tryNext shouldBe a[Success[_]] - } - - it("is accepted if the trust source trusts it.") { - val attestationTrustSource: Option[AttestationTrustSource] = - trustedRootCert - .orElse(testData.attestationCertChain.lastOption.map(_._1)) - .map( - trustSourceWith( - _, - crls = testData.attestationCertChain.lastOption - .map({ - case (cert, key) => - Set( - TestAuthenticator.buildCrl( - JcaX500NameUtil.getSubject(cert), - key, - "SHA256withECDSA", - clock.instant(), - clock.instant().plusSeconds(3600 * 24), - ) - ) - }), - enableRevocationChecking = enableRevocationChecking, - policyTreeValidator = policyTreeValidator, - ) - ) - val steps = finishRegistration( - testData = testData, - attestationTrustSource = attestationTrustSource, - clock = clock, - origins = origins, - ) - val step: FinishRegistrationSteps#Step21 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next - - step.validations shouldBe a[Success[_]] - step.attestationTrusted should be(true) - step.tryNext shouldBe a[Success[_]] - } - - it("is rejected if the attestation root cert appears in getCertStore but not in findTrustRoots.") { - val rootCert = trustedRootCert.getOrElse( - testData.attestationCertChain.last._1 - ) - val crl: Option[CRL] = - testData.attestationCertChain.lastOption - .map({ - case (cert, key) => - TestAuthenticator.buildCrl( - JcaX500NameUtil.getSubject(cert), - key, - "SHA256withECDSA", - clock.instant(), - clock.instant().plusSeconds(3600 * 24), - ) - }) - val certStore = CertStore.getInstance( - "Collection", - new CollectionCertStoreParameters( - (List(rootCert) ++ crl).asJava - ), - ) - - { - // First, check that the attestation is not trusted if the root cert appears only in getCertStore. - val attestationTrustSource = new AttestationTrustSource { - override def findTrustRoots( - attestationCertificateChain: util.List[X509Certificate], - aaguid: Optional[ByteArray], - ): TrustRootsResult = - TrustRootsResult - .builder() - .trustRoots(Collections.emptySet()) - .certStore(certStore) - .enableRevocationChecking(enableRevocationChecking) - .policyTreeValidator(policyTreeValidator.orNull) - .build() - } - val steps = finishRegistration( - testData = testData, - attestationTrustSource = Some(attestationTrustSource), - clock = clock, - origins = origins, - ) - val step: FinishRegistrationSteps#Step21 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next - - step.validations shouldBe a[Failure[_]] - step.attestationTrusted should be(false) - step.tryNext shouldBe a[Failure[_]] - } - - { - // Since the above assertions would also pass if the cert chain happens to be broken, or CRL resolution fails, etc, make sure that the attestation is indeed trusted if the root cert appears in findTrustRoots. - val attestationTrustSource = new AttestationTrustSource { - override def findTrustRoots( - attestationCertificateChain: util.List[X509Certificate], - aaguid: Optional[ByteArray], - ): TrustRootsResult = - TrustRootsResult - .builder() - .trustRoots(Collections.singleton(rootCert)) - .certStore(certStore) - .enableRevocationChecking(enableRevocationChecking) - .policyTreeValidator(policyTreeValidator.orNull) - .build() - } - val steps = finishRegistration( - testData = testData, - attestationTrustSource = Some(attestationTrustSource), - clock = clock, - origins = origins, - ) - val step: FinishRegistrationSteps#Step21 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next - - step.validations shouldBe a[Success[_]] - step.attestationTrusted should be(true) - step.tryNext shouldBe a[Success[_]] - } - } - } - - describe("An android-key basic attestation") { - ignore("fails for now.") { - fail("Test not implemented.") - } - } - - describe("An android-safetynet basic attestation") { - generateTests( - testData = RegistrationTestData.AndroidSafetynet.RealExample, - Clock - .fixed(Instant.parse("2019-01-01T00:00:00Z"), ZoneOffset.UTC), - trustedRootCert = Some( - CertificateParser.parsePem( - new String( - BinaryUtil.readAll( - getClass() - .getResourceAsStream("/globalsign-root-r2.pem") - ), - StandardCharsets.UTF_8, - ) - ) - ), - enableRevocationChecking = - false, // CRLs for this example are no longer available - ) - } - - describe("A fido-u2f basic attestation") { - generateTests( - testData = RegistrationTestData.FidoU2f.BasicAttestation, - Clock.fixed( - TestAuthenticator.Defaults.certValidFrom, - ZoneOffset.UTC, - ), - ) - } - - describe("A packed basic attestation") { - generateTests( - testData = RegistrationTestData.Packed.BasicAttestation, - Clock.fixed( - TestAuthenticator.Defaults.certValidFrom, - ZoneOffset.UTC, - ), - ) - } - - describe("A tpm attestation") { - val testData = RealExamples.WindowsHelloTpm.asRegistrationTestData - generateTests( - testData = testData, - clock = Clock.fixed( - Instant.parse("2022-08-25T16:00:00Z"), - ZoneOffset.UTC, - ), - origins = Some(Set(testData.clientData.getOrigin)), - trustedRootCert = Some(testData.attestationRootCertificate.get), - enableRevocationChecking = false, - policyTreeValidator = Some(_ => true), - ) - } - - describe("Critical certificate policy extensions") { - def init( - policyTreeValidator: Option[Predicate[PolicyNode]] - ): FinishRegistrationSteps#Step21 = { - val testData = - RealExamples.WindowsHelloTpm.asRegistrationTestData - val clock = Clock.fixed( - Instant.parse("2022-08-25T16:00:00Z"), - ZoneOffset.UTC, - ) - val steps = finishRegistration( - allowUntrustedAttestation = false, - origins = Some(Set(testData.clientData.getOrigin)), - testData = testData, - attestationTrustSource = Some( - trustSourceWith( - testData.attestationRootCertificate.get, - enableRevocationChecking = false, - policyTreeValidator = policyTreeValidator, - ) - ), - clock = clock, - ) - - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next - } - - it("are rejected if no policy tree validator is set.") { - // BouncyCastle provider does not reject critical policy extensions - // TODO Mark test as ignored instead of just skipping (assume() and cancel() currently break pitest) - if ( - !Security.getProviders - .exists(p => p.isInstanceOf[BouncyCastleProvider]) - ) { - val step = init(policyTreeValidator = None) - - step.validations shouldBe a[Failure[_]] - step.attestationTrusted should be(false) - step.tryNext shouldBe a[Failure[_]] - } - } - - it("are accepted if a policy tree validator is set and accepts the policy tree.") { - val step = init(policyTreeValidator = Some(_ => true)) - - step.validations shouldBe a[Success[_]] - step.attestationTrusted should be(true) - step.tryNext shouldBe a[Success[_]] - } - - it("are rejected if a policy tree validator is set and does not accept the policy tree.") { - val step = init(policyTreeValidator = Some(_ => false)) - - step.validations shouldBe a[Failure[_]] - step.attestationTrusted should be(false) - step.tryNext shouldBe a[Failure[_]] - } - } - } - } - - describe("22. Check that the credentialId is not yet registered to any other user. If registration is requested for a credential that is already registered to a different user, the Relying Party SHOULD fail this registration ceremony, or it MAY decide to accept the registration, e.g. while deleting the older registration.") { - - val testData = RegistrationTestData.FidoU2f.SelfAttestation - - it("Registration is aborted if the given credential ID is already registered.") { - val credentialRepository = - Helpers.CredentialRepositoryV2.withUser( - testData.userId, - credentialId = testData.response.getId, - publicKeyCose = - testData.response.getResponse.getAttestation.getAuthenticatorData.getAttestedCredentialData.get.getCredentialPublicKey, - signatureCount = 1337, - ) - - val steps = finishRegistration( - allowUntrustedAttestation = true, - testData = testData, - credentialRepository = credentialRepository, - ) - val step: FinishRegistrationSteps#Step22 = - 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] - step.tryNext shouldBe an[Failure[_]] - } - - it("Registration proceeds if the given credential ID is not already registered.") { - val steps = finishRegistration( - allowUntrustedAttestation = true, - testData = testData, - credentialRepository = Helpers.CredentialRepositoryV2.empty, - ) - val step: FinishRegistrationSteps#Step22 = - 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[_]] - } - } - - describe("23. If the attestation statement attStmt verified successfully and is found to be trustworthy, then register the new credential with the account that was denoted in options.user:") { - val testData = RegistrationTestData.FidoU2f.BasicAttestation - val steps = finishRegistration( - testData = testData, - attestationTrustSource = Some( - trustSourceWith( - testData.attestationCertChain.last._1, - crls = Some( - testData.attestationCertChain.tail - .map({ - case (cert, key) => - TestAuthenticator.buildCrl( - JcaX500NameUtil.getSubject(cert), - key, - "SHA256withECDSA", - TestAuthenticator.Defaults.certValidFrom, - TestAuthenticator.Defaults.certValidTo, - ) - }) - .toSet - ), - ) - ), - credentialRepository = Helpers.CredentialRepositoryV2.empty, - clock = Clock.fixed( - TestAuthenticator.Defaults.certValidFrom, - ZoneOffset.UTC, - ), - ) - val result = steps.run() - result.isAttestationTrusted should be(true) - - it("Associate the user’s account with the credentialId and credentialPublicKey in authData.attestedCredentialData, as appropriate for the Relying Party's system.") { - result.getKeyId.getId should be(testData.response.getId) - result.getPublicKeyCose should be( - testData.response.getResponse.getAttestation.getAuthenticatorData.getAttestedCredentialData.get.getCredentialPublicKey - ) - } - - it("Associate the credentialId with a new stored signature counter value initialized to the value of authData.signCount.") { - result.getSignatureCount should be( - testData.response.getResponse.getAttestation.getAuthenticatorData.getSignatureCounter - ) - } - - describe("It is RECOMMENDED to also:") { - it("Associate the credentialId with the transport hints returned by calling credential.response.getTransports(). This value SHOULD NOT be modified before or after storing it. It is RECOMMENDED to use this value to populate the transports of the allowCredentials option in future get() calls to help the client know how to find a suitable authenticator.") { - result.getKeyId.getTransports.toScala should equal( - Some( - testData.response.getResponse.getTransports - ) - ) - } - } - } - - describe("24. If the attestation statement attStmt successfully verified but is not trustworthy per step 21 above, the Relying Party SHOULD fail the registration ceremony.") { - it("The test case with self attestation succeeds, but reports attestation is not trusted.") { - val testData = RegistrationTestData.Packed.SelfAttestation - val steps = finishRegistration( - testData = testData, - allowUntrustedAttestation = true, - credentialRepository = Helpers.CredentialRepositoryV2.empty, - attestationTrustSource = Some(emptyTrustSource), - ) - steps.run.getKeyId.getId should be(testData.response.getId) - steps.run.isAttestationTrusted should be(false) - } - - describe("The test case with unknown attestation") { - val testData = RegistrationTestData.FidoU2f.BasicAttestation - .setAttestationStatementFormat("urgel") - - it("passes if the RP allows untrusted attestation.") { - val steps = finishRegistration( - testData = testData, - allowUntrustedAttestation = true, - credentialRepository = Helpers.CredentialRepositoryV2.empty, - ) - val result = Try(steps.run) - result shouldBe a[Success[_]] - result.get.isAttestationTrusted should be(false) - result.get.getAttestationType should be(AttestationType.UNKNOWN) - } - - it("fails if the RP required trusted attestation.") { - val steps = finishRegistration( - testData = testData, - allowUntrustedAttestation = false, - credentialRepository = Helpers.CredentialRepositoryV2.empty, - ) - val result = Try(steps.run) - result shouldBe a[Failure[_]] - result.failed.get shouldBe an[IllegalArgumentException] - } - } - - def testUntrusted(testData: RegistrationTestData): Unit = { - val fmt = - new AttestationObject(testData.attestationObject).getFormat - it(s"""A test case with good "${fmt}" attestation but no attestation trust source succeeds, but reports attestation as not trusted.""") { - val testData = RegistrationTestData.FidoU2f.BasicAttestation - val steps = finishRegistration( - testData = testData, - attestationTrustSource = None, - allowUntrustedAttestation = true, - credentialRepository = Helpers.CredentialRepositoryV2.empty, - ) - steps.run.getKeyId.getId should be(testData.response.getId) - steps.run.isAttestationTrusted should be(false) - } - } - - testUntrusted(RegistrationTestData.AndroidKey.BasicAttestation) - testUntrusted(RegistrationTestData.AndroidSafetynet.BasicAttestation) - testUntrusted(RegistrationTestData.FidoU2f.BasicAttestation) - testUntrusted(RegistrationTestData.NoneAttestation.Default) - testUntrusted(RealExamples.WindowsHelloTpm.asRegistrationTestData) - } - } - } - - describe("The default RelyingParty settings") { - - val rp = RelyingParty - .builder() - .identity( - RelyingPartyIdentity - .builder() - .id("localhost") - .name("Test party") - .build() - ) - .credentialRepositoryV2(Helpers.CredentialRepositoryV2.empty) - .build() - - val request = rp - .startRegistration( - StartRegistrationOptions - .builder() - .user( - UserIdentity - .builder() - .name("test") - .displayName("Test Testsson") - .id(new ByteArray(Array())) - .build() - ) - .build() - ) - .toBuilder() - .challenge( - RegistrationTestData.NoneAttestation.Default.clientData.getChallenge - ) - .build() - - it("accept registrations with no attestation.") { - val result = rp.finishRegistration( - FinishRegistrationOptions - .builder() - .request(request) - .response(RegistrationTestData.NoneAttestation.Default.response) - .build() - ) - - result.isAttestationTrusted should be(false) - result.getAttestationType should be(AttestationType.NONE) - result.getKeyId.getId should equal( - RegistrationTestData.NoneAttestation.Default.response.getId - ) - } - - it( - "accept registrations with unknown attestation statement format." - ) { - val testData = RegistrationTestData.FidoU2f.BasicAttestation - .setAttestationStatementFormat("urgel") - val result = rp.finishRegistration( - FinishRegistrationOptions - .builder() - .request(request) - .response(testData.response) - .build() - ) - - result.isAttestationTrusted should be(false) - result.getAttestationType should be(AttestationType.UNKNOWN) - result.getKeyId.getId should equal(testData.response.getId) - } - - it("accept android-key attestations but report they're untrusted.") { - val result = rp.finishRegistration( - FinishRegistrationOptions - .builder() - .request(request) - .response( - RegistrationTestData.AndroidKey.BasicAttestation.response - ) - .build() - ) - - result.isAttestationTrusted should be(false) - result.getKeyId.getId should equal( - RegistrationTestData.AndroidKey.BasicAttestation.response.getId - ) - } - - it("accept TPM attestations but report they're untrusted.") { - val testData = RealExamples.WindowsHelloTpm.asRegistrationTestData - val result = rp.toBuilder - .identity(testData.rpId) - .origins(Set("https://dev.d2urpypvrhb05x.amplifyapp.com").asJava) - .build() - .finishRegistration( - FinishRegistrationOptions - .builder() - .request( - request.toBuilder.challenge(testData.responseChallenge).build() - ) - .response(testData.response) - .build() - ) - - result.isAttestationTrusted should be(false) - result.getKeyId.getId should equal( - RealExamples.WindowsHelloTpm.asRegistrationTestData.response.getId - ) - } - - describe("accept apple attestations but report they're untrusted:") { - it("iOS") { - val result = rp - .toBuilder() - .identity(RealExamples.AppleAttestationIos.rp) - .origins( - Set( - RealExamples.AppleAttestationIos.attestation.collectedClientData.getOrigin - ).asJava - ) - .build() - .finishRegistration( - FinishRegistrationOptions - .builder() - .request( - request - .toBuilder() - .challenge( - RealExamples.AppleAttestationIos.attestation.collectedClientData.getChallenge - ) - .build() - ) - .response( - RealExamples.AppleAttestationIos.attestation.credential - ) - .build() - ) - - result.isAttestationTrusted should be(false) - RealExamples.AppleAttestationIos.attestation.credential.getResponse.getAttestation.getFormat should be( - "apple" - ) - result.getAttestationType should be( - AttestationType.ANONYMIZATION_CA - ) - result.getKeyId.getId should equal( - RealExamples.AppleAttestationIos.attestation.credential.getId - ) - } - - it("MacOS") { - val result = rp - .toBuilder() - .identity(RealExamples.AppleAttestationMacos.rp) - .origins( - Set( - RealExamples.AppleAttestationMacos.attestation.collectedClientData.getOrigin - ).asJava - ) - .build() - .finishRegistration( - FinishRegistrationOptions - .builder() - .request( - request - .toBuilder() - .challenge( - RealExamples.AppleAttestationMacos.attestation.collectedClientData.getChallenge - ) - .build() - ) - .response( - RealExamples.AppleAttestationMacos.attestation.credential - ) - .build() - ) - - result.isAttestationTrusted should be(false) - RealExamples.AppleAttestationMacos.attestation.credential.getResponse.getAttestation.getFormat should be( - "apple" - ) - result.getAttestationType should be( - AttestationType.ANONYMIZATION_CA - ) - result.getKeyId.getId should equal( - RealExamples.AppleAttestationMacos.attestation.credential.getId - ) - } - } - - describe("accept all test examples in the validExamples list.") { - RegistrationTestData.defaultSettingsValidExamples.zipWithIndex - .foreach { - case (testData, i) => - it(s"Succeeds for example index ${i} (${testData.alg}, ${testData.attestationStatementFormat}).") { - val rp = RelyingParty - .builder() - .identity(testData.rpId) - .credentialRepositoryV2( - Helpers.CredentialRepositoryV2.empty - ) - .origins(Set(testData.clientData.getOrigin).asJava) - .build() - - val request = rp - .startRegistration( - StartRegistrationOptions - .builder() - .user(testData.userId) - .build() - ) - .toBuilder - .challenge(testData.request.getChallenge) - .build() - - val result = rp.finishRegistration( - FinishRegistrationOptions - .builder() - .request(request) - .response(testData.response) - .build() - ) - - result.getKeyId.getId should equal(testData.response.getId) - } - } - } - - describe("generate pubKeyCredParams which") { - val pkcco = rp.startRegistration( - StartRegistrationOptions - .builder() - .user( - UserIdentity - .builder() - .name("foo") - .displayName("Foo") - .id(ByteArray.fromHex("aabbccdd")) - .build() - ) - .build() - ) - - val pubKeyCredParams = pkcco.getPubKeyCredParams.asScala - - describe("include") { - it("ES256.") { - pubKeyCredParams should contain( - PublicKeyCredentialParameters.ES256 - ) - pubKeyCredParams map (_.getAlg) should contain( - COSEAlgorithmIdentifier.ES256 - ) - } - - it("ES384.") { - pubKeyCredParams should contain( - PublicKeyCredentialParameters.ES384 - ) - pubKeyCredParams map (_.getAlg) should contain( - COSEAlgorithmIdentifier.ES384 - ) - } - - it("ES512.") { - pubKeyCredParams should contain( - PublicKeyCredentialParameters.ES512 - ) - pubKeyCredParams map (_.getAlg) should contain( - COSEAlgorithmIdentifier.ES512 - ) - } - - it("EdDSA, when available.") { - // The RelyingParty constructor call needs to be here inside the `it` call in order to have the right JCA provider environment - val rp = RelyingParty - .builder() - .identity( - RelyingPartyIdentity - .builder() - .id("localhost") - .name("Test party") - .build() - ) - .credentialRepositoryV2(Helpers.CredentialRepositoryV2.empty) - .build() - - val pkcco = rp.startRegistration( - StartRegistrationOptions - .builder() - .user( - UserIdentity - .builder() - .name("foo") - .displayName("Foo") - .id(ByteArray.fromHex("aabbccdd")) - .build() - ) - .build() - ) - val pubKeyCredParams = pkcco.getPubKeyCredParams.asScala - - if (Try(KeyFactory.getInstance("EdDSA")).isSuccess) { - pubKeyCredParams should contain( - PublicKeyCredentialParameters.EdDSA - ) - pubKeyCredParams map (_.getAlg) should contain( - COSEAlgorithmIdentifier.EdDSA - ) - } else { - pubKeyCredParams should not contain ( - PublicKeyCredentialParameters.EdDSA - ) - pubKeyCredParams map (_.getAlg) should not contain ( - COSEAlgorithmIdentifier.EdDSA - ) - } - } - - it("RS256.") { - pubKeyCredParams should contain( - PublicKeyCredentialParameters.RS256 - ) - pubKeyCredParams map (_.getAlg) should contain( - COSEAlgorithmIdentifier.RS256 - ) - } - - it("RS384.") { - pubKeyCredParams should contain( - PublicKeyCredentialParameters.RS384 - ) - pubKeyCredParams map (_.getAlg) should contain( - COSEAlgorithmIdentifier.RS384 - ) - } - - it("RS512.") { - pubKeyCredParams should contain( - PublicKeyCredentialParameters.RS512 - ) - pubKeyCredParams map (_.getAlg) should contain( - COSEAlgorithmIdentifier.RS512 - ) - } - } - - describe("do not include") { - it("RS1.") { - pubKeyCredParams should not contain PublicKeyCredentialParameters.RS1 - pubKeyCredParams map (_.getAlg) should not contain COSEAlgorithmIdentifier.RS1 - } - } - } - - describe("expose the credProps extension output as RegistrationResult.isDiscoverable()") { - val testDataBase = RegistrationTestData.Packed.BasicAttestation - val testData = testDataBase.copy(requestedExtensions = - testDataBase.request.getExtensions.toBuilder.credProps().build() - ) - - it("when set to true.") { - val result = rp.finishRegistration( - FinishRegistrationOptions - .builder() - .request(testData.request) - .response( - testData.response.toBuilder - .clientExtensionResults( - ClientRegistrationExtensionOutputs - .builder() - .credProps( - CredentialPropertiesOutput.builder().rk(true).build() - ) - .build() - ) - .build() - ) - .build() - ) - - result.isDiscoverable.toScala should equal(Some(true)) - } - - it("when set to false.") { - val result = rp.finishRegistration( - FinishRegistrationOptions - .builder() - .request(testData.request) - .response( - testData.response.toBuilder - .clientExtensionResults( - ClientRegistrationExtensionOutputs - .builder() - .credProps( - CredentialPropertiesOutput.builder().rk(false).build() - ) - .build() - ) - .build() - ) - .build() - ) - - result.isDiscoverable.toScala should equal(Some(false)) - } - - it("when not available.") { - val result = rp.finishRegistration( - FinishRegistrationOptions - .builder() - .request(testData.request) - .response(testData.response) - .build() - ) - - result.isDiscoverable.toScala should equal(None) - } - } - - describe("support the largeBlob extension") { - it("being enabled at registration time.") { - val testData = RegistrationTestData.Packed.BasicAttestation - val result = rp.finishRegistration( - FinishRegistrationOptions - .builder() - .request( - testData.request.toBuilder - .extensions( - RegistrationExtensionInputs - .builder() - .largeBlob(LargeBlobSupport.REQUIRED) - .build() - ) - .build() - ) - .response( - testData.response.toBuilder - .clientExtensionResults( - ClientRegistrationExtensionOutputs - .builder() - .largeBlob( - LargeBlobRegistrationOutput.supported(true) - ) - .build() - ) - .build() - ) - .build() - ) - - result.getClientExtensionOutputs.get.getLargeBlob.get.isSupported should be( - true - ) - } - } - - describe("support the uvm extension") { - it("at registration time.") { - - // Example from spec: https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#sctn-uvm-extension - // A1 -- extension: CBOR map of one element - // 63 -- Key 1: CBOR text string of 3 bytes - // 75 76 6d -- "uvm" [=UTF-8 encoded=] string - // 82 -- Value 1: CBOR array of length 2 indicating two factor usage - // 83 -- Item 1: CBOR array of length 3 - // 02 -- Subitem 1: CBOR integer for User Verification Method Fingerprint - // 04 -- Subitem 2: CBOR short for Key Protection Type TEE - // 02 -- Subitem 3: CBOR short for Matcher Protection Type TEE - // 83 -- Item 2: CBOR array of length 3 - // 04 -- Subitem 1: CBOR integer for User Verification Method Passcode - // 01 -- Subitem 2: CBOR short for Key Protection Type Software - // 01 -- Subitem 3: CBOR short for Matcher Protection Type Software - val uvmCborExample = ByteArray.fromHex("A16375766d828302040283040101") - - val challenge = TestAuthenticator.Defaults.challenge - val (cred, _, _) = TestAuthenticator.createUnattestedCredential( - authenticatorExtensions = - Some(JacksonCodecs.cbor().readTree(uvmCborExample.getBytes)), - challenge = challenge, - ) - - val result = rp.finishRegistration( - FinishRegistrationOptions - .builder() - .request( - PublicKeyCredentialCreationOptions - .builder() - .rp( - RelyingPartyIdentity - .builder() - .id(TestAuthenticator.Defaults.rpId) - .name("Test RP") - .build() - ) - .user( - UserIdentity - .builder() - .name("foo") - .displayName("Foo User") - .id(ByteArray.fromHex("00010203")) - .build() - ) - .challenge(challenge) - .pubKeyCredParams( - List(PublicKeyCredentialParameters.ES256).asJava - ) - .extensions( - RegistrationExtensionInputs - .builder() - .uvm() - .build() - ) - .build() - ) - .response(cred) - .build() - ) - - result.getAuthenticatorExtensionOutputs.get.getUvm.toScala should equal( - Some( - List( - new UvmEntry( - UserVerificationMethod.USER_VERIFY_FINGERPRINT_INTERNAL, - KeyProtectionType.KEY_PROTECTION_TEE, - MatcherProtectionType.MATCHER_PROTECTION_TEE, - ), - new UvmEntry( - UserVerificationMethod.USER_VERIFY_PASSCODE_INTERNAL, - KeyProtectionType.KEY_PROTECTION_SOFTWARE, - MatcherProtectionType.MATCHER_PROTECTION_SOFTWARE, - ), - ).asJava - ) - ) - } - } - } - - describe("RelyingParty supports registering") { - it("a real packed attestation with an RSA key.") { - val rp = RelyingParty - .builder() - .identity( - RelyingPartyIdentity - .builder() - .id("demo3.yubico.test") - .name("Yubico WebAuthn demo") - .build() - ) - .credentialRepositoryV2(Helpers.CredentialRepositoryV2.empty) - .origins(Set("https://demo3.yubico.test:8443").asJava) - .build() - - val testData = RegistrationTestData.Packed.BasicAttestationRsaReal - val result = rp.finishRegistration( - FinishRegistrationOptions - .builder() - .request(testData.request) - .response(testData.response) - .build() - ) - - result.isAttestationTrusted should be(false) - result.getKeyId.getId should equal(testData.response.getId) - } - } - - describe("The RegistrationResult") { - describe("exposes getTransports() which") { - - val rp = RelyingParty - .builder() - .identity( - RelyingPartyIdentity - .builder() - .id("example.com") - .name("Example RP") - .build() - ) - .credentialRepositoryV2(Helpers.CredentialRepositoryV2.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("contains the returned transports when available.") { - val result = rp.finishRegistration( - FinishRegistrationOptions - .builder() - .request(request) - .response(PublicKeyCredential.parseRegistrationResponseJson("""{ - "type": "public-key", - "id": "LbYHDfeoEJ-ItG8lq6fjNVnhg6kgbebGjYWEf32ZpyChibGv4gJU1OGM0nOQQY5G", - "response": { - "clientDataJSON": "eyJ0eXBlIjogIndlYmF1dGhuLmNyZWF0ZSIsICJjbGllbnRFeHRlbnNpb25zIjoge30sICJjaGFsbGVuZ2UiOiAiWTJoaGJHeGxibWRsIiwgIm9yaWdpbiI6ICJodHRwczovL2V4YW1wbGUuY29tIn0", - "attestationObject": "o2NmbXRoZmlkby11MmZnYXR0U3RtdKJjc2lnWEcwRQIhAJKRPuYlfW8dZZlsJrJiwA-BvAyOvIe1TScv5qlek1SQAiAnglgs-nRjA7kpc61PewQ4VULjdlzLmReI7-MJT1TLrGN4NWOBWQLBMIICvTCCAaWgAwIBAgIEMAIspTANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZdWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAwMDBaGA8yMDUwMDkwNDAwMDAwMFowbjELMAkGA1UEBhMCU0UxEjAQBgNVBAoMCVl1YmljbyBBQjEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjEnMCUGA1UEAwweWXViaWNvIFUyRiBFRSBTZXJpYWwgODA1NDQ4ODY5MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE-66HSEytO3plXno3zPhH1k-zFwWxESIdrTbQp4HSEuzFum1Mwpy8itoOosBQksnIrefLHkTRNUtV8jIrFKAvbaNsMGowIgYJKwYBBAGCxAoCBBUxLjMuNi4xLjQuMS40MTQ4Mi4xLjEwEwYLKwYBBAGC5RwCAQEEBAMCBDAwIQYLKwYBBAGC5RwBAQQEEgQQbUS6m_bsLkm5MAyP6SDLczAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4IBAQBlZXnJy-X3fJfNdlIdIQlFpO5-A5uM41jJ2XgdRag_8rSxXCz98t_jyoWth5FQF9As96Ags3p-Lyaqb1bpEc9RfmkxiiqwDzDI56Sj4HKlANF2tddm-ew29H9yaNbpU5y6aleCeH2rR4t1cFgcBRAV84IndIH0cYASRnyrFbHjI80vlPNR0z4j-_W9vYEWBpLeS_wrdKPVW7C7wyuc4bobauCyhElBPZUwblR_Ll0iovmfazD17VLCBMA4p_SVVTwSXpKyZjMiCotj8mDhQ1ymhvCepkK82EwnrBMJIzCi_joxAXqxLPMs6yJrz_hFUkZaloa1ZS6f7aGAmAKhRNO2aGF1dGhEYXRhWMSjeab27q-5pV43jBGANOJ1Hmgvq58tMKsT0hJVhs4ZR0EAAAAAAAAAAAAAAAAAAAAAAAAAAABAJT086Ym5LhLsK6MRwYRSdjVn9jVYVtwiGwgq_bDPpVuI3aaOW7UQfqGWdos-kVwHnQccbDRnQDvQmCDqy6QdSaUBAgMmIAEhWCCRGd2Bo0vIj-suQxM-cOCXovv1Ag6azqHn8PE31Fcu4iJYIOiLha_PR9JwOhCw4SC2Xq7cOackGAMsq4UUJ_IRCCcq", - "transports": ["nfc", "usb"] - }, - "clientExtensionResults": {} - }""")) - .build() - ) - - result.getKeyId.getTransports.toScala.map(_.asScala) should equal( - Some(Set(AuthenticatorTransport.USB, AuthenticatorTransport.NFC)) - ) - } - - it( - "returns present but empty when transport hints are not available." - ) { - val result = rp.finishRegistration( - FinishRegistrationOptions - .builder() - .request(request) - .response(PublicKeyCredential.parseRegistrationResponseJson("""{ - "type": "public-key", - "id": "LbYHDfeoEJ-ItG8lq6fjNVnhg6kgbebGjYWEf32ZpyChibGv4gJU1OGM0nOQQY5G", - "response": { - "clientDataJSON": "eyJ0eXBlIjogIndlYmF1dGhuLmNyZWF0ZSIsICJjbGllbnRFeHRlbnNpb25zIjoge30sICJjaGFsbGVuZ2UiOiAiWTJoaGJHeGxibWRsIiwgIm9yaWdpbiI6ICJodHRwczovL2V4YW1wbGUuY29tIn0", - "attestationObject": "o2NmbXRoZmlkby11MmZnYXR0U3RtdKJjc2lnWEcwRQIhAJKRPuYlfW8dZZlsJrJiwA-BvAyOvIe1TScv5qlek1SQAiAnglgs-nRjA7kpc61PewQ4VULjdlzLmReI7-MJT1TLrGN4NWOBWQLBMIICvTCCAaWgAwIBAgIEMAIspTANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZdWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAwMDBaGA8yMDUwMDkwNDAwMDAwMFowbjELMAkGA1UEBhMCU0UxEjAQBgNVBAoMCVl1YmljbyBBQjEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjEnMCUGA1UEAwweWXViaWNvIFUyRiBFRSBTZXJpYWwgODA1NDQ4ODY5MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE-66HSEytO3plXno3zPhH1k-zFwWxESIdrTbQp4HSEuzFum1Mwpy8itoOosBQksnIrefLHkTRNUtV8jIrFKAvbaNsMGowIgYJKwYBBAGCxAoCBBUxLjMuNi4xLjQuMS40MTQ4Mi4xLjEwEwYLKwYBBAGC5RwCAQEEBAMCBDAwIQYLKwYBBAGC5RwBAQQEEgQQbUS6m_bsLkm5MAyP6SDLczAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4IBAQBlZXnJy-X3fJfNdlIdIQlFpO5-A5uM41jJ2XgdRag_8rSxXCz98t_jyoWth5FQF9As96Ags3p-Lyaqb1bpEc9RfmkxiiqwDzDI56Sj4HKlANF2tddm-ew29H9yaNbpU5y6aleCeH2rR4t1cFgcBRAV84IndIH0cYASRnyrFbHjI80vlPNR0z4j-_W9vYEWBpLeS_wrdKPVW7C7wyuc4bobauCyhElBPZUwblR_Ll0iovmfazD17VLCBMA4p_SVVTwSXpKyZjMiCotj8mDhQ1ymhvCepkK82EwnrBMJIzCi_joxAXqxLPMs6yJrz_hFUkZaloa1ZS6f7aGAmAKhRNO2aGF1dGhEYXRhWMSjeab27q-5pV43jBGANOJ1Hmgvq58tMKsT0hJVhs4ZR0EAAAAAAAAAAAAAAAAAAAAAAAAAAABAJT086Ym5LhLsK6MRwYRSdjVn9jVYVtwiGwgq_bDPpVuI3aaOW7UQfqGWdos-kVwHnQccbDRnQDvQmCDqy6QdSaUBAgMmIAEhWCCRGd2Bo0vIj-suQxM-cOCXovv1Ag6azqHn8PE31Fcu4iJYIOiLha_PR9JwOhCw4SC2Xq7cOackGAMsq4UUJ_IRCCcq" - }, - "clientExtensionResults": {} - }""")) - .build() - ) - - result.getKeyId.getTransports.toScala.map(_.asScala) should equal( - Some(Set.empty) - ) - } - - it("returns present but empty when transport hints are empty.") { - val result = rp.finishRegistration( - FinishRegistrationOptions - .builder() - .request(request) - .response(PublicKeyCredential.parseRegistrationResponseJson("""{ - "type": "public-key", - "id": "LbYHDfeoEJ-ItG8lq6fjNVnhg6kgbebGjYWEf32ZpyChibGv4gJU1OGM0nOQQY5G", - "response": { - "clientDataJSON": "eyJ0eXBlIjogIndlYmF1dGhuLmNyZWF0ZSIsICJjbGllbnRFeHRlbnNpb25zIjoge30sICJjaGFsbGVuZ2UiOiAiWTJoaGJHeGxibWRsIiwgIm9yaWdpbiI6ICJodHRwczovL2V4YW1wbGUuY29tIn0", - "attestationObject": "o2NmbXRoZmlkby11MmZnYXR0U3RtdKJjc2lnWEcwRQIhAJKRPuYlfW8dZZlsJrJiwA-BvAyOvIe1TScv5qlek1SQAiAnglgs-nRjA7kpc61PewQ4VULjdlzLmReI7-MJT1TLrGN4NWOBWQLBMIICvTCCAaWgAwIBAgIEMAIspTANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZdWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAwMDBaGA8yMDUwMDkwNDAwMDAwMFowbjELMAkGA1UEBhMCU0UxEjAQBgNVBAoMCVl1YmljbyBBQjEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjEnMCUGA1UEAwweWXViaWNvIFUyRiBFRSBTZXJpYWwgODA1NDQ4ODY5MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE-66HSEytO3plXno3zPhH1k-zFwWxESIdrTbQp4HSEuzFum1Mwpy8itoOosBQksnIrefLHkTRNUtV8jIrFKAvbaNsMGowIgYJKwYBBAGCxAoCBBUxLjMuNi4xLjQuMS40MTQ4Mi4xLjEwEwYLKwYBBAGC5RwCAQEEBAMCBDAwIQYLKwYBBAGC5RwBAQQEEgQQbUS6m_bsLkm5MAyP6SDLczAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4IBAQBlZXnJy-X3fJfNdlIdIQlFpO5-A5uM41jJ2XgdRag_8rSxXCz98t_jyoWth5FQF9As96Ags3p-Lyaqb1bpEc9RfmkxiiqwDzDI56Sj4HKlANF2tddm-ew29H9yaNbpU5y6aleCeH2rR4t1cFgcBRAV84IndIH0cYASRnyrFbHjI80vlPNR0z4j-_W9vYEWBpLeS_wrdKPVW7C7wyuc4bobauCyhElBPZUwblR_Ll0iovmfazD17VLCBMA4p_SVVTwSXpKyZjMiCotj8mDhQ1ymhvCepkK82EwnrBMJIzCi_joxAXqxLPMs6yJrz_hFUkZaloa1ZS6f7aGAmAKhRNO2aGF1dGhEYXRhWMSjeab27q-5pV43jBGANOJ1Hmgvq58tMKsT0hJVhs4ZR0EAAAAAAAAAAAAAAAAAAAAAAAAAAABAJT086Ym5LhLsK6MRwYRSdjVn9jVYVtwiGwgq_bDPpVuI3aaOW7UQfqGWdos-kVwHnQccbDRnQDvQmCDqy6QdSaUBAgMmIAEhWCCRGd2Bo0vIj-suQxM-cOCXovv1Ag6azqHn8PE31Fcu4iJYIOiLha_PR9JwOhCw4SC2Xq7cOackGAMsq4UUJ_IRCCcq", - "transports": [] - }, - "clientExtensionResults": {} - }""")) - .build() - ) - - result.getKeyId.getTransports.toScala.map(_.asScala) should equal( - Some(Set.empty) - ) - } - } - - describe( - "exposes getAttestationTrustPath() with the attestation trust path" - ) { - it("for a fido-u2f attestation.") { - val testData = RegistrationTestData.FidoU2f.BasicAttestation - val steps = finishRegistration( - testData = testData, - attestationTrustSource = Some( - trustSourceWith( - testData.attestationCertChain.last._1, - crls = Some( - testData.attestationCertChain - .map({ - case (cert, key) => - TestAuthenticator.buildCrl( - JcaX500NameUtil.getSubject(cert), - key, - "SHA256withECDSA", - TestAuthenticator.Defaults.certValidFrom, - TestAuthenticator.Defaults.certValidTo, - ) - }) - .toSet - ), - ) - ), - credentialRepository = Helpers.CredentialRepositoryV2.empty, - clock = Clock.fixed( - TestAuthenticator.Defaults.certValidFrom, - ZoneOffset.UTC, - ), - ) - val result = steps.run() - result.isAttestationTrusted should be(true) - result.getAttestationTrustPath.toScala.map(_.asScala) should equal( - Some(testData.attestationCertChain.init.map(_._1)) - ) - } - - it("for a tpm attestation.") { - val testData = RealExamples.WindowsHelloTpm.asRegistrationTestData - val steps = finishRegistration( - testData = testData, - origins = Some(Set("https://dev.d2urpypvrhb05x.amplifyapp.com")), - attestationTrustSource = Some( - trustSourceWith( - testData.attestationRootCertificate.get, - enableRevocationChecking = false, - policyTreeValidator = Some(_ => true), - ) - ), - credentialRepository = Helpers.CredentialRepositoryV2.empty, - clock = Clock.fixed( - Instant.parse("2022-05-11T12:34:50Z"), - ZoneOffset.UTC, - ), - ) - val result = steps.run() - result.isAttestationTrusted should be(true) - } - } - - it("exposes getAaguid() with the authenticator AAGUID.") { - val testData = RegistrationTestData.Packed.BasicAttestation - val steps = finishRegistration( - testData = testData, - credentialRepository = Helpers.CredentialRepositoryV2.empty, - allowUntrustedAttestation = true, - ) - val result = steps.run() - result.getAaguid should equal( - testData.response.getResponse.getAttestation.getAuthenticatorData.getAttestedCredentialData.get.getAaguid - ) - } - - { - val rp = RelyingParty - .builder() - .identity( - RelyingPartyIdentity - .builder() - .id("localhost") - .name("Example RP") - .build() - ) - .credentialRepositoryV2(Helpers.CredentialRepositoryV2.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 isUserVerified() with the UV flag value in authenticator data.") { - val (pkcWithoutUv, _, _) = - TestAuthenticator.createUnattestedCredential( - flags = Some(new AuthenticatorDataFlags(0x00.toByte)), - challenge = request.getChallenge, - ) - val (pkcWithUv, _, _) = - TestAuthenticator.createUnattestedCredential( - flags = Some(new AuthenticatorDataFlags(0x04.toByte)), - challenge = request.getChallenge, - ) - - val resultWithoutUv = rp.finishRegistration( - FinishRegistrationOptions - .builder() - .request(request) - .response(pkcWithoutUv) - .build() - ) - val resultWithUv = rp.finishRegistration( - FinishRegistrationOptions - .builder() - .request(request) - .response(pkcWithUv) - .build() - ) - - resultWithoutUv.isUserVerified should be(false) - resultWithUv.isUserVerified should be(true) - } - - 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) - } - - it( - "exposes getAuthenticatorAttachment() with the authenticatorAttachment value from the PublicKeyCredential." - ) { - val (pkcTemplate, _, _) = - TestAuthenticator.createUnattestedCredential(challenge = - request.getChallenge - ) - - forAll { authenticatorAttachment: Option[AuthenticatorAttachment] => - val pkc = pkcTemplate.toBuilder - .authenticatorAttachment(authenticatorAttachment.orNull) - .build() - - val result = rp.finishRegistration( - FinishRegistrationOptions - .builder() - .request(request) - .response(pkc) - .build() - ) - - result.getAuthenticatorAttachment should equal( - pkc.getAuthenticatorAttachment - ) - } - } - } - } - - } - - describe("RelyingParty.finishRegistration") { - it("supports 1023 bytes long credential IDs.") { - val rp = RelyingParty - .builder() - .identity( - RelyingPartyIdentity - .builder() - .id("localhost") - .name("Test party") - .build() - ) - .credentialRepositoryV2(Helpers.CredentialRepositoryV2.empty) - .build() - - val pkcco = rp.startRegistration( - StartRegistrationOptions - .builder() - .user( - UserIdentity - .builder() - .name("test") - .displayName("Test Testsson") - .id(new ByteArray(Array())) - .build() - ) - .build() - ) - - forAll(byteArray(1023)) { credId => - val credential = TestAuthenticator - .createUnattestedCredential(challenge = pkcco.getChallenge) - ._1 - .toBuilder() - .id(credId) - .build() - - val result = Try( - rp.finishRegistration( - FinishRegistrationOptions - .builder() - .request(pkcco) - .response(credential) - .build() - ) - ) - result shouldBe a[Success[_]] - result.get.getKeyId.getId should equal(credId) - result.get.getKeyId.getId.size should be(1023) - } - } - - it("throws RegistrationFailedException in case of errors.") { - val rp = RelyingParty - .builder() - .identity( - RelyingPartyIdentity - .builder() - .id("localhost") - .name("Test party") - .build() - ) - .credentialRepositoryV2(Helpers.CredentialRepositoryV2.empty) - .build() - - val pkcco = rp.startRegistration( - StartRegistrationOptions - .builder() - .user( - UserIdentity - .builder() - .name("test") - .displayName("Test Testsson") - .id(new ByteArray(Array())) - .build() - ) - .build() - ) - - val result = Try( - rp.finishRegistration( - FinishRegistrationOptions - .builder() - .request(pkcco) - .response(RegistrationTestData.NoneAttestation.Default.response) - .build() - ) - ) - result shouldBe a[Failure[_]] - result.failed.get shouldBe a[RegistrationFailedException] - result.failed.get.getMessage should include("Incorrect challenge") - } - } - -} diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/test/Helpers.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/test/Helpers.scala index 60d7c540e..ddaeca89b 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/test/Helpers.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/test/Helpers.scala @@ -1,14 +1,8 @@ package com.yubico.webauthn.test -import com.yubico.webauthn.CredentialRecord import com.yubico.webauthn.CredentialRepository -import com.yubico.webauthn.CredentialRepositoryV2 import com.yubico.webauthn.RegisteredCredential import com.yubico.webauthn.RegistrationResult -import com.yubico.webauthn.RegistrationTestData -import com.yubico.webauthn.ToPublicKeyCredentialDescriptor -import com.yubico.webauthn.UsernameRepository -import com.yubico.webauthn.data.AuthenticatorTransport import com.yubico.webauthn.data.ByteArray import com.yubico.webauthn.data.PublicKeyCredentialDescriptor import com.yubico.webauthn.data.UserIdentity @@ -106,156 +100,6 @@ object Helpers { } } - object CredentialRepositoryV2 { - def empty[C <: CredentialRecord] = - new CredentialRepositoryV2[C] { - override def getCredentialDescriptorsForUserHandle( - userHandle: ByteArray - ): java.util.Set[PublicKeyCredentialDescriptor] = Set.empty.asJava - override def lookup( - credentialId: ByteArray, - userHandle: ByteArray, - ): Optional[C] = None.toJava - override def credentialIdExists( - credentialId: ByteArray - ): Boolean = false - } - def unimplemented[C <: CredentialRecord] = - new CredentialRepositoryV2[C] { - override def getCredentialDescriptorsForUserHandle( - userHandle: ByteArray - ): java.util.Set[PublicKeyCredentialDescriptor] = ??? - override def lookup( - credentialId: ByteArray, - userHandle: ByteArray, - ): Optional[C] = ??? - override def credentialIdExists( - credentialId: ByteArray - ): Boolean = ??? - } - - class CountingCalls[C <: CredentialRecord](inner: CredentialRepositoryV2[C]) - extends CredentialRepositoryV2[C] { - var getCredentialIdsCount = 0 - var lookupCount = 0 - var credentialIdExistsCount = 0 - - override def getCredentialDescriptorsForUserHandle( - userHandle: ByteArray - ): java.util.Set[_ <: ToPublicKeyCredentialDescriptor] = { - getCredentialIdsCount += 1 - inner.getCredentialDescriptorsForUserHandle(userHandle) - } - - override def lookup( - credentialId: ByteArray, - userHandle: ByteArray, - ): Optional[C] = { - lookupCount += 1 - inner.lookup(credentialId, userHandle) - } - - override def credentialIdExists(credentialId: ByteArray) = { - credentialIdExistsCount += 1 - inner.credentialIdExists(credentialId) - } - } - - def withUsers[C <: CredentialRecord]( - users: (UserIdentity, C)* - ): CredentialRepositoryV2[C] = { - new CredentialRepositoryV2[C] { - override def getCredentialDescriptorsForUserHandle( - userHandle: ByteArray - ): java.util.Set[_ <: ToPublicKeyCredentialDescriptor] = - users - .filter({ - case (u, c) => - u.getId == userHandle && c.getUserHandle == userHandle - }) - .map({ - case (_, credential) => credential - }) - .toSet - .asJava - - override def lookup( - credentialId: ByteArray, - userHandle: ByteArray, - ): Optional[C] = - users - .find(_._1.getId == userHandle) - .map(_._2) - .filter(cred => - cred.getUserHandle == userHandle && cred.getCredentialId == credentialId - ) - .toJava - - override def credentialIdExists( - credentialId: ByteArray - ): Boolean = - users.exists(_._2.getCredentialId == credentialId) - } - } - - def withUser( - user: UserIdentity, - credentialId: ByteArray, - publicKeyCose: ByteArray, - signatureCount: Long = 0, - be: Option[Boolean] = None, - bs: Option[Boolean] = None, - ): CredentialRepositoryV2[CredentialRecord] = { - withUsers( - ( - user, - credentialRecord( - credentialId = credentialId, - userHandle = user.getId, - publicKeyCose = publicKeyCose, - signatureCount = signatureCount, - be = be, - bs = bs, - ), - ) - ) - } - } - - object UsernameRepository { - val empty = - new UsernameRepository { - override def getUserHandleForUsername( - username: String - ): Optional[ByteArray] = None.toJava - override def getUsernameForUserHandle( - userHandle: ByteArray - ): Optional[String] = None.toJava - } - def unimplemented[C <: CredentialRecord] = - new UsernameRepository { - override def getUserHandleForUsername( - username: String - ): Optional[ByteArray] = ??? - override def getUsernameForUserHandle( - userHandle: ByteArray - ): Optional[String] = ??? - } - - def withUsers(users: UserIdentity*): UsernameRepository = - new UsernameRepository { - override def getUserHandleForUsername( - username: String - ): Optional[ByteArray] = - users.find(_.getName == username).map(_.getId).toJava - - override def getUsernameForUserHandle( - userHandle: ByteArray - ): Optional[String] = - users.find(_.getId == userHandle).map(_.getName).toJava - } - } - def toRegisteredCredential( user: UserIdentity, result: RegistrationResult, @@ -267,46 +111,4 @@ object Helpers { .publicKeyCose(result.getPublicKeyCose) .build() - def credentialRecord( - credentialId: ByteArray, - userHandle: ByteArray, - publicKeyCose: ByteArray, - signatureCount: Long = 0, - transports: Option[Set[AuthenticatorTransport]] = None, - be: Option[Boolean] = None, - bs: Option[Boolean] = None, - ): CredentialRecord = { - new CredentialRecord { - override def getCredentialId: ByteArray = credentialId - override def getUserHandle: ByteArray = userHandle - override def getPublicKeyCose: ByteArray = publicKeyCose - override def getSignatureCount: Long = signatureCount - override def getTransports - : Optional[java.util.Set[AuthenticatorTransport]] = - transports.toJava.map(_.asJava) - override def isBackupEligible: Optional[java.lang.Boolean] = toJava(be) - override def isBackedUp: Optional[java.lang.Boolean] = toJava(bs) - } - } - - def toCredentialRecord( - testData: RegistrationTestData, - signatureCount: Long = 0, - be: Option[Boolean] = None, - bs: Option[Boolean] = None, - ): CredentialRecord = - new CredentialRecord { - override def getCredentialId: ByteArray = testData.response.getId - override def getUserHandle: ByteArray = testData.userId.getId - override def getPublicKeyCose: ByteArray = - testData.response.getResponse.getParsedAuthenticatorData.getAttestedCredentialData.get.getCredentialPublicKey - override def getSignatureCount: Long = signatureCount - - override def getTransports - : Optional[java.util.Set[AuthenticatorTransport]] = - Optional.of(testData.response.getResponse.getTransports) - override def isBackupEligible: Optional[java.lang.Boolean] = toJava(be) - override def isBackedUp: Optional[java.lang.Boolean] = toJava(bs) - } - } diff --git a/webauthn-server-demo/src/main/java/demo/webauthn/InMemoryRegistrationStorage.java b/webauthn-server-demo/src/main/java/demo/webauthn/InMemoryRegistrationStorage.java index 1a4add942..c91054486 100644 --- a/webauthn-server-demo/src/main/java/demo/webauthn/InMemoryRegistrationStorage.java +++ b/webauthn-server-demo/src/main/java/demo/webauthn/InMemoryRegistrationStorage.java @@ -26,12 +26,15 @@ import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; -import com.yubico.webauthn.AssertionResultV2; -import com.yubico.webauthn.CredentialRepositoryV2; -import com.yubico.webauthn.UsernameRepository; +import com.yubico.webauthn.AssertionResult; +import com.yubico.webauthn.CredentialRepository; +import com.yubico.webauthn.RegisteredCredential; import com.yubico.webauthn.data.ByteArray; +import com.yubico.webauthn.data.PublicKeyCredentialDescriptor; +import com.yubico.webauthn.data.PublicKeyCredentialType; import demo.webauthn.data.CredentialRegistration; import java.util.Collection; +import java.util.Collections; import java.util.HashSet; import java.util.NoSuchElementException; import java.util.Optional; @@ -42,8 +45,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class InMemoryRegistrationStorage - implements CredentialRepositoryV2, UsernameRepository { +public class InMemoryRegistrationStorage implements CredentialRepository { private final Cache> storage = CacheBuilder.newBuilder().maximumSize(1000).expireAfterAccess(1, TimeUnit.DAYS).build(); @@ -51,16 +53,29 @@ public class InMemoryRegistrationStorage private static final Logger logger = LoggerFactory.getLogger(InMemoryRegistrationStorage.class); //////////////////////////////////////////////////////////////////////////////// - // The following methods are required by the CredentialRepositoryV2 interface. + // The following methods are required by the CredentialRepository interface. //////////////////////////////////////////////////////////////////////////////// @Override - public Set getCredentialDescriptorsForUserHandle(ByteArray userHandle) { - return getRegistrationsByUserHandle(userHandle); + public Set getCredentialIdsForUsername(String username) { + return getUserHandleForUsername(username) + .map(this::getRegistrationsByUserHandle) + .map( + regs -> + regs.stream() + .map( + reg -> + PublicKeyCredentialDescriptor.builder() + .id(reg.getCredential().getCredentialId()) + .type(PublicKeyCredentialType.PUBLIC_KEY) + .transports(reg.getTransports()) + .build()) + .collect(Collectors.toSet())) + .orElseGet(Collections::emptySet); } @Override - public Optional lookup(ByteArray credentialId, ByteArray userHandle) { + public Optional lookup(ByteArray credentialId, ByteArray userHandle) { Optional registrationMaybe = storage.asMap().values().stream() .flatMap(Collection::stream) @@ -76,20 +91,18 @@ public Optional lookup(ByteArray credentialId, ByteArray userHandle, registrationMaybe); - return registrationMaybe; + return registrationMaybe.map(CredentialRegistration::getCredential); } @Override - public boolean credentialIdExists(ByteArray credentialId) { + public Set lookupAll(ByteArray credentialId) { return storage.asMap().values().stream() .flatMap(Collection::stream) - .anyMatch(reg -> reg.getCredential().getCredentialId().equals(credentialId)); + .map(CredentialRegistration::getCredential) + .filter(credential -> credential.getCredentialId().equals(credentialId)) + .collect(Collectors.toSet()); } - //////////////////////////////////////////////////////////////////////////////// - // The following methods are required by the UsernameRepository interface. - //////////////////////////////////////////////////////////////////////////////// - @Override public Optional getUserHandleForUsername(String username) { return getRegistrationsByUsername(username).stream() @@ -135,19 +148,18 @@ public Set getRegistrationsByUserHandle(ByteArray userHa .collect(Collectors.toSet()); } - public void updateSignatureCount(AssertionResultV2 result) { + public void updateSignatureCount(AssertionResult result) { CredentialRegistration registration = getRegistrationByUsernameAndCredentialId( - result.getCredential().getUsername(), result.getCredential().getCredentialId()) + result.getUsername(), result.getCredential().getCredentialId()) .orElseThrow( () -> new NoSuchElementException( String.format( "Credential \"%s\" is not registered to user \"%s\"", - result.getCredential().getCredentialId(), - result.getCredential().getUsername()))); + result.getCredential().getCredentialId(), result.getUsername()))); - Set regs = storage.getIfPresent(result.getCredential().getUsername()); + Set regs = storage.getIfPresent(result.getUsername()); regs.remove(registration); regs.add( registration.withCredential( diff --git a/webauthn-server-demo/src/main/java/demo/webauthn/WebAuthnServer.java b/webauthn-server-demo/src/main/java/demo/webauthn/WebAuthnServer.java index b4f9165e1..d3fc6d4cd 100644 --- a/webauthn-server-demo/src/main/java/demo/webauthn/WebAuthnServer.java +++ b/webauthn-server-demo/src/main/java/demo/webauthn/WebAuthnServer.java @@ -39,13 +39,12 @@ import com.yubico.internal.util.CertificateParser; import com.yubico.internal.util.JacksonCodecs; import com.yubico.util.Either; -import com.yubico.webauthn.AssertionResultV2; +import com.yubico.webauthn.AssertionResult; import com.yubico.webauthn.FinishAssertionOptions; import com.yubico.webauthn.FinishRegistrationOptions; import com.yubico.webauthn.RegisteredCredential; import com.yubico.webauthn.RegistrationResult; import com.yubico.webauthn.RelyingParty; -import com.yubico.webauthn.RelyingPartyV2; import com.yubico.webauthn.StartAssertionOptions; import com.yubico.webauthn.StartRegistrationOptions; import com.yubico.webauthn.attestation.YubicoJsonMetadataService; @@ -145,7 +144,7 @@ private static MetadataService getMetadataService() private final Clock clock = Clock.systemDefaultZone(); private final ObjectMapper jsonMapper = JacksonCodecs.json(); - private final RelyingPartyV2 rp; + private final RelyingParty rp; public WebAuthnServer() throws CertificateException, @@ -191,8 +190,7 @@ public WebAuthnServer( rp = RelyingParty.builder() .identity(rpIdentity) - .credentialRepositoryV2(this.userStorage) - .usernameRepository(this.userStorage) + .credentialRepository(this.userStorage) .origins(origins) .attestationConveyancePreference(Optional.of(AttestationConveyancePreference.DIRECT)) .attestationTrustSource(metadataService) @@ -490,7 +488,7 @@ public Either, SuccessfulAuthenticationResult> finishAuthentication return Either.left(Arrays.asList("Assertion failed!", "No such assertion in progress.")); } else { try { - AssertionResultV2 result = + AssertionResult result = rp.finishAssertion( FinishAssertionOptions.builder() .request(request.getRequest()) @@ -503,7 +501,7 @@ public Either, SuccessfulAuthenticationResult> finishAuthentication } catch (Exception e) { logger.error( "Failed to update signature count for user \"{}\", credential \"{}\"", - result.getCredential().getUsername(), + result.getUsername(), response.getCredential().getId(), e); } @@ -512,8 +510,8 @@ public Either, SuccessfulAuthenticationResult> finishAuthentication new SuccessfulAuthenticationResult( request, response, - userStorage.getRegistrationsByUsername(result.getCredential().getUsername()), - result.getCredential().getUsername(), + userStorage.getRegistrationsByUsername(result.getUsername()), + result.getUsername(), sessions.createSession(result.getCredential().getUserHandle()))); } else { return Either.left(Collections.singletonList("Assertion failed: Invalid assertion.")); diff --git a/webauthn-server-demo/src/main/java/demo/webauthn/data/CredentialRegistration.java b/webauthn-server-demo/src/main/java/demo/webauthn/data/CredentialRegistration.java index 138352671..a17e17e2c 100644 --- a/webauthn-server-demo/src/main/java/demo/webauthn/data/CredentialRegistration.java +++ b/webauthn-server-demo/src/main/java/demo/webauthn/data/CredentialRegistration.java @@ -26,14 +26,12 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; -import com.yubico.webauthn.CredentialRecord; import com.yubico.webauthn.RegisteredCredential; import com.yubico.webauthn.data.AuthenticatorTransport; import com.yubico.webauthn.data.ByteArray; import com.yubico.webauthn.data.UserIdentity; import java.time.Instant; import java.util.Optional; -import java.util.Set; import java.util.SortedSet; import lombok.Builder; import lombok.NonNull; @@ -43,7 +41,7 @@ @Value @Builder @With -public class CredentialRegistration implements CredentialRecord { +public class CredentialRegistration { UserIdentity userIdentity; Optional credentialNickname; @@ -63,38 +61,7 @@ public String getUsername() { return userIdentity.getName(); } - @Override - public @NonNull ByteArray getCredentialId() { - return credential.getCredentialId(); - } - - @Override public @NonNull ByteArray getUserHandle() { return userIdentity.getId(); } - - @Override - public @NonNull ByteArray getPublicKeyCose() { - return credential.getPublicKeyCose(); - } - - @Override - public long getSignatureCount() { - return credential.getSignatureCount(); - } - - @Override - public Optional> getTransports() { - return Optional.ofNullable(transports); - } - - @Override - public Optional isBackupEligible() { - return credential.isBackupEligible(); - } - - @Override - public Optional isBackedUp() { - return credential.isBackedUp(); - } }