com.yubico.webauthn.FinishAssertionSteps Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of webauthn-server-core Show documentation
Show all versions of webauthn-server-core Show documentation
Yubico WebAuthn server core API
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 static com.yubico.internal.util.ExceptionUtil.assertTrue;
import com.yubico.internal.util.OptionalUtil;
import com.yubico.webauthn.data.AuthenticatorAssertionResponse;
import com.yubico.webauthn.data.ByteArray;
import com.yubico.webauthn.data.COSEAlgorithmIdentifier;
import com.yubico.webauthn.data.ClientAssertionExtensionOutputs;
import com.yubico.webauthn.data.CollectedClientData;
import com.yubico.webauthn.data.PublicKeyCredential;
import com.yubico.webauthn.data.UserVerificationRequirement;
import com.yubico.webauthn.exception.InvalidSignatureCountException;
import com.yubico.webauthn.extension.appid.AppId;
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.AllArgsConstructor;
import lombok.Value;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@AllArgsConstructor
final class FinishAssertionSteps {
private static final String CLIENT_DATA_TYPE = "webauthn.get";
private static final String SPC_CLIENT_DATA_TYPE = "payment.get";
private final AssertionRequest request;
private final PublicKeyCredential
response;
private final Optional callerTokenBindingId;
private final Set origins;
private final String rpId;
private final CredentialRepositoryV2 credentialRepositoryV2;
private final Optional usernameRepository;
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) {
this(
options.getRequest(),
options.getResponse(),
options.getCallerTokenBindingId(),
rp.getOrigins(),
rp.getIdentity().getId(),
rp.getCredentialRepository(),
Optional.ofNullable(rp.getUsernameRepository()),
rp.isAllowOriginPort(),
rp.isAllowOriginSubdomain(),
rp.isValidateSignatureCounter(),
options.isSecurePaymentConfirmation());
}
private Optional getUsernameForUserHandle(final ByteArray userHandle) {
return usernameRepository.flatMap(unameRepo -> unameRepo.getUsernameForUserHandle(userHandle));
}
public Step5 begin() {
return new Step5();
}
public AssertionResult run() throws InvalidSignatureCountException {
return begin().run();
}
public AssertionResultV2 runV2() throws InvalidSignatureCountException {
return begin().runV2();
}
interface Step> {
Next nextStep();
void validate() throws InvalidSignatureCountException;
default Optional result() {
return Optional.empty();
}
default Optional> resultV2() {
return Optional.empty();
}
default Next next() throws InvalidSignatureCountException {
validate();
return nextStep();
}
default AssertionResult run() throws InvalidSignatureCountException {
if (result().isPresent()) {
return result().get();
} else {
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 {
@Override
public Step6 nextStep() {
return new Step6();
}
@Override
public void validate() {
request
.getPublicKeyCredentialRequestOptions()
.getAllowCredentials()
.filter(allowCredentials -> !allowCredentials.isEmpty())
.ifPresent(
allowed -> {
assertTrue(
allowed.stream().anyMatch(allow -> allow.getId().equals(response.getId())),
"Unrequested credential ID: %s",
response.getId());
});
}
}
@Value
class Step6 implements Step {
private final Optional requestedUserHandle;
private final Optional requestedUsername;
private final Optional responseUserHandle;
private final Optional effectiveRequestUserHandle;
private final Optional effectiveRequestUsername;
private final boolean userHandleDerivedFromUsername;
private final Optional finalUserHandle;
private final Optional finalUsername;
private final Optional registration;
public Step6() {
requestedUserHandle = request.getUserHandle();
requestedUsername = request.getUsername();
responseUserHandle = response.getResponse().getUserHandle();
effectiveRequestUserHandle =
OptionalUtil.orElseOptional(
requestedUserHandle,
() ->
usernameRepository.flatMap(
unr -> requestedUsername.flatMap(unr::getUserHandleForUsername)));
effectiveRequestUsername =
OptionalUtil.orElseOptional(
requestedUsername,
() ->
requestedUserHandle.flatMap(FinishAssertionSteps.this::getUsernameForUserHandle));
userHandleDerivedFromUsername =
!requestedUserHandle.isPresent() && effectiveRequestUserHandle.isPresent();
finalUserHandle = OptionalUtil.orOptional(effectiveRequestUserHandle, responseUserHandle);
finalUsername =
OptionalUtil.orElseOptional(
effectiveRequestUsername,
() -> finalUserHandle.flatMap(FinishAssertionSteps.this::getUsernameForUserHandle));
registration =
finalUserHandle.flatMap(uh -> credentialRepositoryV2.lookup(response.getId(), uh));
}
@Override
public Step7 nextStep() {
return new Step7(finalUsername, finalUserHandle.get(), registration);
}
@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.");
if (requestedUserHandle.isPresent() && responseUserHandle.isPresent()) {
assertTrue(
requestedUserHandle.get().equals(responseUserHandle.get()),
"User handle set in request (%s) does not match user handle in response (%s).",
requestedUserHandle.get(),
responseUserHandle.get());
}
if (userHandleDerivedFromUsername && responseUserHandle.isPresent()) {
assertTrue(
effectiveRequestUserHandle.get().equals(responseUserHandle.get()),
"User handle in request (%s) (derived from username: %s) does not match user handle in response (%s).",
effectiveRequestUserHandle.get(),
requestedUsername.get(),
responseUserHandle.get());
}
assertTrue(registration.isPresent(), "Unknown credential: %s", response.getId());
assertTrue(
finalUserHandle.get().equals(registration.get().getUserHandle()),
"User handle %s does not own credential %s",
finalUserHandle.get(),
response.getId());
if (usernameRepository.isPresent()) {
assertTrue(
finalUsername.isPresent(),
"Unknown username for user handle: %s",
finalUserHandle.get());
}
}
}
@Value
class Step7 implements Step {
private final Optional username;
private final ByteArray userHandle;
private final Optional credential;
@Override
public Step8 nextStep() {
return new Step8(username, credential.get());
}
@Override
public void validate() {
assertTrue(
credential.isPresent(),
"Unknown credential. Credential ID: %s, user handle: %s",
response.getId(),
userHandle);
}
}
@Value
class Step8 implements Step {
private final Optional username;
private final C credential;
@Override
public void validate() {
assertTrue(clientData() != null, "Missing client data.");
assertTrue(authenticatorData() != null, "Missing authenticator data.");
assertTrue(signature() != null, "Missing signature.");
}
@Override
public Step10 nextStep() {
return new Step10(username, credential);
}
public ByteArray authenticatorData() {
return response.getResponse().getAuthenticatorData();
}
public ByteArray clientData() {
return response.getResponse().getClientDataJSON();
}
public ByteArray signature() {
return response.getResponse().getSignature();
}
}
// Nothing to do for step 9
@Value
class Step10 implements Step {
private final Optional username;
private final C credential;
@Override
public void validate() {
assertTrue(clientData() != null, "Missing client data.");
}
@Override
public Step11 nextStep() {
return new Step11(username, credential, clientData());
}
public CollectedClientData clientData() {
return response.getResponse().getClientData();
}
}
@Value
class Step11 implements Step {
private final Optional username;
private final C credential;
private final CollectedClientData clientData;
@Override
public void validate() {
final String expectedType =
isSecurePaymentConfirmation ? SPC_CLIENT_DATA_TYPE : CLIENT_DATA_TYPE;
assertTrue(
expectedType.equals(clientData.getType()),
"The \"type\" in the client data must be exactly \"%s\", was: %s",
expectedType,
clientData.getType());
}
@Override
public Step12 nextStep() {
return new Step12(username, credential);
}
}
@Value
class Step12 implements Step {
private final Optional username;
private final C credential;
@Override
public void validate() {
assertTrue(
request
.getPublicKeyCredentialRequestOptions()
.getChallenge()
.equals(response.getResponse().getClientData().getChallenge()),
"Incorrect challenge.");
}
@Override
public Step13 nextStep() {
return new Step13(username, credential);
}
}
@Value
class Step13 implements Step {
private final Optional username;
private final C credential;
@Override
public void validate() {
final String responseOrigin = response.getResponse().getClientData().getOrigin();
assertTrue(
OriginMatcher.isAllowed(responseOrigin, origins, allowOriginPort, allowOriginSubdomain),
"Incorrect origin, please see the RelyingParty.origins setting: %s",
responseOrigin);
}
@Override
public Step14 nextStep() {
return new Step14(username, credential);
}
}
@Value
class Step14 implements Step {
private final Optional username;
private final C credential;
@Override
public void validate() {
TokenBindingValidator.validate(
response.getResponse().getClientData().getTokenBinding(), callerTokenBindingId);
}
@Override
public Step15 nextStep() {
return new Step15(username, credential);
}
}
@Value
class Step15 implements Step {
private final Optional username;
private final C credential;
@Override
public void validate() {
try {
assertTrue(
Crypto.sha256(rpId)
.equals(response.getResponse().getParsedAuthenticatorData().getRpIdHash()),
"Wrong RP ID hash.");
} catch (IllegalArgumentException e) {
Optional appid =
request.getPublicKeyCredentialRequestOptions().getExtensions().getAppid();
if (appid.isPresent()) {
assertTrue(
Crypto.sha256(appid.get().getId())
.equals(response.getResponse().getParsedAuthenticatorData().getRpIdHash()),
"Wrong RP ID hash.");
} else {
throw e;
}
}
}
@Override
public Step16 nextStep() {
return new Step16(username, credential);
}
}
@Value
class Step16 implements Step {
private final Optional username;
private final C credential;
@Override
public void validate() {
assertTrue(
response.getResponse().getParsedAuthenticatorData().getFlags().UP,
"User Presence is required.");
}
@Override
public Step17 nextStep() {
return new Step17(username, credential);
}
}
@Value
class Step17 implements Step {
private final Optional username;
private final C credential;
@Override
public void validate() {
if (request
.getPublicKeyCredentialRequestOptions()
.getUserVerification()
.equals(Optional.of(UserVerificationRequirement.REQUIRED))) {
assertTrue(
response.getResponse().getParsedAuthenticatorData().getFlags().UV,
"User Verification is required.");
}
}
@Override
public PendingStep16 nextStep() {
return new PendingStep16(username, credential);
}
}
@Value
// Step 16 in editor's draft as of 2022-11-09 https://w3c.github.io/webauthn/
// TODO: Finalize this when spec matures
class PendingStep16 implements Step {
private final Optional username;
private final C credential;
@Override
public void validate() {
assertTrue(
!credential.isBackupEligible().isPresent()
|| response.getResponse().getParsedAuthenticatorData().getFlags().BE
== credential.isBackupEligible().get(),
"Backup eligibility must not change; Stored: BE=%s, received: BE=%s for credential: %s",
credential.isBackupEligible(),
response.getResponse().getParsedAuthenticatorData().getFlags().BE,
credential.getCredentialId());
}
@Override
public Step18 nextStep() {
return new Step18(username, credential);
}
}
@Value
class Step18 implements Step {
private final Optional username;
private final C credential;
@Override
public void validate() {}
@Override
public Step19 nextStep() {
return new Step19(username, credential);
}
}
@Value
class Step19 implements Step {
private final Optional username;
private final C credential;
@Override
public void validate() {
assertTrue(clientDataJsonHash().size() == 32, "Failed to compute hash of client data");
}
@Override
public Step20 nextStep() {
return new Step20(username, credential, clientDataJsonHash());
}
public ByteArray clientDataJsonHash() {
return Crypto.sha256(response.getResponse().getClientDataJSON());
}
}
@Value
class Step20 implements Step {
private final Optional username;
private final C credential;
private final ByteArray clientDataJsonHash;
@Override
public void validate() {
final ByteArray cose = credential.getPublicKeyCose();
final PublicKey key;
try {
key = WebAuthnCodecs.importCosePublicKey(cose);
} catch (IOException | InvalidKeySpecException e) {
throw new IllegalArgumentException(
String.format(
"Failed to decode public key: Credential ID: %s COSE: %s",
credential.getCredentialId().getBase64Url(), cose.getBase64Url()),
e);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
final COSEAlgorithmIdentifier alg =
COSEAlgorithmIdentifier.fromPublicKey(cose)
.orElseThrow(
() ->
new IllegalArgumentException(
String.format("Failed to decode \"alg\" from COSE key: %s", cose)));
if (!Crypto.verifySignature(key, signedBytes(), response.getResponse().getSignature(), alg)) {
throw new IllegalArgumentException("Invalid assertion signature.");
}
}
@Override
public Step21 nextStep() {
return new Step21(username, credential);
}
public ByteArray signedBytes() {
return response.getResponse().getAuthenticatorData().concat(clientDataJsonHash);
}
}
@Value
class Step21 implements Step {
private final Optional username;
private final C credential;
private final long assertionSignatureCount;
private final long storedSignatureCountBefore;
public Step21(Optional username, C credential) {
this.username = username;
this.credential = credential;
this.assertionSignatureCount =
response.getResponse().getParsedAuthenticatorData().getSignatureCounter();
this.storedSignatureCountBefore = credential.getSignatureCount();
}
@Override
public void validate() throws InvalidSignatureCountException {
if (validateSignatureCounter && !signatureCounterValid()) {
throw new InvalidSignatureCountException(
response.getId(), storedSignatureCountBefore + 1, assertionSignatureCount);
}
}
private boolean signatureCounterValid() {
return (assertionSignatureCount == 0 && storedSignatureCountBefore == 0)
|| assertionSignatureCount > storedSignatureCountBefore;
}
@Override
public Finished nextStep() {
return new Finished(credential, username, assertionSignatureCount, signatureCounterValid());
}
}
@Value
class Finished implements Step {
private final C credential;
private final Optional username;
private final long assertionSignatureCount;
private final boolean signatureCounterValid;
@Override
public void validate() {
/* No-op */
}
@Override
public Finished nextStep() {
return this;
}
@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));
}
}
}