diff --git a/NEWS b/NEWS index ed0ec0578..0ed283e55 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,11 @@ +== Version 0.4.0 == + +Breaking changes: + +* Field `StartRegistrationOptions.requireResidentKey: boolean` replaced with + field `authenticatorSelection: Optional` + + == Version 0.3.0 == * Major API overhaul; public API changes include but are not limited to: @@ -25,10 +33,6 @@ failures ** Class `MetadataResolver` replaced with interface ** Constructor `CollectedClientData(JsonNode)` deleted - ** Type of fields `StartAssertionOptions.extensions`, - `StartRegistrationOptions.extensions` and - `PublicKeyCredential.clientExtensionOutputs` narrowed from `JsonNode` to - `ObjectNode` ** Parameters `StartRegistrationOptions.excludeCredentials` and `StartAssertionOptions.allowCredentials` deleted; they are now discovered automatically from the `CredentialRepository`. If custom control over diff --git a/README b/README index 2bd2d5252..6469ca280 100644 --- a/README +++ b/README @@ -1,8 +1,8 @@ == java-webauthn-server _Note: This is a work in progress. The https://www.w3.org/TR/webauthn/[Web -Authentication standard] is not yet finished, and any part of this project may -change at any time._ +Authentication standard] is not yet finished, and additional pre-1.0 releases of +this library may introduce breaking API changes._ image:https://travis-ci.org/Yubico/java-webauthn-server.svg?branch=master["Build Status", link="https://travis-ci.org/Yubico/java-webauthn-server"] image:https://coveralls.io/repos/github/Yubico/java-webauthn-server/badge.svg["Coverage Status", link="https://coveralls.io/github/Yubico/java-webauthn-server"] @@ -18,3 +18,39 @@ authenticators and authenticating registered authenticators. See link:webauthn-server-demo[`webauthn-server-demo`] for a complete demo server, which stores authenticator registrations temporarily in memory. + + +=== Building + +Use the included +https://docs.gradle.org/current/userguide/gradle_wrapper.html[Gradle wrapper] to +build the `.jar` artifact: + +``` +$ ./gradlew :webauthn-server-core:jar +``` + +The output is built in the `webauthn-server-core/build/libs/` directory, and the +version is derived from the most recent Git tag. Builds done on a tagged commit +will have a plain `x.y.z` version number, while a build on any other commit will +result in a version number containing the abbreviated commit hash. + +Although the `.jar` artifact of this project can be used in JDK version 8 or +later, the project as a whole currently builds only in JDK 8. This is because +most tests are written in Scala, which +https://docs.scala-lang.org/overviews/jdk-compatibility/overview.html#jdk-9\--up-compatibility-notes[currently +only supports JDK 8]. Therefore compiling the tests can currently only be done +in JDK 8, and so `./gradlew build` and similar tasks will fail in JDKs other +than 8. + +To run the tests (requires JDK 8): + +``` +$ ./gradlew check +``` + +To run the http://pitest.org/[PIT mutation tests] (requires JDK 8): + +``` +$ ./gradlew pitest +``` diff --git a/build.gradle b/build.gradle index b631a220e..b555a6a0e 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ buildscript { } dependencies { configurations.maybeCreate('pitest') - classpath 'com.cinnober.gradle:semver-git:2.3.1' + classpath 'com.cinnober.gradle:semver-git:2.4.0' classpath 'info.solidsoft.gradle.pitest:gradle-pitest-plugin:1.3.0' pitest 'org.pitest:pitest-command-line:1.4.2' // Transitive dependency from pitest plugin } @@ -15,8 +15,11 @@ plugins { id 'net.researchgate.release' version '2.4.0' } +project.ext.isCiBuild = System.env.CI == 'true' -project.ext.publishEnabled = System.env.CI != 'true' && project.hasProperty('ossrhUsername') +project.ext.publishEnabled = !isCiBuild && + project.hasProperty('yubicoPublish') && project.yubicoPublish && + project.hasProperty('ossrhUsername') && project.hasProperty('ossrhPassword') if (publishEnabled) { nexusStaging { @@ -31,22 +34,13 @@ task wrapper(type: Wrapper) { } allprojects { - ext.snapshotSuffix = ".g-SNAPSHOT" + ext.snapshotSuffix = ".g-SNAPSHOT" + ext.dirtyMarker = "-DIRTY" apply plugin: 'com.cinnober.gradle.semver-git' apply plugin: 'java' - apply plugin: 'maven' - apply plugin: 'signing' apply plugin: 'idea' - if (publishEnabled) { - signing { - useGpgCmd() - sign configurations.archives - } - signArchives.dependsOn check - } - group = 'com.yubico' sourceCompatibility = 1.8 @@ -67,6 +61,10 @@ allprojects { test { failFast = true + + testLogging { + showStandardStreams = isCiBuild + } } } @@ -101,7 +99,17 @@ subprojects { } - if (publishEnabled) { + if (publishEnabled && project.hasProperty('publishMe') && project.publishMe) { + + apply plugin: 'maven' + apply plugin: 'signing' + + signing { + useGpgCmd() + sign configurations.archives + } + signArchives.dependsOn check + uploadArchives { repositories { mavenDeployer { diff --git a/webauthn-server-attestation/build.gradle b/webauthn-server-attestation/build.gradle index 95260b756..835fd12e8 100644 --- a/webauthn-server-attestation/build.gradle +++ b/webauthn-server-attestation/build.gradle @@ -2,6 +2,8 @@ description = 'Yubico WebAuthn attestation subsystem' apply plugin: 'java' +project.ext.publishMe = true + dependencies { compile( diff --git a/webauthn-server-core/README b/webauthn-server-core/README new file mode 100644 index 000000000..19ff5f396 --- /dev/null +++ b/webauthn-server-core/README @@ -0,0 +1,29 @@ += Web Authentication server library + +Implementation of a Web Authentication Relying Party (RP). + + +== Running + +An example app is included in the +link:../webauthn-server-demo[`webauthn-server-demo`] directory. See +link:../webauthn-server-demo/README[its README] for instructions on how to run +it. + + +== Unimplemented features + +* https://www.w3.org/TR/webauthn/#ecdaa[ECDAA] attestation type +* Attestation statement formats: + ** https://www.w3.org/TR/webauthn/#tpm-attestation[`tpm`] + ** https://www.w3.org/TR/webauthn/#android-key-attestation[`android-key`] + ** https://www.w3.org/TR/webauthn/#android-safetynet-attestation[`android-safetynet`] +* Extensions: + ** https://www.w3.org/TR/webauthn/#sctn-simple-txauth-extension[`txAuthSimple`] + ** https://www.w3.org/TR/webauthn/#sctn-generic-txauth-extension[`txAuthGeneric`] + ** https://www.w3.org/TR/webauthn/#sctn-authenticator-selection-extension[`authnSel`] + ** https://www.w3.org/TR/webauthn/#sctn-supported-extensions-extension[`exts`] + ** https://www.w3.org/TR/webauthn/#sctn-uvi-extension[`uvi`] + ** https://www.w3.org/TR/webauthn/#sctn-location-extension[`loc`] + ** https://www.w3.org/TR/webauthn/#sctn-uvm-extension[`uvm`] + ** https://www.w3.org/TR/webauthn/#sctn-authenticator-biometric-criteria-extension[`biometricPerfBounds`] diff --git a/webauthn-server-core/README.adoc b/webauthn-server-core/README.adoc new file mode 120000 index 000000000..100b93820 --- /dev/null +++ b/webauthn-server-core/README.adoc @@ -0,0 +1 @@ +README \ No newline at end of file diff --git a/webauthn-server-core/README.md b/webauthn-server-core/README.md deleted file mode 100644 index f523483ba..000000000 --- a/webauthn-server-core/README.md +++ /dev/null @@ -1,33 +0,0 @@ -Web Authentication server library (EXPERIMENTAL) -=== - -This is a prototype implementation of a Web Authentication Relying Party (RP). - - -Running ---- - -An example app is included in the -[webauthn-server-demo](../webauthn-server-demo) directory. See [its -README](../webauthn-server-demo/README) for instructions on how to run it. - - -Implementation status ---- - -The following combinations of user agent and authenticator are known to work: - -- Firefox Nightly 57.0a1 2017-09-15 - - YubiKey 4 - - YubiKey 4 Nano - - YubiKey Neo - although with random failures in `credentials.get()` (login) - - YubiKey 4C - although with random failures in `credentials.get()` (login) - - FIDO U2F Security Key by Yubico - -Test results generated from commit 8120cf1. - -### Registration -![Implementation status: Registration](test-registration.png) - -### Authentication -![Implementation status: Authentication](test-assertion.png) diff --git a/webauthn-server-core/build.gradle b/webauthn-server-core/build.gradle index b5ee76d59..8061e5930 100644 --- a/webauthn-server-core/build.gradle +++ b/webauthn-server-core/build.gradle @@ -1,14 +1,10 @@ -import java.nio.file.Paths - apply plugin: 'info.solidsoft.pitest' description = 'WebAuthn core API' apply plugin: 'scala' -configurations { - scalaRepl { extendsFrom testRuntime } -} +project.ext.publishMe = true dependencies { @@ -31,11 +27,6 @@ dependencies { 'org.scalacheck:scalacheck_2.11:1.13.5', ) - scalaRepl( - 'org.scala-lang:scala-compiler:2.11.3', - jar.outputs.files, - ) - } @@ -68,17 +59,3 @@ pitest { ] } -def scalaReplDistDir = file(Paths.get(project.distsDir.path, 'scala-repl')) -task scalaReplClasspath(type: Sync) { - from configurations.scalaRepl - destinationDir = file(Paths.get(scalaReplDistDir.path, 'lib')) -} - -task makeScalaReplScripts(type: CreateStartScripts) { - inputs.files tasks.scalaReplClasspath.outputs - - applicationName = 'scala-repl' - classpath = configurations.scalaRepl - mainClassName = "scala.tools.nsc.MainGenericRunner" - outputDir = file(Paths.get(scalaReplDistDir.path, 'bin')) -} diff --git a/webauthn-server-core/src/main/java/com/yubico/internal/util/WebAuthnCodecs.java b/webauthn-server-core/src/main/java/com/yubico/internal/util/WebAuthnCodecs.java index 5123df7d3..c5ea4a81a 100644 --- a/webauthn-server-core/src/main/java/com/yubico/internal/util/WebAuthnCodecs.java +++ b/webauthn-server-core/src/main/java/com/yubico/internal/util/WebAuthnCodecs.java @@ -6,6 +6,7 @@ import com.fasterxml.jackson.core.Base64Variants; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.dataformat.cbor.CBORFactory; import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; @@ -28,6 +29,7 @@ public static ObjectMapper cbor() { public static ObjectMapper json() { return new ObjectMapper() .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true) + .configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false) .setSerializationInclusion(Include.NON_ABSENT) .setBase64Variant(Base64Variants.MODIFIED_FOR_URL) .registerModule(new Jdk8Module()) @@ -84,7 +86,7 @@ public static ByteArray rawEcdaKeyToCose(ByteArray key) { Map coseKey = new HashMap<>(); coseKey.put(1L, 2L); // Key type: EC - coseKey.put(3L, javaAlgorithmNameToCoseAlgorithmIdentifier("ES256").getId()); + coseKey.put(3L, COSEAlgorithmIdentifier.ES256.getId()); coseKey.put(-1L, 1L); // Curve: P-256 coseKey.put(-2L, Arrays.copyOfRange(keyBytes, start, start + 32)); // x coseKey.put(-3L, Arrays.copyOfRange(keyBytes, start + 32, start + 64)); // y @@ -100,17 +102,4 @@ public static ECPublicKey importCoseP256PublicKey(ByteArray key) throws CoseExce return new COSE.ECPublicKey(new OneKey(CBORObject.DecodeFromBytes(key.getBytes()))); } - public static COSEAlgorithmIdentifier javaAlgorithmNameToCoseAlgorithmIdentifier(String alg) { - switch (alg) { - case "ECDSA": - case "ES256": - return COSEAlgorithmIdentifier.ES256; - - case "RS256": - return COSEAlgorithmIdentifier.RS256; - } - - throw new IllegalArgumentException("Unknown algorithm: " + alg); - } - } 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 bbf34e658..a6ecb72bb 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 @@ -6,7 +6,6 @@ import com.yubico.webauthn.data.AttestationConveyancePreference; import com.yubico.webauthn.data.AuthenticatorAssertionResponse; import com.yubico.webauthn.data.AuthenticatorAttestationResponse; -import com.yubico.webauthn.data.AuthenticatorSelectionCriteria; import com.yubico.webauthn.data.ByteArray; import com.yubico.webauthn.data.ClientAssertionExtensionOutputs; import com.yubico.webauthn.data.ClientRegistrationExtensionOutputs; @@ -68,11 +67,7 @@ public PublicKeyCredentialCreationOptions startRegistration(StartRegistrationOpt .excludeCredentials( Optional.of(credentialRepository.getCredentialIdsForUsername(startRegistrationOptions.getUser().getName())) ) - .authenticatorSelection(Optional.of( - AuthenticatorSelectionCriteria.builder() - .requireResidentKey(startRegistrationOptions.isRequireResidentKey()) - .build() - )) + .authenticatorSelection(startRegistrationOptions.getAuthenticatorSelection()) .attestation(attestationConveyancePreference.orElse(AttestationConveyancePreference.DEFAULT)) .extensions(startRegistrationOptions.getExtensions()) .build(); diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/StartRegistrationOptions.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/StartRegistrationOptions.java index c442ed816..d9eaf0381 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/StartRegistrationOptions.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/StartRegistrationOptions.java @@ -1,7 +1,9 @@ package com.yubico.webauthn; +import com.yubico.webauthn.data.AuthenticatorSelectionCriteria; import com.yubico.webauthn.data.RegistrationExtensionInputs; import com.yubico.webauthn.data.UserIdentity; +import java.util.Optional; import lombok.Builder; import lombok.NonNull; import lombok.Value; @@ -15,9 +17,10 @@ public class StartRegistrationOptions { @NonNull @Builder.Default - private final RegistrationExtensionInputs extensions = RegistrationExtensionInputs.builder().build(); + private final Optional authenticatorSelection = Optional.empty(); + @NonNull @Builder.Default - private final boolean requireResidentKey = false; + private final RegistrationExtensionInputs extensions = RegistrationExtensionInputs.builder().build(); } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/U2fBadInputException.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/U2fBadInputException.java deleted file mode 100644 index 166f7b9d0..000000000 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/U2fBadInputException.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2014 Yubico. - * Copyright 2014 Google Inc. All rights reserved. - * - * Use of this source code is governed by a BSD-style - * license that can be found in the LICENSE file or at - * https://developers.google.com/open-source/licenses/bsd - */ - -package com.yubico.webauthn; - -/** - * Thrown when invalid data is given to the server by an external caller. - */ -@SuppressWarnings("serial") -class U2fBadInputException extends Exception { - - public U2fBadInputException(String message) { - super(message); - } - - public U2fBadInputException(String message, Throwable cause) { - super(message, cause); - } -} diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/U2fRawRegisterResponse.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/U2fRawRegisterResponse.java index e8a1986fc..269383cab 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/U2fRawRegisterResponse.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/U2fRawRegisterResponse.java @@ -11,12 +11,7 @@ import com.google.common.io.ByteArrayDataOutput; import com.google.common.io.ByteStreams; -import com.yubico.internal.util.ByteInputStream; -import com.yubico.internal.util.CertificateParser; import com.yubico.webauthn.data.ByteArray; -import com.yubico.webauthn.data.exception.Base64UrlException; -import java.io.IOException; -import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import lombok.EqualsAndHashCode; import lombok.ToString; @@ -27,7 +22,6 @@ @EqualsAndHashCode @ToString class U2fRawRegisterResponse { - static final byte REGISTRATION_RESERVED_BYTE_VALUE = (byte) 0x05; private static final byte REGISTRATION_SIGNED_RESERVED_BYTE_VALUE = (byte) 0x00; @EqualsAndHashCode.Exclude @@ -37,31 +31,31 @@ class U2fRawRegisterResponse { * The (uncompressed) x,y-representation of a curve point on the P-256 * NIST elliptic curve. */ - final ByteArray userPublicKey; + private final ByteArray userPublicKey; /** * A handle that allows the U2F token to identify the generated key pair. */ - final ByteArray keyHandle; - final X509Certificate attestationCertificate; + private final ByteArray keyHandle; + private final X509Certificate attestationCertificate; /** * A ECDSA signature (on P-256) */ - final ByteArray signature; + private final ByteArray signature; - public U2fRawRegisterResponse(ByteArray userPublicKey, - ByteArray keyHandle, - X509Certificate attestationCertificate, - ByteArray signature) { + U2fRawRegisterResponse(ByteArray userPublicKey, + ByteArray keyHandle, + X509Certificate attestationCertificate, + ByteArray signature) { this(userPublicKey, keyHandle, attestationCertificate, signature, new BouncyCastleCrypto()); } - public U2fRawRegisterResponse(ByteArray userPublicKey, - ByteArray keyHandle, - X509Certificate attestationCertificate, - ByteArray signature, - Crypto crypto) { + private U2fRawRegisterResponse(ByteArray userPublicKey, + ByteArray keyHandle, + X509Certificate attestationCertificate, + ByteArray signature, + Crypto crypto) { this.userPublicKey = userPublicKey; this.keyHandle = keyHandle; this.attestationCertificate = attestationCertificate; @@ -69,37 +63,12 @@ public U2fRawRegisterResponse(ByteArray userPublicKey, this.crypto = crypto; } - static U2fRawRegisterResponse fromBase64(String rawDataBase64, Crypto crypto) throws U2fBadInputException, Base64UrlException { - ByteInputStream bytes = new ByteInputStream(ByteArray.fromBase64Url(rawDataBase64).getBytes()); - try { - byte reservedByte = bytes.readSigned(); - if (reservedByte != REGISTRATION_RESERVED_BYTE_VALUE) { - throw new U2fBadInputException( - "Incorrect value of reserved byte. Expected: " + REGISTRATION_RESERVED_BYTE_VALUE + - ". Was: " + reservedByte - ); - } - - return new U2fRawRegisterResponse( - new ByteArray(bytes.read(65)), - new ByteArray(bytes.read(bytes.readUnsigned())), - CertificateParser.parseDer(bytes), - new ByteArray(bytes.readAll()), - crypto - ); - } catch (CertificateException e) { - throw new U2fBadInputException("Malformed attestation certificate", e); - } catch (IOException e) { - throw new U2fBadInputException("Truncated registration data", e); - } - } - - public boolean verifySignature(ByteArray appIdHash, ByteArray clientDataHash) { + boolean verifySignature(ByteArray appIdHash, ByteArray clientDataHash) { ByteArray signedBytes = packBytesToSign(appIdHash, clientDataHash, keyHandle, userPublicKey); return crypto.verifySignature(attestationCertificate, signedBytes, signature); } - static ByteArray packBytesToSign(ByteArray appIdHash, ByteArray clientDataHash, ByteArray keyHandle, ByteArray userPublicKey) { + private static ByteArray packBytesToSign(ByteArray appIdHash, ByteArray clientDataHash, ByteArray keyHandle, ByteArray userPublicKey) { ByteArrayDataOutput encoded = ByteStreams.newDataOutput(); encoded.write(REGISTRATION_SIGNED_RESERVED_BYTE_VALUE); encoded.write(appIdHash.getBytes()); diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AssertionExtensionInputs.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AssertionExtensionInputs.java index 42bb461b6..222458c8c 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AssertionExtensionInputs.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AssertionExtensionInputs.java @@ -26,7 +26,6 @@ private AssertionExtensionInputs( } @Override - @JsonIgnore public Set getExtensionIds() { Set ids = new HashSet<>(); diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorAttestationResponse.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorAttestationResponse.java index 23692056b..fced7ff10 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorAttestationResponse.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorAttestationResponse.java @@ -19,6 +19,7 @@ public class AuthenticatorAttestationResponse implements AuthenticatorResponse { private final ByteArray clientDataJSON; @NonNull + @JsonIgnore private final transient AttestationObject attestation; @NonNull diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/ClientAssertionExtensionOutputs.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/ClientAssertionExtensionOutputs.java index dd4a94344..a20342938 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/ClientAssertionExtensionOutputs.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/ClientAssertionExtensionOutputs.java @@ -26,7 +26,6 @@ private ClientAssertionExtensionOutputs( } @Override - @JsonIgnore public Set getExtensionIds() { Set ids = new HashSet<>(); diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/ClientExtensionOutputs.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/ClientExtensionOutputs.java index 07dcd5f9a..f0b8c9aa9 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/ClientExtensionOutputs.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/ClientExtensionOutputs.java @@ -1,9 +1,11 @@ package com.yubico.webauthn.data; +import com.fasterxml.jackson.annotation.JsonIgnore; import java.util.Set; public interface ClientExtensionOutputs { + @JsonIgnore Set getExtensionIds(); } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/ExtensionInputs.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/ExtensionInputs.java index 1495e551e..46d287a3f 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/ExtensionInputs.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/ExtensionInputs.java @@ -1,9 +1,11 @@ package com.yubico.webauthn.data; +import com.fasterxml.jackson.annotation.JsonIgnore; import java.util.Set; public interface ExtensionInputs { + @JsonIgnore Set getExtensionIds(); } diff --git a/webauthn-server-core/src/test/java/com/yubico/u2f/testdata/GnubbyKey.java b/webauthn-server-core/src/test/java/com/yubico/u2f/testdata/GnubbyKey.java deleted file mode 100644 index 6b3d45be5..000000000 --- a/webauthn-server-core/src/test/java/com/yubico/u2f/testdata/GnubbyKey.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.yubico.u2f.testdata; - -import com.yubico.internal.util.CertificateParser; -import java.security.PrivateKey; -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; - -import static com.yubico.webauthn.TestUtils.parsePrivateKey; - -public class GnubbyKey { - - public static final X509Certificate ATTESTATION_CERTIFICATE = getAttestationCertificate(); - - private static X509Certificate getAttestationCertificate() { - try { - return CertificateParser.parseDer(GnubbyKey.class.getResourceAsStream("gnubby/attestation-certificate.der")); - } catch (CertificateException e) { - throw new RuntimeException(e); - } - } - - public static final PrivateKey ATTESTATION_CERTIFICATE_PRIVATE_KEY = - parsePrivateKey(GnubbyKey.class.getResourceAsStream("gnubby/attestation-certificate-private-key.hex")); -} diff --git a/webauthn-server-core/src/test/java/com/yubico/webauthn/CodecTestUtils.java b/webauthn-server-core/src/test/java/com/yubico/webauthn/CodecTestUtils.java deleted file mode 100644 index f97c3786a..000000000 --- a/webauthn-server-core/src/test/java/com/yubico/webauthn/CodecTestUtils.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2014 Yubico. - * Copyright 2014 Google Inc. All rights reserved. - * - * Use of this source code is governed by a BSD-style - * license that can be found in the LICENSE file or at - * https://developers.google.com/open-source/licenses/bsd - */ - -package com.yubico.webauthn; - -import com.google.common.io.ByteArrayDataOutput; -import com.google.common.io.ByteStreams; -import com.yubico.webauthn.data.ByteArray; -import java.security.cert.CertificateEncodingException; - -public class CodecTestUtils { - public static ByteArray encodeRegisterResponse(U2fRawRegisterResponse rawRegisterResponse) throws U2fBadInputException { - ByteArray keyHandle = rawRegisterResponse.keyHandle; - if (keyHandle.getBytes().length > 255) { - throw new U2fBadInputException("keyHandle length cannot be longer than 255 bytes!"); - } - - try { - ByteArrayDataOutput encoded = ByteStreams.newDataOutput(); - encoded.write(U2fRawRegisterResponse.REGISTRATION_RESERVED_BYTE_VALUE); - encoded.write(rawRegisterResponse.userPublicKey.getBytes()); - encoded.write((byte) keyHandle.getBytes().length); - encoded.write(keyHandle.getBytes()); - encoded.write(rawRegisterResponse.attestationCertificate.getEncoded()); - encoded.write(rawRegisterResponse.signature.getBytes()); - return new ByteArray(encoded.toByteArray()); - } catch (CertificateEncodingException e) { - throw new U2fBadInputException("Error when encoding attestation certificate.", e); - } - } -} diff --git a/webauthn-server-core/src/test/java/com/yubico/webauthn/RawCodecTest.java b/webauthn-server-core/src/test/java/com/yubico/webauthn/RawCodecTest.java deleted file mode 100644 index bc51c6d99..000000000 --- a/webauthn-server-core/src/test/java/com/yubico/webauthn/RawCodecTest.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2014 Yubico. - * Copyright 2014 Google Inc. All rights reserved. - * - * Use of this source code is governed by a BSD-style - * license that can be found in the LICENSE file or at - * https://developers.google.com/open-source/licenses/bsd - */ - -package com.yubico.webauthn; - -import com.yubico.u2f.testdata.GnubbyKey; -import com.yubico.webauthn.data.ByteArray; -import org.junit.Assert; -import org.junit.Test; - -import static org.junit.Assert.assertEquals; - -public class RawCodecTest { - - Crypto crypto = new BouncyCastleCrypto(); - - @Test - public void testEncodeRegisterResponse() throws Exception { - U2fRawRegisterResponse rawRegisterResponse = new U2fRawRegisterResponse(TestVectors.USER_PUBLIC_KEY_REGISTER_HEX, - TestVectors.KEY_HANDLE, GnubbyKey.ATTESTATION_CERTIFICATE, TestVectors.SIGNATURE_REGISTER); - ByteArray encodedBytes = CodecTestUtils.encodeRegisterResponse(rawRegisterResponse); - Assert.assertEquals(TestVectors.REGISTRATION_RESPONSE_DATA, encodedBytes); - } - - @Test - public void testEncodeRegisterSignedBytes() { - ByteArray encodedBytes = U2fRawRegisterResponse.packBytesToSign(TestVectors.APP_ID_ENROLL_SHA256, - TestVectors.CLIENT_DATA_ENROLL_SHA256, TestVectors.KEY_HANDLE, TestVectors.USER_PUBLIC_KEY_REGISTER_HEX); - Assert.assertEquals(TestVectors.EXPECTED_REGISTER_SIGNED_BYTES, encodedBytes); - } - - @Test - public void testDecodeRegisterResponse() throws Exception { - U2fRawRegisterResponse rawRegisterResponse = - U2fRawRegisterResponse.fromBase64(TestVectors.REGISTRATION_RESPONSE_DATA.getBase64Url(), crypto); - - assertEquals(new U2fRawRegisterResponse(TestVectors.USER_PUBLIC_KEY_REGISTER_HEX, - TestVectors.KEY_HANDLE, GnubbyKey.ATTESTATION_CERTIFICATE, TestVectors.SIGNATURE_REGISTER), rawRegisterResponse); - } - -} diff --git a/webauthn-server-core/src/test/java/com/yubico/webauthn/SerialCodecTest.java b/webauthn-server-core/src/test/java/com/yubico/webauthn/SerialCodecTest.java deleted file mode 100644 index 09862e197..000000000 --- a/webauthn-server-core/src/test/java/com/yubico/webauthn/SerialCodecTest.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2014 Yubico. - * Copyright 2014 Google Inc. All rights reserved. - * - * Use of this source code is governed by a BSD-style - * license that can be found in the LICENSE file or at - * https://developers.google.com/open-source/licenses/bsd - */ - -package com.yubico.webauthn; - -import com.yubico.webauthn.data.ByteArray; -import org.junit.Assert; -import org.junit.Test; - -import static com.yubico.u2f.testdata.GnubbyKey.ATTESTATION_CERTIFICATE; -import static org.junit.Assert.assertEquals; - -public class SerialCodecTest { - - private static final Crypto crypto = new BouncyCastleCrypto(); - - @Test - public void testEncodeRegisterResponse() throws Exception { - U2fRawRegisterResponse rawRegisterResponse = new U2fRawRegisterResponse(TestVectors.USER_PUBLIC_KEY_REGISTER_HEX, - TestVectors.KEY_HANDLE, ATTESTATION_CERTIFICATE, TestVectors.SIGNATURE_REGISTER); - - ByteArray encodedBytes = CodecTestUtils.encodeRegisterResponse(rawRegisterResponse); - - Assert.assertEquals(TestVectors.REGISTRATION_RESPONSE_DATA, encodedBytes); - } - - @Test - public void testEncodeRegisterSignedBytes() { - ByteArray encodedBytes = U2fRawRegisterResponse.packBytesToSign(TestVectors.APP_ID_ENROLL_SHA256, - TestVectors.CLIENT_DATA_ENROLL_SHA256, TestVectors.KEY_HANDLE, TestVectors.USER_PUBLIC_KEY_REGISTER_HEX); - - Assert.assertEquals(TestVectors.EXPECTED_REGISTER_SIGNED_BYTES, encodedBytes); - } - - @Test - public void testDecodeRegisterResponse() throws Exception { - U2fRawRegisterResponse rawRegisterResponse = - U2fRawRegisterResponse.fromBase64(TestVectors.REGISTRATION_RESPONSE_DATA.getBase64Url(), crypto); - - assertEquals(new U2fRawRegisterResponse(TestVectors.USER_PUBLIC_KEY_REGISTER_HEX, - TestVectors.KEY_HANDLE, ATTESTATION_CERTIFICATE, TestVectors.SIGNATURE_REGISTER), rawRegisterResponse); - } - -} diff --git a/webauthn-server-core/src/test/java/com/yubico/webauthn/TestUtils.java b/webauthn-server-core/src/test/java/com/yubico/webauthn/TestUtils.java deleted file mode 100644 index a546dac62..000000000 --- a/webauthn-server-core/src/test/java/com/yubico/webauthn/TestUtils.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2014 Yubico. - * Copyright 2014 Google Inc. All rights reserved. - * - * Use of this source code is governed by a BSD-style - * license that can be found in the LICENSE file or at - * https://developers.google.com/open-source/licenses/bsd - */ - -package com.yubico.webauthn; - -import java.io.InputStream; -import java.math.BigInteger; -import java.security.KeyFactory; -import java.security.NoSuchAlgorithmException; -import java.security.PrivateKey; -import java.security.Provider; -import java.security.spec.InvalidKeySpecException; -import java.util.Scanner; -import org.bouncycastle.asn1.sec.SECNamedCurves; -import org.bouncycastle.asn1.x9.X9ECParameters; -import org.bouncycastle.jce.spec.ECParameterSpec; -import org.bouncycastle.jce.spec.ECPrivateKeySpec; - -public class TestUtils { - - private static final Provider provider = new BouncyCastleCrypto().getProvider(); - - public static PrivateKey parsePrivateKey(InputStream is) { - String keyBytesHex = new Scanner(is).nextLine(); - return parsePrivateKey(keyBytesHex); - } - - public static PrivateKey parsePrivateKey(String keyBytesHex) { - try { - KeyFactory fac = KeyFactory.getInstance("ECDSA", provider); - X9ECParameters curve = SECNamedCurves.getByName("secp256r1"); - ECParameterSpec curveSpec = new ECParameterSpec( - curve.getCurve(), curve.getG(), curve.getN(), curve.getH()); - ECPrivateKeySpec keySpec = new ECPrivateKeySpec( - new BigInteger(keyBytesHex, 16), - curveSpec); - return fac.generatePrivate(keySpec); - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException(e); - } catch (InvalidKeySpecException e) { - throw new RuntimeException(e); - } - } - -} diff --git a/webauthn-server-core/src/test/java/com/yubico/webauthn/TestVectors.java b/webauthn-server-core/src/test/java/com/yubico/webauthn/TestVectors.java deleted file mode 100644 index 30646c6a1..000000000 --- a/webauthn-server-core/src/test/java/com/yubico/webauthn/TestVectors.java +++ /dev/null @@ -1,164 +0,0 @@ -/* - * Copyright 2014 Yubico. - * Copyright 2014 Google Inc. All rights reserved. - * - * Use of this source code is governed by a BSD-style - * license that can be found in the LICENSE file or at - * https://developers.google.com/open-source/licenses/bsd - */ - -package com.yubico.webauthn; - -import com.google.common.collect.ImmutableSet; -import com.yubico.webauthn.data.ByteArray; -import com.yubico.webauthn.data.exception.HexException; -import java.util.Set; - -final public class TestVectors { - private final static Crypto crypto = new BouncyCastleCrypto(); - - //Test vectors from FIDO U2F: Raw Message Formats - Draft 4 - public static final int COUNTER_VALUE = 1; - public static final Set TRUSTED_DOMAINS = ImmutableSet.of("http://example.com"); - public static final String APP_ID_ENROLL = "http://example.com"; - public static final ByteArray APP_ID_ENROLL_SHA256 = crypto.hash(APP_ID_ENROLL); - public static final String APP_ID_SIGN = "https://gstatic.com/securitykey/a/example.com"; - public static final ByteArray APP_ID_SIGN_SHA256 = crypto.hash(APP_ID_SIGN); - public static final String ORIGIN = "http://example.com"; - public static final String SERVER_CHALLENGE_REGISTER_BASE64 = - "vqrS6WXDe1JUs5_c3i4-LkKIHRr-3XVb3azuA5TifHo"; - public static final String SERVER_CHALLENGE_SIGN_BASE64 = "opsXqUifDriAAmWclinfbS0e-USY0CgyJHe_Otd7z8o"; - - public static final String CHANNEL_ID_STRING = - "{" - + "\"kty\":\"EC\"," - + "\"crv\":\"P-256\"," - + "\"x\":\"HzQwlfXX7Q4S5MtCCnZUNBw3RMzPO9tOyWjBqRl4tJ8\"," - + "\"y\":\"XVguGFLIZx1fXg3wNqfdbn75hi4-_7-BxhMljw42Ht4\"" - + "}"; - public static final String CLIENT_DATA_REGISTER = String.format( - "{" - + "\"typ\":\"navigator.id.finishEnrollment\"," - + "\"challenge\":\"%s\"," - + "\"cid_pubkey\":%s," - + "\"origin\":\"%s\"}", - SERVER_CHALLENGE_REGISTER_BASE64, - CHANNEL_ID_STRING, - ORIGIN); - public static final ByteArray CLIENT_DATA_ENROLL_SHA256 = crypto.hash(CLIENT_DATA_REGISTER); - public static final String CLIENT_DATA_SIGN = String.format( - "{" - + "\"typ\":\"navigator.id.getAssertion\"," - + "\"challenge\":\"%s\"," - + "\"cid_pubkey\":%s," - + "\"origin\":\"%s\"}", - SERVER_CHALLENGE_SIGN_BASE64, - CHANNEL_ID_STRING, - ORIGIN); - public static final ByteArray CLIENT_DATA_SIGN_SHA256 = fromHex( - "ccd6ee2e47baef244d49a222db496bad0ef5b6f93aa7cc4d30c4821b3b9dbc57"); - public static final ByteArray REGISTRATION_REQUEST_DATA = fromHex( - "4142d21c00d94ffb9d504ada8f99b721f4b191ae4e37ca0140f696b6983cfacb" - + "f0e6a6a97042a4f1f1c87f5f7d44315b2d852c2df5c7991cc66241bf7072d1c4"); - public static final ByteArray REGISTRATION_RESPONSE_DATA = fromHex( - "0504b174bc49c7ca254b70d2e5c207cee9cf174820ebd77ea3c65508c26da51b" - + "657c1cc6b952f8621697936482da0a6d3d3826a59095daf6cd7c03e2e60385d2" - + "f6d9402a552dfdb7477ed65fd84133f86196010b2215b57da75d315b7b9e8fe2" - + "e3925a6019551bab61d16591659cbaf00b4950f7abfe6660e2e006f76868b772" - + "d70c253082013c3081e4a003020102020a47901280001155957352300a06082a" - + "8648ce3d0403023017311530130603550403130c476e756262792050696c6f74" - + "301e170d3132303831343138323933325a170d3133303831343138323933325a" - + "3031312f302d0603550403132650696c6f74476e756262792d302e342e312d34" - + "373930313238303030313135353935373335323059301306072a8648ce3d0201" - + "06082a8648ce3d030107034200048d617e65c9508e64bcc5673ac82a6799da3c" - + "1446682c258c463fffdf58dfd2fa3e6c378b53d795c4a4dffb4199edd7862f23" - + "abaf0203b4b8911ba0569994e101300a06082a8648ce3d040302034700304402" - + "2060cdb6061e9c22262d1aac1d96d8c70829b2366531dda268832cb836bcd30d" - + "fa0220631b1459f09e6330055722c8d89b7f48883b9089b88d60d1d9795902b3" - + "0410df304502201471899bcc3987e62e8202c9b39c33c19033f7340352dba80f" - + "cab017db9230e402210082677d673d891933ade6f617e5dbde2e247e70423fd5" - + "ad7804a6d3d3961ef871"); - - public static final ByteArray REGISTRATION_RESPONSE_DATA_WITH_DIFFERENT_APP_ID = fromHex( - "0504b174bc49c7ca254b70d2e5c207cee9cf174820ebd77ea3c65508c26da51b" - + "657c1cc6b952f8621697936482da0a6d3d3826a59095daf6cd7c03e2e60385d2" - + "f6d9402a552dfdb7477ed65fd84133f86196010b2215b57da75d315b7b9e8fe2" - + "e3925a6019551bab61d16591659cbaf00b4950f7abfe6660e2e006f76868b772" - + "d70c253082013c3081e4a003020102020a47901280001155957352300a06082a" - + "8648ce3d0403023017311530130603550403130c476e756262792050696c6f74" - + "301e170d3132303831343138323933325a170d3133303831343138323933325a" - + "3031312f302d0603550403132650696c6f74476e756262792d302e342e312d34" - + "373930313238303030313135353935373335323059301306072a8648ce3d0201" - + "06082a8648ce3d030107034200048d617e65c9508e64bcc5673ac82a6799da3c" - + "1446682c258c463fffdf58dfd2fa3e6c378b53d795c4a4dffb4199edd7862f23" - + "abaf0203b4b8911ba0569994e101300a06082a8648ce3d040302034700304402" - + "2060cdb6061e9c22262d1aac1d96d8c70829b2366531dda268832cb836bcd30d" - + "fa0220631b1459f09e6330055722c8d89b7f48883b9089b88d60d1d9795902b3" - + "0410df3046022100d2b4702fea46b322c5addd921b3f4f0fb15c69737fe7441e" - + "b764c03dc8f49d09022100eef7dcdf6070d8e5a45ed6be18dfc036ebf8b4faaa" - + "ce7287b56e7fac1d2cb552"); - - public static final ByteArray REGISTRATION_RESPONSE_DATA_WITH_DIFFERENT_CLIENT_DATA_TYPE = fromHex( - "0504b174bc49c7ca254b70d2e5c207cee9cf174820ebd77ea3c65508c26da51b" - + "657c1cc6b952f8621697936482da0a6d3d3826a59095daf6cd7c03e2e60385d2" - + "f6d9402a552dfdb7477ed65fd84133f86196010b2215b57da75d315b7b9e8fe2" - + "e3925a6019551bab61d16591659cbaf00b4950f7abfe6660e2e006f76868b772" - + "d70c253082013c3081e4a003020102020a47901280001155957352300a06082a" - + "8648ce3d0403023017311530130603550403130c476e756262792050696c6f74" - + "301e170d3132303831343138323933325a170d3133303831343138323933325a" - + "3031312f302d0603550403132650696c6f74476e756262792d302e342e312d34" - + "373930313238303030313135353935373335323059301306072a8648ce3d0201" - + "06082a8648ce3d030107034200048d617e65c9508e64bcc5673ac82a6799da3c" - + "1446682c258c463fffdf58dfd2fa3e6c378b53d795c4a4dffb4199edd7862f23" - + "abaf0203b4b8911ba0569994e101300a06082a8648ce3d040302034700304402" - + "2060cdb6061e9c22262d1aac1d96d8c70829b2366531dda268832cb836bcd30d" - + "fa0220631b1459f09e6330055722c8d89b7f48883b9089b88d60d1d9795902b3" - + "0410df30450220176386c89021f4335d953c56a0c831f98380dc198c95794a85" - + "b08f0c4ba849ff022100a10114749d0c28e13a9ffe6dde6e622c33163b249ac1" - + "ffb1c8e25b3cc4907e3c"); - - public static final ByteArray KEY_HANDLE = fromHex( - "2a552dfdb7477ed65fd84133f86196010b2215b57da75d315b7b9e8fe2e3925a" - + "6019551bab61d16591659cbaf00b4950f7abfe6660e2e006f76868b772d70c25"); - public static final ByteArray USER_PUBLIC_KEY_REGISTER_HEX = fromHex( - "04b174bc49c7ca254b70d2e5c207cee9cf174820ebd77ea3c65508c26da51b65" - + "7c1cc6b952f8621697936482da0a6d3d3826a59095daf6cd7c03e2e60385d2f6" - + "d9"); - public static final ByteArray SIGN_RESPONSE_DATA = fromHex( - "0100000001304402204b5f0cd17534cedd8c34ee09570ef542a353df4436030c" - + "e43d406de870b847780220267bb998fac9b7266eb60e7cb0b5eabdfd5ba9614f" - + "53c7b22272ec10047a923f"); - public static final ByteArray EXPECTED_REGISTER_SIGNED_BYTES = fromHex( - "00f0e6a6a97042a4f1f1c87f5f7d44315b2d852c2df5c7991cc66241bf7072d1" - + "c44142d21c00d94ffb9d504ada8f99b721f4b191ae4e37ca0140f696b6983cfa" - + "cb2a552dfdb7477ed65fd84133f86196010b2215b57da75d315b7b9e8fe2e392" - + "5a6019551bab61d16591659cbaf00b4950f7abfe6660e2e006f76868b772d70c" - + "2504b174bc49c7ca254b70d2e5c207cee9cf174820ebd77ea3c65508c26da51b" - + "657c1cc6b952f8621697936482da0a6d3d3826a59095daf6cd7c03e2e60385d2" - + "f6d9"); - public static final ByteArray EXPECTED_SIGN_SIGNED_BYTES = fromHex( - "4b0be934baebb5d12d26011b69227fa5e86df94e7d94aa2949a89f2d493992ca" - + "0100000001ccd6ee2e47baef244d49a222db496bad0ef5b6f93aa7cc4d30c482" - + "1b3b9dbc57"); - public static final ByteArray SIGNATURE_REGISTER = fromHex( - "304502201471899bcc3987e62e8202c9b39c33c19033f7340352dba80fcab017" - + "db9230e402210082677d673d891933ade6f617e5dbde2e247e70423fd5ad7804" - + "a6d3d3961ef871"); - public static final ByteArray SIGNATURE_SIGN = fromHex( - "304402204b5f0cd17534cedd8c34ee09570ef542a353df4436030ce43d406de8" - + "70b847780220267bb998fac9b7266eb60e7cb0b5eabdfd5ba9614f53c7b22272" - + "ec10047a923f"); - - public static final ByteArray SIGN_RESPONSE_INVALID_USER_PRESENCE = fromHex( - "00000000013045022100adf3521ceb4e143fb3966d3017510bfbc9085a44ff13c6945aadd8" - + "e26ec5cc00022004916d120830f2ee44ab3c6c58c80a3dd6f5a09b01599e686d" - + "ea2e7288903cae"); - - private static ByteArray fromHex(String hex) { - try { - return ByteArray.fromHex(hex); - } catch (HexException e) { - throw new RuntimeException(e); - } - } -} diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/TestAuthenticator.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/TestAuthenticator.scala index aa9b43312..2056892bc 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/TestAuthenticator.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/TestAuthenticator.scala @@ -406,7 +406,7 @@ object TestAuthenticator { Map("sig" -> f.binaryNode(signature.getBytes)) ++ ( selfAttestationKey match { - case Some(key) => Map("alg" -> f.numberNode((alg getOrElse WebAuthnCodecs.javaAlgorithmNameToCoseAlgorithmIdentifier(key.getAlgorithm)).getId)) + case Some(key) => Map("alg" -> f.numberNode((alg getOrElse COSEAlgorithmIdentifier.valueOf(key.getAlgorithm)).getId)) case None => Map("x5c" -> f.arrayNode().add(f.binaryNode(cert.getEncoded))) } ) diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/JsonIoSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/JsonIoSpec.scala index ff4b8628b..eab47aa2d 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/JsonIoSpec.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/JsonIoSpec.scala @@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonInclude.Include import com.fasterxml.jackson.core.`type`.TypeReference import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.databind.SerializationFeature import com.fasterxml.jackson.datatype.jdk8.Jdk8Module import com.yubico.webauthn.data.Generators._ import com.yubico.webauthn.extension.appid.AppId @@ -21,6 +22,7 @@ class JsonIoSpec extends FunSpec with Matchers with GeneratorDrivenPropertyCheck def json: ObjectMapper = new ObjectMapper() .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true) + .configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false) .setSerializationInclusion(Include.NON_ABSENT) .registerModule(new Jdk8Module()) diff --git a/webauthn-server-core/test-assertion.png b/webauthn-server-core/test-assertion.png deleted file mode 100644 index e7f8142d2..000000000 Binary files a/webauthn-server-core/test-assertion.png and /dev/null differ diff --git a/webauthn-server-core/test-registration.png b/webauthn-server-core/test-registration.png deleted file mode 100644 index 3615a1c63..000000000 Binary files a/webauthn-server-core/test-registration.png and /dev/null differ diff --git a/webauthn-server-demo/README b/webauthn-server-demo/README index 29ffc40c3..25240a0dd 100644 --- a/webauthn-server-demo/README +++ b/webauthn-server-demo/README @@ -7,9 +7,9 @@ one can build on API to enable advanced features like *authenticated actions*, such as adding an additional authenticator or deregistering a credential. The central part is the -https://github.com/Yubico/java-webauthn-server/blob/master/webauthn-server-demo/src/main/scala/demo/webauthn/WebAuthnServer.java[WebAuthnServer] +https://github.com/Yubico/java-webauthn-server/blob/master/webauthn-server-demo/src/main/java/demo/webauthn/WebAuthnServer.java[WebAuthnServer] class, and the -https://github.com/Yubico/java-webauthn-server/blob/master/webauthn-server-demo/src/main/scala/demo/webauthn/WebAuthnRestResource.java[WebAuthnRestResource] +https://github.com/Yubico/java-webauthn-server/blob/master/webauthn-server-demo/src/main/java/demo/webauthn/WebAuthnRestResource.java[WebAuthnRestResource] class which provides the REST API on top of it. @@ -20,21 +20,21 @@ https://github.com/Yubico/java-webauthn-server/blob/master/webauthn-server-core/ library: - The front end interacts with the server via a *REST API*, implemented in - https://github.com/Yubico/java-webauthn-server/blob/master/webauthn-server-demo/src/main/scala/demo/webauthn/WebAuthnRestResource.java[WebAuthnRestResource]. + https://github.com/Yubico/java-webauthn-server/blob/master/webauthn-server-demo/src/main/java/demo/webauthn/WebAuthnRestResource.java[WebAuthnRestResource]. + This layer manages translation between JSON request/response payloads and domain objects, and most methods simply call into analogous methods in the server layer. - The REST API then delegates to the *server layer*, implemented in - https://github.com/Yubico/java-webauthn-server/blob/master/webauthn-server-demo/src/main/scala/demo/webauthn/WebAuthnServer.java[WebAuthnServer]. + https://github.com/Yubico/java-webauthn-server/blob/master/webauthn-server-demo/src/main/java/demo/webauthn/WebAuthnServer.java[WebAuthnServer]. + This layer manages the general architecture of the system, and is where most business logic and integration code would go. The demo server implements the "persistent" storage of users and credential registrations - the -https://github.com/Yubico/java-webauthn-server/blob/master/webauthn-server-core/src/main/scala/com/yubico/webauthn/CredentialRepository.scala[CredentialRepository] +https://github.com/Yubico/java-webauthn-server/blob/master/webauthn-server-core/src/main/java/com/yubico/webauthn/CredentialRepository.java[CredentialRepository] integration point - as the -https://github.com/Yubico/java-webauthn-server/blob/master/webauthn-server-demo/src/main/scala/demo/webauthn/InMemoryRegistrationStorage.java[InMemoryRegistrationStorage] +https://github.com/Yubico/java-webauthn-server/blob/master/webauthn-server-demo/src/main/java/demo/webauthn/InMemoryRegistrationStorage.java[InMemoryRegistrationStorage] class, which simply keeps them stored in memory for a limited time. The transient storage of pending challenges is also kept in memory, but for a shorter duration. @@ -46,7 +46,7 @@ would be specific to a particular Relying Party (RP) would go in this layer. - The server layer in turn calls the *library layer*, which is where the https://github.com/Yubico/java-webauthn-server/blob/master/webauthn-server-core/[`webauthn-server-core`] library gets involved. The entry point into the library is the - https://github.com/Yubico/java-webauthn-server/blob/master/webauthn-server-core/src/main/scala/com/yubico/webauthn/RelyingParty.scala[RelyingParty] + https://github.com/Yubico/java-webauthn-server/blob/master/webauthn-server-core/src/main/java/com/yubico/webauthn/RelyingParty.java[RelyingParty] class. + This layer implements the Web Authentication @@ -57,59 +57,102 @@ and exposes integration points for storage of challenges and credentials. Some notable integration points are: + ** The library user must provide an implementation of the -https://github.com/Yubico/java-webauthn-server/blob/master/webauthn-server-core/src/main/scala/com/yubico/webauthn/CredentialRepository.scala[CredentialRepository] +https://github.com/Yubico/java-webauthn-server/blob/master/webauthn-server-core/src/main/java/com/yubico/webauthn/CredentialRepository.java[CredentialRepository] interface to use for looking up stored public keys, user handles and signature counters. ** The library user can optionally provide an instance of the -https://github.com/Yubico/java-webauthn-server/blob/master/webauthn-server-core/src/main/scala/com/yubico/u2f/attestation/MetadataService.java[MetadataService] -class to enable identification and validation of authenticator models. This -instance is then used to look up trusted attestation root certificates. +https://github.com/Yubico/java-webauthn-server/blob/master/webauthn-server-core/src/main/java/com/yubico/webauthn/attestation/MetadataService.java[MetadataService] +interface to enable identification and validation of authenticator models. This +instance is then used to look up trusted attestation root certificates. The +https://github.com/Yubico/java-webauthn-server/blob/master/webauthn-server-attestation/[`webauthn-server-attestation`] +sibling library provides implementations of this interface that are pre-seeded +with Yubico device metadata. == Usage -=== 1. Clone - git clone https://github.com/Yubico/java-webauthn-server.git +This subproject includes two ways to run the demo server: -=== 2. Run - ./gradlew :webauthn-server-demo:appRun +- As a `.war` archive to be deployed in any Java web server +- As a standalone Java executable -=== 3. Try it out -Then point a WebAuthn compatible web browser to -link:https://localhost:8443/webauthn/[https://localhost:8443/webauthn/]. + +=== `.war` archive + +The `.war` archive includes a simple web GUI hosted at `/`, relative to the +context root of the deployed `.war` application, which communicates with the +server via a REST API served at `/api/v1/`. + +To build it, run + + $ ../gradlew war + +To build and run it via Gradle, run + + $ ../gradlew appRun + +This will serve the application under `https://localhost:8443/webauthn/` by +default. NOTE: Since WebAuthn requires a HTTPS connection, this demo server uses a dummy certificate. This will cause your browser to show a warning, which is safe to -bypass. +bypass. The dummy certificate is not included in the `.war` artifact; it is only +used when running via Gradle. + + +=== Standalone Java executable + +The standalone Java executable has the main class +https://github.com/Yubico/java-webauthn-server/blob/master/webauthn-server-demo/src/main/java/demo/webauthn/EmbeddedServer.java[`demo.webauthn.EmbeddedServer`]. +This server serves only the REST API at `/v1/`, and does not include the web +GUI. + +To build it, run one of the following: + + $ ../gradlew distTar + $ ../gradlew distZip + +This will build an archive which can be unpacked and run anywhere with a Java +environment as: + + $ webauthn-server-demo-$VERSION/bin/webauthn-server-demo + $ webauthn-server-demo-$VERSION/bin/webauthn-server-demo.bat + +To build and run it via Gradle, run: + + $ ./gradlew run +This will serve the application under `http://localhost:8080/` by default. -== Standalone REST server +HTTPS support for this mode is not yet implemented. - 1. Build the standalone REST server distribution: - ../gradlew distTar +=== Configuration - 2. Unpack `build/distributions/webauthn-server-demo-X.Y.Z.tar` somewhere +Both modes of running the server accept the following environment variables for +configuration. Note that if running via Gradle, you may need to disable the +Gradle daemon (`--no-daemon`) in order for the server process to have the +correct environment. - 3. Run `webauthn-server-demo-X.Y.Z/bin/webauthn-server-demo`. You should also - set the following environment variables: + - `YUBICO_WEBAUTHN_PORT`: Port number to run the server on. Example: + `YUBICO_WEBAUTHN_PORT=8081` - - `YUBICO_WEBAUTHN_PORT`: Port number to run the server on. Example: - `YUBICO_WEBAUTHN_PORT=8081` + This is ignored when running as a `.war` artifact, since the port is + controlled by the parent web server. - - `YUBICO_WEBAUTHN_ALLOWED_ORIGINS`: Comma-separated list of origins the - server will accept requests for. Example: - `YUBICO_WEBAUTHN_ALLOWED_ORIGINS=http://demo.yubico.com:8080` + - `YUBICO_WEBAUTHN_ALLOWED_ORIGINS`: Comma-separated list of origins the + server will accept requests for. Example: + `YUBICO_WEBAUTHN_ALLOWED_ORIGINS=http://demo.yubico.com:8080` - - `YUBICO_WEBAUTHN_RP_ID`: The https://www.w3.org/TR/webauthn/#rp-id[RP ID] - the server will report. Example: `YUBICO_WEBAUTHN_RP_ID=demo.yubico.com` + - `YUBICO_WEBAUTHN_RP_ID`: The https://www.w3.org/TR/webauthn/#rp-id[RP ID] + the server will report. Example: `YUBICO_WEBAUTHN_RP_ID=demo.yubico.com` - - `YUBICO_WEBAUTHN_RP_NAME`: The human-readable - https://www.w3.org/TR/webauthn/#dom-publickeycredentialentity-name[RP - name] the server will report. Example: `YUBICO_WEBAUTHN_RP_ID='Yubico Web - Authentication demo'` + - `YUBICO_WEBAUTHN_RP_NAME`: The human-readable + https://www.w3.org/TR/webauthn/#dom-publickeycredentialentity-name[RP name] + the server will report. Example: `YUBICO_WEBAUTHN_RP_ID='Yubico Web + Authentication demo'` - - `YUBICO_WEBAUTHN_RP_ICON`: An optional URL to an - https://www.w3.org/TR/webauthn/#dom-publickeycredentialentity-icon[icon] - to represent this Relying Party. Example: - `YUBICO_WEBAUTHN_RP_ICON='https://www.yubico.com/wp-content/uploads/2014/09/favicon.ico'` + - `YUBICO_WEBAUTHN_RP_ICON`: An optional URL to an + https://www.w3.org/TR/webauthn/#dom-publickeycredentialentity-icon[icon] to + represent this Relying Party. Example: + `YUBICO_WEBAUTHN_RP_ICON='https://www.yubico.com/wp-content/uploads/2014/09/favicon.ico'` diff --git a/webauthn-server-demo/build.gradle b/webauthn-server-demo/build.gradle index c7e3d3aec..3c48c93a2 100644 --- a/webauthn-server-demo/build.gradle +++ b/webauthn-server-demo/build.gradle @@ -1,13 +1,26 @@ +plugins { + id 'com.bmuschko.docker-remote-api' version '3.6.1' +} + +import com.bmuschko.gradle.docker.tasks.image.DockerBuildImage + +project.ext.dockerGroup = 'yubico' +project.ext.dockerName = project.name + description = 'WebAuthn demo' apply plugin: 'scala' apply plugin: 'war' apply plugin: 'application' -apply from: 'gretty-2.0.0.plugin' +apply from: 'gretty-2.2.0.plugin' sourceCompatibility = '1.8' targetCompatibility = '1.8' +configurations { + forJdk10 +} + dependencies { compile( @@ -22,7 +35,13 @@ dependencies { 'org.glassfish.jersey.containers:jersey-container-jetty-http:2.26', ) + forJdk10( + 'javax.activation:activation:1.1.1', + 'javax.xml.bind:jaxb-api:2.3.0', + ) + runtime( + configurations.forJdk10, 'ch.qos.logback:logback-classic:1.2.3', 'org.glassfish.jersey.containers:jersey-container-servlet:2.26', 'org.glassfish.jersey.inject:jersey-hk2:2.26', @@ -56,3 +75,21 @@ gretty { sslKeyStorePassword = 'p@ssw0rd' sslKeyStorePath = file('keystore.jks') } + +task dockerPrepare(type: Sync) { + from file('docker') + from file('keystore.jks') + from(war.outputs) { + rename ~/${war.baseName}.*\.${war.extension}/, "${war.baseName}.${war.extension}" + } + into file("${project.buildDir}/docker") +} + +task dockerBuild(type: DockerBuildImage) { + inputs.files dockerPrepare.outputs.files + inputDir = dockerPrepare.destinationDir + tags = [ + "${project.dockerGroup}/${project.dockerName}:${project.version}", + "${project.dockerGroup}/${project.dockerName}:latest", + ] +} diff --git a/webauthn-server-demo/docker/Dockerfile b/webauthn-server-demo/docker/Dockerfile index 38733f7d2..07108da7b 100644 --- a/webauthn-server-demo/docker/Dockerfile +++ b/webauthn-server-demo/docker/Dockerfile @@ -2,5 +2,6 @@ FROM tomcat:jre8 RUN rm -rf /usr/local/tomcat/webapps/* +COPY keystore.jks /usr/local/tomcat/conf/ssl/ COPY server.xml /usr/local/tomcat/conf/server.xml COPY webauthn-server-demo.war /usr/local/tomcat/webapps/ROOT.war diff --git a/webauthn-server-demo/docker/server.xml b/webauthn-server-demo/docker/server.xml index 262a95a49..11c7f9ee7 100644 --- a/webauthn-server-demo/docker/server.xml +++ b/webauthn-server-demo/docker/server.xml @@ -85,20 +85,21 @@ AprLifecycleListener. Either JSSE or OpenSSL style configuration may be used regardless of the SSLImplementation selected. JSSE style configuration is used below. + --> - - --> + @@ -109,6 +110,7 @@ type="RSA" /> + --> diff --git a/webauthn-server-demo/gretty-2.0.0.plugin b/webauthn-server-demo/gretty-2.2.0.plugin similarity index 82% rename from webauthn-server-demo/gretty-2.0.0.plugin rename to webauthn-server-demo/gretty-2.2.0.plugin index 5d865c45f..ff3af513a 100644 --- a/webauthn-server-demo/gretty-2.0.0.plugin +++ b/webauthn-server-demo/gretty-2.2.0.plugin @@ -4,7 +4,7 @@ buildscript { } dependencies { - classpath 'org.akhikhl.gretty:gretty:2.0.0' + classpath 'org.gretty:gretty:2.2.0' } } diff --git a/webauthn-server-demo/src/main/java/com/yubico/webauthn/U2fVerifier.java b/webauthn-server-demo/src/main/java/com/yubico/webauthn/U2fVerifier.java new file mode 100644 index 000000000..2b7220f35 --- /dev/null +++ b/webauthn-server-demo/src/main/java/com/yubico/webauthn/U2fVerifier.java @@ -0,0 +1,53 @@ +package com.yubico.webauthn; + +import com.fasterxml.jackson.databind.JsonNode; +import com.yubico.internal.util.CertificateParser; +import com.yubico.internal.util.ExceptionUtil; +import com.yubico.internal.util.WebAuthnCodecs; +import com.yubico.webauthn.data.ByteArray; +import com.yubico.webauthn.data.exception.Base64UrlException; +import com.yubico.webauthn.extension.appid.AppId; +import demo.webauthn.data.RegistrationRequest; +import demo.webauthn.data.U2fRegistrationResponse; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +public class U2fVerifier { + + private static final Crypto crypto = new BouncyCastleCrypto(); + + public static boolean verify(AppId appId, RegistrationRequest request, U2fRegistrationResponse response) throws CertificateException, IOException, Base64UrlException { + final ByteArray appIdHash = crypto.hash(appId.getId()); + final ByteArray clientDataHash = crypto.hash(response.getCredential().getU2fResponse().getClientDataJSON()); + + final JsonNode clientData = WebAuthnCodecs.json().readTree(response.getCredential().getU2fResponse().getClientDataJSON().getBytes()); + final String challengeBase64 = clientData.get("challenge").textValue(); + + ExceptionUtil.assure( + request.getPublicKeyCredentialCreationOptions().getChallenge().equals(ByteArray.fromBase64Url(challengeBase64)), + "Wrong challenge." + ); + + InputStream attestationCertAndSignatureStream = new ByteArrayInputStream(response.getCredential().getU2fResponse().getAttestationCertAndSignature().getBytes()); + + final X509Certificate attestationCert = CertificateParser.parseDer(attestationCertAndSignatureStream); + + byte[] signatureBytes = new byte[attestationCertAndSignatureStream.available()]; + attestationCertAndSignatureStream.read(signatureBytes); + final ByteArray signature = new ByteArray(signatureBytes); + + return new U2fRawRegisterResponse( + response.getCredential().getU2fResponse().getPublicKey(), + response.getCredential().getU2fResponse().getKeyHandle(), + attestationCert, + signature + ).verifySignature( + appIdHash, + clientDataHash + ); + } + +} diff --git a/webauthn-server-demo/src/main/java/demo/webauthn/Config.java b/webauthn-server-demo/src/main/java/demo/webauthn/Config.java index d1c6ff48d..66730a971 100644 --- a/webauthn-server-demo/src/main/java/demo/webauthn/Config.java +++ b/webauthn-server-demo/src/main/java/demo/webauthn/Config.java @@ -100,26 +100,33 @@ private static RelyingPartyIdentity computeRpIdentity() throws MalformedURLExcep logger.debug("RP ID: {}", id); logger.debug("RP icon: {}", icon); - final RelyingPartyIdentity result; + RelyingPartyIdentity.RelyingPartyIdentityBuilder resultBuilder = DEFAULT_RP_ID.toBuilder(); - if (name == null || id == null) { - logger.debug("RP name or ID not given - using default."); - result = DEFAULT_RP_ID; + if (name == null) { + logger.debug("RP name not given - using default."); } else { - if (icon == null) { - result = RelyingPartyIdentity.builder().name(name).id(id).build(); - } else { - try { - result = RelyingPartyIdentity.builder().name(name).id(id).icon(Optional.of(new URL(icon))).build(); - } catch (MalformedURLException e) { - logger.error("Invalid icon URL: {}", icon, e); - throw e; - } + resultBuilder.name(name); + } + + if (id == null) { + logger.debug("RP ID not given - using default."); + } else { + resultBuilder.id(id); + } + + if (icon == null) { + logger.debug("RP icon not given - using none."); + } else { + try { + resultBuilder.icon(Optional.of(new URL(icon))); + } catch (MalformedURLException e) { + logger.error("Invalid icon URL: {}", icon, e); + throw e; } } + final RelyingPartyIdentity result = resultBuilder.build(); logger.info("RP identity: {}", result); - return result; } diff --git a/webauthn-server-demo/src/main/java/demo/webauthn/EmbeddedServer.java b/webauthn-server-demo/src/main/java/demo/webauthn/EmbeddedServer.java index c591763fe..2498fc1d2 100644 --- a/webauthn-server-demo/src/main/java/demo/webauthn/EmbeddedServer.java +++ b/webauthn-server-demo/src/main/java/demo/webauthn/EmbeddedServer.java @@ -10,6 +10,10 @@ import org.glassfish.jersey.jetty.JettyHttpContainerFactory; import org.glassfish.jersey.server.ResourceConfig; +/** + * Standalone Java application launcher that runs the demo server with the API + * but no static resources (i.e., no web GUI) + */ public class EmbeddedServer { public static void main(String[] args) throws Exception { diff --git a/webauthn-server-demo/src/main/java/demo/webauthn/WebAuthnRestResource.java b/webauthn-server-demo/src/main/java/demo/webauthn/WebAuthnRestResource.java index 985e47d82..34cd443f5 100644 --- a/webauthn-server-demo/src/main/java/demo/webauthn/WebAuthnRestResource.java +++ b/webauthn-server-demo/src/main/java/demo/webauthn/WebAuthnRestResource.java @@ -163,7 +163,7 @@ public Response finishRegistration(@NonNull String responseJson) { @POST public Response finishU2fRegistration(@NonNull String responseJson) { logger.trace("finishRegistration responseJson: {}", responseJson); - Either, WebAuthnServer.SuccessfulU2fRegistrationResult> result = server.insecureFinishU2fRegistration(responseJson); + Either, WebAuthnServer.SuccessfulU2fRegistrationResult> result = server.finishU2fRegistration(responseJson); return finishResponse( result, "U2F registration failed; further error message(s) were unfortunately lost to an internal server error.", @@ -241,6 +241,7 @@ private StartAuthenticatedActionResponse(AssertionRequest request) throws Malfor } private final class StartAuthenticatedActionActions { public final URL finish = uriInfo.getAbsolutePathBuilder().path("finish").build().toURL(); + public final URL finishU2f = uriInfo.getAbsolutePathBuilder().path("finish-u2f").build().toURL(); private StartAuthenticatedActionActions() throws MalformedURLException { } } @@ -279,6 +280,12 @@ public Response finishAddCredential(@NonNull String responseJson) { return finishRegistration(responseJson); } + @Path("action/add-credential/finish/finish-u2f") + @POST + public Response finishU2fAddCredential(@NonNull String responseJson) { + return finishU2fRegistration(responseJson); + } + @Path("action/deregister") @POST public Response deregisterCredential( 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 bfce6fb27..5094ab918 100644 --- a/webauthn-server-demo/src/main/java/demo/webauthn/WebAuthnServer.java +++ b/webauthn-server-demo/src/main/java/demo/webauthn/WebAuthnServer.java @@ -8,6 +8,7 @@ import com.google.common.io.CharStreams; import com.google.common.io.Closeables; import com.yubico.internal.util.CertificateParser; +import com.yubico.internal.util.ExceptionUtil; import com.yubico.internal.util.WebAuthnCodecs; import com.yubico.util.Either; import com.yubico.webauthn.ChallengeGenerator; @@ -17,6 +18,7 @@ import com.yubico.webauthn.RelyingParty; import com.yubico.webauthn.StartAssertionOptions; import com.yubico.webauthn.StartRegistrationOptions; +import com.yubico.webauthn.U2fVerifier; import com.yubico.webauthn.attestation.Attestation; import com.yubico.webauthn.attestation.MetadataResolver; import com.yubico.webauthn.attestation.MetadataService; @@ -27,6 +29,7 @@ import com.yubico.webauthn.data.AssertionResult; import com.yubico.webauthn.data.AttestationConveyancePreference; import com.yubico.webauthn.data.AttestationType; +import com.yubico.webauthn.data.AuthenticatorSelectionCriteria; import com.yubico.webauthn.data.ByteArray; import com.yubico.webauthn.data.PublicKeyCredentialDescriptor; import com.yubico.webauthn.data.PublicKeyCredentialParameters; @@ -155,7 +158,10 @@ public Either startRegistration( .id(challengeGenerator.generateChallenge()) .build() ) - .requireResidentKey(requireResidentKey) + .authenticatorSelection(Optional.of(AuthenticatorSelectionCriteria.builder() + .requireResidentKey(requireResidentKey) + .build() + )) .build() ) ); @@ -193,7 +199,10 @@ public Either, AssertionRequest> startAddCredential( rp.startRegistration( StartRegistrationOptions.builder() .user(existingUser) - .requireResidentKey(requireResidentKey) + .authenticatorSelection(Optional.of(AuthenticatorSelectionCriteria.builder() + .requireResidentKey(requireResidentKey) + .build() + )) .build() ) ); @@ -313,25 +322,13 @@ public Either, SuccessfulRegistrationResult> finishRegistration(Str } } - /** - * NOTE: This method does not validate the result. This sole purpose of - * this feature is to enable testing that the "appid" extension works. This - * requires a credential registered via the U2F API instead of the WebAuthn - * API. Since WebAuthn is backwards compatible only with U2F - * authentication, not registration, this method is used to sidestep the - * WebAuthn module and just add the credential to the database of - * registrations. - * - * This is NOT an example of good code. DO NOT base any code off this as an - * example. - */ - public Either, SuccessfulU2fRegistrationResult> insecureFinishU2fRegistration(String responseJson) { - logger.trace("insecureFinishU2fRegistration responseJson: {}", responseJson); + public Either, SuccessfulU2fRegistrationResult> finishU2fRegistration(String responseJson) { + logger.trace("finishU2fRegistration responseJson: {}", responseJson); U2fRegistrationResponse response = null; try { response = jsonMapper.readValue(responseJson, U2fRegistrationResponse.class); } catch (IOException e) { - logger.error("JSON error in insecureFinishU2fRegistration; responseJson: {}", responseJson, e); + logger.error("JSON error in finishU2fRegistration; responseJson: {}", responseJson, e); return Either.left(Arrays.asList("Registration failed!", "Failed to decode response object.", e.getMessage())); } @@ -339,14 +336,25 @@ public Either, SuccessfulU2fRegistrationResult> insecureFinishU2fRe registerRequestStorage.invalidate(response.getRequestId()); if (request == null) { - logger.debug("fail insecureFinishU2fRegistration responseJson: {}", responseJson); + logger.debug("fail finishU2fRegistration responseJson: {}", responseJson); return Either.left(Arrays.asList("Registration failed!", "No such registration in progress.")); } else { + + try { + ExceptionUtil.assure( + U2fVerifier.verify(rp.getAppId().get(), request, response), + "Failed to verify signature." + ); + } catch (Exception e) { + logger.debug("Failed to verify U2F signature.", e); + return Either.left(Arrays.asList("Failed to verify signature.", e.getMessage())); + } + X509Certificate attestationCert = null; try { - attestationCert = CertificateParser.parseDer(response.getCredential().getU2fResponse().getAttestationCert().getBytes()); + attestationCert = CertificateParser.parseDer(response.getCredential().getU2fResponse().getAttestationCertAndSignature().getBytes()); } catch (CertificateException e) { - logger.error("Failed to parse attestation certificate: {}", response.getCredential().getU2fResponse().getAttestationCert(), e); + logger.error("Failed to parse attestation certificate: {}", response.getCredential().getU2fResponse().getAttestationCertAndSignature(), e); } Optional attestation = Optional.empty(); @@ -377,7 +385,7 @@ public Either, SuccessfulU2fRegistrationResult> insecureFinishU2fRe result ), result.isAttestationTrusted(), - Optional.of(new AttestationCertInfo(response.getCredential().getU2fResponse().getAttestationCert())) + Optional.of(new AttestationCertInfo(response.getCredential().getU2fResponse().getAttestationCertAndSignature())) ) ); } diff --git a/webauthn-server-demo/src/main/java/demo/webauthn/data/U2fCredentialResponse.java b/webauthn-server-demo/src/main/java/demo/webauthn/data/U2fCredentialResponse.java index ad2862b54..c5c75ec26 100644 --- a/webauthn-server-demo/src/main/java/demo/webauthn/data/U2fCredentialResponse.java +++ b/webauthn-server-demo/src/main/java/demo/webauthn/data/U2fCredentialResponse.java @@ -11,17 +11,20 @@ public class U2fCredentialResponse { private final ByteArray keyHandle; private final ByteArray publicKey; - private final ByteArray attestationCert; + private final ByteArray attestationCertAndSignature; + private final ByteArray clientDataJSON; @JsonCreator public U2fCredentialResponse( @NonNull @JsonProperty("keyHandle") ByteArray keyHandle, @NonNull@JsonProperty("publicKey") ByteArray publicKey, - @NonNull@JsonProperty("attestationCert") ByteArray attestationCert + @NonNull@JsonProperty("attestationCertAndSignature") ByteArray attestationCertAndSignature, + @NonNull@JsonProperty("clientDataJSON") ByteArray clientDataJSON ) { this.keyHandle = keyHandle; this.publicKey = publicKey; - this.attestationCert = attestationCert; + this.attestationCertAndSignature = attestationCertAndSignature; + this.clientDataJSON = clientDataJSON; } } diff --git a/webauthn-server-demo/src/main/webapp/index.html b/webauthn-server-demo/src/main/webapp/index.html index 7ee306124..cdffcd607 100644 --- a/webauthn-server-demo/src/main/webapp/index.html +++ b/webauthn-server-demo/src/main/webapp/index.html @@ -117,6 +117,7 @@ username, displayName, credentialNickname, + requireResidentKey, }), method: 'POST', }) diff --git a/webauthn-server-demo/src/main/webapp/js/webauthn.js b/webauthn-server-demo/src/main/webapp/js/webauthn.js index 2aedbdaf1..f5c280436 100644 --- a/webauthn-server-demo/src/main/webapp/js/webauthn.js +++ b/webauthn-server-demo/src/main/webapp/js/webauthn.js @@ -99,6 +99,15 @@ /** Turn a PublicKeyCredential object into a plain object with base64url encoded binary values */ function responseToObject(response) { + + let clientExtensionResults = {}; + + try { + clientExtensionResults = response.getClientExtensionResults(); + } catch (e) { + console.error('getClientExtensionResults failed', e); + } + if (response.response.attestationObject) { return { type: response.type, @@ -107,6 +116,7 @@ attestationObject: base64url.fromByteArray(response.response.attestationObject), clientDataJSON: base64url.fromByteArray(response.response.clientDataJSON), }, + clientExtensionResults, }; } else { return { @@ -116,7 +126,9 @@ authenticatorData: base64url.fromByteArray(response.response.authenticatorData), clientDataJSON: base64url.fromByteArray(response.response.clientDataJSON), signature: base64url.fromByteArray(response.response.signature), + userHandle: response.response.userHandle && base64url.fromByteArray(response.response.userHandle), }, + clientExtensionResults, }; } } diff --git a/yubico-util/build.gradle b/yubico-util/build.gradle index ba6661b0b..7597df343 100644 --- a/yubico-util/build.gradle +++ b/yubico-util/build.gradle @@ -2,6 +2,8 @@ description = 'Yubico internal utilities' apply plugin: 'scala' +project.ext.publishMe = true + dependencies { compile(