All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.yubico.webauthn.RelyingPartyV2 Maven / Gradle / Ivy

The newest version!
// 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()); 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()); 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)); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy