diff --git a/.github/workflows/master.yml b/.github/workflows/build.yml
similarity index 86%
rename from .github/workflows/master.yml
rename to .github/workflows/build.yml
index 908576446..261669cdf 100644
--- a/.github/workflows/master.yml
+++ b/.github/workflows/build.yml
@@ -1,10 +1,7 @@
# This name is shown in the status badge in the README
name: build
-on:
- push:
- branches:
- - master
+on: [push, pull_request]
jobs:
test:
@@ -13,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
- java: [8, 10, 11, 12]
+ java: [8, 11]
steps:
- name: Check out code
diff --git a/.github/workflows/release-verify-signatures.yml b/.github/workflows/release-verify-signatures.yml
index 5e2159720..57dbdafd2 100644
--- a/.github/workflows/release-verify-signatures.yml
+++ b/.github/workflows/release-verify-signatures.yml
@@ -11,14 +11,9 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
- java: [10, 11, 12]
+ java: [11]
steps:
- - name: Download signatures
- run: |
- wget https://github.com/${GITHUB_REPOSITORY}/releases/download/${GITHUB_REF}/webauthn-server-attestation-${GITHUB_REF}.jar.asc
- wget https://github.com/${GITHUB_REPOSITORY}/releases/download/${GITHUB_REF}/webauthn-server-core-${GITHUB_REF}.jar.asc
-
- name: check out code
uses: actions/checkout@v1
@@ -31,9 +26,24 @@ jobs:
run: ./gradlew jar
- name: Fetch keys
- run: gpg --recv-keys 57A9DEED4C6D962A923BB691816F3ED99921835E
+ run: gpg --no-default-keyring --keyring yubico --recv-keys 57A9DEED4C6D962A923BB691816F3ED99921835E
+
+ - name: Verify signatures from GitHub release
+ run: |
+ export TAGNAME=${GITHUB_REF#refs/tags/}
- - name: Verify signatures
+ wget https://github.com/${GITHUB_REPOSITORY}/releases/download/${TAGNAME}/webauthn-server-attestation-${TAGNAME}.jar.asc
+ wget https://github.com/${GITHUB_REPOSITORY}/releases/download/${TAGNAME}/webauthn-server-core-${TAGNAME}.jar.asc
+
+ gpg --no-default-keyring --keyring yubico --verify webauthn-server-attestation-${TAGNAME}.jar.asc webauthn-server-attestation/build/libs/webauthn-server-attestation-${TAGNAME}.jar
+ gpg --no-default-keyring --keyring yubico --verify webauthn-server-core-${TAGNAME}.jar.asc webauthn-server-core/build/libs/webauthn-server-core-${TAGNAME}.jar
+
+ - name: Verify signatures from Maven Central
run: |
- gpg --verify webauthn-server-attestation-${GITHUB_REF}.jar.asc webauthn-server-attestation/build/libs/webauthn-server-attestation-${GITHUB_REF}.jar
- gpg --verify webauthn-server-core-${GITHUB_REF}.jar.asc webauthn-server-core/build/libs/webauthn-server-core-${GITHUB_REF}.jar
+ export TAGNAME=${GITHUB_REF#refs/tags/}
+
+ wget -O webauthn-server-core-${TAGNAME}.jar.mavencentral.asc https://repo1.maven.org/maven2/com/yubico/webauthn-server-core/${TAGNAME}/webauthn-server-core-${TAGNAME}.jar.asc
+ wget -O webauthn-server-attestation-${TAGNAME}.jar.mavencentral.asc https://repo1.maven.org/maven2/com/yubico/webauthn-server-attestation/${TAGNAME}/webauthn-server-attestation-${TAGNAME}.jar.asc
+
+ gpg --no-default-keyring --keyring yubico --verify webauthn-server-attestation-${TAGNAME}.jar.mavencentral.asc webauthn-server-attestation/build/libs/webauthn-server-attestation-${TAGNAME}.jar
+ gpg --no-default-keyring --keyring yubico --verify webauthn-server-core-${TAGNAME}.jar.mavencentral.asc webauthn-server-core/build/libs/webauthn-server-core-${TAGNAME}.jar
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
deleted file mode 100644
index db0c9f4ab..000000000
--- a/.github/workflows/test.yml
+++ /dev/null
@@ -1,39 +0,0 @@
-name: test
-
-on:
- push:
- branches:
- - '*'
- - '!master'
- tags:
- - '*'
- pull_request:
- branches:
- - '*'
- - '!master'
- tags:
- - '*'
-
-jobs:
- test:
- name: JDK ${{matrix.java}}
-
- runs-on: ubuntu-latest
- strategy:
- matrix:
- java: [8, 10, 11, 12]
-
- steps:
- - name: Check out code
- uses: actions/checkout@v1
-
- - name: Set up JDK
- uses: actions/setup-java@v1
- with:
- java-version: ${{ matrix.java }}
-
- - name: Run tests
- run: ./gradlew check
-
- - name: Build JavaDoc
- run: ./gradlew assembleJavadoc
diff --git a/.gitignore b/.gitignore
index 319a141d9..b5ae30103 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,6 +9,7 @@ out/
*.iml
*.iws
*/out/
+.attach_pid*
# Mac
.DS_Store
@@ -19,5 +20,4 @@ target/
# Gradle
.gradle/
-/build/
-/*/build/
+build/
diff --git a/.travis.yml b/.travis.yml
index 0d153293c..5e6de831f 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -7,7 +7,6 @@ branches:
jdk:
- oraclejdk11
- openjdk8
- - openjdk10
- openjdk11
script:
diff --git a/NEWS b/NEWS
index eb8189b06..168e729fd 100644
--- a/NEWS
+++ b/NEWS
@@ -1,3 +1,44 @@
+== Version 1.5.0 ==
+
+Changes:
+
+* `RelyingParty` now makes an immutable copy of the `origins` argument, instead
+ of storing a reference to a possibly mutable value.
+* The enum `AuthenticatorTransport` has been replaced by a value class
+ containing methods and value constants equivalent to the previous enum.
+* The return type of `PublicKeyCredentialDescriptor.getTransports()` is now a
+ `SortedSet` instead of `Set`. The builder still accepts a plain `Set`.
+* Registration ceremony now verifies that the returned credential public key
+ matches one of the algorithms specified in
+ `RelyingParty.preferredPubkeyParams` and can be successfully parsed.
+
+New features:
+
+* Origin matching can now be relaxed via two new `RelyingParty` options:
+ * `allowOriginPort` (default `false`): Allow any port number in the origin
+ * `allowOriginSubdomain` (default `false`): Allow any subdomain of any origin
+ listed in `RelyingParty.origins`
+ * See JavaDoc for details and examples.
+* The new `AuthenticatorTransport` can now contain any string value as the
+ transport identifier, as required in the editor's draft of the L2 spec. See:
+ https://github.com/w3c/webauthn/pull/1275
+* Added support for RS1 credentials. Registration of RS1 credentials is not
+ enabled by default, but can be enabled by setting
+ `RelyingParty.preferredPubKeyCredParams` to a list containing
+ `PublicKeyCredentialParameters.RS1`.
+ * New constant `PublicKeyCredentialParameters.RS1`
+ * New constant `COSEAlgorithmIdentifier.RS1`
+
+
+== Version 1.4.1 ==
+
+Packaging fixes:
+
+* Fixed dependency declarations so API dependencies are correctly propagated as
+ compile-time dependencies of dependent projects.
+* Fixed Specification-Version release date in webauthn-server-core jar manifest.
+
+
== Version 1.4.0 ==
Changes:
diff --git a/README b/README
index 0c6bc02ee..f781da809 100644
--- a/README
+++ b/README
@@ -26,7 +26,7 @@ Maven:
com.yubico
webauthn-server-core
- 1.3.0
+ 1.5.0
compile
----------
@@ -34,7 +34,7 @@ Maven:
Gradle:
----------
-compile 'com.yubico:webauthn-server-core:1.3.0'
+compile 'com.yubico:webauthn-server-core:1.5.0'
----------
diff --git a/build.gradle b/build.gradle
index 081b939e8..5df027eee 100644
--- a/build.gradle
+++ b/build.gradle
@@ -7,9 +7,9 @@ buildscript {
}
}
plugins {
- id 'com.github.kt3k.coveralls' version '2.8.2'
+ id 'com.github.kt3k.coveralls' version '2.8.4'
id 'io.codearte.nexus-staging' version '0.9.0'
- id 'io.franzbecker.gradle-lombok' version '1.14'
+ id 'io.franzbecker.gradle-lombok' version '3.1.0'
}
import io.franzbecker.gradle.lombok.LombokPlugin
@@ -34,20 +34,48 @@ wrapper {
}
allprojects {
+ ext.snapshotSuffix = ".g-SNAPSHOT"
+ ext.dirtyMarker = "-DIRTY"
+
apply plugin: 'com.cinnober.gradle.semver-git'
apply plugin: 'idea'
group = 'com.yubico'
- ext.snapshotSuffix = ".g-SNAPSHOT"
- ext.dirtyMarker = "-DIRTY"
-
idea.module {
downloadJavadoc = true
downloadSources = true
}
}
+Map dependencyVersions = [
+ 'ch.qos.logback:logback-classic:1.2.3',
+ 'com.augustcellars.cose:cose-java:1.0.0',
+ 'com.fasterxml.jackson.core:jackson-databind:2.9.9.3',
+ 'com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:2.9.9',
+ 'com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.9.9',
+ 'com.google.guava:guava:19.0',
+ 'com.upokecenter:cbor:4.0.1',
+ 'javax.activation:activation:1.1.1',
+ 'javax.ws.rs:javax.ws.rs-api:2.1',
+ 'javax.xml.bind:jaxb-api:2.3.0',
+ 'junit:junit:4.12',
+ 'org.apache.httpcomponents:httpclient:4.5.2',
+ 'org.bouncycastle:bcpkix-jdk15on:1.62',
+ 'org.bouncycastle:bcprov-jdk15on:1.62',
+ 'org.eclipse.jetty:jetty-server:9.4.9.v20180320',
+ 'org.eclipse.jetty:jetty-servlet:9.4.9.v20180320',
+ 'org.glassfish.jersey.containers:jersey-container-servlet-core:2.26',
+ 'org.glassfish.jersey.containers:jersey-container-servlet:2.26',
+ 'org.glassfish.jersey.inject:jersey-hk2:2.26',
+ 'org.mockito:mockito-core:2.27.0',
+ 'org.scala-lang:scala-library:2.12.8',
+ 'org.scalacheck:scalacheck_2.12:1.14.0',
+ 'org.scalatest:scalatest_2.12:3.0.4',
+ 'org.slf4j:slf4j-api:1.7.25',
+].collectEntries { [(it.split(':')[0..1].join(':')): it] }
+rootProject.ext.addVersion = { dep -> dependencyVersions[dep] }
+
subprojects {
apply plugin: LombokPlugin
@@ -63,7 +91,9 @@ subprojects {
}
}
-evaluationDependsOnChildren()
+allprojects {
+ evaluationDependsOnChildren()
+}
task assembleJavadoc(type: Sync) {
from("docs/index.html") {
@@ -72,6 +102,15 @@ task assembleJavadoc(type: Sync) {
destinationDir = file("${rootProject.buildDir}/javadoc")
}
+String getGitCommit() {
+ def proc = "git rev-parse HEAD".execute(null, projectDir)
+ proc.waitFor()
+ if (proc.exitValue() != 0) {
+ throw new RuntimeException("Failed to get git commit ID");
+ }
+ return proc.text.trim()
+}
+
subprojects { project ->
sourceCompatibility = 1.8
@@ -88,6 +127,10 @@ subprojects { project ->
reproducibleFileOrder = true
}
+ tasks.withType(Sign) {
+ it.dependsOn check
+ }
+
test {
testLogging {
showStandardStreams = isCiBuild
diff --git a/doc/releasing.md b/doc/releasing.md
new file mode 100644
index 000000000..1c31ef522
--- /dev/null
+++ b/doc/releasing.md
@@ -0,0 +1,124 @@
+Release procedure
+====================
+
+Release candidate versions
+---
+
+ 1. Make sure release notes in `NEWS` are up to date.
+
+ 2. Make sure you're running Gradle in JDK 11.
+
+ 3. Run the tests one more time:
+
+ ```
+ $ ./gradlew clean check
+ ```
+
+ 4. Tag the head commit with an `X.Y.Z-RCN` tag:
+
+ ```
+ $ git tag -a -s 1.4.0-RC1 -m "Pre-release 1.4.0-RC1"
+ ```
+
+ No tag body needed.
+
+ 5. Publish to Sonatype Nexus:
+
+ ```
+ $ ./gradlew publish closeAndReleaseRepository
+ ```
+
+ 6. Push to GitHub:
+
+ ```
+ $ git push origin master 1.4.0-RC1
+ ```
+
+ 7. Make GitHub release.
+
+ - Use the new tag as the release tag
+ - Check the pre-release checkbox
+ - Copy the release notes from `NEWS` into the GitHub release notes; reformat
+ from ASCIIdoc to Markdown and remove line wraps. Include only
+ changes/additions since the previous release or pre-release.
+ - Attach the signature files from
+ `webauthn-server-attestation/build/libs/webauthn-server-attestation-X.Y.Z-RCN.jar.asc`
+ and
+ `webauthn-server-core/build/libs/webauthn-server-core-X.Y.Z-RCN.jar.asc`.
+ - Note which JDK version was used to build the artifacts.
+
+
+Release versions
+---
+
+ 1. Make sure release notes in `NEWS` are up to date.
+
+ 2. Make sure you're running Gradle in JDK 11.
+
+ 3. Make a no-fast-forward merge from the last (non release candidate) release
+ to the commit to be released:
+
+ ```
+ $ git checkout 1.3.0
+ $ git checkout -b release-1.4.0
+ $ git merge --no-ff master
+ ```
+
+ Copy the release notes for this version from `NEWS` into the merge commit
+ message; reformat it from ASCIIdoc to Markdown and re-wrap line widths at
+ the conventional 72 columns (see
+ [this](https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html)
+ and [this](https://chris.beams.io/posts/git-commit/)). See previous release
+ commits for examples.
+
+ ```
+ $ git checkout master
+ $ git merge --ff-only release-1.4.0
+ $ git branch -d release-1.4.0
+ ```
+
+ 4. Remove the "(unreleased)" tag from `NEWS`.
+
+ 5. Amend this change into the merge commit:
+
+ ```
+ $ git add NEWS
+ $ git commit --amend --reset-author
+ ```
+
+ 6. Run the tests one more time:
+
+ ```
+ $ ./gradlew clean check
+ ```
+
+ 7. Tag the merge commit with an `X.Y.Z` tag:
+
+ ```
+ $ git tag -a -s 1.4.0 -m "Release 1.4.0"
+ ```
+
+ No tag body needed since that's included in the commit.
+
+ 8. Publish to Sonatype Nexus:
+
+ ```
+ $ ./gradlew publish closeAndReleaseRepository
+ ```
+
+ 9. Push to GitHub:
+
+ ```
+ $ git push origin master 1.4.0
+ ```
+
+10. Make GitHub release.
+
+ - Use the new tag as the release tag
+ - Copy the release notes from `NEWS` into the GitHub release notes; reformat
+ from ASCIIdoc to Markdown and remove line wraps. Include all changes since
+ the previous release (not just changes since the previous pre-release).
+ - Attach the signature files from
+ `webauthn-server-attestation/build/libs/webauthn-server-attestation-X.Y.Z.jar.asc`
+ and `webauthn-server-core/build/libs/webauthn-server-core-X.Y.Z.jar.asc`.
+ - Note which JDK version was used to build the artifacts.
diff --git a/settings.gradle b/settings.gradle
index 64788676b..14392fbb1 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -4,3 +4,7 @@ include ':webauthn-server-core'
include ':webauthn-server-demo'
include ':yubico-util'
include ':yubico-util-scala'
+
+include ':test-dependent-projects:java-dep-webauthn-server-attestation'
+include ':test-dependent-projects:java-dep-webauthn-server-core'
+include ':test-dependent-projects:java-dep-yubico-util'
diff --git a/test-dependent-projects/build.gradle.kts b/test-dependent-projects/build.gradle.kts
new file mode 100644
index 000000000..2221c1110
--- /dev/null
+++ b/test-dependent-projects/build.gradle.kts
@@ -0,0 +1,4 @@
+plugins {
+ // This is needed because the root project needs the sourceCompatibility property to exist.
+ `java-library`
+}
diff --git a/test-dependent-projects/java-dep-webauthn-server-attestation/build.gradle.kts b/test-dependent-projects/java-dep-webauthn-server-attestation/build.gradle.kts
new file mode 100644
index 000000000..16c3375cb
--- /dev/null
+++ b/test-dependent-projects/java-dep-webauthn-server-attestation/build.gradle.kts
@@ -0,0 +1,9 @@
+plugins {
+ `java-library`
+}
+
+dependencies {
+ implementation(project(":webauthn-server-attestation"))
+ testImplementation("junit:junit:4.12")
+}
+
diff --git a/test-dependent-projects/java-dep-webauthn-server-attestation/src/main/java/com/yubico/test/compilability/ThisShouldCompile.java b/test-dependent-projects/java-dep-webauthn-server-attestation/src/main/java/com/yubico/test/compilability/ThisShouldCompile.java
new file mode 100644
index 000000000..4320448a1
--- /dev/null
+++ b/test-dependent-projects/java-dep-webauthn-server-attestation/src/main/java/com/yubico/test/compilability/ThisShouldCompile.java
@@ -0,0 +1,24 @@
+package com.yubico.test.compilability;
+
+import com.yubico.webauthn.attestation.AttestationResolver;
+import java.security.cert.X509Certificate;
+import java.util.List;
+import java.util.Optional;
+
+public class ThisShouldCompile {
+
+ public AttestationResolver getResolver() {
+ return new AttestationResolver() {
+ @Override
+ public Optional resolve(X509Certificate attestationCertificate, List certificateChain) {
+ return Optional.empty();
+ }
+
+ @Override
+ public com.yubico.webauthn.attestation.Attestation untrustedFromCertificate(X509Certificate attestationCertificate) {
+ return null;
+ }
+ };
+ }
+
+}
diff --git a/test-dependent-projects/java-dep-webauthn-server-attestation/src/test/java/com/yubico/webauthn/attestation/ManifestInfoTest.java b/test-dependent-projects/java-dep-webauthn-server-attestation/src/test/java/com/yubico/webauthn/attestation/ManifestInfoTest.java
new file mode 100644
index 000000000..afb20d93c
--- /dev/null
+++ b/test-dependent-projects/java-dep-webauthn-server-attestation/src/test/java/com/yubico/webauthn/attestation/ManifestInfoTest.java
@@ -0,0 +1,40 @@
+package com.yubico.webauthn.attestation;
+
+import java.io.IOException;
+import java.net.URL;
+import java.util.Enumeration;
+import java.util.NoSuchElementException;
+import java.util.jar.Manifest;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+public class ManifestInfoTest {
+
+ private static String lookup(String key) throws IOException {
+ final Enumeration resources = AttestationResolver.class.getClassLoader().getResources("META-INF/MANIFEST.MF");
+
+ while (resources.hasMoreElements()) {
+ final URL resource = resources.nextElement();
+ final Manifest manifest = new Manifest(resource.openStream());
+ if ("java-webauthn-server-attestation".equals(manifest.getMainAttributes().getValue("Implementation-Id"))) {
+ return manifest.getMainAttributes().getValue(key);
+ }
+ }
+ throw new NoSuchElementException("Could not find \"" + key + "\" in manifest.");
+ }
+
+ @Test
+ public void standardImplementationPropertiesAreSet() throws IOException {
+ assertTrue(lookup("Implementation-Title").contains("attestation"));
+ assertTrue(lookup("Implementation-Version").matches("^\\d+\\.\\d+\\.\\d+(-.*)?"));
+ assertEquals("Yubico", lookup("Implementation-Vendor"));
+ }
+
+ @Test
+ public void customImplementationPropertiesAreSet() throws IOException {
+ assertTrue(lookup("Git-Commit").matches("^[a-f0-9]{40}$"));
+ }
+
+}
diff --git a/test-dependent-projects/java-dep-webauthn-server-core/build.gradle.kts b/test-dependent-projects/java-dep-webauthn-server-core/build.gradle.kts
new file mode 100644
index 000000000..bbc21c1fc
--- /dev/null
+++ b/test-dependent-projects/java-dep-webauthn-server-core/build.gradle.kts
@@ -0,0 +1,9 @@
+plugins {
+ `java-library`
+}
+
+dependencies {
+ implementation(project(":webauthn-server-core"))
+ testImplementation("junit:junit:4.12")
+}
+
diff --git a/test-dependent-projects/java-dep-webauthn-server-core/src/main/java/com/yubico/test/compilability/ThisShouldCompile.java b/test-dependent-projects/java-dep-webauthn-server-core/src/main/java/com/yubico/test/compilability/ThisShouldCompile.java
new file mode 100644
index 000000000..4c1a8b761
--- /dev/null
+++ b/test-dependent-projects/java-dep-webauthn-server-core/src/main/java/com/yubico/test/compilability/ThisShouldCompile.java
@@ -0,0 +1,44 @@
+package com.yubico.test.compilability;
+
+import com.yubico.webauthn.CredentialRepository;
+import com.yubico.webauthn.RegisteredCredential;
+import com.yubico.webauthn.RelyingParty;
+import com.yubico.webauthn.data.ByteArray;
+import com.yubico.webauthn.data.PublicKeyCredentialDescriptor;
+import com.yubico.webauthn.data.PublicKeyCredentialType;
+import com.yubico.webauthn.data.RelyingPartyIdentity;
+import java.util.Optional;
+import java.util.Set;
+
+public class ThisShouldCompile {
+
+ public RelyingParty getRp() {
+ return RelyingParty.builder()
+ .identity(RelyingPartyIdentity.builder()
+ .id("localhost")
+ .name("Example RP")
+ .build())
+ .credentialRepository(new CredentialRepository() {
+ @Override public Set getCredentialIdsForUsername(String username) { return null; }
+ @Override public Optional getUserHandleForUsername(String username) { return Optional.empty(); }
+ @Override public Optional getUsernameForUserHandle(ByteArray userHandle) { return Optional.empty(); }
+ @Override public Optional lookup(ByteArray credentialId, ByteArray userHandle) { return Optional.empty(); }
+ @Override public Set lookupAll(ByteArray credentialId) { return null; }
+ })
+ .build();
+ }
+
+ public ByteArray getByteArray() {
+ ByteArray a = new ByteArray(new byte[] {1, 2, 3, 4});
+ byte[] b = a.getBytes();
+ return a;
+ }
+
+ public PublicKeyCredentialType getPublicKeyCredentialType() {
+ PublicKeyCredentialType a = PublicKeyCredentialType.PUBLIC_KEY;
+ String b = a.toJsonString();
+ return a;
+ }
+
+
+}
diff --git a/test-dependent-projects/java-dep-webauthn-server-core/src/test/java/com/yubico/webauthn/meta/ManifestInfoTest.java b/test-dependent-projects/java-dep-webauthn-server-core/src/test/java/com/yubico/webauthn/meta/ManifestInfoTest.java
new file mode 100644
index 000000000..4a4aefd6b
--- /dev/null
+++ b/test-dependent-projects/java-dep-webauthn-server-core/src/test/java/com/yubico/webauthn/meta/ManifestInfoTest.java
@@ -0,0 +1,58 @@
+package com.yubico.webauthn.meta;
+
+import com.yubico.webauthn.RelyingParty;
+import java.io.IOException;
+import java.net.URL;
+import java.time.LocalDate;
+import java.util.Enumeration;
+import java.util.NoSuchElementException;
+import java.util.jar.Manifest;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+public class ManifestInfoTest {
+
+ private static String lookup(String key) throws IOException {
+ final Enumeration resources = RelyingParty.class.getClassLoader().getResources("META-INF/MANIFEST.MF");
+
+ while (resources.hasMoreElements()) {
+ final URL resource = resources.nextElement();
+ final Manifest manifest = new Manifest(resource.openStream());
+ if ("java-webauthn-server".equals(manifest.getMainAttributes().getValue("Implementation-Id"))) {
+ return manifest.getMainAttributes().getValue(key);
+ }
+ }
+ throw new NoSuchElementException("Could not find \"" + key + "\" in manifest.");
+ }
+
+ @Test
+ public void standardSpecPropertiesAreSet() throws IOException {
+ assertTrue(lookup("Specification-Title").startsWith("Web Authentication"));
+ assertTrue(lookup("Specification-Version").startsWith("Level"));
+ assertEquals("World Wide Web Consortium", lookup("Specification-Vendor"));
+ }
+
+
+ @Test
+ public void customSpecPropertiesAreSet() throws IOException {
+ assertTrue(lookup("Specification-Url").startsWith("https://"));
+ assertTrue(lookup("Specification-Url-Latest").startsWith("https://"));
+ assertTrue(DocumentStatus.fromString(lookup("Specification-W3c-Status")).isPresent());
+ assertTrue(LocalDate.parse(lookup("Specification-Release-Date")).isAfter(LocalDate.of(2019, 3, 3)));
+ }
+
+ @Test
+ public void standardImplementationPropertiesAreSet() throws IOException {
+ assertTrue(lookup("Implementation-Title").contains("Web Authentication"));
+ assertTrue(lookup("Implementation-Version").matches("^\\d+\\.\\d+\\.\\d+(-.*)?"));
+ assertEquals("Yubico", lookup("Implementation-Vendor"));
+ }
+
+ @Test
+ public void customImplementationPropertiesAreSet() throws IOException {
+ assertTrue(lookup("Git-Commit").matches("^[a-f0-9]{40}$"));
+ }
+
+}
diff --git a/test-dependent-projects/java-dep-webauthn-server-core/src/test/java/com/yubico/webauthn/meta/VersionInfoTest.java b/test-dependent-projects/java-dep-webauthn-server-core/src/test/java/com/yubico/webauthn/meta/VersionInfoTest.java
new file mode 100644
index 000000000..3ef4f822c
--- /dev/null
+++ b/test-dependent-projects/java-dep-webauthn-server-core/src/test/java/com/yubico/webauthn/meta/VersionInfoTest.java
@@ -0,0 +1,42 @@
+package com.yubico.webauthn.meta;
+
+import java.time.LocalDate;
+import org.junit.Test;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Since this depends on the manifest of the core jar, and the manifest is build by Gradle, this test is likely to fail
+ * when run in an IDE. It works as expected when run via Gradle.
+ */
+public class VersionInfoTest {
+
+ final VersionInfo versionInfo = VersionInfo.getInstance();
+
+ @Test
+ public void specPropertiesAreSet() {
+ final Specification spec = versionInfo.getSpecification();
+ assertTrue(spec.getLatestVersionUrl().toExternalForm().startsWith("https://"));
+ assertTrue(spec.getUrl().toExternalForm().startsWith("https://"));
+ assertTrue(spec.getReleaseDate().isAfter(LocalDate.of(2019, 3, 3)));
+ assertNotNull(spec.getStatus());
+ }
+
+ @Test
+ public void implementationPropertiesAreSet() {
+ final Implementation impl = versionInfo.getImplementation();
+ assertTrue(impl.getSourceCodeUrl().toExternalForm().startsWith("https://"));
+ assertTrue(impl.getVersion().matches("^\\d+\\.\\d+\\.\\d+(-.*)?"));
+ assertTrue(impl.getGitCommit().matches("^[a-f0-9]{40}$"));
+ }
+
+ @Test
+ public void majorVersionIsAtLeast1() {
+ final String version = versionInfo.getImplementation().getVersion();
+ String[] splits = version.split("\\.");
+ final int majorVersion = Integer.parseInt(splits[0]);
+ assertTrue(majorVersion >= 1);
+ }
+
+}
diff --git a/test-dependent-projects/java-dep-yubico-util/build.gradle.kts b/test-dependent-projects/java-dep-yubico-util/build.gradle.kts
new file mode 100644
index 000000000..822271db6
--- /dev/null
+++ b/test-dependent-projects/java-dep-yubico-util/build.gradle.kts
@@ -0,0 +1,8 @@
+plugins {
+ `java-library`
+}
+
+dependencies {
+ implementation(project(":yubico-util"))
+}
+
diff --git a/test-dependent-projects/java-dep-yubico-util/src/main/java/com/yubico/test/compilability/ThisShouldCompile.java b/test-dependent-projects/java-dep-yubico-util/src/main/java/com/yubico/test/compilability/ThisShouldCompile.java
new file mode 100644
index 000000000..71f885e21
--- /dev/null
+++ b/test-dependent-projects/java-dep-yubico-util/src/main/java/com/yubico/test/compilability/ThisShouldCompile.java
@@ -0,0 +1,12 @@
+package com.yubico.test.compilability;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.yubico.internal.util.JacksonCodecs;
+
+public class ThisShouldCompile {
+
+ public String getEncodedValue() throws JsonProcessingException {
+ return JacksonCodecs.json().writeValueAsString("hej");
+ }
+
+}
diff --git a/webauthn-server-attestation/build.gradle b/webauthn-server-attestation/build.gradle
index fb74b0159..7560b3b38 100644
--- a/webauthn-server-attestation/build.gradle
+++ b/webauthn-server-attestation/build.gradle
@@ -11,27 +11,30 @@ evaluationDependsOn(':webauthn-server-core')
dependencies {
- implementation(
+ api(
project(':webauthn-server-core'),
+ )
+
+ implementation(
project(':yubico-util'),
- 'com.fasterxml.jackson.core:jackson-databind:2.9.9.3',
- 'com.google.guava:guava:19.0',
- 'org.bouncycastle:bcprov-jdk15on:1.62',
- 'org.slf4j:slf4j-api:1.7.25',
+ addVersion('com.google.guava:guava'),
+ addVersion('com.fasterxml.jackson.core:jackson-databind'),
+ addVersion('org.bouncycastle:bcprov-jdk15on'),
+ addVersion('org.slf4j:slf4j-api'),
)
testImplementation(
project(':webauthn-server-core').sourceSets.test.output,
project(':yubico-util-scala'),
- 'junit:junit:4.12',
- 'org.mockito:mockito-core:2.27.0',
- 'org.scala-lang:scala-library:2.12.8',
- 'org.scalacheck:scalacheck_2.12:1.14.0',
- 'org.scalatest:scalatest_2.12:3.0.4',
+ addVersion('junit:junit'),
+ addVersion('org.mockito:mockito-core'),
+ addVersion('org.scala-lang:scala-library'),
+ addVersion('org.scalacheck:scalacheck_2.12'),
+ addVersion('org.scalatest:scalatest_2.12'),
)
testRuntimeOnly(
// Transitive dependency from :webauthn-server-core:test
- 'org.bouncycastle:bcpkix-jdk15on:1.62',
+ addVersion('org.bouncycastle:bcpkix-jdk15on'),
)
}
@@ -43,6 +46,7 @@ jar {
'Implementation-Title': project.description,
'Implementation-Version': project.version,
'Implementation-Vendor': 'Yubico',
+ 'Git-Commit': getGitCommit(),
])
}
}
diff --git a/webauthn-server-attestation/src/test/scala/com/yubico/webauthn/attestation/StandardMetadataServiceSpec.scala b/webauthn-server-attestation/src/test/scala/com/yubico/webauthn/attestation/StandardMetadataServiceSpec.scala
index d952f2af3..5f9abd8fd 100644
--- a/webauthn-server-attestation/src/test/scala/com/yubico/webauthn/attestation/StandardMetadataServiceSpec.scala
+++ b/webauthn-server-attestation/src/test/scala/com/yubico/webauthn/attestation/StandardMetadataServiceSpec.scala
@@ -24,6 +24,8 @@
package com.yubico.webauthn.attestation
+import java.security.cert.X509Certificate
+import java.util.Base64
import java.util.Collections
import com.fasterxml.jackson.databind.node.JsonNodeFactory
@@ -61,6 +63,13 @@ class StandardMetadataServiceSpec extends FunSpec with Matchers {
)
}
+ def toPem(cert: X509Certificate): String = (
+ "-----BEGIN CERTIFICATE-----\n"
+ + Base64.getMimeEncoder(64, System.getProperty("line.separator").getBytes("UTF-8"))
+ .encodeToString(cert.getEncoded)
+ + "\n-----END CERTIFICATE-----\n"
+ )
+
describe("StandardMetadataService") {
describe("has a getAttestation method which") {
@@ -102,7 +111,7 @@ class StandardMetadataServiceSpec extends FunSpec with Matchers {
s"""{
"identifier": "44c87ead-4455-423e-88eb-9248e0ebe847",
"version": 1,
- "trustedCertificates": ["${TestAuthenticator.toPem(caCert).linesIterator.mkString(raw"\n")}"],
+ "trustedCertificates": ["${toPem(caCert).linesIterator.mkString(raw"\n")}"],
"vendorInfo": {},
"devices": [
{
@@ -160,7 +169,7 @@ class StandardMetadataServiceSpec extends FunSpec with Matchers {
s"""{
"identifier": "44c87ead-4455-423e-88eb-9248e0ebe847",
"version": 1,
- "trustedCertificates": ["${TestAuthenticator.toPem(caCert).linesIterator.mkString(raw"\n")}"],
+ "trustedCertificates": ["${toPem(caCert).linesIterator.mkString(raw"\n")}"],
"vendorInfo": {},
"devices": []
}"""
@@ -198,7 +207,7 @@ class StandardMetadataServiceSpec extends FunSpec with Matchers {
s"""{
"identifier": "44c87ead-4455-423e-88eb-9248e0ebe847",
"version": 1,
- "trustedCertificates": ["${TestAuthenticator.toPem(cacaca._1).linesIterator.mkString(raw"\n")}"],
+ "trustedCertificates": ["${toPem(cacaca._1).linesIterator.mkString(raw"\n")}"],
"vendorInfo": {},
"devices": [
{
@@ -228,7 +237,7 @@ class StandardMetadataServiceSpec extends FunSpec with Matchers {
s"""{
"identifier": "44c87ead-4455-423e-88eb-9248e0ebe847",
"version": 1,
- "trustedCertificates": ["${TestAuthenticator.toPem(caCert).linesIterator.mkString(raw"\n")}"],
+ "trustedCertificates": ["${toPem(caCert).linesIterator.mkString(raw"\n")}"],
"vendorInfo": {},
"devices": [
{
diff --git a/webauthn-server-core/build.gradle b/webauthn-server-core/build.gradle
index f2e0f3247..c72f63201 100644
--- a/webauthn-server-core/build.gradle
+++ b/webauthn-server-core/build.gradle
@@ -10,25 +10,29 @@ project.ext.publishMe = true
dependencies {
- implementation(
+ api(
project(':yubico-util'),
- 'com.augustcellars.cose:cose-java:0.9.10',
- 'com.fasterxml.jackson.core:jackson-databind:2.9.9.3',
- 'com.google.guava:guava:19.0',
- 'org.apache.httpcomponents:httpclient:4.5.2',
- 'org.bouncycastle:bcprov-jdk15on:1.62',
- 'org.slf4j:slf4j-api:1.7.25',
+ )
+
+ implementation(
+ addVersion('com.augustcellars.cose:cose-java'),
+ addVersion('com.google.guava:guava'),
+ addVersion('com.fasterxml.jackson.core:jackson-databind'),
+ addVersion('com.upokecenter:cbor'),
+ addVersion('org.apache.httpcomponents:httpclient'),
+ addVersion('org.bouncycastle:bcprov-jdk15on'),
+ addVersion('org.slf4j:slf4j-api'),
)
testImplementation(
project(':yubico-util-scala'),
- 'com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.9.9',
- 'junit:junit:4.12',
- 'org.bouncycastle:bcpkix-jdk15on:1.62',
- 'org.mockito:mockito-core:2.27.0',
- 'org.scala-lang:scala-library:2.12.8',
- 'org.scalacheck:scalacheck_2.12:1.14.0',
- 'org.scalatest:scalatest_2.12:3.0.4',
+ addVersion('com.fasterxml.jackson.datatype:jackson-datatype-jdk8'),
+ addVersion('junit:junit'),
+ addVersion('org.bouncycastle:bcpkix-jdk15on'),
+ addVersion('org.mockito:mockito-core'),
+ addVersion('org.scala-lang:scala-library'),
+ addVersion('org.scalacheck:scalacheck_2.12'),
+ addVersion('org.scalatest:scalatest_2.12'),
)
}
@@ -38,12 +42,20 @@ jar {
manifest {
attributes([
'Specification-Title': 'Web Authentication: An API for accessing Public Key Credentials',
- 'Specification-Version': 'Level 1 Proposed Recommendation 2019-01-17',
+ 'Specification-Version': 'Level 1 Recommendation 2019-03-04',
'Specification-Vendor': 'World Wide Web Consortium',
+
+ 'Specification-Url': 'https://www.w3.org/TR/2019/REC-webauthn-1-20190304/',
+ 'Specification-Url-Latest': 'https://www.w3.org/TR/webauthn/',
+ 'Specification-W3c-Status': 'recommendation',
+ 'Specification-Release-Date': '2019-03-04',
+
'Implementation-Id': 'java-webauthn-server',
'Implementation-Title': 'Yubico Web Authentication server library',
'Implementation-Version': project.version,
'Implementation-Vendor': 'Yubico',
+ 'Implementation-Source-Url': 'https://github.com/Yubico/java-webauthn-server',
+ 'Git-Commit': getGitCommit(),
])
}
}
diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/BouncyCastleCrypto.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/BouncyCastleCrypto.java
index 46ecbc325..7a1c98042 100755
--- a/webauthn-server-core/src/main/java/com/yubico/webauthn/BouncyCastleCrypto.java
+++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/BouncyCastleCrypto.java
@@ -30,6 +30,7 @@
package com.yubico.webauthn;
import com.yubico.webauthn.data.ByteArray;
+import com.yubico.webauthn.data.COSEAlgorithmIdentifier;
import java.security.GeneralSecurityException;
import java.security.KeyFactory;
import java.security.MessageDigest;
@@ -53,30 +54,13 @@ public Provider getProvider() {
return provider;
}
- public boolean verifySignature(X509Certificate attestationCertificate, ByteArray signedBytes, ByteArray signature) {
- return verifySignature(attestationCertificate.getPublicKey(), signedBytes, signature);
+ public boolean verifySignature(X509Certificate attestationCertificate, ByteArray signedBytes, ByteArray signature, COSEAlgorithmIdentifier alg) {
+ return verifySignature(attestationCertificate.getPublicKey(), signedBytes, signature, alg);
}
- public boolean verifySignature(PublicKey publicKey, ByteArray signedBytes, ByteArray signatureBytes) {
+ public boolean verifySignature(PublicKey publicKey, ByteArray signedBytes, ByteArray signatureBytes, COSEAlgorithmIdentifier alg) {
try {
- final String algName;
- switch (publicKey.getAlgorithm()) {
- case "EC":
- algName = "SHA256withECDSA";
- break;
-
- case "Ed25519":
- algName = "EDDSA";
- break;
-
- case "RSA":
- algName = "SHA256withRSA";
- break;
-
- default:
- throw new IllegalArgumentException("Unsupported public key algorithm: " + publicKey);
- }
- Signature signature = Signature.getInstance(algName, provider);
+ Signature signature = Signature.getInstance(WebAuthnCodecs.getJavaAlgorithmName(alg), provider);
signature.initVerify(publicKey);
signature.update(signedBytes.getBytes());
return signature.verify(signatureBytes.getBytes());
diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishAssertionSteps.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishAssertionSteps.java
index 4a0cf4e71..a5070f374 100644
--- a/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishAssertionSteps.java
+++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishAssertionSteps.java
@@ -29,6 +29,7 @@
import com.yubico.internal.util.CollectionUtil;
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;
@@ -66,10 +67,10 @@ final class FinishAssertionSteps {
private final String rpId;
private final CredentialRepository credentialRepository;
- @Builder.Default
- private final boolean validateSignatureCounter = true;
- @Builder.Default
- private final boolean allowUnrequestedExtensions = false;
+ @Builder.Default private final boolean allowOriginPort = false;
+ @Builder.Default private final boolean allowOriginSubdomain = false;
+ @Builder.Default private final boolean allowUnrequestedExtensions = false;
+ @Builder.Default private final boolean validateSignatureCounter = true;
public Step0 begin() {
return new Step0();
@@ -372,12 +373,16 @@ class Step9 implements Step {
@Override
public void validate() {
- final String responseOrigin;
- responseOrigin = response.getResponse().getClientData().getOrigin();
-
- if (origins.stream().noneMatch(o -> o.equals(responseOrigin))) {
- throw new IllegalArgumentException("Incorrect origin: " + responseOrigin);
- }
+ final String responseOrigin = response.getResponse().getClientData().getOrigin();
+ assure(
+ OriginMatcher.isAllowed(
+ responseOrigin,
+ origins,
+ allowOriginPort,
+ allowOriginSubdomain
+ ),
+ "Incorrect origin: " + responseOrigin
+ );
}
@Override
@@ -561,11 +566,15 @@ public void validate() {
throw new RuntimeException(e);
}
+ final COSEAlgorithmIdentifier alg = WebAuthnCodecs.getCoseKeyAlg(cose).orElseThrow(() ->
+ new IllegalArgumentException(String.format("Failed to decode \"alg\" from COSE key: %s", cose)));
+
if (!
crypto.verifySignature(
key,
signedBytes(),
- response.getResponse().getSignature()
+ response.getResponse().getSignature(),
+ alg
)
) {
throw new IllegalArgumentException("Invalid assertion signature.");
diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishRegistrationSteps.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishRegistrationSteps.java
index 83e8fca3c..ae47d614f 100644
--- a/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishRegistrationSteps.java
+++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishRegistrationSteps.java
@@ -25,6 +25,7 @@
package com.yubico.webauthn;
import COSE.CoseException;
+import com.upokecenter.cbor.CBORObject;
import com.yubico.internal.util.CollectionUtil;
import com.yubico.webauthn.attestation.Attestation;
import com.yubico.webauthn.attestation.MetadataService;
@@ -40,19 +41,23 @@
import com.yubico.webauthn.data.PublicKeyCredentialDescriptor;
import com.yubico.webauthn.data.UserVerificationRequirement;
import java.io.IOException;
+import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
+import java.security.spec.InvalidKeySpecException;
import java.util.ArrayList;
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.Value;
import lombok.extern.slf4j.Slf4j;
import static com.yubico.internal.util.ExceptionUtil.assure;
+import static com.yubico.internal.util.ExceptionUtil.wrapAndLog;
@Builder
@Slf4j
@@ -71,8 +76,9 @@ final class FinishRegistrationSteps {
private final Optional metadataService;
private final CredentialRepository credentialRepository;
- @Builder.Default
- private final boolean allowUnrequestedExtensions = false;
+ @Builder.Default private final boolean allowOriginPort = false;
+ @Builder.Default private final boolean allowOriginSubdomain = false;
+ @Builder.Default private final boolean allowUnrequestedExtensions = false;
public Step1 begin() {
@@ -213,9 +219,15 @@ class Step5 implements Step {
@Override
public void validate() {
+ final String responseOrigin = clientData.getOrigin();
assure(
- origins.stream().anyMatch(o -> o.equals(clientData.getOrigin())),
- "Incorrect origin: " + clientData.getOrigin()
+ OriginMatcher.isAllowed(
+ responseOrigin,
+ origins,
+ allowOriginPort,
+ allowOriginSubdomain
+ ),
+ "Incorrect origin: " + responseOrigin
);
}
@@ -308,7 +320,10 @@ class Step10 implements Step {
@Override
public void validate() {
- assure(response.getResponse().getParsedAuthenticatorData().getFlags().UP, "User Presence is required.");
+ assure(
+ response.getResponse().getParsedAuthenticatorData().getFlags().UP,
+ "User Presence is required."
+ );
}
@Override
@@ -325,8 +340,16 @@ class Step11 implements Step {
@Override
public void validate() {
- if (request.getAuthenticatorSelection().map(AuthenticatorSelectionCriteria::getUserVerification).orElse(UserVerificationRequirement.PREFERRED) == UserVerificationRequirement.REQUIRED) {
- assure(response.getResponse().getParsedAuthenticatorData().getFlags().UV, "User Verification is required.");
+ if (
+ request.getAuthenticatorSelection()
+ .map(AuthenticatorSelectionCriteria::getUserVerification)
+ .orElse(UserVerificationRequirement.PREFERRED)
+ == UserVerificationRequirement.REQUIRED
+ ) {
+ assure(
+ response.getResponse().getParsedAuthenticatorData().getFlags().UV,
+ "User Verification is required."
+ );
}
}
@@ -625,7 +648,27 @@ public Step19 nextStep() {
}
@Value
- class Step19 implements Step {
+ class Step19 implements Step {
+ private final AttestationType attestationType;
+ private final Optional attestationMetadata;
+ private final boolean attestationTrusted;
+ private final List prevWarnings;
+
+ @Override
+ public void validate() {
+ }
+
+ @Override
+ public CustomLastStep nextStep() {
+ return new CustomLastStep(attestationType, attestationMetadata, attestationTrusted, allWarnings());
+ }
+ }
+
+ /**
+ * Steps that aren't yet standardised in a stable edition of the spec
+ */
+ @Value
+ class CustomLastStep implements Step {
private final AttestationType attestationType;
private final Optional attestationMetadata;
private final boolean attestationTrusted;
@@ -633,6 +676,20 @@ class Step19 implements Step {
@Override
public void validate() {
+ ByteArray publicKeyCose = response.getResponse().getAttestation().getAuthenticatorData().getAttestedCredentialData().get().getCredentialPublicKey();
+ CBORObject publicKeyCbor = CBORObject.DecodeFromBytes(publicKeyCose.getBytes());
+ int alg = publicKeyCbor.get(CBORObject.FromObject(3)).AsInt32();
+ assure(
+ request.getPubKeyCredParams().stream().anyMatch(pkcparam -> pkcparam.getAlg().getId() == alg),
+ "Unrequested credential key algorithm: got %d, expected one of: %s",
+ alg,
+ request.getPubKeyCredParams().stream().map(pkcparam -> pkcparam.getAlg()).collect(Collectors.toList())
+ );
+ try {
+ WebAuthnCodecs.importCosePublicKey(publicKeyCose);
+ } catch (CoseException | IOException | InvalidKeySpecException | NoSuchAlgorithmException e) {
+ throw wrapAndLog(log, "Failed to parse credential public key", e);
+ }
}
@Override
diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/OriginMatcher.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/OriginMatcher.java
new file mode 100644
index 000000000..576c962fa
--- /dev/null
+++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/OriginMatcher.java
@@ -0,0 +1,74 @@
+package com.yubico.webauthn;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Set;
+import lombok.experimental.UtilityClass;
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+@UtilityClass
+class OriginMatcher {
+
+ static boolean isAllowed(
+ String origin,
+ Set allowedOrigins,
+ boolean allowPort,
+ boolean allowSubdomain
+ ) {
+ log.trace("isAllowed({}, {}, {}, {})", origin, allowedOrigins, allowPort, allowSubdomain);
+
+ URL tmpOriginUrl;
+ try {
+ tmpOriginUrl = new URL(origin);
+ } catch (MalformedURLException e) {
+ log.debug("Origin in client data is not a valid URL; will only match exactly: {}", origin);
+ tmpOriginUrl = null;
+ }
+ final URL originUrl = tmpOriginUrl;
+
+ return allowedOrigins.stream().anyMatch(allowedOriginString -> {
+ if (allowedOriginString.equals(origin)) {
+ log.debug("Exact match: {} == {}", origin, allowedOriginString);
+ return true;
+ } else if (originUrl != null && (allowPort || allowSubdomain)) {
+ final URL allowedOrigin;
+ try {
+ allowedOrigin = new URL(allowedOriginString);
+ } catch (MalformedURLException e) {
+ log.error("Allowed origin is not a valid URL; skipping port/subdomain matching: {}", allowedOriginString);
+ return false;
+ }
+
+ final boolean portAccepted = isPortAccepted(allowPort, allowedOrigin, originUrl);
+ final boolean domainAccepted = isDomainAccepted(allowSubdomain, allowedOrigin, originUrl);
+
+ log.debug("portAccepted: {}, domainAccepted: {}", portAccepted, domainAccepted);
+ return portAccepted && domainAccepted;
+ } else {
+ log.debug("No match: {} != {}", origin, allowedOriginString);
+ return false;
+ }
+ });
+ }
+
+ private static boolean isPortAccepted(boolean allowAnyPort, URL allowedOrigin, URL origin) {
+ if (allowAnyPort) {
+ return true;
+ } else {
+ return origin.getPort() == allowedOrigin.getPort();
+ }
+ }
+
+ private static boolean isDomainAccepted(boolean allowSubdomain, URL allowedOrigin, URL origin) {
+ final String allowedDomain = allowedOrigin.getHost();
+ final String originDomain = origin.getHost();
+
+ if (allowSubdomain) {
+ return originDomain.equals(allowedDomain) || originDomain.endsWith("." + allowedDomain);
+ } else {
+ return originDomain.equals(allowedDomain);
+ }
+ }
+
+}
diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/PackedAttestationStatementVerifier.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/PackedAttestationStatementVerifier.java
index 9d23518da..610b45903 100644
--- a/webauthn-server-core/src/main/java/com/yubico/webauthn/PackedAttestationStatementVerifier.java
+++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/PackedAttestationStatementVerifier.java
@@ -134,7 +134,7 @@ private boolean verifySelfAttestationSignature(AttestationObject attestationObje
throw ExceptionUtil.wrapAndLog(log, ".binaryValue() of \"sig\" failed", e);
}
- return crypto.verifySignature(pubkey, signedData, signature);
+ return crypto.verifySignature(pubkey, signedData, signature, keyAlg);
}
private boolean verifyX5cSignature(AttestationObject attestationObject, ByteArray clientDataHash) {
@@ -150,7 +150,6 @@ private boolean verifyX5cSignature(AttestationObject attestationObject, ByteArra
}
return attestationCert.map(attestationCertificate -> {
JsonNode signatureNode = attestationObject.getAttestationStatement().get("sig");
-
if (signatureNode == null) {
throw new IllegalArgumentException("Packed attestation statement must have field \"sig\".");
}
@@ -163,9 +162,18 @@ private boolean verifyX5cSignature(AttestationObject attestationObject, ByteArra
throw ExceptionUtil.wrapAndLog(log, "signatureNode.isBinary() was true but signatureNode.binaryValue() failed", e);
}
+ JsonNode algNode = attestationObject.getAttestationStatement().get("alg");
+ if (algNode == null) {
+ throw new IllegalArgumentException("Packed attestation statement must have field \"alg\".");
+ }
+ ExceptionUtil.assure(algNode.isIntegralNumber(), "Field \"alg\" in packed attestation statement must be a COSEAlgorithmIdentifier.");
+ final Long sigAlgId = algNode.asLong();
+ final COSEAlgorithmIdentifier sigAlg = COSEAlgorithmIdentifier.fromId(sigAlgId)
+ .orElseThrow(() -> new IllegalArgumentException("Unsupported COSE algorithm identifier: " + sigAlgId));
+
ByteArray signedData = attestationObject.getAuthenticatorData().getBytes().concat(clientDataHash);
- final String signatureAlgorithmName = "SHA256with" + WebAuthnCodecs.getSignatureAlgorithmName(attestationCertificate.getPublicKey());
+ final String signatureAlgorithmName = WebAuthnCodecs.getJavaAlgorithmName(sigAlg);
Signature signatureVerifier;
try {
signatureVerifier = Signature.getInstance(signatureAlgorithmName, crypto.getProvider());
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 b815da555..ab05db447 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
@@ -24,6 +24,7 @@
package com.yubico.webauthn;
+import com.yubico.internal.util.CollectionUtil;
import com.yubico.webauthn.attestation.MetadataService;
import com.yubico.webauthn.data.AssertionExtensionInputs;
import com.yubico.webauthn.data.AttestationConveyancePreference;
@@ -45,6 +46,8 @@
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.SecureRandom;
import java.util.ArrayList;
import java.util.Arrays;
@@ -55,6 +58,7 @@
import lombok.Builder;
import lombok.NonNull;
import lombok.Value;
+import lombok.extern.slf4j.Slf4j;
/**
@@ -65,6 +69,7 @@
* versions (function closures) of these four operations rather than a stateful object.
*
*/
+@Slf4j
@Builder(toBuilder = true)
@Value
public class RelyingParty {
@@ -96,8 +101,25 @@ public class RelyingParty {
*
*
*
- * A successful registration or authentication operation requires {@link CollectedClientData#getOrigin()} to exactly
- * equal one of these values.
+ * If {@link RelyingPartyBuilder#allowOriginPort(boolean) allowOriginPort} and {@link
+ * RelyingPartyBuilder#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 RelyingPartyBuilder#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 RelyingPartyBuilder#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()
@@ -209,6 +231,111 @@ public class RelyingParty {
PublicKeyCredentialParameters.RS256
));
+ /**
+ * 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 RelyingPartyBuilder#origins(Set) origins}.
+ *
+ *
+ * The default is false
.
+ *
+ *
+ *
+ * Examples with origins: ["https://example.org", "https://acme.com:8443"]
+ *
+ *
+ *
+ */
+ @Builder.Default
+ private final boolean allowOriginSubdomain = false;
+
/**
* If true
, {@link #finishRegistration(FinishRegistrationOptions) finishRegistration} and {@link
* #finishAssertion(FinishAssertionOptions) finishAssertion} will accept responses containing extension outputs for
@@ -260,17 +387,30 @@ private RelyingParty(
@NonNull Optional appId,
@NonNull Optional attestationConveyancePreference,
@NonNull Optional metadataService, List preferredPubkeyParams,
+ boolean allowOriginPort,
+ boolean allowOriginSubdomain,
boolean allowUnrequestedExtensions,
boolean allowUntrustedAttestation,
boolean validateSignatureCounter
) {
this.identity = identity;
- this.origins = origins != null ? origins : Collections.singleton("https://" + identity.getId());
+ 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.appId = appId;
this.attestationConveyancePreference = attestationConveyancePreference;
this.metadataService = metadataService;
this.preferredPubkeyParams = preferredPubkeyParams;
+ this.allowOriginPort = allowOriginPort;
+ this.allowOriginSubdomain = allowOriginSubdomain;
this.allowUnrequestedExtensions = allowUnrequestedExtensions;
this.allowUntrustedAttestation = allowUntrustedAttestation;
this.validateSignatureCounter = validateSignatureCounter;
@@ -326,6 +466,8 @@ FinishRegistrationSteps _finishRegistration(
.credentialRepository(credentialRepository)
.origins(origins)
.rpId(identity.getId())
+ .allowOriginPort(allowOriginPort)
+ .allowOriginSubdomain(allowOriginSubdomain)
.allowUnrequestedExtensions(allowUnrequestedExtensions)
.allowUntrustedAttestation(allowUntrustedAttestation)
.metadataService(metadataService)
@@ -394,6 +536,8 @@ FinishAssertionSteps _finishAssertion(
.origins(origins)
.rpId(identity.getId())
.credentialRepository(credentialRepository)
+ .allowOriginPort(allowOriginPort)
+ .allowOriginSubdomain(allowOriginSubdomain)
.allowUnrequestedExtensions(allowUnrequestedExtensions)
.validateSignatureCounter(validateSignatureCounter)
.build();
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 2c269126b..602e2170a 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
@@ -32,6 +32,7 @@
import com.google.common.io.ByteArrayDataOutput;
import com.google.common.io.ByteStreams;
import com.yubico.webauthn.data.ByteArray;
+import com.yubico.webauthn.data.COSEAlgorithmIdentifier;
import java.security.cert.X509Certificate;
import lombok.Value;
@@ -72,7 +73,7 @@ class U2fRawRegisterResponse {
boolean verifySignature(ByteArray appIdHash, ByteArray clientDataHash) {
ByteArray signedBytes = packBytesToSign(appIdHash, clientDataHash, keyHandle, userPublicKey);
- return crypto.verifySignature(attestationCertificate, signedBytes, signature);
+ return crypto.verifySignature(attestationCertificate, signedBytes, signature, COSEAlgorithmIdentifier.ES256);
}
private static ByteArray packBytesToSign(ByteArray appIdHash, ByteArray clientDataHash, ByteArray keyHandle, ByteArray userPublicKey) {
diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/WebAuthnCodecs.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/WebAuthnCodecs.java
index 643d32228..0809381a0 100644
--- a/webauthn-server-core/src/main/java/com/yubico/webauthn/WebAuthnCodecs.java
+++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/WebAuthnCodecs.java
@@ -35,14 +35,11 @@
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.interfaces.ECPublicKey;
-import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.RSAPublicKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Arrays;
-import java.util.HashMap;
-import java.util.Map;
-import org.bouncycastle.jcajce.provider.asymmetric.edec.BCEdDSAPublicKey;
+import java.util.Optional;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
@@ -50,7 +47,7 @@ final class WebAuthnCodecs {
private static final ByteArray ED25519_CURVE_OID = new ByteArray(new byte[]{0x30, 0x05, 0x06, 0x03, 0x2B, 0x65, 0x70});
- public static ByteArray ecPublicKeyToRaw(ECPublicKey key) {
+ static ByteArray ecPublicKeyToRaw(ECPublicKey key) {
byte[] x = key.getW().getAffineX().toByteArray();
byte[] y = key.getW().getAffineY().toByteArray();
byte[] xPadding = new byte[Math.max(0, 32 - x.length)];
@@ -72,57 +69,7 @@ public static ByteArray ecPublicKeyToRaw(ECPublicKey key) {
));
}
- public static ByteArray rawEcdaKeyToCose(ByteArray key) {
- final byte[] keyBytes = key.getBytes();
-
- if (!(keyBytes.length == 64 || (keyBytes.length == 65 && keyBytes[0] == 0x04))) {
- throw new IllegalArgumentException(String.format(
- "Raw key must be 64 bytes long or be 65 bytes long and start with 0x04, was %d bytes starting with %02x",
- keyBytes.length,
- keyBytes[0]
- ));
- }
-
- final int start = keyBytes.length == 64 ? 0 : 1;
-
- Map coseKey = new HashMap<>();
-
- coseKey.put(1L, 2L); // Key type: EC
- 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
-
- return new ByteArray(CBORObject.FromObject(coseKey).EncodeToBytes());
- }
-
- public static ByteArray ecPublicKeyToCose(ECPublicKey key) {
- return rawEcdaKeyToCose(ecPublicKeyToRaw(key));
- }
-
- public static ByteArray eddsaPublicKeyToCose(BCEdDSAPublicKey key) {
- Map coseKey = new HashMap<>();
-
- coseKey.put(1L, 1L); // Key type: octet key pair
- coseKey.put(3L, COSEAlgorithmIdentifier.RS256.getId());
- coseKey.put(-1L, 6L); // crv: Ed25519
- coseKey.put(-2L, key.getEncoded());
-
- return new ByteArray(CBORObject.FromObject(coseKey).EncodeToBytes());
- }
-
- public static ByteArray rsaPublicKeyToCose(RSAPublicKey key) {
- Map coseKey = new HashMap<>();
-
- coseKey.put(1L, 3L); // Key type: RSA
- coseKey.put(3L, COSEAlgorithmIdentifier.RS256.getId());
- coseKey.put(-1L, key.getModulus().toByteArray()); // public modulus n
- coseKey.put(-2L, key.getPublicExponent().toByteArray()); // public exponent e
-
- return new ByteArray(CBORObject.FromObject(coseKey).EncodeToBytes());
- }
-
- public static PublicKey importCosePublicKey(ByteArray key) throws CoseException, IOException, InvalidKeySpecException, NoSuchAlgorithmException {
+ static PublicKey importCosePublicKey(ByteArray key) throws CoseException, IOException, InvalidKeySpecException, NoSuchAlgorithmException {
CBORObject cose = CBORObject.DecodeFromBytes(key.getBytes());
final int kty = cose.get(CBORObject.FromObject(1)).AsInt32();
switch (kty) {
@@ -146,7 +93,7 @@ private static ECPublicKey importCoseP256PublicKey(CBORObject cose) throws CoseE
return (ECPublicKey) new OneKey(cose).AsPublicKey();
}
- private static PublicKey importCoseEdDsaPublicKey(CBORObject cose) {
+ private static PublicKey importCoseEdDsaPublicKey(CBORObject cose) throws InvalidKeySpecException, NoSuchAlgorithmException {
final int curveId = cose.get(CBORObject.FromObject(-1)).AsInt32();
switch (curveId) {
case 6: return importCoseEd25519PublicKey(cose);
@@ -155,30 +102,34 @@ private static PublicKey importCoseEdDsaPublicKey(CBORObject cose) {
}
}
- private static PublicKey importCoseEd25519PublicKey(CBORObject cose) {
+ private static PublicKey importCoseEd25519PublicKey(CBORObject cose) throws InvalidKeySpecException, NoSuchAlgorithmException {
final ByteArray rawKey = new ByteArray(cose.get(CBORObject.FromObject(-2)).GetByteString());
final ByteArray x509Key = new ByteArray(new byte[]{0x30, (byte) (ED25519_CURVE_OID.size() + 3 + rawKey.size()) })
.concat(ED25519_CURVE_OID)
.concat(new ByteArray(new byte[]{ 0x03, (byte) (rawKey.size() + 1), 0}))
.concat(rawKey);
- try {
- KeyFactory kFact = KeyFactory.getInstance("EdDSA", new BouncyCastleProvider());
- return kFact.generatePublic(new X509EncodedKeySpec(x509Key.getBytes()));
- } catch (InvalidKeySpecException | NoSuchAlgorithmException e) {
- throw new RuntimeException(e);
- }
+ KeyFactory kFact = KeyFactory.getInstance("EdDSA", new BouncyCastleProvider());
+ return kFact.generatePublic(new X509EncodedKeySpec(x509Key.getBytes()));
}
- public static String getSignatureAlgorithmName(PublicKey key) {
- if (key.getAlgorithm().equals("EC")) {
- return "ECDSA";
- } else {
- return key.getAlgorithm();
+ static Optional getCoseKeyAlg(ByteArray key) {
+ CBORObject cose = CBORObject.DecodeFromBytes(key.getBytes());
+ final int alg = cose.get(CBORObject.FromObject(3)).AsInt32();
+ return COSEAlgorithmIdentifier.fromId(alg);
+ }
+
+ static String getJavaAlgorithmName(COSEAlgorithmIdentifier alg) {
+ switch (alg) {
+ case EdDSA: return "EDDSA";
+ case ES256: return "SHA256withECDSA";
+ case RS256: return "SHA256withRSA";
+ case RS1: return "SHA1withRSA";
+ default: throw new IllegalArgumentException("Unknown algorithm: " + alg);
}
}
- public static String jwsAlgorithmNameToJavaAlgorithmName(String alg) {
+ static String jwsAlgorithmNameToJavaAlgorithmName(String alg) {
switch (alg) {
case "RS256":
return "SHA256withRSA";
diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorTransport.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorTransport.java
index 4f483cbe2..842cd83c6 100644
--- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorTransport.java
+++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorTransport.java
@@ -25,14 +25,14 @@
package com.yubico.webauthn.data;
-import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.yubico.internal.util.json.JsonStringSerializable;
import com.yubico.internal.util.json.JsonStringSerializer;
-import java.util.Optional;
import java.util.stream.Stream;
+import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.NonNull;
+import lombok.Value;
/**
* Authenticators may communicate with Clients using a variety of transports. This enumeration defines a hint as to how
@@ -52,43 +52,70 @@
* Transport Enumeration (enum AuthenticatorTransport)
*/
@JsonSerialize(using = JsonStringSerializer.class)
-@AllArgsConstructor
-public enum AuthenticatorTransport implements JsonStringSerializable {
+@Value
+@AllArgsConstructor(access = AccessLevel.PRIVATE)
+public class AuthenticatorTransport implements Comparable, JsonStringSerializable {
+
+ @NonNull
+ private final String id;
/**
* Indicates the respective authenticator can be contacted over removable USB.
*/
- USB("usb"),
+ public static final AuthenticatorTransport USB = new AuthenticatorTransport("usb");
/**
* Indicates the respective authenticator can be contacted over Near Field Communication (NFC).
*/
- NFC("nfc"),
+ public static final AuthenticatorTransport NFC = new AuthenticatorTransport("nfc");
/**
* Indicates the respective authenticator can be contacted over Bluetooth Smart (Bluetooth Low Energy / BLE).
*/
- BLE("ble"),
+ public static final AuthenticatorTransport BLE = new AuthenticatorTransport("ble");
/**
* Indicates the respective authenticator is contacted using a client device-specific transport. These
* authenticators are not removable from the client device.
*/
- INTERNAL("internal")
- ;
+ public static final AuthenticatorTransport INTERNAL = new AuthenticatorTransport("internal");
- @NonNull
- private final String id;
+ /**
+ * @return An array containing all predefined values of {@link AuthenticatorTransport} known by this implementation.
+ */
+ public static AuthenticatorTransport[] values() {
+ return new AuthenticatorTransport[]{ USB, NFC, BLE, INTERNAL };
+ }
- private static Optional fromString(@NonNull String id) {
- return Stream.of(values()).filter(v -> v.id.equals(id)).findAny();
+ /**
+ * @return If id
is the same as that of any of {@link #USB}, {@link #NFC}, {@link #BLE} or {@link
+ * #INTERNAL}, returns that constant instance. Otherwise returns a new instance containing id
.
+ * @see #valueOf(String)
+ */
+ public static AuthenticatorTransport of(@NonNull String id) {
+ return Stream.of(values())
+ .filter(v -> v.getId().equals(id))
+ .findAny()
+ .orElseGet(() -> new AuthenticatorTransport(id));
}
- @JsonCreator
- private static AuthenticatorTransport fromJsonString(@NonNull String id) {
- return fromString(id).orElseThrow(() -> new IllegalArgumentException(String.format(
- "Unknown %s value: %s", AuthenticatorTransport.class.getSimpleName(), id
- )));
+ /**
+ * @return If name
equals "USB"
, "NFC"
, "BLE"
or
+ * "INTERNAL"
, returns the constant by that name.
+ * @throws IllegalArgumentException
+ * if name
is anything else.
+ *
+ * @see #of(String)
+ */
+ public static AuthenticatorTransport valueOf(String name) {
+ switch (name) {
+ case "USB": return USB;
+ case "NFC": return NFC;
+ case "BLE": return BLE;
+ case "INTERNAL": return INTERNAL;
+ default:
+ throw new IllegalArgumentException("No enum constant com.yubico.webauthn.data.AuthenticatorTransport." + name);
+ }
}
@Override
@@ -96,4 +123,9 @@ public String toJsonString() {
return id;
}
+ @Override
+ public int compareTo(AuthenticatorTransport other) {
+ return id.compareTo(other.id);
+ }
+
}
diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorTransportTest.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorTransportTest.java
new file mode 100644
index 000000000..754a6be46
--- /dev/null
+++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorTransportTest.java
@@ -0,0 +1,4 @@
+package com.yubico.webauthn.data;
+
+public class AuthenticatorTransportTest {
+}
diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/COSEAlgorithmIdentifier.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/COSEAlgorithmIdentifier.java
index 744da5dc9..10ecc17ab 100644
--- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/COSEAlgorithmIdentifier.java
+++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/COSEAlgorithmIdentifier.java
@@ -43,7 +43,8 @@
public enum COSEAlgorithmIdentifier implements JsonLongSerializable {
EdDSA(-8),
ES256(-7),
- RS256(-257);
+ RS256(-257),
+ RS1(-65535);
@Getter
private final long id;
diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialDescriptor.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialDescriptor.java
index c40a977a9..22acd050a 100644
--- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialDescriptor.java
+++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialDescriptor.java
@@ -27,9 +27,10 @@
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.yubico.internal.util.CollectionUtil;
-import com.yubico.internal.util.EnumUtil;
+import com.yubico.internal.util.ComparableUtil;
import java.util.Optional;
import java.util.Set;
+import java.util.SortedSet;
import java.util.TreeSet;
import lombok.Builder;
import lombok.NonNull;
@@ -65,7 +66,7 @@ public class PublicKeyCredentialDescriptor implements Comparable transports;
+ private final SortedSet transports;
@JsonCreator
private PublicKeyCredentialDescriptor(
@@ -94,7 +95,7 @@ public int compareTo(PublicKeyCredentialDescriptor other) {
} else if (getTransports().isPresent() && !other.getTransports().isPresent()) {
return 1;
} else if (getTransports().isPresent() && other.getTransports().isPresent()) {
- int transportsComparison = EnumUtil.compareSets(getTransports().get(), other.getTransports().get(), AuthenticatorTransport.class);
+ int transportsComparison = ComparableUtil.compareComparableSets(getTransports().get(), other.getTransports().get());
if (transportsComparison != 0) {
return transportsComparison;
}
@@ -136,7 +137,7 @@ public PublicKeyCredentialDescriptorBuilder transports(Set> getTransports() {
+ public Optional> getTransports() {
return Optional.ofNullable(transports);
}
diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialParameters.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialParameters.java
index 6b96cc661..b6b56e62a 100644
--- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialParameters.java
+++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialParameters.java
@@ -73,6 +73,11 @@ private PublicKeyCredentialParameters(
*/
public static final PublicKeyCredentialParameters ES256 = builder().alg(COSEAlgorithmIdentifier.ES256).build();
+ /**
+ * Algorithm {@link COSEAlgorithmIdentifier#RS1} and type {@link PublicKeyCredentialType#PUBLIC_KEY}.
+ */
+ public static final PublicKeyCredentialParameters RS1 = builder().alg(COSEAlgorithmIdentifier.RS1).build();
+
/**
* Algorithm {@link COSEAlgorithmIdentifier#RS256} and type {@link PublicKeyCredentialType#PUBLIC_KEY}.
*/
diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/meta/DocumentStatus.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/meta/DocumentStatus.java
index a326286d8..0ba028704 100644
--- a/webauthn-server-core/src/main/java/com/yubico/webauthn/meta/DocumentStatus.java
+++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/meta/DocumentStatus.java
@@ -27,7 +27,10 @@
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.yubico.internal.util.json.JsonStringSerializable;
import com.yubico.internal.util.json.JsonStringSerializer;
+import java.util.Optional;
+import java.util.stream.Stream;
import lombok.AllArgsConstructor;
+import lombok.NonNull;
/**
* A representation of Web Authentication specification document statuses.
@@ -62,6 +65,10 @@ public enum DocumentStatus implements JsonStringSerializable {
private final String id;
+ static Optional fromString(@NonNull String id) {
+ return Stream.of(values()).filter(v -> v.id.equals(id)).findAny();
+ }
+
/**
* Used by JSON serializer.
*/
diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/meta/Implementation.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/meta/Implementation.java
index 6895733a5..a3b2faa3e 100644
--- a/webauthn-server-core/src/main/java/com/yubico/webauthn/meta/Implementation.java
+++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/meta/Implementation.java
@@ -42,6 +42,7 @@ public class Implementation {
/**
* The version number of this release of the library.
*/
+ @NonNull
private final String version;
/**
@@ -50,8 +51,10 @@ public class Implementation {
@NonNull
private final URL sourceCodeUrl;
- public Optional getVersion() {
- return Optional.ofNullable(version);
- }
+ /**
+ * The commit ID of the source code the library was built from, if known.
+ */
+ @NonNull
+ private final String gitCommit;
}
diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/meta/VersionInfo.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/meta/VersionInfo.java
index 8ca9496fb..d236fec0d 100644
--- a/webauthn-server-core/src/main/java/com/yubico/webauthn/meta/VersionInfo.java
+++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/meta/VersionInfo.java
@@ -29,10 +29,9 @@
import java.net.URL;
import java.time.LocalDate;
import java.util.Enumeration;
+import java.util.NoSuchElementException;
import java.util.Optional;
import java.util.jar.Manifest;
-import lombok.AccessLevel;
-import lombok.AllArgsConstructor;
import lombok.Value;
import lombok.extern.slf4j.Slf4j;
@@ -64,35 +63,35 @@ public static VersionInfo getInstance() {
* Represents the specification this implementation is based on
*/
private final Specification specification = Specification.builder()
- .url(new URL("https://www.w3.org/TR/2019/REC-webauthn-1-20190304/"))
- .latestVersionUrl(new URL("https://www.w3.org/TR/webauthn/"))
- .status(DocumentStatus.RECOMMENDATION)
- .releaseDate(LocalDate.parse("2019-03-04"))
+ .url(new URL(findValueInManifest("Specification-Url")))
+ .latestVersionUrl(new URL(findValueInManifest("Specification-Url-Latest")))
+ .status(DocumentStatus.fromString(findValueInManifest("Specification-W3c-Status")).get())
+ .releaseDate(LocalDate.parse(findValueInManifest("Specification-Release-Date")))
.build();
/**
* Description of this version of this library
*/
private final Implementation implementation = new Implementation(
- findImplementationVersionInManifest().orElse(null),
- new URL("https://github.com/Yubico/java-webauthn-server")
+ findValueInManifest("Implementation-Version"),
+ new URL(findValueInManifest("Implementation-Source-Url")),
+ findValueInManifest("Git-Commit")
);
private VersionInfo() throws IOException {
}
- private Optional findImplementationVersionInManifest() throws IOException {
+ private String findValueInManifest(String key) throws IOException {
final Enumeration resources = getClass().getClassLoader().getResources("META-INF/MANIFEST.MF");
while (resources.hasMoreElements()) {
final URL resource = resources.nextElement();
final Manifest manifest = new Manifest(resource.openStream());
if ("java-webauthn-server".equals(manifest.getMainAttributes().getValue("Implementation-Id"))) {
- return Optional.ofNullable(manifest.getMainAttributes().getValue("Implementation-Version"));
+ return manifest.getMainAttributes().getValue(key);
}
}
-
- return Optional.empty();
+ throw new NoSuchElementException("Could not find \"" + key + "\" in manifest.");
}
}
diff --git a/webauthn-server-core/src/test/java/com/yubico/webauthn/RelyingPartyTest.java b/webauthn-server-core/src/test/java/com/yubico/webauthn/RelyingPartyTest.java
index b4382b4ac..276227ffe 100644
--- a/webauthn-server-core/src/test/java/com/yubico/webauthn/RelyingPartyTest.java
+++ b/webauthn-server-core/src/test/java/com/yubico/webauthn/RelyingPartyTest.java
@@ -3,15 +3,23 @@
import com.yubico.webauthn.attestation.Attestation;
import com.yubico.webauthn.attestation.MetadataService;
import com.yubico.webauthn.data.AttestationConveyancePreference;
+import com.yubico.webauthn.data.ByteArray;
+import com.yubico.webauthn.data.PublicKeyCredentialDescriptor;
+import com.yubico.webauthn.data.RelyingPartyIdentity;
import com.yubico.webauthn.extension.appid.AppId;
import com.yubico.webauthn.extension.appid.InvalidAppIdException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.util.Collections;
+import java.util.HashSet;
import java.util.List;
import java.util.Optional;
+import java.util.Set;
import org.junit.Test;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
public class RelyingPartyTest {
@Test(expected = NullPointerException.class)
@@ -38,4 +46,34 @@ public void itHasTheseBuilderMethods() throws InvalidAppIdException {
;
}
+ @Test
+ public void originsIsImmutable() {
+ Set origins = new HashSet<>();
+
+ RelyingParty rp = RelyingParty.builder()
+ .identity(RelyingPartyIdentity.builder().id("localhost").name("Test").build())
+ .credentialRepository(new CredentialRepository() {
+ @Override public Set getCredentialIdsForUsername(String username) { return null; }
+ @Override public Optional getUserHandleForUsername(String username) { return Optional.empty(); }
+ @Override public Optional getUsernameForUserHandle(ByteArray userHandle) { return Optional.empty(); }
+ @Override public Optional lookup(ByteArray credentialId, ByteArray userHandle) { return Optional.empty(); }
+ @Override public Set lookupAll(ByteArray credentialId) { return null; }
+ })
+ .origins(origins)
+ .build()
+ ;
+
+ assertEquals(0, rp.getOrigins().size());
+
+ origins.add("test");
+ assertEquals(0, rp.getOrigins().size());
+
+ try {
+ rp.getOrigins().add("test");
+ fail("Expected UnsupportedOperationException to be thrown");
+ } catch (UnsupportedOperationException e) {
+ assertEquals(0, rp.getOrigins().size());
+ }
+ }
+
}
diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/OriginMatcherSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/OriginMatcherSpec.scala
new file mode 100644
index 000000000..14aac7245
--- /dev/null
+++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/OriginMatcherSpec.scala
@@ -0,0 +1,246 @@
+// Copyright (c) 2019, 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 java.net.URL
+
+import org.junit.runner.RunWith
+import org.scalacheck.Arbitrary
+import org.scalacheck.Arbitrary.arbitrary
+import org.scalacheck.Gen
+import org.scalatest.FunSpec
+import org.scalatest.Matchers
+import org.scalatest.junit.JUnitRunner
+import org.scalatest.prop.GeneratorDrivenPropertyChecks
+
+import scala.collection.JavaConverters._
+
+@RunWith(classOf[JUnitRunner])
+class OriginMatcherSpec extends FunSpec with Matchers with GeneratorDrivenPropertyChecks {
+
+ private def urlWithMaybePort(protocol: String, host: String, port: Option[Int], file: String): URL =
+ port
+ .map(port => new URL(protocol, host, port, file))
+ .getOrElse(new URL(protocol, host, file))
+
+ private def replacePort(url: URL, port: Int): URL = new URL(url.getProtocol, url.getHost, port, url.getFile)
+
+ private implicit val arbitraryUrl: Arbitrary[URL] = Arbitrary(for {
+ scheme <- Gen.oneOf("http", "https")
+ host <- Gen.alphaNumStr suchThat { _.nonEmpty }
+ port <- Gen.option(Gen.posNum[Int])
+ file = ""
+ } yield urlWithMaybePort(scheme, host, port, file))
+
+ private val urlOrArbitraryString: Gen[String] = Gen.oneOf(
+ arbitrary[URL].map(_.toExternalForm),
+ arbitrary[String]
+ )
+
+ private val urlWithoutPort: Gen[URL] = for {
+ url <- arbitrary[URL]
+ } yield new URL(url.getProtocol, url.getHost, url.getFile)
+
+ private val urlWithPort: Gen[URL] = for {
+ url <- arbitrary[URL]
+ port <- Gen.posNum[Int]
+ } yield replacePort(url, port)
+
+ private val superAndSubdomain: Gen[(URL, URL)] = for {
+ superdomain <- urlWithoutPort
+ subdomainPrefixParts <- Gen.nonEmptyListOf(Gen.alphaNumStr suchThat { _.nonEmpty })
+ subdomainPrefix = subdomainPrefixParts.reduceLeft(_ + "." + _)
+ host = subdomainPrefix + "." + superdomain.getHost
+ subdomain = new URL(superdomain.getProtocol, host, superdomain.getFile)
+ } yield (superdomain, subdomain)
+
+ private val superAndSubdomainWithPorts: Gen[(URL, URL)] = for {
+ (superdomain, subdomain) <- superAndSubdomain
+ superport <- Gen.posNum[Int]
+ subport <- Gen.posNum[Int]
+ } yield (replacePort(superdomain, superport), replacePort(subdomain, subport))
+
+ private def invalidize(url: URL): String = {
+ val port = if (url.getPort == -1) "" else (":" + url.getPort)
+ s"htps:/${url.getHost}${port}/${url.getPath}"
+ }
+
+ describe("The origin matcher") {
+ it("accepts nothing if no allowed origins are given.") {
+ forAll(urlOrArbitraryString, arbitrary[Boolean], arbitrary[Boolean]) { (origin, allowPort, allowSubdomain) =>
+ println(origin)
+ OriginMatcher.isAllowed(
+ origin,
+ Set.empty[String].asJava,
+ allowPort,
+ allowSubdomain
+ ) shouldBe (false)
+ }
+ }
+
+ it("always accepts string equality even for invalid URLs.") {
+ forAll(urlOrArbitraryString, arbitrary[Boolean], arbitrary[Boolean]) { (origin, allowPort, allowSubdomain) =>
+ println(origin)
+ OriginMatcher.isAllowed(
+ origin,
+ Set(origin).asJava,
+ allowPort,
+ allowSubdomain
+ ) shouldBe (true)
+ }
+ }
+
+ it("does not accept superdomains.") {
+ forAll(superAndSubdomain) { case (origin: URL, allowedOrigin: URL) =>
+ println(allowedOrigin, origin)
+ OriginMatcher.isAllowed(
+ origin.toExternalForm,
+ Set(allowedOrigin.toExternalForm).asJava,
+ true,
+ true
+ ) shouldBe (false)
+ }
+ }
+
+ describe("does not accept subdomains") {
+ it("by default.") {
+ forAll(superAndSubdomain, arbitrary[Boolean]) { (origins, allowPort) =>
+ val (allowedOrigin: URL, origin: URL) = origins
+ println(allowedOrigin, origin)
+
+ OriginMatcher.isAllowed(
+ origin.toExternalForm,
+ Set(allowedOrigin.toExternalForm).asJava,
+ allowPort,
+ false
+ ) shouldBe (false)
+ }
+ }
+
+ it("when allowed origin is an invalid URL.") {
+ forAll(superAndSubdomain) { case (allowedOrigin: URL, origin: URL) =>
+ val invalidAllowedOrigin = invalidize(allowedOrigin)
+ println(allowedOrigin, origin, invalidAllowedOrigin)
+
+ OriginMatcher.isAllowed(
+ origin.toExternalForm,
+ Set(invalidAllowedOrigin).asJava,
+ true,
+ true
+ ) shouldBe (false)
+ }
+ }
+
+ it("when client data origin is an invalid URL.") {
+ forAll(superAndSubdomain) { case (allowedOrigin: URL, origin: URL) =>
+ val invalidOrigin = invalidize(origin)
+ println(allowedOrigin, origin, invalidOrigin)
+
+ OriginMatcher.isAllowed(
+ invalidOrigin,
+ Set(allowedOrigin.toExternalForm).asJava,
+ true,
+ true
+ ) shouldBe (false)
+ }
+ }
+
+ it("unless configured to.") {
+ forAll(superAndSubdomain, arbitrary[Boolean]) { (origins, allowPort) =>
+ val (allowedOrigin: URL, origin: URL) = origins
+ println(allowedOrigin, origin)
+
+ OriginMatcher.isAllowed(
+ origin.toExternalForm,
+ Set(allowedOrigin.toExternalForm).asJava,
+ allowPort,
+ true
+ ) shouldBe (true)
+ }
+ }
+ }
+
+ describe("does not accept ports") {
+ it("by default.") {
+ forAll(urlWithoutPort, Gen.posNum[Int], arbitrary[Boolean]) { (allowedOrigin, port, allowSubdomain) =>
+ whenever(port > 0) {
+ val origin = replacePort(allowedOrigin, port)
+ println(allowedOrigin, origin)
+
+ OriginMatcher.isAllowed(
+ origin.toExternalForm,
+ Set(allowedOrigin.toExternalForm).asJava,
+ false,
+ allowSubdomain
+ ) shouldBe (false)
+ }
+ }
+ }
+
+ it("unless the same port is specified in an allowed origin.") {
+ forAll(urlWithPort, arbitrary[Boolean]) { (origin: URL, allowSubdomain: Boolean) =>
+ println(origin)
+
+ OriginMatcher.isAllowed(
+ origin.toExternalForm,
+ Set(origin.toExternalForm).asJava,
+ false,
+ allowSubdomain
+ ) shouldBe (true)
+ }
+ }
+
+ it("unless configured to.") {
+ forAll(arbitrary[URL], Gen.option(Gen.posNum[Int]), arbitrary[Boolean]) { (allowedOrigin, port, allowSubdomain) =>
+ whenever(port.forall(_ > 0)) {
+ val origin = urlWithMaybePort(allowedOrigin.getProtocol, allowedOrigin.getHost, port, allowedOrigin.getFile)
+ println(allowedOrigin, origin)
+
+ OriginMatcher.isAllowed(
+ origin.toExternalForm,
+ Set(allowedOrigin.toExternalForm).asJava,
+ true,
+ allowSubdomain
+ ) shouldBe (true)
+ }
+ }
+ }
+ }
+
+ it("accepts subdomains and arbitrary ports when configured to.") {
+ forAll(superAndSubdomainWithPorts) { case (allowedOrigin, origin) =>
+ println(allowedOrigin, origin)
+
+ OriginMatcher.isAllowed(
+ origin.toExternalForm,
+ Set(allowedOrigin.toExternalForm).asJava,
+ true,
+ true
+ ) shouldBe (true)
+ }
+ }
+ }
+
+}
diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/PackedAttestationStatementVerifierSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/PackedAttestationStatementVerifierSpec.scala
index c2c2e293e..7c0fa2316 100644
--- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/PackedAttestationStatementVerifierSpec.scala
+++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/PackedAttestationStatementVerifierSpec.scala
@@ -26,6 +26,9 @@ package com.yubico.webauthn
import com.yubico.webauthn.data.ByteArray
import com.yubico.webauthn.test.Util
+import com.yubico.webauthn.TestAuthenticator.AttestationCert
+import com.yubico.webauthn.data.COSEAlgorithmIdentifier
+import com.yubico.webauthn.TestAuthenticator.AttestationMaker
import org.junit.runner.RunWith
import org.scalatest.FunSpec
import org.scalatest.Matchers
@@ -59,9 +62,8 @@ class PackedAttestationStatementVerifierSpec extends FunSpec with Matchers {
describe("supports attestation certificates with the algorithm") {
it ("ECDSA.") {
val (cert, key) = TestAuthenticator.generateAttestationCertificate()
- val ((credential, _), _) = TestAuthenticator.createBasicAttestedCredential(
- attestationCertAndKey = Some((cert, key)),
- attestationStatementFormat = "packed"
+ val (credential, _) = TestAuthenticator.createBasicAttestedCredential(
+ attestationMaker = AttestationMaker.packed(new AttestationCert(COSEAlgorithmIdentifier.ES256, (cert, key))),
)
val result = verifier.verifyAttestationSignature(
@@ -75,9 +77,8 @@ class PackedAttestationStatementVerifierSpec extends FunSpec with Matchers {
it ("RSA.") {
val (cert, key) = TestAuthenticator.generateRsaCertificate()
- val ((credential, _), _) = TestAuthenticator.createBasicAttestedCredential(
- attestationCertAndKey = Some((cert, key)),
- attestationStatementFormat = "packed"
+ val (credential, _) = TestAuthenticator.createBasicAttestedCredential(
+ attestationMaker = AttestationMaker.packed(new AttestationCert(COSEAlgorithmIdentifier.RS256, (cert, key))),
)
val result = verifier.verifyAttestationSignature(
diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RegistrationTestData.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RegistrationTestData.scala
index 2d73c48bd..851460340 100644
--- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RegistrationTestData.scala
+++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RegistrationTestData.scala
@@ -48,28 +48,37 @@ import com.yubico.webauthn.data.PublicKeyCredentialParameters
import com.yubico.webauthn.data.RegistrationExtensionInputs
import com.yubico.webauthn.data.RelyingPartyIdentity
import com.yubico.webauthn.data.UserIdentity
+import com.yubico.webauthn.TestAuthenticator.AttestationCert
+import com.yubico.webauthn.TestAuthenticator.AttestationMaker
+import com.yubico.webauthn.TestAuthenticator.AttestationSigner
+import com.yubico.webauthn.data.AuthenticatorAssertionResponse
+import com.yubico.webauthn.data.ClientAssertionExtensionOutputs
import org.bouncycastle.asn1.x500.X500Name
import scala.collection.JavaConverters._
+import scala.util.Failure
+import scala.util.Success
+import scala.util.Try
object RegistrationTestDataGenerator extends App {
regenerateTestData()
def printTestDataCode(
- credential: PublicKeyCredential[AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs],
- keypair: KeyPair,
- caCert: Option[X509Certificate]
+ testData: RegistrationTestData,
): Unit = {
- for { caCert <- caCert } {
- println(s"""attestationCaCert = Some(CertificateParser.parseDer(BinaryUtil.fromHex("${BinaryUtil.toHex(caCert.getEncoded)}"))),""")
- }
- println(s"""attestationObject = new ByteArray(BinaryUtil.fromHex("${credential.getResponse.getAttestationObject.getHex}")),
- |clientDataJson = \"\"\"${new String(credential.getResponse.getClientDataJSON.getBytes, "UTF-8")}\"\"\",
- |privateKey = Some(\"\"\"${new ByteArray(keypair.getPrivate.getEncoded).getHex}\"\"\"),
- |
- |
+ println(s"""attestationObject = ByteArray.fromHex("${testData.attestationObject.getHex}"),
+ |clientDataJson = \"\"\"${testData.clientDataJson}\"\"\",
+ |privateKey = Some(ByteArray.fromHex("${testData.privateKey.get.getHex}")),
""".stripMargin)
+
+ testData.assertion foreach { assertion =>
+ println(s"""|assertion = Some(AssertionTestData(
+ | request = JacksonCodecs.json().readValue(\"\"\"${JacksonCodecs.json().writeValueAsString(assertion.request)}\"\"\", classOf[AssertionRequest]),
+ | response = PublicKeyCredential.parseAssertionResponseJson(\"\"\"${JacksonCodecs.json().writeValueAsString(assertion.response)}\"\"\")
+ |)),
+ """.stripMargin)
+ }
}
def regenerateTestData(): Unit = {
@@ -84,14 +93,20 @@ object RegistrationTestDataGenerator extends App {
td.Packed.BasicAttestation,
td.Packed.BasicAttestationEdDsa,
td.Packed.BasicAttestationRsa,
+ td.Packed.BasicAttestationRs1,
td.Packed.BasicAttestationWithoutAaguidExtension,
td.Packed.BasicAttestationWithWrongAaguidExtension,
td.Packed.SelfAttestation,
- td.Packed.SelfAttestationWithWrongAlgValue
+ td.Packed.SelfAttestationRs1,
).zipWithIndex } {
- val ((cred, keypair), cert) = testData.regenerate()
- println(i)
- printTestDataCode(cred, keypair, cert)
+ testData.regenerateFull() match {
+ case Success(newTestData) =>
+ println(i)
+ printTestDataCode(newTestData)
+ case Failure(e) =>
+ println("Failed to regenerate")
+ e.printStackTrace()
+ }
}
}
}
@@ -99,7 +114,7 @@ object RegistrationTestDataGenerator extends App {
object RegistrationTestData {
private def jsonFactory: JsonNodeFactory = JsonNodeFactory.instance
- def validExamples = List(
+ def defaultSettingsValidExamples = List(
AndroidSafetynet.RealExample,
AndroidSafetynet.BasicAttestation,
FidoU2f.BasicAttestation,
@@ -109,6 +124,7 @@ object RegistrationTestData {
Packed.BasicAttestationEdDsa,
Packed.BasicAttestationRsa,
Packed.BasicAttestationRsaReal,
+ Packed.SelfAttestation,
)
object AndroidKey {
@@ -116,45 +132,47 @@ object RegistrationTestData {
}
object AndroidSafetynet {
val RealExample: RegistrationTestData = new RegistrationTestData(
- attestationCaCert = None,
+ alg = COSEAlgorithmIdentifier.ES256,
attestationObject = ByteArray.fromBase64Url("o2NmbXRxYW5kcm9pZC1zYWZldHluZXRnYXR0U3RtdKJjdmVyaDE0Nzk5MDIxaHJlc3BvbnNlWRS9ZXlKaGJHY2lPaUpTVXpJMU5pSXNJbmcxWXlJNld5Sk5TVWxHYTJwRFEwSkljV2RCZDBsQ1FXZEpVVkpZY205T01GcFBaRkpyUWtGQlFVRkJRVkIxYm5wQlRrSm5hM0ZvYTJsSE9YY3dRa0ZSYzBaQlJFSkRUVkZ6ZDBOUldVUldVVkZIUlhkS1ZsVjZSV1ZOUW5kSFFURlZSVU5vVFZaU01qbDJXako0YkVsR1VubGtXRTR3U1VaT2JHTnVXbkJaTWxaNlRWSk5kMFZSV1VSV1VWRkVSWGR3U0ZaR1RXZFJNRVZuVFZVNGVFMUNORmhFVkVVMFRWUkJlRTFFUVROTlZHc3dUbFp2V0VSVVJUVk5WRUYzVDFSQk0wMVVhekJPVm05M1lrUkZURTFCYTBkQk1WVkZRbWhOUTFaV1RYaEZla0ZTUW1kT1ZrSkJaMVJEYTA1b1lrZHNiV0l6U25WaFYwVjRSbXBCVlVKblRsWkNRV05VUkZVeGRtUlhOVEJaVjJ4MVNVWmFjRnBZWTNoRmVrRlNRbWRPVmtKQmIxUkRhMlIyWWpKa2MxcFRRazFVUlUxNFIzcEJXa0puVGxaQ1FVMVVSVzFHTUdSSFZucGtRelZvWW0xU2VXSXliR3RNYlU1MllsUkRRMEZUU1hkRVVWbEtTMjlhU1doMlkwNUJVVVZDUWxGQlJHZG5SVkJCUkVORFFWRnZRMmRuUlVKQlRtcFlhM293WlVzeFUwVTBiU3N2UnpWM1QyOHJXRWRUUlVOeWNXUnVPRGh6UTNCU04yWnpNVFJtU3pCU2FETmFRMWxhVEVaSWNVSnJOa0Z0V2xaM01rczVSa2N3VHpseVVsQmxVVVJKVmxKNVJUTXdVWFZ1VXpsMVowaEROR1ZuT1c5MmRrOXRLMUZrV2pKd09UTllhSHAxYmxGRmFGVlhXRU40UVVSSlJVZEtTek5UTW1GQlpucGxPVGxRVEZNeU9XaE1ZMUYxV1ZoSVJHRkROMDlhY1U1dWIzTnBUMGRwWm5NNGRqRnFhVFpJTDNob2JIUkRXbVV5YkVvck4wZDFkSHBsZUV0d2VIWndSUzkwV2xObVlsazVNRFZ4VTJ4Q2FEbG1jR293TVRWamFtNVJSbXRWYzBGVmQyMUxWa0ZWZFdWVmVqUjBTMk5HU3pSd1pYWk9UR0Y0UlVGc0swOXJhV3hOZEVsWlJHRmpSRFZ1Wld3MGVFcHBlWE0wTVROb1lXZHhWekJYYUdnMVJsQXpPV2hIYXpsRkwwSjNVVlJxWVhwVGVFZGtkbGd3YlRaNFJsbG9hQzh5VmsxNVdtcFVORXQ2VUVwRlEwRjNSVUZCWVU5RFFXeG5kMmRuU2xWTlFUUkhRVEZWWkVSM1JVSXZkMUZGUVhkSlJtOUVRVlJDWjA1V1NGTlZSVVJFUVV0Q1oyZHlRbWRGUmtKUlkwUkJWRUZOUW1kT1ZraFNUVUpCWmpoRlFXcEJRVTFDTUVkQk1WVmtSR2RSVjBKQ1VYRkNVWGRIVjI5S1FtRXhiMVJMY1hWd2J6UlhObmhVTm1veVJFRm1RbWRPVmtoVFRVVkhSRUZYWjBKVFdUQm1hSFZGVDNaUWJTdDRaMjU0YVZGSE5rUnlabEZ1T1V0NlFtdENaMmR5UW1kRlJrSlJZMEpCVVZKWlRVWlpkMHAzV1VsTGQxbENRbEZWU0UxQlIwZEhNbWd3WkVoQk5reDVPWFpaTTA1M1RHNUNjbUZUTlc1aU1qbHVUREprTUdONlJuWk5WRUZ5UW1kbmNrSm5SVVpDVVdOM1FXOVpabUZJVWpCalJHOTJURE5DY21GVE5XNWlNamx1VERKa2VtTnFTWFpTTVZKVVRWVTRlRXh0VG5sa1JFRmtRbWRPVmtoU1JVVkdha0ZWWjJoS2FHUklVbXhqTTFGMVdWYzFhMk50T1hCYVF6VnFZakl3ZDBsUldVUldVakJuUWtKdmQwZEVRVWxDWjFwdVoxRjNRa0ZuU1hkRVFWbExTM2RaUWtKQlNGZGxVVWxHUVhwQmRrSm5UbFpJVWpoRlMwUkJiVTFEVTJkSmNVRm5hR2cxYjJSSVVuZFBhVGgyV1ROS2MweHVRbkpoVXpWdVlqSTVia3d3WkZWVmVrWlFUVk0xYW1OdGQzZG5aMFZGUW1kdmNrSm5SVVZCWkZvMVFXZFJRMEpKU0RGQ1NVaDVRVkJCUVdSM1EydDFVVzFSZEVKb1dVWkpaVGRGTmt4TldqTkJTMUJFVjFsQ1VHdGlNemRxYW1RNE1FOTVRVE5qUlVGQlFVRlhXbVJFTTFCTVFVRkJSVUYzUWtsTlJWbERTVkZEVTFwRFYyVk1Tblp6YVZaWE5rTm5LMmRxTHpsM1dWUktVbnAxTkVocGNXVTBaVmswWXk5dGVYcHFaMGxvUVV4VFlta3ZWR2g2WTNweGRHbHFNMlJyTTNaaVRHTkpWek5NYkRKQ01HODNOVWRSWkdoTmFXZGlRbWRCU0ZWQlZtaFJSMjFwTDFoM2RYcFVPV1ZIT1ZKTVNTdDRNRm95ZFdKNVdrVldla0UzTlZOWlZtUmhTakJPTUVGQlFVWnRXRkU1ZWpWQlFVRkNRVTFCVW1wQ1JVRnBRbU5EZDBFNWFqZE9WRWRZVURJM09IbzBhSEl2ZFVOSWFVRkdUSGx2UTNFeVN6QXJlVXhTZDBwVlltZEpaMlk0WjBocWRuQjNNbTFDTVVWVGFuRXlUMll6UVRCQlJVRjNRMnR1UTJGRlMwWlZlVm8zWmk5UmRFbDNSRkZaU2t0dldrbG9kbU5PUVZGRlRFSlJRVVJuWjBWQ1FVazVibFJtVWt0SlYyZDBiRmRzTTNkQ1REVTFSVlJXTm10aGVuTndhRmN4ZVVGak5VUjFiVFpZVHpReGExcDZkMG8yTVhkS2JXUlNVbFF2VlhORFNYa3hTMFYwTW1Nd1JXcG5iRzVLUTBZeVpXRjNZMFZYYkV4UldUSllVRXg1Um1wclYxRk9ZbE5vUWpGcE5GY3lUbEpIZWxCb2RETnRNV0kwT1doaWMzUjFXRTAyZEZnMVEzbEZTRzVVYURoQ2IyMDBMMWRzUm1sb2VtaG5iamd4Ukd4a2IyZDZMMHN5VlhkTk5sTTJRMEl2VTBWNGEybFdabllyZW1KS01ISnFkbWM1TkVGc1pHcFZabFYzYTBrNVZrNU5ha1ZRTldVNGVXUkNNMjlNYkRabmJIQkRaVVkxWkdkbVUxZzBWVGw0TXpWdmFpOUpTV1F6VlVVdlpGQndZaTl4WjBkMmMydG1aR1Y2ZEcxVmRHVXZTMU50Y21sM1kyZFZWMWRsV0daVVlra3plbk5wYTNkYVltdHdiVkpaUzIxcVVHMW9kalJ5YkdsNlIwTkhkRGhRYmpod2NUaE5Na3RFWmk5UU0ydFdiM1F6WlRFNFVUMGlMQ0pOU1VsRlUycERRMEY2UzJkQmQwbENRV2RKVGtGbFR6QnRjVWRPYVhGdFFrcFhiRkYxUkVGT1FtZHJjV2hyYVVjNWR6QkNRVkZ6UmtGRVFrMU5VMEYzU0dkWlJGWlJVVXhGZUdSSVlrYzVhVmxYZUZSaFYyUjFTVVpLZG1JelVXZFJNRVZuVEZOQ1UwMXFSVlJOUWtWSFFURlZSVU5vVFV0U01uaDJXVzFHYzFVeWJHNWlha1ZVVFVKRlIwRXhWVVZCZUUxTFVqSjRkbGx0Um5OVk1teHVZbXBCWlVaM01IaE9la0V5VFZSVmQwMUVRWGRPUkVwaFJuY3dlVTFVUlhsTlZGVjNUVVJCZDA1RVNtRk5SVWw0UTNwQlNrSm5UbFpDUVZsVVFXeFdWRTFTTkhkSVFWbEVWbEZSUzBWNFZraGlNamx1WWtkVloxWklTakZqTTFGblZUSldlV1J0YkdwYVdFMTRSWHBCVWtKblRsWkNRVTFVUTJ0a1ZWVjVRa1JSVTBGNFZIcEZkMmRuUldsTlFUQkhRMU54UjFOSllqTkVVVVZDUVZGVlFVRTBTVUpFZDBGM1oyZEZTMEZ2U1VKQlVVUlJSMDA1UmpGSmRrNHdOWHByVVU4NUszUk9NWEJKVW5aS2VucDVUMVJJVnpWRWVrVmFhRVF5WlZCRGJuWlZRVEJSYXpJNFJtZEpRMlpMY1VNNVJXdHpRelJVTW1aWFFsbHJMMnBEWmtNelVqTldXazFrVXk5a1RqUmFTME5GVUZwU2NrRjZSSE5wUzFWRWVsSnliVUpDU2pWM2RXUm5lbTVrU1UxWlkweGxMMUpIUjBac05YbFBSRWxMWjJwRmRpOVRTa2d2VlV3clpFVmhiSFJPTVRGQ2JYTkxLMlZSYlUxR0t5dEJZM2hIVG1oeU5UbHhUUzg1YVd3M01Va3laRTQ0UmtkbVkyUmtkM1ZoWldvMFlsaG9jREJNWTFGQ1ltcDRUV05KTjBwUU1HRk5NMVEwU1N0RWMyRjRiVXRHYzJKcWVtRlVUa001ZFhwd1JteG5UMGxuTjNKU01qVjRiM2x1VlhoMk9IWk9iV3R4TjNwa1VFZElXR3Q0VjFrM2IwYzVhaXRLYTFKNVFrRkNhemRZY2twbWIzVmpRbHBGY1VaS1NsTlFhemRZUVRCTVMxY3dXVE42Tlc5Nk1rUXdZekYwU2t0M1NFRm5UVUpCUVVkcVoyZEZlazFKU1VKTWVrRlBRbWRPVmtoUk9FSkJaamhGUWtGTlEwRlpXWGRJVVZsRVZsSXdiRUpDV1hkR1FWbEpTM2RaUWtKUlZVaEJkMFZIUTBOelIwRlJWVVpDZDAxRFRVSkpSMEV4VldSRmQwVkNMM2RSU1UxQldVSkJaamhEUVZGQmQwaFJXVVJXVWpCUFFrSlpSVVpLYWxJclJ6UlJOamdyWWpkSFEyWkhTa0ZpYjA5ME9VTm1NSEpOUWpoSFFURlZaRWwzVVZsTlFtRkJSa3AyYVVJeFpHNUlRamRCWVdkaVpWZGlVMkZNWkM5alIxbFpkVTFFVlVkRFEzTkhRVkZWUmtKM1JVSkNRMnQzU25wQmJFSm5aM0pDWjBWR1FsRmpkMEZaV1ZwaFNGSXdZMFJ2ZGt3eU9XcGpNMEYxWTBkMGNFeHRaSFppTW1OMldqTk9lVTFxUVhsQ1owNVdTRkk0UlV0NlFYQk5RMlZuU21GQmFtaHBSbTlrU0ZKM1QyazRkbGt6U25OTWJrSnlZVk0xYm1JeU9XNU1NbVI2WTJwSmRsb3pUbmxOYVRWcVkyMTNkMUIzV1VSV1VqQm5Ra1JuZDA1cVFUQkNaMXB1WjFGM1FrRm5TWGRMYWtGdlFtZG5ja0puUlVaQ1VXTkRRVkpaWTJGSVVqQmpTRTAyVEhrNWQyRXlhM1ZhTWpsMlduazVlVnBZUW5aak1td3dZak5LTlV4NlFVNUNaMnR4YUd0cFJ6bDNNRUpCVVhOR1FVRlBRMEZSUlVGSGIwRXJUbTV1TnpoNU5uQlNhbVE1V0d4UlYwNWhOMGhVWjJsYUwzSXpVazVIYTIxVmJWbElVRkZ4TmxOamRHazVVRVZoYW5aM1VsUXlhVmRVU0ZGeU1ESm1aWE54VDNGQ1dUSkZWRlYzWjFwUksyeHNkRzlPUm5ab2MwODVkSFpDUTA5SllYcHdjM2RYUXpsaFNqbDRhblUwZEZkRVVVZzRUbFpWTmxsYVdpOVlkR1ZFVTBkVk9WbDZTbkZRYWxrNGNUTk5SSGh5ZW0xeFpYQkNRMlkxYnpodGR5OTNTalJoTWtjMmVIcFZjalpHWWpaVU9FMWpSRTh5TWxCTVVrdzJkVE5OTkZSNmN6TkJNazB4YWpaaWVXdEtXV2s0ZDFkSlVtUkJka3RNVjFwMUwyRjRRbFppZWxsdGNXMTNhMjAxZWt4VFJGYzFia2xCU21KRlRFTlJRMXAzVFVnMU5uUXlSSFp4YjJaNGN6WkNRbU5EUmtsYVZWTndlSFUyZURaMFpEQldOMU4yU2tORGIzTnBjbE50U1dGMGFpODVaRk5UVmtSUmFXSmxkRGh4THpkVlN6UjJORnBWVGpnd1lYUnVXbm94ZVdjOVBTSmRmUS5leUp1YjI1alpTSTZJa3RDT1hwd01tTlJXRlpLVUdkbWIzWkpNREp6UVRkbk1rMVFMMWh2UzFaQlpHZzRNMjA1U2podGFWRTlJaXdpZEdsdFpYTjBZVzF3VFhNaU9qRTFORGMxTmpFNE1qSXhNakVzSW1Gd2ExQmhZMnRoWjJWT1lXMWxJam9pWTI5dExtZHZiMmRzWlM1aGJtUnliMmxrTG1kdGN5SXNJbUZ3YTBScFoyVnpkRk5vWVRJMU5pSTZJa3B4UzBNNVoweGlWMHRNZFM4eU1ubElLMDl0VUdSc1RuQXljemhOVlZGa1VUWXhXVUp5ZGpoQ2QyODlJaXdpWTNSelVISnZabWxzWlUxaGRHTm9JanAwY25WbExDSmhjR3REWlhKMGFXWnBZMkYwWlVScFoyVnpkRk5vWVRJMU5pSTZXeUk0VURGelZ6QkZVRXBqYzJ4M04xVjZVbk5wV0V3Mk5IY3JUelV3UldRclVrSkpRM1JoZVRGbk1qUk5QU0pkTENKaVlYTnBZMGx1ZEdWbmNtbDBlU0k2ZEhKMVpYMC5TVEp1N0xnR29VWHQzZmtveXZMZEIyd0VHZ3VHOUc0QnpEUWxFYkQ3a0dSVWdmZDd2ZHUxUGwtbWNlUUd6cnJWUEY5eHRERXFHWnR6YkpILWN1amVVN0RCbTF4eDRzNlBIenZra0xibjg0b1ROUFJNcmhFalpER0tFUHdVZzVXb0M0UUlPSERMN2xpbUN4alhxYVl5eTh0Tk4ybW1yWlBPV3oxRVJGOG1XSG1tU0VrSWNJeVpyTDhuVS1jMkhVM2pyYkF3SDFoWFoxZU1yMG9kUTllbWM2TU8tWUFhSjZ6X2g1a29MSnhGVkxnZTh3dmp6UkE3R0hZdXlzQ3FPTXdCb3FnbVBlVzUxODBLV0VtTXdJUVljd1VXanZDbTRILUlWWUl6RElGcHdMSUFaaFQxd1NHbUoyeDBaQ3lpMlF4SmhRR0RjR1JuZjZyTlllc3FnSXpqV0FoYXV0aERhdGFYxcRs74KtG1Rkd1kdAIsIdZ7D5tLstPOUdL_qaWmSXQO3RQAAAAC5P9lh8uZGL7EiggAiR954AEEBEAglXhhzprEVlCnJSjQ0f59qby7VJKFLROYDyglb0hOGk9VmPeojeh8mwnsf3exgIBoVovCmIaGggiF3YPIV8KUBAgMmIAEhWCDbwAl__SPL0bDsj9WldwIqhh0thFFVRWt0HHm8MT5AVyJYIL1z6R8jvPutwAinX77M3ahwoNxFWPvR15vuhv1af8c6"),
clientDataJson = """{"type":"webauthn.create","challenge":"jQsu5pUma1KIlCaAUfptSrCZv7a8Qpcxs-N52OuO5ms","origin":"https:\/\/demo.yubico.com","androidPackageName":"com.android.chrome"}""",
rpId = RelyingPartyIdentity.builder().id("demo.yubico.com").name("").build()
)
val BasicAttestation: RegistrationTestData = new RegistrationTestData(
- attestationCaCert = Some(CertificateParser.parseDer(BinaryUtil.fromHex("308201d83082017da00302010202020539300a06082a8648ce3d040302306a3126302406035504030c1d59756269636f20576562417574686e20756e6974207465737473204341310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b3009060355040613025345301e170d3138303930363137343230305a170d3138303930363137343230305a306a3126302406035504030c1d59756269636f20576562417574686e20756e6974207465737473204341310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b30090603550406130253453059301306072a8648ce3d020106082a8648ce3d03010703420004694dba54b48f8666c2da40e2498d21ddf427df9c2028e298f04fd3bba0cbef9d94d96e39c1cccf911a6c8e534fe6e62fe8e3ffa8ba51018cce79b5b9f3e04e5ca3133011300f0603551d130101ff040530030101ff300a06082a8648ce3d0403020349003046022100ab460eff9d6ed30059f1e771a4270a1f70df0396b40141f33598fd528852dbbd022100fdbd9447115d838cfb9ca2981f048c69b84a1d7ae636605559a492bbe83fbccc"))),
+ alg = COSEAlgorithmIdentifier.ES256,
attestationObject = new ByteArray(BinaryUtil.fromHex("bf68617574684461746158a449960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97634100000539000102030405060708090a0b0c0d0e0f0020d787c0d88c8b258e0e147af3f4442103fc7d41718ea92dd2b1be925c6a06113aa52258200b159bf750abbfa9bf8d6a4a806eff06533e9ac9f3576113f57ce6919306a51b03260102215820b9e1f17d71fc5cfbc2e19528b2fa6c9dd9e9a8bd35e692f5e12d71233e91950f200163666d7471616e64726f69642d7361666574796e65746761747453746d74bf6376657268313437393930323168726573706f6e73655907e665794a68624763694f694a53557a49314e694973496e67315979493657794a4e53556c4463565244513046724b32644264306c435157644a51304a55613364445a316c4a53323961535870714d45564264306c335957704662553144555564424d565646515864335a466459566d6c685630353253555a6b62466c72526a466b5232683153556857645746595557646b52315a365a45684e5a31457752586845656b464f516d644f566b4a42623031436247777857573173616d4a3652576c4e513046485154465652554e33643170525746597759556457645752486247705a57464a3259326c43516d5249556d786a4d314a6f5a456473646d4a715255784e515774485154465652554a6f54554e564d4656335347686a546b31555a336450564545795456526a4d4531715158645861474e4f5456526e6430395551544a4e56474d775457704264316471516d5a4e556e4e335231465a52465a5255555245516b706f5a45685362474d7a5558565a567a567259323035634670444e5770694d6a423452487042546b4a6e546c5a435157394e516d78734d566c7462477069656b567054554e42523045785655564464336461555668574d474648566e566b5232787157566853646d4e70516b4a6b53464a73597a4e536147524862485a69616b564d54554672523045785655564361453144565442566432646e52576c4e5154424851314e7852314e4a596a4e4555555643515646565155453053554a45643046335a3264465330467653554a425555524f6332307761544248624535514d575a4455474e4757444e3361316444533370315a466f3051324a5051304a544d6d394b4c7a4a335a6e5a7152555a324f456c4f5630703364325a73554539794b3063784e4552744f466734517a4d7256574e5251565a5763305249625535435a32314b513346475257307654473936633278685a5574335333465052326c3461484e464f53744e53477830616c7079634564614f467069596b527a4f554e705132357a646e6b7754564a4b645464495a4456784e585a7357553947596a4d315545563055585977516b5972557a426b4c3146685344647857476b30646a645a616b6f7a4d485672515752544d306f795747593262544e786153395563537455656c52716245637a53565277545539426556597653304a6b64574934613170774f456f775556497a556a6868614374615a554a7064564630513146335155746b636d314e63456c43555870496455356f613231614c324e736554597956554a31533264714c33646e4d55773156554533527a5a4f645734785a4642326157354d56557868526b6f3251334633513231576248426a53314635636b4a59596d70476445527962475a5256335a4f536c526c5633647a564756706345466e54554a4251556471536c5242616b31445255644465584e4851564652516d643156574e4255555646516b4a4a5255564251554a425a303146516c465a53454e42613074446433644f5247633464304e6e57556c4c6231704a656d6f77525546335355525451554633556c464a5a314e74555456504b7a6c3152576c784d544e70546d6b78566e5a4a645538335430357564546c46516b597a534549794f45687562445661554774445356464563474e68626d3958526b4a784d3074684d7a6c71536e68554e32784457473153616b4e6c54307868567a4a715a6d39715547316a63575a435a7a3039496c31392e65794a68634778445a584a3061575a70593246305a5552705a32567a64464e6f595449314e69493657794a4d5132457759544a714c3368764c7a56744d46553453465243516b3543546b4e4d57454a725a7a63725a79745a63475670523070744e54593050534a644c434a756232356a5a534936496c427165585a6b524539304e554a365a56523561474643646b307a554552705a303158636b6732515652736256464c636c6730566d68314e47733949697769595842725547466a6132466e5a553568625755694f694a6a623230756558566961574e764c6e646c596d4631644768754c6e526c633351694c434a6959584e7059306c756447566e636d6c306553493664484a315a53776964476c745a584e305957317754584d694f6a45314e546b314e6a6b794d7a67314f446773496d4677613052705a32567a64464e6f595449314e694936496b7844595442684d6d6f76654738764e5730775654684956454a43546b4a4f51307859516d746e4e79746e4b316c775a576c48536d30314e6a5139496977695933527a55484a765a6d6c735a55316864474e6f496a7030636e566c66512e76546e4f4f6151514b6e7959356747394d58636c63455237513145347275705a76415f6d77494a4e47745f2d5039436e6d64736366597a7a512d67714b3668537467714c326f6453485f6b6d473441464a4d367a304843712d324d41756262515f38435346507855615f6a5674397334552d7446415a564b64424b796e495f4b3863456b66683364684a664138774c70363268485f6f6b5a63744c6b5f437549787446646f4b587854793675706f4a4e496a4342614a4a314855304b4c424c78704e4543494346363876375368413855466e6e6863726a47456575794d6d634e5f6179535570306a3858536d5651496d7a754c714c4763476c545f647143486b764a673773303850616c53534131726e30634f505a526b41656c37706e65627746623854497373444852475a696639493255787530474774644b31364164797059555242633278642d4769302d30566c616631587577ffff")),
clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","type":"webauthn.create","tokenBinding":{"status":"supported"},"clientExtensions":{}}"""
- ) { override def regenerate() = TestAuthenticator.createBasicAttestedCredential(attestationStatementFormat = "android-safetynet", generateRsaCert = true, certSubject = Some(new X500Name("CN=attest.android.com, O=Yubico, OU=Authenticator Attestation, C=SE"))) }
+ ) { override def regenerate() = TestAuthenticator.createBasicAttestedCredential(attestationMaker = AttestationMaker.androidSafetynet(AttestationSigner.ca(alg = COSEAlgorithmIdentifier.RS256, certSubject = new X500Name("CN=attest.android.com, O=Yubico, OU=Authenticator Attestation, C=SE")))) }
val WrongHostname: RegistrationTestData = new RegistrationTestData(
- attestationCaCert = Some(CertificateParser.parseDer(BinaryUtil.fromHex("308201d83082017da00302010202020539300a06082a8648ce3d040302306a3126302406035504030c1d59756269636f20576562417574686e20756e6974207465737473204341310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b3009060355040613025345301e170d3138303930363137343230305a170d3138303930363137343230305a306a3126302406035504030c1d59756269636f20576562417574686e20756e6974207465737473204341310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b30090603550406130253453059301306072a8648ce3d020106082a8648ce3d030107034200046eacc56c3e0fc9efe8ecd4f993a850aeb3b560e04950acc409a50eb687abf5eff56432e8469640a0d4ffe077dff455945c01357af23044b85d75986cb0dd92e5a3133011300f0603551d130101ff040530030101ff300a06082a8648ce3d040302034900304602210086a8309e4990b850d1850d66b1b2ef9572e59e554bf00e3bd3d0106132c9bf89022100ec189d08af6125b8cf7705123b6af182be1a9862d9f3a53bf4c097f04ae7cf1e"))),
+ alg = COSEAlgorithmIdentifier.ES256,
attestationObject = new ByteArray(BinaryUtil.fromHex("bf68617574684461746158a449960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97634100000539000102030405060708090a0b0c0d0e0f0020bdbf5179112add51874c0824f0d860083d1ee7cb7f44c25ffa26c39271c3ed0aa5225820c35b4c850d7c13334432f7f17a5f1f9a5b1c85bff9b407dc0c962f0eb95eb9d10326010221582041d565e8ca57023f4510f150481d551f84f2dd09894fbc20d1c4707896943638200163666d7471616e64726f69642d7361666574796e65746761747453746d74bf6376657268313437393930323168726573706f6e73655907f165794a68624763694f694a53557a49314e694973496e67315979493657794a4e53556c4463315244513046735a57644264306c435157644a51304a55613364445a316c4a53323961535870714d45564264306c335957704662553144555564424d565646515864335a466459566d6c685630353253555a6b62466c72526a466b5232683153556857645746595557646b52315a365a45684e5a31457752586845656b464f516d644f566b4a42623031436247777857573173616d4a3652576c4e513046485154465652554e33643170525746597759556457645752486247705a57464a3259326c43516d5249556d786a4d314a6f5a456473646d4a715255784e515774485154465652554a6f54554e564d4656335347686a546b31555a336450564545795456526a4d4531715158645861474e4f5456526e6430395551544a4e56474d775457704264316471516d354e553031335356465a52465a5255555245516e42615a46644b63466b794f4764574d6c5a70555668574d4746484e47646b567a56775a454e434d467059546a426a656b56515455457752304578565556445a3364485631685761574658546e5a4e55306c335355465a52465a5255557845516d78435a466853623170584e5442685630356f5a45633565556c46526a426b52315a365a4564474d4746584f58564e55584e335131465a52465a525555644664307055556c5244513046545358644555566c4b53323961535768325930354255555643516c46425247646e5256424252454e44515646765132646e52554a4254573432535539336255786d5356427253546c5256315236546e6c5a4e304d3464444e6956444a5254484179654374324b3342325a6c70424d47686b5a31457852586875656d78574e57314f4e334a744d4735354b3046695a334646546d397265545533646a45764f57644359334d314c7a464e4c315a36555464735743746c4e325275516a4233516c42755355524b5a4374304d3064345a6d597659544645596b4977555535364d567033516a4d325545744f4e5856594e3274364b314d35516b5668616b644f4e47786a5a466c425232644654454a33513168745a55465a62316c47567974475a4763345a473034626d6f76575778314e3168426248464d4c7a423661577053576b6c535530704c4e46705157454e6f626a42314e3246464d553578567a52696332566a536b6c70656e5a34534868546547567257544134656b4673656c4579636a5235626e70754e3235334f476f30566a6c3357575254526d347a5630396d616c5654565578326344453061546c324f4549324e6d677264565a6f4e4578504d3231365a586c5652445a444e587035516e5a5a5757706a6256565a516d383162585268556a527164334250517a6c4963585a4b536a5a4c4e46453364336c4651304633525546425955317354554e4e64306c525755784c64316c43516b4648517a565364304a4255564646525764525555464252554e4264314647516d646a53554e5262307845515442505248704253304a6e5a33466f61327050554646525245466e546b6c4252454a4751576c46515445354e4442744d6e426865485a615645355251584e4953544678516d68756255737253335a564e7939704d6b52714b306c50623239775956564453555a6c4d453945515739584f4735484f544a74654531694e6d314a4e324e354c304d355a6b4535565445335755687161334673656b6b7a623273695858302e65794a68634778445a584a3061575a70593246305a5552705a32567a64464e6f595449314e69493657794a4d5132457759544a714c3368764c7a56744d46553453465243516b3543546b4e4d57454a725a7a63725a79745a63475670523070744e54593050534a644c434a756232356a5a534936496b4e4c544856715958413157444a475346566e61537444636b353153473072556c4a7456484a4b566c5642656b46504f5667305569746c557a513949697769595842725547466a6132466e5a553568625755694f694a6a623230756558566961574e764c6e646c596d4631644768754c6e526c633351694c434a6959584e7059306c756447566e636d6c306553493664484a315a53776964476c745a584e305957317754584d694f6a45314e546b314e6a6b794d7a67334f545173496d4677613052705a32567a64464e6f595449314e694936496b7844595442684d6d6f76654738764e5730775654684956454a43546b4a4f51307859516d746e4e79746e4b316c775a576c48536d30314e6a5139496977695933527a55484a765a6d6c735a55316864474e6f496a7030636e566c66512e54672d627854616c77687a78794c52415837576c66432d595a4e3670484a56434b4b6a5952556d6331467166496e45396a335847636f32596b697974306c4456635266494934314d4c7134464733585f4c476773553649344d323572706e69796b5f64706278684e486655564c4c326d4b4f314546704d4b6d51787549644d51584f33635f31746a42386f6279334c524179545652366a337144463449495f676b466a69755a5a4b42715741746b66694f636f78506b3559474d452d6f525968694e6879457063446650376a5963365443414861682d714c5337696a7a5a48736c6a504a326f6c534e7a593673587550316b544650475744496d4e5647616f795f4a63576a626e476c705f585932506e46716f6247424a49714d43446354674f344330335934746879387343557849365f484f43754c694b6d5a3268536554673076544873635052744c444733755969464d65355051ffff")),
clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","type":"webauthn.create","tokenBinding":{"status":"supported"},"clientExtensions":{}}"""
- ) { override def regenerate() = TestAuthenticator.createBasicAttestedCredential(attestationStatementFormat = "android-safetynet", generateRsaCert = true) }
+ ) { override def regenerate() = TestAuthenticator.createBasicAttestedCredential(attestationMaker = AttestationMaker.androidSafetynet(AttestationSigner.selfsigned(COSEAlgorithmIdentifier.RS256))) }
val FalseCtsProfileMatch: RegistrationTestData = new RegistrationTestData(
- attestationCaCert = Some(CertificateParser.parseDer(BinaryUtil.fromHex("308201d73082017da00302010202020539300a06082a8648ce3d040302306a3126302406035504030c1d59756269636f20576562417574686e20756e6974207465737473204341310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b3009060355040613025345301e170d3138303930363137343230305a170d3138303930363137343230305a306a3126302406035504030c1d59756269636f20576562417574686e20756e6974207465737473204341310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b30090603550406130253453059301306072a8648ce3d020106082a8648ce3d0301070342000450991ca55415c469f3946b05e9548a5a71e0f5b383f83907bb68cfb73c288887ea89e1a679899557bbdc72f9a12168b9d7b40fd25d1f7552d5f34bfa95572b36a3133011300f0603551d130101ff040530030101ff300a06082a8648ce3d0403020348003045022040981452997022eb8b475fdc2274995be70eff89808563f4451fa6d7beac71e7022100b4489b852487e782baa3c887df24479b4e019d12aa8942776e2e4d8d80f52f78"))),
+ alg = COSEAlgorithmIdentifier.ES256,
attestationObject = new ByteArray(BinaryUtil.fromHex("bf68617574684461746158a449960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97634100000539000102030405060708090a0b0c0d0e0f0020a3c7abed63a16885b1cbf572b1bb29c6614812a886c222a00e07bc7c600764e4a52258209ab2b20ef874f994a3f99ad8f1e61ba590cf67357e5c0997dafcb71edf42ff9b0326010221582027834a6fcb02f7d3182bad5f2b3d16856c93fb574703503a847a36e457996275200163666d7471616e64726f69642d7361666574796e65746761747453746d74bf6376657268313437393930323168726573706f6e73655907e765794a68624763694f694a53557a49314e694973496e67315979493657794a4e53556c4463565244513046724b32644264306c435157644a51304a55613364445a316c4a53323961535870714d45564264306c335957704662553144555564424d565646515864335a466459566d6c685630353253555a6b62466c72526a466b5232683153556857645746595557646b52315a365a45684e5a31457752586845656b464f516d644f566b4a42623031436247777857573173616d4a3652576c4e513046485154465652554e33643170525746597759556457645752486247705a57464a3259326c43516d5249556d786a4d314a6f5a456473646d4a715255784e515774485154465652554a6f54554e564d4656335347686a546b31555a336450564545795456526a4d4531715158645861474e4f5456526e6430395551544a4e56474d775457704264316471516d5a4e556e4e335231465a52465a5255555245516b706f5a45685362474d7a5558565a567a567259323035634670444e5770694d6a423452487042546b4a6e546c5a435157394e516d78734d566c7462477069656b567054554e42523045785655564464336461555668574d474648566e566b5232787157566853646d4e70516b4a6b53464a73597a4e536147524862485a69616b564d54554672523045785655564361453144565442566432646e52576c4e5154424851314e7852314e4a596a4e4555555643515646565155453053554a45643046335a3264465330467653554a425555524d5a47317863474a616244687162454d3254304e364d484e7052475a7a4d48525157455644536a4a6e5230465253304e504d474d7261303832597a4a6d536b354b53473479617a427661553534644468595a4859794e6b704a5479394c555538764f4649794f55394b595567305233677a6247463656336c7065574d765a6e5651614845775a6a645a4b31686c536d354f575374494d3039365a4770534d474674575667345430565763565645526b74316358467862444251536e6c7a4e6a4e31516c70505548563256585a5556793931626b395a5430706d563352725655393563446c6d593235725a6d354c5230646e51326477574756485547786861486b3165455a465333564b4e6a4673536e565063455a5361484a725545737a596d78424f5455785745466f576b4e744e31685252444e4856323578636d687361546834567a4a79546a424b4f585a7a4e6a524b4f453169526d46724d465673546d353561305261623342724d46706a5746464252556f30545664595231527a4f44463554465a574f544e425931597261464e48534642545a3370334f4731564c32784d4f456452553235455530686a527a4251526c7042595456524d32704661307852536d59345745466e54554a4251556471536c5242616b31445255644465584e4851564652516d643156574e4255555646516b4a4a5255564251554a425a303146516c465a53454e42613074446433644f5247633464304e6e57556c4c6231704a656d6f77525546335355525451554633556c464a5a324d7a56316c5856533936596e6c3151334e6d6345704f4f544a46575563766457644b4d6c70564e31564b4d57526b4b324d76576b645252576444535646456232733461336c565a6a4e32535374735456687a5a565a57645452754e5667315332314d636b3575566b68485757566d4d326c565632457251543039496c31392e65794a68634778445a584a3061575a70593246305a5552705a32567a64464e6f595449314e69493657794a4d5132457759544a714c3368764c7a56744d46553453465243516b3543546b4e4d57454a725a7a63725a79745a63475670523070744e54593050534a644c434a756232356a5a534936496b59354d5559354c30526c616b49345a6a4a31516c646b596e425764557076655764346257744865486c77576c52516256646d625746695a57733949697769595842725547466a6132466e5a553568625755694f694a6a623230756558566961574e764c6e646c596d4631644768754c6e526c633351694c434a6959584e7059306c756447566e636d6c306553493664484a315a53776964476c745a584e305957317754584d694f6a45314e546b314e6a6b794d7a6b794d545973496d4677613052705a32567a64464e6f595449314e694936496b7844595442684d6d6f76654738764e5730775654684956454a43546b4a4f51307859516d746e4e79746e4b316c775a576c48536d30314e6a5139496977695933527a55484a765a6d6c735a55316864474e6f496a706d5957787a5a58302e415a6938386e4264644c315a4d336c4c6a654943724332384675386a657063573937686e79446a4d327a366b7870322d49514c484d3779706468674d424c386b307a6637436572647272634c4e63345671494f7a694a694677334752704a656a4a61625f65734d7137324f71742d3855727a48506849476d494251744347547647646577654551715453656131784f6b356576786a4f667630564571563272497263562d46445f5568527437586e38654479536658744e4254784a4765374a727858436b537061374f65465932577a4d6d76536e316c745f482d507a35784b6a5f6665564e34317362425a4e70624649496c724879324e4361374e676c5a50347a6948373769766979316e564f4977517a5866545a496b6b6f7434426a775630484a6557776a447166577375726c467578346b32715047327a6a41574b755a575877617137584751565f53687068384f38652d4d4977ffff")),
clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","type":"webauthn.create","tokenBinding":{"status":"supported"},"clientExtensions":{}}"""
- ) { override def regenerate() = TestAuthenticator.createBasicAttestedCredential(attestationStatementFormat = "android-safetynet", generateRsaCert = true, certSubject = Some(new X500Name("CN=attest.android.com, O=Yubico, OU=Authenticator Attestation, C=SE")), safetynetCtsProfileMatch = false) }
+ ) { override def regenerate() = TestAuthenticator.createBasicAttestedCredential(attestationMaker = AttestationMaker.androidSafetynet(AttestationSigner.ca(alg = COSEAlgorithmIdentifier.RS256, certSubject = new X500Name("CN=attest.android.com, O=Yubico, OU=Authenticator Attestation, C=SE")), ctsProfileMatch = false)) }
}
object FidoU2f {
val BasicAttestation: RegistrationTestData = new RegistrationTestData(
- attestationCaCert = Some(CertificateParser.parseDer(BinaryUtil.fromHex("308201d63082017da00302010202020539300a06082a8648ce3d040302306a3126302406035504030c1d59756269636f20576562417574686e20756e6974207465737473204341310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b3009060355040613025345301e170d3138303930363137343230305a170d3138303930363137343230305a306a3126302406035504030c1d59756269636f20576562417574686e20756e6974207465737473204341310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b30090603550406130253453059301306072a8648ce3d020106082a8648ce3d030107034200048a551913988ee12c2330b4d3a503607defd0ca1eb5f44edf8a4cee2d48df692efbeeb3e9749bbddd960483b6fa930f49ee45318f0de4e014ad07b54b5d88a862a3133011300f0603551d130101ff040530030101ff300a06082a8648ce3d040302034700304402202ab9e33dd3fcaee34bfe44f370656b73ccd591bbf1a41b01ee3fb83a3b8fd83f022036b95c02cfa90b751c93612f487a3773fc2b85276de059bc972ad0a47ed3304d"))),
+ alg = COSEAlgorithmIdentifier.ES256,
attestationObject = new ByteArray(BinaryUtil.fromHex("bf68617574684461746158a449960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97634100000539000102030405060708090a0b0c0d0e0f0020e50fe8ab67d1e773463decf62cfe9a9d5928ece4fd98a013b80478301bb8e29ea5225820d06403b07cf09311ca10b2478979deaaad9c65751e749c503fe9fb935686fcae03260102215820bfa61c3ae256f6a887d2ae9b2075b5246896ba9f44a2a6874ab746acfe7db9e3200163666d74686669646f2d7532666761747453746d74bf63783563815901eb308201e73082018ca00302010202020539300a06082a8648ce3d040302306a3126302406035504030c1d59756269636f20576562417574686e20756e6974207465737473204341310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b3009060355040613025345301e170d3138303930363137343230305a170d3138303930363137343230305a30673123302106035504030c1a59756269636f20576562417574686e20756e6974207465737473310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b30090603550406130253453059301306072a8648ce3d020106082a8648ce3d030107034200040bd659232377a4f910fdcfccaec55511d00beacbdf417f49c9de938137f98df03971b3553bc11a2bd4ef5089ed290d15cc84e005443c794b13dc5e230916c591a32530233021060b2b0601040182e51c01010404120410000102030405060708090a0b0c0d0e0f300a06082a8648ce3d04030203490030460221008546464190caa7a603cd5c8dd60f30a23a9d227ca69603c1421c179092d8e4a1022100891b766c83b9def81518e354db14068d0ade9c8651927b347f4a63454b12add36373696758473045022100c88c93d88194e183f5522ec471a77f8a78d82fa7f99292f8d5f0c20cec6277d702203e289df8dd0568d9bd0b7d294fd30afcf3b264f5fb63f3163b46bb725c8fb31fffff")),
clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","type":"webauthn.create","tokenBinding":{"status":"supported"},"clientExtensions":{}}"""
- ) { override def regenerate() = TestAuthenticator.createBasicAttestedCredential(attestationStatementFormat = "fido-u2f") }
+ ) { override def regenerate() = TestAuthenticator.createBasicAttestedCredential(attestationMaker = AttestationMaker.fidoU2f(AttestationSigner.selfsigned(COSEAlgorithmIdentifier.ES256))) }
val SelfAttestation: RegistrationTestData = new RegistrationTestData(
+ alg = COSEAlgorithmIdentifier.ES256,
attestationObject = new ByteArray(BinaryUtil.fromHex("bf68617574684461746158a449960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97634100000539000102030405060708090a0b0c0d0e0f00205558386f4ed61a6c98a3fed94060fff66808947953754a0dff2aea9ae2164635a52258208d05cb87cec921d5e6fbc22c32a07fb35ed89c19a3f0a2866fcf4a248194e650032601022158202bb1c0846fca809059b41272f0c2953d733b31b50c14453b7a9855b7bfc98229200163666d74686669646f2d7532666761747453746d74bf63783563815901e7308201e330820189a00302010202020539300a06082a8648ce3d04030230673123302106035504030c1a59756269636f20576562417574686e20756e6974207465737473310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b3009060355040613025345301e170d3138303930363137343230305a170d3138303930363137343230305a30673123302106035504030c1a59756269636f20576562417574686e20756e6974207465737473310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b30090603550406130253453059301306072a8648ce3d020106082a8648ce3d030107034200042bb1c0846fca809059b41272f0c2953d733b31b50c14453b7a9855b7bfc982298d05cb87cec921d5e6fbc22c32a07fb35ed89c19a3f0a2866fcf4a248194e650a32530233021060b2b0601040182e51c01010404120410000102030405060708090a0b0c0d0e0f300a06082a8648ce3d0403020348003045022100a91c5499a6518bc59648bde7e7467488736e1ae82b5eb85c14957a0f82d23dfc02205a4b9963f88dbabaa0fa298eae6f0876b9f5e65650c4bd29f1f3f7eeb1312c24637369675847304502205af7085152ec65cc5ee097c5890316e6cac286379c32925a969ab414b013aa59022100b9b9d56cf4314e10c13caa57fb1fb0a01e87ffdec623c62637fddf56a8c4c62cffff")),
clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","type":"webauthn.create","tokenBinding":{"status":"supported"},"clientExtensions":{}}"""
- ) { override def regenerate() = TestAuthenticator.createSelfAttestedCredential(attestationStatementFormat = "fido-u2f") }
+ ) { override def regenerate() = TestAuthenticator.createSelfAttestedCredential(AttestationMaker.fidoU2f(_)) }
}
object NoneAttestation {
val Default = new RegistrationTestData(
+ alg = COSEAlgorithmIdentifier.ES256,
attestationObject = new ByteArray(BinaryUtil.fromHex("bf68617574684461746158a449960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97634100000539000102030405060708090a0b0c0d0e0f002082e7622c8c35a5786e66815f44a82b954628df497361169e77af23bb9bea1b69a5225820ae947a15818d883351ac00b957ad794c4b0206e2df34ec7b52969016a215800e03260102215820763f33278817151fad81d172493b8826c3a736cb1acf884e38c26fbe65c2438a200163666d74646e6f6e656761747453746d74bfffff")),
clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","type":"webauthn.create","tokenBinding":{"status":"supported"},"clientExtensions":{}}"""
) { override def regenerate() = TestAuthenticator.createUnattestedCredential() }
@@ -162,26 +180,42 @@ object RegistrationTestData {
object Packed {
val BasicAttestation: RegistrationTestData = new RegistrationTestData(
- attestationCaCert = Some(CertificateParser.parseDer(BinaryUtil.fromHex("308201d63082017da00302010202020539300a06082a8648ce3d040302306a3126302406035504030c1d59756269636f20576562417574686e20756e6974207465737473204341310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b3009060355040613025345301e170d3138303930363137343230305a170d3138303930363137343230305a306a3126302406035504030c1d59756269636f20576562417574686e20756e6974207465737473204341310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b30090603550406130253453059301306072a8648ce3d020106082a8648ce3d0301070342000474d182bbb3aaab864ac3e0c7e93d3f3eb65299cf36ed0ea7795d4da0246f517bd3d6ef2a8a359246ea78734f6bd71c4bd6394e499e658815415edc0d14b43735a3133011300f0603551d130101ff040530030101ff300a06082a8648ce3d040302034700304402206429f7885dc57981fba4e12a1e4e415cb27c0228dc824231b123bc7cbb3ee0ae02202b89913cacff206d9ea7d6246c6b5fa8d7949bc157ad364d9f754b783d660109"))),
- attestationObject = new ByteArray(BinaryUtil.fromHex("bf68617574684461746158a449960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97634100000539000102030405060708090a0b0c0d0e0f00203479233993ce33d113bca341cbff7b17f8da6477f4f052067eee7431a741cf33a5225820930d76c61326ca11ebac918dd4374a652177739519b45e1d12484d1c815714690326010221582077c8934918d445fb70c2371f5d132693adf2ac90ba7609809e53ee24efaff148200163666d74667061636b65646761747453746d74bf63736967584730450220192f0fb8fa4488bb62f1712f0cb35b2e27cecedae5c81fbb220989c851ced05a022100bcb5d4dcccd30360490bcbd843fdb1cdd5d2e5acd78665c87e46bac865b9543063783563815901ea308201e63082018ca00302010202020539300a06082a8648ce3d040302306a3126302406035504030c1d59756269636f20576562417574686e20756e6974207465737473204341310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b3009060355040613025345301e170d3138303930363137343230305a170d3138303930363137343230305a30673123302106035504030c1a59756269636f20576562417574686e20756e6974207465737473310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b30090603550406130253453059301306072a8648ce3d020106082a8648ce3d03010703420004f09bbbfa47bfaf423f143154763b25373f3d9d94af225a1a1629df5a5ff75034d23e015902e0c97dfc1ace1c2821907d8fe090b50c39aad032596b88fd6068efa32530233021060b2b0601040182e51c01010404120410000102030405060708090a0b0c0d0e0f300a06082a8648ce3d04030203480030450220186f6aba1f39dd8431f566e7993ee8928ac365f88475bfad2c783da69d93b59402210091f8b72046427284ad51ef1068b4892795d21bdcdc14e625b4ecfccb166f4172ffff")),
- clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","type":"webauthn.create","tokenBinding":{"status":"supported"},"clientExtensions":{}}"""
- ) { override def regenerate() = TestAuthenticator.createBasicAttestedCredential(attestationStatementFormat = "packed") }
+ alg = COSEAlgorithmIdentifier.ES256,
+ attestationObject = ByteArray.fromHex("bf68617574684461746158a449960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97634100000539000102030405060708090a0b0c0d0e0f00206b1549b3cf2524c30089b001f8a0f100de9a97681910d2c8181337c516cb2eb6a503260102200121582079c229789b5a262e7b3b2057ef8636b7a20930f262fac3636682e70bdcd4d906225820ca5084617d404d831791a8281eba451aa165726267f9d480dfc315313c95408d63666d74667061636b65646761747453746d74bf63616c67266373696758483046022100f1b2138ab5e8dbce9d0e88862295f574c1b636aa740b57d6705646c799084dd5022100d87f9df13302b854a1c6a726481afbd96ddd2caeb51f4cba89bd248676e9af1063783563825901ed308201e93082018fa00302010202020539300a06082a8648ce3d040302306a3126302406035504030c1d59756269636f20576562417574686e20756e6974207465737473204341310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b3009060355040613025345301e170d3138303930363137343230305a170d3138303930363137343230305a306a3126302406035504030c1d59756269636f20576562417574686e20756e6974207465737473204341310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b30090603550406130253453059301306072a8648ce3d020106082a8648ce3d030107034200043c010b106a69efe039327ab79f57f8e43285f59ad56a50cfd0264b8ba88f79bf2291d561768bb686431aadce9dddf56858aac55b1638d5c03d2a2c426b64b64aa32530233021060b2b0601040182e51c01010404120410000102030405060708090a0b0c0d0e0f300a06082a8648ce3d0403020348003045022100aa1943235627b47852deace94c46e2499a4b2bcab17ffe5502d0c5d17f0f883d022076402b6fe8f66040e4f157e74f732e4a4d31268115e2880faa999f248a0485e05901db308201d73082017da00302010202020539300a06082a8648ce3d040302306a3126302406035504030c1d59756269636f20576562417574686e20756e6974207465737473204341310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b3009060355040613025345301e170d3138303930363137343230305a170d3138303930363137343230305a306a3126302406035504030c1d59756269636f20576562417574686e20756e6974207465737473204341310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b30090603550406130253453059301306072a8648ce3d020106082a8648ce3d0301070342000448f77f8679a4c7bfff4a3ec8291f18995444d21b8624aeefdf2821e69444ac66ced7c7c10ea30d9167836ee84042a9b944d2c239f2a493d5fb2896a2ca0b83d0a3133011300f0603551d130101ff040530030101ff300a06082a8648ce3d0403020348003045022100acc2e79b65faaa5206b27714102f8cdb95ee656c567b7ae7511467b6c324e8e802202a5ac41e505ac43f9efcf3985db215a7506244ba67eb19bdf17aabef8773e1c1ffff"),
+ clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","type":"webauthn.create","tokenBinding":{"status":"supported"},"clientExtensions":{}}""",
+ ) { override def regenerate() = TestAuthenticator.createBasicAttestedCredential(attestationMaker = AttestationMaker.packed(AttestationSigner.ca(COSEAlgorithmIdentifier.ES256))) }
val BasicAttestationEdDsa: RegistrationTestData = new RegistrationTestData(
- attestationCaCert = Some(CertificateParser.parseDer(BinaryUtil.fromHex("308201d63082017da00302010202020539300a06082a8648ce3d040302306a3126302406035504030c1d59756269636f20576562417574686e20756e6974207465737473204341310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b3009060355040613025345301e170d3138303930363137343230305a170d3138303930363137343230305a306a3126302406035504030c1d59756269636f20576562417574686e20756e6974207465737473204341310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b30090603550406130253453059301306072a8648ce3d020106082a8648ce3d0301070342000436a18972db7a76bf269a68461d5326d83a3cecd5bded6526ca0137b9c2a8d192bdf4554eb5572f30916474f206783bb0500abc1983446d7f56a7d912b4b983afa3133011300f0603551d130101ff040530030101ff300a06082a8648ce3d040302034700304402205fc7a9389494a8e1b102200550e52913ab95e5ac20d37a05ac9eff483f85169c022062e9cfc141b014ed6058e9b42b99afe53ab596ad1dd85426f010cb02207b9d88"))),
- attestationObject = new ByteArray(BinaryUtil.fromHex("bf686175746844617461588f49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97634100000539000102030405060708090a0b0c0d0e0f00203f1a8b132e03a186ff9e6c8ae6524f740ea77b1df686c9b4a2b0a9f3a5d00c90a403390100010121582c302a300506032b65700321004212a8a0396b0bbbe5d1226287becdd21907f493248f819888bff174b1910a4e200663666d74667061636b65646761747453746d74bf6373696758483046022100b98243a47abc89c5a32e08af5542750b7467a65b6eadb34032915065f3d1c74a022100891d2278f0c07dd31120c2ad59340ecabaf1a238dae3e6441c12ada443f4165363783563815901ea308201e63082018ca00302010202020539300a06082a8648ce3d040302306a3126302406035504030c1d59756269636f20576562417574686e20756e6974207465737473204341310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b3009060355040613025345301e170d3138303930363137343230305a170d3138303930363137343230305a30673123302106035504030c1a59756269636f20576562417574686e20756e6974207465737473310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b30090603550406130253453059301306072a8648ce3d020106082a8648ce3d03010703420004df92b815b785ceddf1dd3e9e8e99b6e92873af812e717bd9db1777bfe20201e57d4e0a21f8cea8709b8646676cd5db1656e19a9ec8d8b3bda518c23bd845ff81a32530233021060b2b0601040182e51c01010404120410000102030405060708090a0b0c0d0e0f300a06082a8648ce3d0403020348003045022100c385bb5783129c36d5148b784b086e6cb86a464d70ad578d03a7f97fa63e836702205433da460b40d770eea995bebfe524bfa7d0bc329bed0f53b68432a769f3cdfcffff")),
+ alg = COSEAlgorithmIdentifier.EdDSA,
+ attestationObject = ByteArray.fromHex("bf686175746844617461588149960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97634100000539000102030405060708090a0b0c0d0e0f002089b13dc1075db05f34ea0f2e2fd843ce0c0b262a4a852f5eb03d3b2668f437dfa403270101200621582051be73800d9386b8bcfa03f80143ed1279486f95acb714515616b849b588298963666d74667061636b65646761747453746d74bf63616c6726637369675846304402207ef99a22fb1d6fac37ce859f768a3b3d85477ef3825ea53fb7824bb292b12139022073ba899784179bcd06fb3e75657a99cd710ed84a98edbdc8370ac9df885eb8bb63783563815901e7308201e330820189a00302010202020539300a06082a8648ce3d04030230673123302106035504030c1a59756269636f20576562417574686e20756e6974207465737473310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b3009060355040613025345301e170d3138303930363137343230305a170d3138303930363137343230305a30673123302106035504030c1a59756269636f20576562417574686e20756e6974207465737473310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b30090603550406130253453059301306072a8648ce3d020106082a8648ce3d03010703420004ba072ce8a10f63a776c3ce83972e20259089b0d2072501678daedaea755175ee34c785c7cc47e06561fac2b48b1f22e795173c4b89cdfd651a661bb7b9b180f1a32530233021060b2b0601040182e51c01010404120410000102030405060708090a0b0c0d0e0f300a06082a8648ce3d0403020348003045022100b75626efe7b98fb81dcf8dbb301a2a2a0dea354c5b43592368bb0b7345e1e6ea022003deb0739996db0c3a3b40c116f070d10d03e7261459426378fa2896a92e5024ffff"),
clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","type":"webauthn.create","tokenBinding":{"status":"supported"},"clientExtensions":{}}""",
- ) { override def regenerate() = TestAuthenticator.createBasicAttestedCredential(attestationStatementFormat = "packed", keyAlgorithm = COSEAlgorithmIdentifier.EdDSA) }
+ privateKey = Some(ByteArray.fromHex("3051020101300506032b657004220420098ff1cf173564547f5631f6db3f8dae75713b99d486604e8a09c755c53e11ee81210051be73800d9386b8bcfa03f80143ed1279486f95acb714515616b849b5882989")),
+ assertion = Some(AssertionTestData(
+ request = JacksonCodecs.json().readValue("""{"publicKeyCredentialRequestOptions":{"challenge":"N3LjI2J5ylyWe3ED5OT4XHLRqHwm_J48_D_hoJOFf30","userVerification":"preferred","extensions":{}},"username":"test@test.org"}""", classOf[AssertionRequest]),
+ response = PublicKeyCredential.parseAssertionResponseJson("""{"id":"ibE9wQddsF806g8uL9hDzgwLJipKhS9esD07Jmj0N98","response":{"authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MBAAAFOQ","clientDataJSON":"eyJjaGFsbGVuZ2UiOiJOM0xqSTJKNXlseVdlM0VENU9UNFhITFJxSHdtX0o0OF9EX2hvSk9GZjMwIiwib3JpZ2luIjoiaHR0cHM6Ly9sb2NhbGhvc3QiLCJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwidG9rZW5CaW5kaW5nIjp7InN0YXR1cyI6InN1cHBvcnRlZCJ9LCJjbGllbnRFeHRlbnNpb25zIjp7fX0","signature":"-8AKZkFZSNUemUihJhsUp8LqXFHgVTjfCuKVvf1kbIkuwz5ClZK2u562C8rkUnIorxtzD7ujYh1z4FstXKyRDg"},"clientExtensionResults":{},"type":"public-key"}""")
+ )),
+ ) { override def regenerate() = TestAuthenticator.createBasicAttestedCredential(keyAlgorithm = COSEAlgorithmIdentifier.EdDSA, attestationMaker = AttestationMaker.packed(AttestationSigner.selfsigned(COSEAlgorithmIdentifier.ES256))) }
val BasicAttestationRsa: RegistrationTestData = new RegistrationTestData(
- attestationCaCert = Some(CertificateParser.parseDer(BinaryUtil.fromHex("308201d63082017da00302010202020539300a06082a8648ce3d040302306a3126302406035504030c1d59756269636f20576562417574686e20756e6974207465737473204341310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b3009060355040613025345301e170d3138303930363137343230305a170d3138303930363137343230305a306a3126302406035504030c1d59756269636f20576562417574686e20756e6974207465737473204341310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b30090603550406130253453059301306072a8648ce3d020106082a8648ce3d03010703420004bff996ecb8a8436ae9de965d1515258af215962d3125f84c8922cd2b20ddccaaf2458a4c5a12492a108961158e5717da7172e5cec2cfca8d784d6dcc6242ffe6a3133011300f0603551d130101ff040530030101ff300a06082a8648ce3d0403020347003044022068e1a42513f0dbb22d24d2c51fc609a33aa7b800119c394e2bce115c1d08179b02207e2d910e4b63c095c0599d6e9027d5c8084a4cc10efe8cef01c82a50988391ca"))),
- attestationObject = new ByteArray(BinaryUtil.fromHex("bf68617574684461746159016849960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97634100000539000102030405060708090a0b0c0d0e0f0020a447a72a802c3fd5d6b1c1cd40c3599ef53ac1894199b155d22019b666d7a4dfa403390100010321430100012059010100cfe2799c34e0c905e5a8d9ba805d1651ff50d32514bfde5bda35e04e5cf3a5b0446a72aaed2d6bbd47fbc5fffb6480b6f03906de66521852f6bf3f38916065a4f4e9c3e3ff0ae661a75ad6b311315d059f5ae12ac5bc5eb31e71db208fa61fca4a9a93ddac5cb07c8f76fa1e9713d8fb08c5e5bb1ffbb9dd811a5fd3875a347ff1cdfaa2f89dbe048cbd8bf99292a9b70236abf96e62c1742c3e7af0f2246dd8dbcaec39cd477b2b33497f96eb1d6dd11ef9b36146456803a47d71795b026d5ccebc0b1d217018b2ee6cc859c4551d57715c978f1bd4d55ac3ddb49927706382b22ad016e4d6f1baa7c890e6d5f05411511f594232b76e3320cc208eaf310de963666d74667061636b65646761747453746d74bf6373696758473045022100f08623e12693d5af5cdb10c74deff4ee9da9dd9d66adefd0a203d53a6b6d5fe002202d183a5ff69d83394653d6ae02124c16bb36a97809dcdae3bda17fd724cad86d63783563815901e9308201e53082018ca00302010202020539300a06082a8648ce3d040302306a3126302406035504030c1d59756269636f20576562417574686e20756e6974207465737473204341310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b3009060355040613025345301e170d3138303930363137343230305a170d3138303930363137343230305a30673123302106035504030c1a59756269636f20576562417574686e20756e6974207465737473310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b30090603550406130253453059301306072a8648ce3d020106082a8648ce3d030107034200043a74a7a2d1855509b7d7aaab40f1e159db771e8de5da0e86dbc4bf9a07eea3989665e31a9a2c4fe71249782940592a4221e8457c6ea27d5b0277904fd5063193a32530233021060b2b0601040182e51c01010404120410000102030405060708090a0b0c0d0e0f300a06082a8648ce3d04030203470030440220182ad112298caf8b732ed1b225ef3318a2df05848b9183f13095711a2cb0b54a02205cd0ff52eb61aa93a7243daf18a991baa708193acd60b8d6b3ed0b1238b54a3bffff")),
- clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","type":"webauthn.create","tokenBinding":{"status":"supported"},"clientExtensions":{}}"""
- ) { override def regenerate() = TestAuthenticator.createBasicAttestedCredential(attestationStatementFormat = "packed", keyAlgorithm = COSEAlgorithmIdentifier.RS256) }
+ alg = COSEAlgorithmIdentifier.RS256,
+ attestationObject = ByteArray.fromHex("bf68617574684461746159016849960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97634100000539000102030405060708090a0b0c0d0e0f00202d027aec938e6fcf40460eb328f8596e43af1cbd99fddf61c1ffae6e7b0e404ba40339010001032059010100ab8f0b1c4ceef20c093cd67fe6cc264e3ba8208467f410e53b22415ef201ad1ad525ac1be334926ce4f565cfc777135924c1a9bfc3fad24e3d504d618602937b200fd1972ea0097ff9e7d33f68633263a8ce347550213de95228c9c093ca700042f782eb6c16da1b75ed2f481815b04c222cae865340592deeba809fee80e6c1199a3e36b50b400ef87570234754566b276a8fb0cbca7a6ffa1d24369878c8c831e415747b3142cce244ae8d4e0df921a4c9400ed615c1e9c98479af90be09fb2880512bf9d52f825ea031ff10daac369862df3da0d1a2782888415430d8040a0671a749269dcdc4ac22a66b42cf0ac3a3365a64c6ce82ff2548bfac493f6bad214301000163666d74667061636b65646761747453746d74bf63616c673901006373696759010054de4d2aae25f9bd1b9d0e20a9d4168a5feded7178fe1f47ee0fb9a8f19439c8cc1aeab7a7269e4d4edb29c7c9864fbd8202d8cc69584da0e73b4c1d731bff3ec29599964ebef12068a9791d0e52a0c9579d881c565e1ae8a0fc7f2de9ec8882d13919a164b362ab2a89faec3be869635f187b3ef30cd20986ec6f2ff667cb1a279871f77dd9d037f49a7da784cdf846e2d7220683aa928e3b422616be8b0609385a16e0509365a609e162a5239bdc1c4e7aa60c9a1860de753b99705173a72c9fc0390f42886ff9ff839f045cf6457ecb7cf26da34e95511fde6343e4812f40ceb8ff2e7dd24dafdd9c513225bf3418df4a7c1c0f5bc6a0155a31d9c2ddfe8c63783563815903733082036f30820257a00302010202020539300d06092a864886f70d01010b050030673123302106035504030c1a59756269636f20576562417574686e20756e6974207465737473310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b3009060355040613025345301e170d3138303930363137343230305a170d3138303930363137343230305a30673123302106035504030c1a59756269636f20576562417574686e20756e6974207465737473310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b300906035504061302534530820122300d06092a864886f70d01010105000382010f003082010a028201010097c332c418daf7cb31e27bf321f7f72f48c614650f6215db969d17a6ba24fc08d3140fa4d45ff2b2b0ce95fbd87b629e23ba84533dbf2ed90c4e2a770db459690ddfa433288a06fa0c2b1c012887926f1d366d2beac622788560d0a4197b4d90ba7bfd6f4b3250cc37f54e5f350160ad61136bca94b560ec783334cf0376cca042ff40b288049881f7fb3c265f6bbfd625c18efe5802c7dbd384b0b6f328ae9a1bbeb4a184b8eddf16ff419a76adef00d20b57e0927e997c2dfec964c24fb2f023848916c41b0de26636be72356b555d4d1090f2cbcf9003eff39d4b6f77498481d6fe8b2f2bfe2e895382494ca4495c8ac9a47c9fbc8832dc66f727852f814d0203010001a32530233021060b2b0601040182e51c01010404120410000102030405060708090a0b0c0d0e0f300d06092a864886f70d01010b050003820101002a801c27a9a78f74db8de08bc367a8877f53007c7edc01354716be772d7e1450ab99c7b9d4e1c30c05080e51cc69c98068f0130aeccea535e1eb4e7834413bba888633a0c3aad9b7286096084425500b8b442a30ffd52cb77520ee28e8341e2640c39b81be07d9fce48d49ee3bad11b6015c78505e2c1aeaeb829c167bd86bbb714310f6559f481bb9b970dbe8184c7b24d8a4ef2030331d6c8d41b966d5fb4bf08f8f736adedc918fe039100330a5c6a79c54c92351c907608abda0fc98f019ac182ed2858f3c65aeeb282562d1036a06573edac5bed696553b5d347620cf9412faddaab3319080263378085812b315357a3cbe3618ff81d2760c7276ded4afffff"),
+ clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","type":"webauthn.create","tokenBinding":{"status":"supported"},"clientExtensions":{}}""",
+ ) { override def regenerate() = TestAuthenticator.createBasicAttestedCredential(keyAlgorithm = COSEAlgorithmIdentifier.RS256, attestationMaker = AttestationMaker.packed(AttestationSigner.selfsigned(COSEAlgorithmIdentifier.RS256)))}
+
+ val BasicAttestationRs1: RegistrationTestData = new RegistrationTestData(
+ alg = COSEAlgorithmIdentifier.RS1,
+ attestationObject = ByteArray.fromHex("bf68617574684461746159016849960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97634100000539000102030405060708090a0b0c0d0e0f00200dccf988353c4ba273b0cb871029fcfd3ea2ba0474a3c8aa34120cdf386d41f8a40339fffe010320590101009dc7b1317f0d4791afa7312dc4189fa272d7891853d48bd93064057c785248a0592cf2dcc3a31430218bd960f4ab7df989f142f4ab31dd4e481b28cf8e715656d07b3ecf1d88b6a2621f4bed5972e18be692ef155887cc0d03e200d0e5144995c1c1eee75cd68ad625c586dbfc2beeedf911615bbc7c0f14933a46c9bf4506f14337fec3ef57dccda236e4df1d83aad4bcd53ff9e754da7775bd45a09447483173ae265bee5560960fe581ab5a29b57ebbd10c2b07406ba259cbf20e7b22d96dafcdfb9b2d475853d3d5ccb6e619994f2ba6ef112165cae2db9d608f6c68dfaabae056dc19f933080d26c29dbe47dcf88a5435e9582df63e5f24dc35ac1fe88f214301000163666d74667061636b65646761747453746d74bf63616c6739fffe6373696759010008003116f6b02c14a059d8a0e92fdb5653b0c459528761cfbb2d34a192d12247ca9cfe7f164322ea38db77e9ae470d85ff00a892bab69dfb06b71bcda93b3b8c8beb1a530cfbbfa06f021e78230a31f5554f9547e34c1f9a47fb1cba3d76871796d92c5ee98ac367740d8ec36fe58dc9fdcb0e6a343880d83e1efa02895924278ecdf20a6803a2ac2c0309166346a8325ad6068a066fc12997df73ea0c0e32d05ecedc5d4c6de917fc1bc8e8cbc910a17e87159dc73552d8788477410d271e42fa261cc22c1d8edd464b3d082452b16dc5b19e81426b6bf7ab7de362faffc1697a9b23b971301f50fe38596b453bd614c04dd9a75f1e0aa1d38153e2e5a9268363783563815903733082036f30820257a00302010202020539300d06092a864886f70d010105050030673123302106035504030c1a59756269636f20576562417574686e20756e6974207465737473310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b3009060355040613025345301e170d3138303930363137343230305a170d3138303930363137343230305a30673123302106035504030c1a59756269636f20576562417574686e20756e6974207465737473310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b300906035504061302534530820122300d06092a864886f70d01010105000382010f003082010a0282010100d22a48cd3bdc9fa809a24a04dc158976b089296e54a949b29b7092dd5f16d2db81ffac4c814e2aaaf0be4f7e8214227ceb30cfe5da668d442999a40be8b2525b449084e7b5bdc3f29f16e303d3610500851e4d32053b1b0397ea285fa60a035df598618b5d67b2d1d8631575edc6d9de7873f4fc3156be00a59815adb226cfc274c86075bb3ff00d9e17bc1114220f91c23707ff415917ffaf34320845f50a01464a7b191385d8cac693ac68c26ed5589bec92f9db757df64bb025085bdee285f3b88e49d959f7ecaf0a70fbf5a3815bdb947bca995ac21c66d765ca380d8d348da06292375f7a8e9d5919a25f96168e61e67d097b7727eeca3645bf039fd4590203010001a32530233021060b2b0601040182e51c01010404120410000102030405060708090a0b0c0d0e0f300d06092a864886f70d0101050500038201010030d14a17813ccd2e2374300525cbbf6e55cb2f0b7e83fee0cc7345addca2cc139d40bba79846739630a9d60959d74eec2d2ea1015a6ec3fa9660be494b5efe80b3888287c6e275f4121f6b7dd076850de8e85538576ce44a71fd487f1dba264350dc1926eee25968c69556db43f4821272385e46c44715e3a7d603d5f6f3ae46abee46abb89070bd4628d4165b8c34bb77854b9b03a37efe3bf9220cc2ef1c4ae88c820eae5e984fbd54a280358d5198cdd3bd6bf54ab14c2253abaa59cd607769b71e8ed7b5a9b0a80d96002ab0cec9f0c5b387bcf44ea3a5b53f421a0ae8035be68e3c9afad1f6328afe05ad2a90407aa2778e1ea88062b0834dde1baf14d6ffff"),
+ clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","type":"webauthn.create","tokenBinding":{"status":"supported"},"clientExtensions":{}}""",
+ privateKey = Some(ByteArray.fromHex("308204bc020100300d06092a864886f70d0101010500048204a6308204a202010002820101009dc7b1317f0d4791afa7312dc4189fa272d7891853d48bd93064057c785248a0592cf2dcc3a31430218bd960f4ab7df989f142f4ab31dd4e481b28cf8e715656d07b3ecf1d88b6a2621f4bed5972e18be692ef155887cc0d03e200d0e5144995c1c1eee75cd68ad625c586dbfc2beeedf911615bbc7c0f14933a46c9bf4506f14337fec3ef57dccda236e4df1d83aad4bcd53ff9e754da7775bd45a09447483173ae265bee5560960fe581ab5a29b57ebbd10c2b07406ba259cbf20e7b22d96dafcdfb9b2d475853d3d5ccb6e619994f2ba6ef112165cae2db9d608f6c68dfaabae056dc19f933080d26c29dbe47dcf88a5435e9582df63e5f24dc35ac1fe88f02030100010282010001c33bc5aba41e31d9be5cd7db5489609437010f38a8871124cb1b8c02db8753521e004a48825e505e31db9f1dcee8a5cc9690675a79ae46cf75fac8e119362e6d3eac5f1964a6e23dceaad77aad0d7291166e46c2a0dc07240d3fdfa7d65a53861c77865a714cd931071e506d665688453279cb88b5173c52e4bcc5ff5a4e7d1a25ec361c703f032a0f927a33348779bdc877b206d19c71b3851d572417206d76a605cdadd3f4701c76d98bb77d3c0daa516219cda1a1378ff1bc57e5a53d2ca4acaea01a4ba3daa1436d0b6e5773ad25711802d974e3bdcfafee572cd5cc4690a8aa861a64d110cdb108be6b5de2d2f575adaa8f8a6a4379581da7ad70f86902818100d71c1b9085370c186c92ecf0c9665c84caf7308b97c47b6e8c42b7e7de2ef0b904228d59a7285fec01f181f76039bba36177d89eff2ea5109e1d8267ab85b88e20d988d9ceee81d2d86f373fc52938549b944ac21eca3329e8b629afb557d28258753d19fd429cf84983fdc3335dab622330a4e0051de4ff707db659ed6fefcb02818100bbc5beeca7fe242ec3e4d1193f6df05bfe1516ccfd0577008863533d3b2be42d2ebd0a8d6bd172edf8eff30462b3beb31cbddb449853b6de0d58eb4269b7374ae80dd189c0243ca8da1d19097359784bf124dd92949b460e90b075469a676da2f00c94382cf9b5b6afa9828f4706331720bc12d37463812019812f6bded449cd028180429d12b02b80c37f20c85315b1d8c017e35eaf2adb61de337abe02838c5b8ef24ca4828f5be375e8f92517f14a5c368e3ed5c5405f97cb481d1ed84e506085a985e4b7ab73988a9d87a6d13e2f49378783f265403e16b1c76da853ba74f6f05aab180b46ec15dfd447b7d732c6ca60137100545e87571d9e38f0c5328e03d70702818049b32ce4006ffccdaa2fd66e7d79ee3c7d36d3d33380809be1ec725077381c002bf720fc2f146f72be219815e193c146d60222dd0298e10eb8d86cc68d6dcf33046fe00d9c2fdceb3d68ec59cc3f92bae3f45f4f582ab5cda3b6cee11e5b7829dae4650cc382637347f155805d152eda660bcbabd963f0dba3871410d7ce250502818039ee632c8dc74bae5b99b28d2c53c80934709101aa2c64f696d23f53842074814b023e579525105ffac163a6b22f25c5ff4b5a2961d12eca826f7ace40af34c0c5563ba6103d119e93cc900fbbe9f12677f6eba80598d305af3d706a625250c991a1bf3f8b149aec248777ecc0c52f56b44980a5c994d4b9d9f4f74aa4464b80")),
+ assertion = Some(AssertionTestData(
+ request = JacksonCodecs.json().readValue("""{"publicKeyCredentialRequestOptions":{"challenge":"N3LjI2J5ylyWe3ED5OT4XHLRqHwm_J48_D_hoJOFf30","userVerification":"preferred","extensions":{}},"username":"test@test.org"}""", classOf[AssertionRequest]),
+ response = PublicKeyCredential.parseAssertionResponseJson("""{"id":"Dcz5iDU8S6JzsMuHECn8_T6iugR0o8iqNBIM3zhtQfg","response":{"authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MBAAAFOQ","clientDataJSON":"eyJjaGFsbGVuZ2UiOiJOM0xqSTJKNXlseVdlM0VENU9UNFhITFJxSHdtX0o0OF9EX2hvSk9GZjMwIiwib3JpZ2luIjoiaHR0cHM6Ly9sb2NhbGhvc3QiLCJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwidG9rZW5CaW5kaW5nIjp7InN0YXR1cyI6InN1cHBvcnRlZCJ9LCJjbGllbnRFeHRlbnNpb25zIjp7fX0","signature":"g3lWw0SG1AkAaxelbkPnrYvBBRg8VQZIkNHBp6Ogn-2E2zOan8Xe_FBItM_P1K_p49G9SpsljIrQxakH1kZMGBMflHYyaJC1duX0wqgUdFwz_p3sEfo9_vYpXt_Ytj6QYCOUjlJav_eGhtA_K-AWrw3Gz74nUrnjiBaFw-Iqno9ZucpRDo_0vKuTb7ARDSOWYo0eHWzcfY3CvXuEVxDlamUeA_JRtM2t4BKFaUo_91_D4XIvGO9KBWdM0d3KaU5hotO6kLjk0-EdQHrBNSweU0KeJEqBlceFj4AiPN8RFot5qXq1w_Zs9orLME-HwvkVykAGRZSdu2Pcjr2tNpQohg"},"clientExtensionResults":{},"type":"public-key"}""")
+ )),
+ ) { override def regenerate() = TestAuthenticator.createBasicAttestedCredential(keyAlgorithm = COSEAlgorithmIdentifier.RS1, attestationMaker = AttestationMaker.packed(AttestationSigner.selfsigned(COSEAlgorithmIdentifier.RS1))) }
val BasicAttestationRsaReal: RegistrationTestData = new RegistrationTestData(
+ alg = COSEAlgorithmIdentifier.RS256,
// Real attestation object from SKY3
- attestationCaCert = Some(CertificateParser.parseDer( "MIIDHjCCAgagAwIBAgIEG1BT9zANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZdWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAwMDBaGA8yMDUwMDkwNDAwMDAwMFowLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290IENBIFNlcmlhbCA0NTcyMDA2MzEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC/jwYuhBVlqaiYWEMsrWFisgJ+PtM91eSrpI4TK7U53mwCIawSDHy8vUmk5N2KAj9abvT9NP5SMS1hQi3usxoYGonXQgfO6ZXyUA9a+KAkqdFnBnlyugSeCOep8EdZFfsaRFtMjkwz5Gcz2Py4vIYvCdMHPtwaz0bVuzneueIEz6TnQjE63Rdt2zbwnebwTG5ZybeWSwbzy+BJ34ZHcUhPAY89yJQXuE0IzMZFcEBbPNRbWECRKgjq//qT9nmDOFVlSRCt2wiqPSzluwn+v+suQEBsUjTGMEd25tKXXTkNW21wIWbxeSyUoTXwLvGS6xlwQSgNpk2qXYwf8iXg7VWZAgMBAAGjQjBAMB0GA1UdDgQWBBQgIvz0bNGJhjgpToksyKpP9xv9oDAPBgNVHRMECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAQEAjvjuOMDSa+JXFCLyBKsycXtBVZsJ4Ue3LbaEsPY4MYN/hIQ5ZM5p7EjfcnMG4CtYkNsfNHc0AhBLdq45rnT87q/6O3vUEtNMafbhU6kthX7Y+9XFN9NpmYxr+ekVY5xOxi8h9JDIgoMP4VB1uS0aunL1IGqrNooL9mmFnL2kLVVee6/VR6C5+KSTCMCWppMuJIZII2v9o4dkoZ8Y7QRjQlLfYzd3qGtKbw7xaF1UsG/5xUb/Btwb2X2g4InpiB/yt/3CpQXpiWX/K4mBvUKiGn05ZsqeY1gx4g0xLBqcU9psmyPzK+Vsgw2jeRQ5JlKDyqE0hebfC1tvFu0CCrJFcw==")),
attestationObject = ByteArray.fromBase64Url( "o2NmbXRmcGFja2VkaGF1dGhEYXRhWQFXAU4Ai_91hLmkf2mxjxj_SJrA3qTIOjr6tw1rluqSp_5FAAAAAG1Eupv27C5JuTAMj-kgy3MAEApbxn7DR_LpWJ6yjXeHxIGkAQMDOQEAIFkBAPm_XOU-DioXdG6YXFo5gpHPNxJDimlbnXCro2D_hvzBsxoY4oEzNyRDgK_PoDedZ4tJyk12_I8qJ8g5HqbpT6YUekYegcP4ugL1Omr31gGqTwsF45fIITcSWXcoJbqPnwotbaM98Hu15mSIT8NeXDce0MVNYJ6PULRm6xiiWXHk1cxwrHd9xPCjww6CjRKDc06hP--noBbToW3xx43eh7kGlisWPeU1naIMe7CZAjIMhNlu_uxQssaPAhEXNzDENpK99ieUg290Ym4YNAGbWdW4irkeTt7h_yC-ARrJUu4ygwwGaqCTl9QIMrwZGuiQD11LC0uKraIA2YHaGa2UGKshQwEAAWdhdHRTdG10o2NhbGcmY3NpZ1hHMEUCIQDLKMt6O4aKJkl71VhyIcuI6lqyFTHMDuCO5Y4Jdq2_xQIgPm2_1GF0ivkR816opfVQMWq0s-Hx0uJjcX5l5tm9ZgFjeDVjgVkCwTCCAr0wggGloAMCAQICBCrnYmMwDQYJKoZIhvcNAQELBQAwLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290IENBIFNlcmlhbCA0NTcyMDA2MzEwIBcNMTQwODAxMDAwMDAwWhgPMjA1MDA5MDQwMDAwMDBaMG4xCzAJBgNVBAYTAlNFMRIwEAYDVQQKDAlZdWJpY28gQUIxIjAgBgNVBAsMGUF1dGhlbnRpY2F0b3IgQXR0ZXN0YXRpb24xJzAlBgNVBAMMHll1YmljbyBVMkYgRUUgU2VyaWFsIDcxOTgwNzA3NTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCoDhl5gQ9meEf8QqiVUV4S_Ca-Oax47MhcpIW9VEhqM2RDTmd3HaL3-SnvH49q8YubSRp_1Z1uP-okMynSGnj-jbDBqMCIGCSsGAQQBgsQKAgQVMS4zLjYuMS40LjEuNDE0ODIuMS4xMBMGCysGAQQBguUcAgEBBAQDAgQwMCEGCysGAQQBguUcAQEEBBIEEG1Eupv27C5JuTAMj-kgy3MwDAYDVR0TAQH_BAIwADANBgkqhkiG9w0BAQsFAAOCAQEAclfQPNzD4RVphJDW-A75W1MHI3PZ5kcyYysR3Nx3iuxr1ZJtB-F7nFQweI3jL05HtFh2_4xVIgKb6Th4eVcjMecncBaCinEbOcdP1sEli9Hk2eVm1XB5A0faUjXAPw_-QLFCjgXG6ReZ5HVUcWkB7riLsFeJNYitiKrTDXFPLy-sNtVNutcQnFsCerDKuM81TvEAigkIbKCGlq8M_NvBg5j83wIxbCYiyV7mIr3RwApHieShzLdJo1S6XydgQjC-_64G5r8C-8AVvNFR3zXXCpio5C3KRIj88HEEIYjf6h1fdLfqeIsq-cUUqbq5T-c4nNoZUZCysTB9v5EY4akp-A"),
clientDataJson = new String(ByteArray.fromBase64Url("ew0KCSJ0eXBlIiA6ICJ3ZWJhdXRobi5jcmVhdGUiLA0KCSJjaGFsbGVuZ2UiIDogImxaMllKbUZ2YWkteGhYMElteG9fQlk1SkpVdmREa3JXd1ZGZllmcHQtNmciLA0KCSJvcmlnaW4iIDogImh0dHBzOi8vZGVtbzMueXViaWNvLnRlc3Q6ODQ0MyIsDQoJInRva2VuQmluZGluZyIgOiANCgl7DQoJCSJzdGF0dXMiIDogInN1cHBvcnRlZCINCgl9DQp9").getBytes, StandardCharsets.UTF_8),
rpId = RelyingPartyIdentity.builder().id("demo3.yubico.test").name("").build(),
@@ -190,25 +224,34 @@ object RegistrationTestData {
)
val BasicAttestationWithoutAaguidExtension: RegistrationTestData = new RegistrationTestData(
- attestationObject = new ByteArray(BinaryUtil.fromHex("bf68617574684461746158a449960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97634100000539000102030405060708090a0b0c0d0e0f0020b5d7943ea57b200148e0d87b735269ee2c09108087916b0dab1aabb0f78599cda5225820619f68d30b6c4dddf73f4dbb86d4585f06d0b0d2c8978b5d351ffa2e5c060d54032601022158208c68e6bc94460133d137d0bd11eea5067512ed470f6f479f0ba699052959d822200163666d74667061636b65646761747453746d74bf637369675846304402205f52f52e3f44618945f542646a3c459e6438abe2ef036ed8daef223d164ab338022077cdb39f441957215d8b7d68c7f697d0a121bd7e17ddbc341cad2d713bbbe25d63783563815901bf308201bb30820162a00302010202020539300a06082a8648ce3d04030230673123302106035504030c1a59756269636f20576562417574686e20756e6974207465737473310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b3009060355040613025345301e170d3138303930363137343230305a170d3138303930363137343230305a30673123302106035504030c1a59756269636f20576562417574686e20756e6974207465737473310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b30090603550406130253453059301306072a8648ce3d020106082a8648ce3d03010703420004bc0bd10e9f28f94715aca7dc586100a5fb6fa442ae2038a5f6f3667d5f9a134d8e1dc2aa55a6f56dfb44b2456028d64540ec2aaba78226593fe544884f4d7c65300a06082a8648ce3d0403020347003044022028f69c5ab6cb118296305743d3781840552eaf54bd01803cb857e6b07ed77ec402201a3efb279c314b5eba4635d0c62ba3d81703b63061c5b603df80b39ac4088218ffff")),
- clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","type":"webauthn.create","tokenBinding":{"status":"supported"},"clientExtensions":{}}"""
- ) { override def regenerate() = TestAuthenticator.createBasicAttestedCredential(attestationCertAndKey = Some(TestAuthenticator.generateAttestationCertificate(extensions = Nil)), attestationStatementFormat = "packed") }
+ alg = COSEAlgorithmIdentifier.ES256,
+ attestationObject = ByteArray.fromHex("bf68617574684461746158a449960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97634100000539000102030405060708090a0b0c0d0e0f00209e5f31be46db4229e201138428cc52ee5cf95649af97192b0c0bd95c6bf7da42a5032601022001215820edd3776120e992e2917f67dea1bd9ab796d4766e1c8d7f158d12b0b2c4932ba4225820393783915791af3e5b4ee9a8be59f9fc36b8c95b084d2d44c0095572ad8ac90263666d74667061636b65646761747453746d74bf63616c67266373696758473045022100e4653ca6e7334f6043e95636bafe4f4ca17eecfc21bd17d7b849e4fc723d07560220389513d56b8c030e964d4a286acd3cc5f74aaf1665a84a06421ef23cc2db0a3963783563815901c1308201bd30820162a00302010202020539300a06082a8648ce3d04030230673123302106035504030c1a59756269636f20576562417574686e20756e6974207465737473310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b3009060355040613025345301e170d3138303930363137343230305a170d3138303930363137343230305a30673123302106035504030c1a59756269636f20576562417574686e20756e6974207465737473310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b30090603550406130253453059301306072a8648ce3d020106082a8648ce3d03010703420004b83a0a452140254924a85f18868222eef921abf3859a6c4bc1704c4a3a55131be3191da5c8f2bde0e3f7fc0042e3ced4821112139a085ccd331ab0d9a36ba2a2300a06082a8648ce3d0403020349003046022100a9542f7287013fdefd29edadb84ad61f5b90c938d315c4dbf72005ed2808b149022100d4235ec51d66d892ff9447585167f728ce87733a29e41bac97b437b45ee1571dffff"),
+ clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","type":"webauthn.create","tokenBinding":{"status":"supported"},"clientExtensions":{}}""",
+ ) { override def regenerate() = TestAuthenticator.createBasicAttestedCredential(attestationMaker = AttestationMaker.packed(new AttestationCert(COSEAlgorithmIdentifier.ES256, TestAuthenticator.generateAttestationCertificate(alg = COSEAlgorithmIdentifier.ES256, extensions = Nil)))) }
val BasicAttestationWithWrongAaguidExtension: RegistrationTestData = new RegistrationTestData(
- attestationCaCert = Some(CertificateParser.parseDer(BinaryUtil.fromHex("308201d63082017da00302010202020539300a06082a8648ce3d040302306a3126302406035504030c1d59756269636f20576562417574686e20756e6974207465737473204341310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b3009060355040613025345301e170d3138303930363137343230305a170d3138303930363137343230305a306a3126302406035504030c1d59756269636f20576562417574686e20756e6974207465737473204341310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b30090603550406130253453059301306072a8648ce3d020106082a8648ce3d03010703420004caa58b4a5bbcca24c5e398e4653dafb882a327960ccb72963bf62fa2a1c03f82671b11f0cccb1e1c476125f04afae64b5d1f4f7a6fb5bd1abecd18eeab9d5126a3133011300f0603551d130101ff040530030101ff300a06082a8648ce3d0403020347003044022072f6ae460c82ffe89f9ad1f1bd188ba0c3b50540e02edda0a99c37c6efe2fab5022038754e4e088f0e749946975eab9017eeebb621e830ab853119aae1998e750ab2"))),
- attestationObject = new ByteArray(BinaryUtil.fromHex("bf68617574684461746158a449960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d976341000005390f0e0d0c0b0a09080706050403020100002028856c20f5018aaffce3835765cc58ae6c73e37d37acde80f0aa1611602fa815a52258200348c1c7f2fd5dc658252f7865ebbde62ce968c03e9d97988612809f5abbbe7503260102215820a9342b20b06f71fd8b7b0e7ade89ab438c0d9f541edacedbf0d43f9494d06874200163666d74667061636b65646761747453746d74bf637369675846304402205b1ee9a9def2fb631423fec4c02fa132a5562ad1a32ea4b3edaf8300fd920bd1022069799c95206c10a7a50a36a14bc990a25a54c48ba62c73af55ec8c117329170b63783563815901e9308201e53082018ca00302010202020539300a06082a8648ce3d040302306a3126302406035504030c1d59756269636f20576562417574686e20756e6974207465737473204341310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b3009060355040613025345301e170d3138303930363137343230305a170d3138303930363137343230305a30673123302106035504030c1a59756269636f20576562417574686e20756e6974207465737473310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b30090603550406130253453059301306072a8648ce3d020106082a8648ce3d03010703420004ecc94d2f374915a217b9558ed6746f40ebc3de98e953742d2a7963288cbaf017506988d24b8caf1ec728008c70b749f513007106913c39828772d75b0591db03a32530233021060b2b0601040182e51c01010404120410000102030405060708090a0b0c0d0e0f300a06082a8648ce3d0403020347003044022013c65c197c02e710acca16da432659c16313a1e19f2d8a3e9d47ee22cedc57a702205373349d9a58e8d7170032a2b64dd056d24d84dded75fac0002375a27037ee73ffff")),
- clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","type":"webauthn.create","tokenBinding":{"status":"supported"},"clientExtensions":{}}"""
- ) { override def regenerate() = TestAuthenticator.createBasicAttestedCredential(aaguid = new ByteArray(Array(15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0)), attestationStatementFormat = "packed") }
+ alg = COSEAlgorithmIdentifier.ES256,
+
+ attestationObject = ByteArray.fromHex("bf68617574684461746158a449960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d976341000005390f0e0d0c0b0a0908070605040302010000207b733c7c32c0303159eceb83a77e359de67b8aff51b4ae82af5e34e7b39b3a24a50326010220012158205d2702c2d02739b3a8bfbe84011cdf4c39b3dd1da73f92cb70c8ebe557ee277f225820d60faf92a4fcb6d49dbcbc59260d2fb031ce5c8a95f93d56553662bfa050ab0363666d74667061636b65646761747453746d74bf63616c6726637369675846304402201355a030930063732001ecbddf42e2b8de03ab3fbf96c492fd224929310c36e0022014704aa8426eb36229d5eb59db825f8184ad29ad1a3b6ab7f29a9a8304ea00de63783563815901e7308201e330820189a00302010202020539300a06082a8648ce3d04030230673123302106035504030c1a59756269636f20576562417574686e20756e6974207465737473310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b3009060355040613025345301e170d3138303930363137343230305a170d3138303930363137343230305a30673123302106035504030c1a59756269636f20576562417574686e20756e6974207465737473310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b30090603550406130253453059301306072a8648ce3d020106082a8648ce3d03010703420004e2a3cd9d0a55a1204a3eb3681b793cc3251a28d948428111241359d6c45f5af1ba36a50e0b5cd1c3fd81974cddd9fdb4aba0fd1352e1e107721433b32f34c717a32530233021060b2b0601040182e51c01010404120410000102030405060708090a0b0c0d0e0f300a06082a8648ce3d0403020348003045022100e5818e204920da08899fb97942c57b792bd769c2bfbe7ccd5c25d2169b1588b402207b7446fe3b419d0a4850a87abf3679611086f83df605e908ad3026cd8695f749ffff"),
+ clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","type":"webauthn.create","tokenBinding":{"status":"supported"},"clientExtensions":{}}""",
+ ) { override def regenerate() = TestAuthenticator.createBasicAttestedCredential(aaguid = new ByteArray(Array(15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0)), attestationMaker = AttestationMaker.packed(AttestationSigner.selfsigned(COSEAlgorithmIdentifier.ES256))) }
val SelfAttestation: RegistrationTestData = new RegistrationTestData(
+ alg = COSEAlgorithmIdentifier.ES256,
attestationObject = new ByteArray(BinaryUtil.fromHex("bf68617574684461746158a449960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97634100000539000102030405060708090a0b0c0d0e0f0020fa616cbe1c046d224524e773b386f9f3fd0d0fb6d4c20700023288034e48f093a52258208b02052aeec1d7cfaf1244d9b72296a6bfaf9542c132273c4be8fc01388ee8f30326010221582081906607ef7095eaa3dea2517cfc5a7c0c9768685e30ddb5865f2ada0f5cc63c200163666d74667061636b65646761747453746d74bf6373696758473045022010511b27bd566c7bcdf6e4f08ef2fe4ea20a56826b76761253bbcc31b0be1fa2022100b2659e3efc858fd4389dc48cd0651487f2e7bc4f5eba59db154bdcd0ae60c9d163616c6726ffff")),
clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","type":"webauthn.create","tokenBinding":{"status":"supported"},"clientExtensions":{}}"""
- ) { override def regenerate() = TestAuthenticator.createSelfAttestedCredential(attestationStatementFormat = "packed") }
+ ) { override def regenerate() = TestAuthenticator.createSelfAttestedCredential(AttestationMaker.packed(_)) }
- val SelfAttestationWithWrongAlgValue = new RegistrationTestData(
- attestationObject = new ByteArray(BinaryUtil.fromHex("bf68617574684461746158a449960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97634100000539000102030405060708090a0b0c0d0e0f00203022c626739f52e583ac292c31b80b80759d546f9956a5baf65216faae61313da52258202f752354da475fb5f6c0d35fef2ed8eea3e6dbf225c08b7fed567e813ae41402032601022158207fc7d8d3d5e8dce8bbfda0395f89f0d3c9ea0d9de1d6e62d0f0df9db7661cb9b200163666d74667061636b65646761747453746d74bf6373696758463044022078cf79efde68909ee2518b8feeb727b17a689db2e4b9d13dc3a34e9c46b9390002201e94861f46b7f19f5df5bedef08f91fb862e5eb07c23e6c3b28151917f3e5c2963616c67390100ffff")),
- clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","type":"webauthn.create","tokenBinding":{"status":"supported"},"clientExtensions":{}}"""
- ) { override def regenerate() = TestAuthenticator.createSelfAttestedCredential(attestationStatementFormat = "packed", alg = Some(COSEAlgorithmIdentifier.RS256)) }
+ val SelfAttestationRs1: RegistrationTestData = new RegistrationTestData(
+ alg = COSEAlgorithmIdentifier.RS1,
+ attestationObject = ByteArray.fromHex("bf68617574684461746159016849960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97634100000539000102030405060708090a0b0c0d0e0f0020a8330648c97e09686004be4c429ad3e46886f6033117632cb1aabf261c5d11cda40339fffe01032059010100cdb2b9448221d1432be58b681f27ad204e82fe4c6176c64aed49792fe57c5ca9ddf7ce6fe22f81b67205df310d96668c1c1107ea6e250f4107692842c555c13d6e3df41ca701ce153705269658a186d9a1abe013b127dd51483323f3c82e281962eccfd4f59c05d778ecbbfbfb5eb5902dc91e1e187aacd97d42373a3c3e05218d291989133cf32641d322e6e472c3e4812e613d9ddbb67e74580570d5ef173561c146d81c56bf7bc6353fde611b54cd1fe632a314ac7d3e74ac18c0b7886a70ddcce226dd836791444a88ac9323877adbc5978a51d2abca189651ad5b71169df782f065908edd8ab9edccdc997212c32071b577fd58d55b22557d303d070b77214301000163666d74667061636b65646761747453746d74bf63616c6739fffe63736967590100bbf85e350e87886d80591e44b1a8e8f7fe7a4b3c4748b112ac6bbd88096a7b83c5e2f268154eecec230784729d6418809ce1ea370c374fd3e6151790d0a7f5a7a9e57dcbfd2e0cad26b11002232087eaaa0baf7fdef65c30518237d4ae7d36b7c49cc96b499afb6c0eab2c6a728fa847595071b56515c049d909707fbea2ee22ce0a325939af3b9021e1371bfea19cd14fc9caa1d1a41d5408cba381197c5fddc4e33245411d720c3acb4e53b415b120581d8093e25d710e5acef7e77889a71e5dee935f02992a559eab33725c832f3f24bf3934de2f5ac2eb32a9cc23a652bf08fc7e94c342ef62b555524b733447a19b3307fb41257794e041e91d1e1fbb37ffff"),
+ clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","type":"webauthn.create","tokenBinding":{"status":"supported"},"clientExtensions":{}}""",
+ privateKey = Some(ByteArray.fromHex("308204bd020100300d06092a864886f70d0101010500048204a7308204a30201000282010100cdb2b9448221d1432be58b681f27ad204e82fe4c6176c64aed49792fe57c5ca9ddf7ce6fe22f81b67205df310d96668c1c1107ea6e250f4107692842c555c13d6e3df41ca701ce153705269658a186d9a1abe013b127dd51483323f3c82e281962eccfd4f59c05d778ecbbfbfb5eb5902dc91e1e187aacd97d42373a3c3e05218d291989133cf32641d322e6e472c3e4812e613d9ddbb67e74580570d5ef173561c146d81c56bf7bc6353fde611b54cd1fe632a314ac7d3e74ac18c0b7886a70ddcce226dd836791444a88ac9323877adbc5978a51d2abca189651ad5b71169df782f065908edd8ab9edccdc997212c32071b577fd58d55b22557d303d070b7702030100010282010029573fb3fca17650d53c3399f01505cf05e87eda74062e9135827c483b8b94861155f217fb7207d456b34669b1ece5dc47f1c650ffe513dd427388836eecaec33d6a572b0107b45700315951832ba7920ad3a3dbe1517d420e4c34f0146dee6237c717781a0acb03c4ca73778fcc379a6c114d2bc848b37f9c9497cbecfa9c0607181290eda54e995999b2c18286ce3abba2d1a18f0785b76b163335fe1d7a805f1fe17ed592eabca18be4da7857bc384ec6398a5784e022e16dcd4e61dad8d285a475600d9f11d6e5aa7989a590ffbc99b45283282433e3e4f8d96d3f422c90c850ef3f906ba935dec95ce01f1192685d0e7ab3da7593aca13f4d2890cd112502818100fbe8098b0b37fd43c7b10bb49212e9e162e9be02d6c559d6a1e30a87d8dea7970ea76425226980c1d5bb63ddefcba787fcab8601e89d070dba758d3bf39f4407be8ad6e95abeb86c60be1939614c67720f75ab140837955e037812462733dd372e4751baa5fe87e074064e98d70c201342e9a4d47d6cb88fc6df5db6ac89e14302818100d10a744597179abf260100f7b295f24bed809f101f5a9b388bb04378665461b48c1016677768e6612690ce2f794428eba2a8fa0821f58f713be04b29aa83664f07b3b962c004a60286ad35c585ed4bacfe66682490f7ab7e62529232be325cebe52876e6dcef53373171861b7d40520f69b74c8620ffac0fe64623358a1effbd02818035f843bb277f2a62d030cd5a358599d83111f524b490f9ab7369aa42eaa2e1730aafb0540868642ea3350fb36801d0f5e09b7b0d83a1c8f61701c26d9ac77f92cd2effd6651bc1756ed0aba4d084c710f7e0f4f348c367dc09903b120eaa1cf60a933b1e6b1bfa4e8b6d227fba6b1da022d0de00ac929384324e7ecc7970dcf302818100cffcfdd92bcf419a04bf24ee4f53204469a7fb1bb886974078c4452d6b6b73d787308e8a1de652aac10b7d0b01364f1cbcb832269b5b4f8093d9c40f4de7f588969a3ccf434c9cbc90b19079da9a531c69f70c91ad67afcb4d1ae8f9f201fc307dce78179625cd7f720389329ab9bfac343c3bb88ce6b6950f4223d0268057650281800193dfb5d9612213bbdcbfd274061e5c02d439e2bcbecee0fc6cbe53b2c009b3c2b9438ee48e8c56af5703b12551bf3480761132fa483b26b024387397fd6e6e1f90717b84ce5a24bbccee01180ff113363e5c83c5fb49fa8475db93cd7fa79965853f5c196717ec2ef0047302a7943df5ba2cc462f5f5fc3068d1f72b15a565")),
+ assertion = Some(AssertionTestData(
+ request = JacksonCodecs.json().readValue("""{"publicKeyCredentialRequestOptions":{"challenge":"N3LjI2J5ylyWe3ED5OT4XHLRqHwm_J48_D_hoJOFf30","userVerification":"preferred","extensions":{}},"username":"test@test.org"}""", classOf[AssertionRequest]),
+ response = PublicKeyCredential.parseAssertionResponseJson("""{"id":"qDMGSMl-CWhgBL5MQprT5GiG9gMxF2Mssaq_JhxdEc0","response":{"authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MBAAAFOQ","clientDataJSON":"eyJjaGFsbGVuZ2UiOiJOM0xqSTJKNXlseVdlM0VENU9UNFhITFJxSHdtX0o0OF9EX2hvSk9GZjMwIiwib3JpZ2luIjoiaHR0cHM6Ly9sb2NhbGhvc3QiLCJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwidG9rZW5CaW5kaW5nIjp7InN0YXR1cyI6InN1cHBvcnRlZCJ9LCJjbGllbnRFeHRlbnNpb25zIjp7fX0","signature":"YQuXZwJLOXeEfrOxzG42yJxShEGHFbfD2oYURkOiZOI2LSzfcv5t5KDq4dfJ9S-U5aaylfIlD72u8rQeMIVyf5e8jD5z0bnPP5STZzDsJneoPGOQ6BQfuYGSGSO_JxjU9O6KduNTXKrm2KqaCptOTJHyf9geA2wR7_XSmEdg_OSq7e164ZIK12jiG-RFdEEVpWhuoJPva0TeHfe2tAnQPNreV7v8DaIOWJiBblQTirP0oUn5LrCNhl_Tsgz2-F8R53k48JpesiMhEM6r-e7DI83CrNRZWJnmO-04hMEbdNqO3TmZ3Fmtw9ufpn3zygeK0jrIw3SamFe2NgVvbcIHTg"},"clientExtensionResults":{},"type":"public-key"}""")
+ )),
+ ) { override def regenerate() = TestAuthenticator.createSelfAttestedCredential(AttestationMaker.packed(_), keyAlgorithm = COSEAlgorithmIdentifier.RS1) }
}
object Tpm {
val PrivacyCa: RegistrationTestData = Packed.BasicAttestation.editAttestationObject("fmt", "tpm")
@@ -216,18 +259,38 @@ object RegistrationTestData {
}
case class RegistrationTestData(
+ alg: COSEAlgorithmIdentifier,
+ assertion: Option[AssertionTestData] = None,
attestationObject: ByteArray,
clientDataJson: String,
authenticatorSelection: Option[AuthenticatorSelectionCriteria] = None,
clientExtensionResults: ClientRegistrationExtensionOutputs = ClientRegistrationExtensionOutputs.builder().build(),
+ privateKey: Option[ByteArray] = None,
origin: Option[String] = None,
overrideRequest: Option[PublicKeyCredentialCreationOptions] = None,
requestedExtensions: RegistrationExtensionInputs = RegistrationExtensionInputs.builder().build(),
rpId: RelyingPartyIdentity = RelyingPartyIdentity.builder().id("localhost").name("Test party").build(),
userId: UserIdentity = UserIdentity.builder().name("test@test.org").displayName("Test user").id(new ByteArray(Array(42, 13, 37))).build(),
- attestationCaCert: Option[X509Certificate] = None
) {
- def regenerate(): ((PublicKeyCredential[AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs], KeyPair), Option[X509Certificate]) = null
+ validate()
+
+ def regenerate(): (PublicKeyCredential[AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs], KeyPair) = ???
+ def regenerateFull(): Try[RegistrationTestData] = Try({
+ val (credential, keypair) = regenerate()
+ val newValue = copy(
+ attestationObject = credential.getResponse.getAttestationObject,
+ clientDataJson = new String(credential.getResponse.getClientDataJSON.getBytes, StandardCharsets.UTF_8),
+ privateKey = Some(new ByteArray(keypair.getPrivate.getEncoded)),
+ )
+ newValue.copy(
+ assertion = newValue.assertion.map(_.regenerate(newValue))
+ )
+ })
+
+ protected def validate() {
+ val alg = WebAuthnCodecs.getCoseKeyAlg(response.getResponse.getParsedAuthenticatorData.getAttestedCredentialData.get.getCredentialPublicKey).get
+ assert(alg == this.alg, s"Expected alg: ${this.alg}; was: ${alg}")
+ }
def clientDataJsonBytes: ByteArray = new ByteArray(clientDataJson.getBytes("UTF-8"))
def clientData = new CollectedClientData(clientDataJsonBytes)
@@ -242,6 +305,10 @@ case class RegistrationTestData(
.binaryValue
)
+ def attestationCaCert: Option[X509Certificate] = Option(new AttestationObject(attestationObject).getAttestationStatement.get("x5c"))
+ .map(x5c => x5c.elements().asScala.toList.last)
+ .map(node => CertificateParser.parseDer(node.binaryValue()))
+
def editClientData[A <: JsonNode](updater: ObjectNode => A): RegistrationTestData = copy(
clientDataJson = JacksonCodecs.json.writeValueAsString(
updater(JacksonCodecs.json.readTree(clientDataJson).asInstanceOf[ObjectNode])
@@ -290,7 +357,7 @@ case class RegistrationTestData(
.rp(rpId)
.user(userId)
.challenge(clientData.getChallenge)
- .pubKeyCredParams(List(PublicKeyCredentialParameters.builder().alg(COSEAlgorithmIdentifier.ES256).build()).asJava)
+ .pubKeyCredParams(List(PublicKeyCredentialParameters.ES256, PublicKeyCredentialParameters.EdDSA, PublicKeyCredentialParameters.RS256).asJava)
.extensions(requestedExtensions)
.authenticatorSelection(authenticatorSelection.asJava)
.build()
@@ -305,4 +372,25 @@ case class RegistrationTestData(
)
.clientExtensionResults(clientExtensionResults)
.build()
+
+ def keypair: Option[KeyPair] = privateKey map { privateKey =>
+ val pubKeyCoseBytes = new AttestationObject(attestationObject).getAuthenticatorData.getAttestedCredentialData.get.getCredentialPublicKey
+ val pubkey = WebAuthnCodecs.importCosePublicKey(pubKeyCoseBytes)
+ val prikey = WebAuthnTestCodecs.importPrivateKey(privateKey, WebAuthnCodecs.getCoseKeyAlg(pubKeyCoseBytes).get)
+ new KeyPair(pubkey, prikey)
+ }
+}
+
+case class AssertionTestData(
+ request: AssertionRequest,
+ response: PublicKeyCredential[AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs],
+) {
+ def regenerate(testData: RegistrationTestData): AssertionTestData = {
+ copy(
+ response = TestAuthenticator.createAssertionFromTestData(
+ testData,
+ request.getPublicKeyCredentialRequestOptions,
+ )
+ )
+ }
}
diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyAssertionSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyAssertionSpec.scala
index b0bcc2036..8b693385e 100644
--- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyAssertionSpec.scala
+++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyAssertionSpec.scala
@@ -47,6 +47,7 @@ import com.yubico.webauthn.data.PublicKeyCredentialCreationOptions
import com.yubico.webauthn.data.PublicKeyCredentialDescriptor
import com.yubico.webauthn.data.PublicKeyCredentialRequestOptions
import com.yubico.webauthn.data.RelyingPartyIdentity
+import com.yubico.webauthn.data.UserIdentity
import com.yubico.webauthn.data.UserVerificationRequirement
import com.yubico.webauthn.exception.InvalidSignatureCountException
import com.yubico.webauthn.extension.appid.AppId
@@ -84,15 +85,15 @@ class RelyingPartyAssertionSpec extends FunSpec with Matchers with GeneratorDriv
val rpId = RelyingPartyIdentity.builder().id("localhost").name("Test party").build()
- // These values were generated using TestAuthenticator.makeCredentialExample(TestAuthenticator.createCredential())
+ // These values were generated using TestAuthenticator.makeAssertionExample()
val authenticatorData: ByteArray = ByteArray.fromHex("49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97630100000539")
- val clientDataJson: String = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"localhost","hashAlgorithm":"SHA-256","type":"webauthn.get","tokenBinding":{"status":"supported"}}"""
- val credentialId: ByteArray = ByteArray.fromBase64Url("aqFjEQkzH8I55SnmIyNM632MsPI_qZ60aGTSHZMwcKY")
+ val clientDataJson: String = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","type":"webauthn.get","tokenBinding":{"status":"supported"},"clientExtensions":{}}"""
+ val credentialId: ByteArray = ByteArray.fromBase64Url("AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8")
val credentialKey: KeyPair = TestAuthenticator.importEcKeypair(
- privateBytes = ByteArray.fromHex("308193020100301306072a8648ce3d020106082a8648ce3d0301070479307702010104206a88f478910df685bc0cfcc2077e64fb3a8ba770fb23fbbcd1f6572ce35cf360a00a06082a8648ce3d030107a14403420004d8020a2ec718c2c595bb890fcdaf9b81cc742118efdbb8812ac4a9dd5ace2990ec22a48faf1544df0fe5fe0e2e7a69720e63a83d7f46aa022f1323eaf7967762"),
- publicBytes = ByteArray.fromHex("3059301306072a8648ce3d020106082a8648ce3d03010703420004d8020a2ec718c2c595bb890fcdaf9b81cc742118efdbb8812ac4a9dd5ace2990ec22a48faf1544df0fe5fe0e2e7a69720e63a83d7f46aa022f1323eaf7967762")
+ privateBytes = ByteArray.fromHex("308193020100301306072a8648ce3d020106082a8648ce3d030107047930770201010420449d91b8a2a508b2927cd5cf4dde32db8e58f237fc155e395d3aad127e115f5aa00a06082a8648ce3d030107a1440342000446c68a2eb75057b1f19b6d06dd3733381063d021391b3637889b0b432c54aaa2b184b35e44d433c70e63a9dd82568dd1ec02c5daba3e66b90a3a881c0c1f4c1a"),
+ publicBytes = ByteArray.fromHex("3059301306072a8648ce3d020106082a8648ce3d0301070342000446c68a2eb75057b1f19b6d06dd3733381063d021391b3637889b0b432c54aaa2b184b35e44d433c70e63a9dd82568dd1ec02c5daba3e66b90a3a881c0c1f4c1a")
)
- val signature: ByteArray = ByteArray.fromHex("30450221008d478e4c24894d261c7fd3790363ba9687facf4dd1d59610933a2c292cffc3d902205069264c167833d239d6af4c7bf7326c4883fb8c3517a2c86318aa3060d8b441")
+ val signature: ByteArray = ByteArray.fromHex("304502201dfef99d44222410686605e23227853f19e9bf89cbab181fdb52b7f40d79f0d5022100c167309d699a03416887af363de0628d7d77f678a01d135da996f0ecbed7e8a5")
// These values are not signed over
val username: String = "foo-user"
@@ -119,10 +120,35 @@ class RelyingPartyAssertionSpec extends FunSpec with Matchers with GeneratorDriv
else
???
- private def getPublicKeyBytes(credentialKey: KeyPair): ByteArray = WebAuthnCodecs.ecPublicKeyToCose(credentialKey.getPublic.asInstanceOf[ECPublicKey])
+ private def getPublicKeyBytes(credentialKey: KeyPair): ByteArray = WebAuthnTestCodecs.ecPublicKeyToCose(credentialKey.getPublic.asInstanceOf[ECPublicKey])
+
+ private def credRepoWithUser(user: UserIdentity, credential: RegisteredCredential): CredentialRepository = new CredentialRepository {
+ override def getCredentialIdsForUsername(username: String): java.util.Set[PublicKeyCredentialDescriptor] =
+ if (username == user.getName)
+ Set(PublicKeyCredentialDescriptor.builder().id(credential.getCredentialId).build()).asJava
+ else Set.empty.asJava
+ override def getUserHandleForUsername(username: String): Optional[ByteArray] =
+ if (username == user.getName)
+ Some(user.getId).asJava
+ else None.asJava
+ override def getUsernameForUserHandle(userHandle: ByteArray): Optional[String] =
+ if (userHandle == user.getId)
+ Some(user.getName).asJava
+ else None.asJava
+ override def lookup(credentialId: ByteArray, userHandle: ByteArray): Optional[RegisteredCredential] =
+ if (credentialId == credential.getCredentialId && userHandle == user.getId)
+ Some(credential).asJava
+ else None.asJava
+ override def lookupAll(credentialId: ByteArray): java.util.Set[RegisteredCredential] =
+ if (credentialId == credential.getCredentialId)
+ Set(credential).asJava
+ else Set.empty.asJava
+ }
def finishAssertion(
allowCredentials: Option[java.util.List[PublicKeyCredentialDescriptor]] = Some(List(PublicKeyCredentialDescriptor.builder().id(Defaults.credentialId).build()).asJava),
+ allowOriginPort: Boolean = false,
+ allowOriginSubdomain: Boolean = false,
authenticatorData: ByteArray = Defaults.authenticatorData,
callerTokenBindingId: Option[ByteArray] = None,
challenge: ByteArray = Defaults.challenge,
@@ -131,7 +157,7 @@ class RelyingPartyAssertionSpec extends FunSpec with Matchers with GeneratorDriv
credentialId: ByteArray = Defaults.credentialId,
credentialKey: KeyPair = Defaults.credentialKey,
credentialRepository: Option[CredentialRepository] = None,
- origin: String = Defaults.rpId.getId,
+ origins: Option[Set[String]] = None,
requestedExtensions: AssertionExtensionInputs = Defaults.requestedExtensions,
rpId: RelyingPartyIdentity = Defaults.rpId,
signature: ByteArray = Defaults.signature,
@@ -171,7 +197,7 @@ class RelyingPartyAssertionSpec extends FunSpec with Matchers with GeneratorDriv
.clientExtensionResults(clientExtensionResults)
.build()
- RelyingParty.builder()
+ val builder = RelyingParty.builder()
.identity(rpId)
.credentialRepository(
credentialRepository getOrElse new CredentialRepository {
@@ -194,9 +220,14 @@ class RelyingPartyAssertionSpec extends FunSpec with Matchers with GeneratorDriv
}
)
.preferredPubkeyParams(Nil.asJava)
- .origins(Set(origin).asJava)
+ .allowOriginPort(allowOriginPort)
+ .allowOriginSubdomain(allowOriginSubdomain)
.allowUntrustedAttestation(false)
.validateSignatureCounter(validateSignatureCounter)
+
+ origins.map(_.asJava).foreach(builder.origins _)
+
+ builder
.build()
._finishAssertion(request, response, callerTokenBindingId.asJava)
}
@@ -493,13 +524,203 @@ class RelyingPartyAssertionSpec extends FunSpec with Matchers with GeneratorDriv
step.tryNext shouldBe a [Failure[_]]
}
- it("9. Verify that the value of C.origin matches the Relying Party's origin.") {
- val steps = finishAssertion(origin = "root.evil")
- val step: FinishAssertionSteps#Step9 = steps.begin.next.next.next.next.next.next.next.next.next
+ describe("9. Verify that the value of C.origin matches the Relying Party's origin.") {
+ def checkAccepted(
+ origin: String,
+ origins: Option[Set[String]] = None,
+ allowOriginPort: Boolean = false,
+ allowOriginSubdomain: Boolean = false
+ ): Unit = {
+ val clientDataJson: String = Defaults.clientDataJson.replace("\"https://localhost\"", "\"" + origin + "\"")
+ val steps = finishAssertion(
+ clientDataJson = clientDataJson,
+ origins = origins,
+ allowOriginPort = allowOriginPort,
+ allowOriginSubdomain = allowOriginSubdomain
+ )
+ val step: FinishAssertionSteps#Step9 = steps.begin.next.next.next.next.next.next.next.next.next
- step.validations shouldBe a [Failure[_]]
- step.validations.failed.get shouldBe an [IllegalArgumentException]
- step.tryNext shouldBe a [Failure[_]]
+ step.validations shouldBe a [Success[_]]
+ step.tryNext shouldBe a [Success[_]]
+ }
+
+ def checkRejected(
+ origin: String,
+ origins: Option[Set[String]] = None,
+ allowOriginPort: Boolean = false,
+ allowOriginSubdomain: Boolean = false
+ ): Unit = {
+ val clientDataJson: String = Defaults.clientDataJson.replace("\"https://localhost\"", "\"" + origin + "\"")
+ val steps = finishAssertion(
+ clientDataJson = clientDataJson,
+ origins = origins,
+ allowOriginPort = allowOriginPort,
+ allowOriginSubdomain = allowOriginSubdomain
+ )
+ val step: FinishAssertionSteps#Step9 = steps.begin.next.next.next.next.next.next.next.next.next
+
+ step.validations shouldBe a [Failure[_]]
+ step.validations.failed.get shouldBe an [IllegalArgumentException]
+ step.tryNext shouldBe a [Failure[_]]
+ }
+
+ it("Fails if origin is different.") {
+ checkRejected(origin = "https://root.evil")
+ }
+
+ describe("Explicit ports are") {
+ val origin = "https://localhost:8080"
+
+ it("by default not allowed.") {
+ checkRejected(origin = origin)
+ }
+
+ it("allowed if RP opts in to it.") {
+ checkAccepted(origin = origin, allowOriginPort = true)
+ }
+ }
+
+ describe("Subdomains are") {
+ val origin = "https://foo.localhost"
+
+ it("by default not allowed.") {
+ checkRejected(origin = origin)
+ }
+
+ it("allowed if RP opts in to it.") {
+ checkAccepted(origin = origin, allowOriginSubdomain = true)
+ }
+ }
+
+ describe("Subdomains and explicit ports at the same time are") {
+ val origin = "https://foo.localhost:8080"
+
+ it("by default not allowed.") {
+ checkRejected(origin = origin)
+ }
+
+ it("not allowed if only subdomains are allowed.") {
+ checkRejected(origin = origin, allowOriginSubdomain = true)
+ }
+
+ it("not allowed if only explicit ports are allowed.") {
+ checkRejected(origin = origin, allowOriginPort = true)
+ }
+
+ it("allowed if RP opts in to both.") {
+ checkAccepted(origin = origin, allowOriginPort = true, allowOriginSubdomain = true)
+ }
+ }
+
+ describe("The examples in JavaDoc are correct:") {
+ def check(
+ origins: Set[String],
+ acceptOrigins: Iterable[String],
+ rejectOrigins: Iterable[String],
+ allowOriginPort: Boolean = false,
+ allowOriginSubdomain: Boolean = false
+ ): Unit = {
+ for { origin <- acceptOrigins } {
+ it(s"${origin} is accepted.") {
+ checkAccepted(
+ origin = origin,
+ origins = Some(origins),
+ allowOriginPort = allowOriginPort,
+ allowOriginSubdomain = allowOriginSubdomain
+ )
+ }
+ }
+
+ for { origin <- rejectOrigins } {
+ it(s"${origin} is rejected.") {
+ checkRejected(
+ origin = origin,
+ origins = Some(origins),
+ allowOriginPort = allowOriginPort,
+ allowOriginSubdomain = allowOriginSubdomain
+ )
+ }
+ }
+ }
+
+ describe("For allowOriginPort:") {
+ val origins = Set("https://example.org", "https://accounts.example.org", "https://acme.com:8443")
+
+ describe("false,") {
+ check(
+ origins = origins,
+ acceptOrigins = List(
+ "https://example.org",
+ "https://accounts.example.org",
+ "https://acme.com:8443"
+ ),
+ rejectOrigins = List(
+ "https://example.org:8443",
+ "https://shop.example.org",
+ "https://acme.com",
+ "https://acme.com:9000"
+ ),
+ allowOriginPort = false
+ )
+ }
+
+ describe("true,") {
+ check(
+ origins = origins,
+ acceptOrigins = List(
+ "https://example.org",
+ "https://example.org:8443",
+ "https://accounts.example.org",
+ "https://acme.com",
+ "https://acme.com:8443",
+ "https://acme.com:9000"
+ ),
+ rejectOrigins = List(
+ "https://shop.example.org"
+ ),
+ allowOriginPort = true
+ )
+ }
+ }
+
+ describe("For allowOriginSubdomain:") {
+ val origins = Set("https://example.org", "https://acme.com:8443")
+
+ describe("false,") {
+ check(
+ origins = origins,
+ acceptOrigins = List(
+ "https://example.org",
+ "https://acme.com:8443"
+ ),
+ rejectOrigins = List(
+ "https://example.org:8443",
+ "https://accounts.example.org",
+ "https://acme.com",
+ "https://shop.acme.com:8443"
+ ),
+ allowOriginSubdomain = false
+ )
+ }
+
+ describe("true,") {
+ check(
+ origins = origins,
+ acceptOrigins = List(
+ "https://example.org",
+ "https://accounts.example.org",
+ "https://acme.com:8443",
+ "https://shop.acme.com:8443"
+ ),
+ rejectOrigins = List(
+ "https://example.org:8443",
+ "https://acme.com"
+ ),
+ allowOriginSubdomain = true
+ )
+ }
+ }
+ }
}
describe("10. Verify that the value of C.tokenBinding.status matches the state of Token Binding for the TLS connection over which the attestation was obtained.") {
@@ -512,7 +733,7 @@ class RelyingPartyAssertionSpec extends FunSpec with Matchers with GeneratorDriv
}
it("Verification succeeds if client data specifies token binding is unsupported, and RP does not use it.") {
- val clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"localhost","hashAlgorithm":"SHA-256","type":"webauthn.get"}"""
+ val clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","hashAlgorithm":"SHA-256","type":"webauthn.get"}"""
val steps = finishAssertion(clientDataJson = clientDataJson)
val step: FinishAssertionSteps#Step10 = steps.begin.next.next.next.next.next.next.next.next.next.next
@@ -521,7 +742,7 @@ class RelyingPartyAssertionSpec extends FunSpec with Matchers with GeneratorDriv
}
it("Verification succeeds if client data specifies token binding is supported, and RP does not use it.") {
- val clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"localhost","hashAlgorithm":"SHA-256","tokenBinding":{"status":"supported"},"type":"webauthn.get"}"""
+ val clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","hashAlgorithm":"SHA-256","tokenBinding":{"status":"supported"},"type":"webauthn.get"}"""
val steps = finishAssertion(clientDataJson = clientDataJson)
val step: FinishAssertionSteps#Step10 = steps.begin.next.next.next.next.next.next.next.next.next.next
@@ -530,7 +751,7 @@ class RelyingPartyAssertionSpec extends FunSpec with Matchers with GeneratorDriv
}
it("Verification fails if client data does not specify token binding status and RP specifies token binding ID.") {
- val clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"localhost","hashAlgorithm":"SHA-256","type":"webauthn.get"}"""
+ val clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","hashAlgorithm":"SHA-256","type":"webauthn.get"}"""
val steps = finishAssertion(
callerTokenBindingId = Some(ByteArray.fromBase64Url("YELLOWSUBMARINE")),
clientDataJson = clientDataJson
@@ -543,7 +764,7 @@ class RelyingPartyAssertionSpec extends FunSpec with Matchers with GeneratorDriv
}
it("Verification succeeds if client data does not specify token binding status and RP does not specify token binding ID.") {
- val clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"localhost","hashAlgorithm":"SHA-256","type":"webauthn.get"}"""
+ val clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","hashAlgorithm":"SHA-256","type":"webauthn.get"}"""
val steps = finishAssertion(
callerTokenBindingId = None,
clientDataJson = clientDataJson
@@ -554,7 +775,7 @@ class RelyingPartyAssertionSpec extends FunSpec with Matchers with GeneratorDriv
step.tryNext shouldBe a [Success[_]]
}
it("Verification fails if client data specifies token binding ID but RP does not.") {
- val clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"localhost","hashAlgorithm":"SHA-256","tokenBinding":{"status":"present","id":"YELLOWSUBMARINE"},"type":"webauthn.get"}"""
+ val clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","hashAlgorithm":"SHA-256","tokenBinding":{"status":"present","id":"YELLOWSUBMARINE"},"type":"webauthn.get"}"""
val steps = finishAssertion(
callerTokenBindingId = None,
clientDataJson = clientDataJson
@@ -568,7 +789,7 @@ class RelyingPartyAssertionSpec extends FunSpec with Matchers with GeneratorDriv
describe("If Token Binding was used on that TLS connection, also verify that C.tokenBinding.id matches the base64url encoding of the Token Binding ID for the connection.") {
it("Verification succeeds if both sides specify the same token binding ID.") {
- val clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"localhost","hashAlgorithm":"SHA-256","tokenBinding":{"status":"present","id":"YELLOWSUBMARINE"},"type":"webauthn.get"}"""
+ val clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","hashAlgorithm":"SHA-256","tokenBinding":{"status":"present","id":"YELLOWSUBMARINE"},"type":"webauthn.get"}"""
val steps = finishAssertion(
callerTokenBindingId = Some(ByteArray.fromBase64Url("YELLOWSUBMARINE")),
clientDataJson = clientDataJson
@@ -580,7 +801,7 @@ class RelyingPartyAssertionSpec extends FunSpec with Matchers with GeneratorDriv
}
it("Verification fails if ID is missing from tokenBinding in client data.") {
- val clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"localhost","hashAlgorithm":"SHA-256","tokenBinding":{"status":"present"},"type":"webauthn.get"}"""
+ val clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","hashAlgorithm":"SHA-256","tokenBinding":{"status":"present"},"type":"webauthn.get"}"""
val steps = finishAssertion(
callerTokenBindingId = Some(ByteArray.fromBase64Url("YELLOWSUBMARINE")),
clientDataJson = clientDataJson
@@ -593,7 +814,7 @@ class RelyingPartyAssertionSpec extends FunSpec with Matchers with GeneratorDriv
}
it("Verification fails if RP specifies token binding ID but client does not support it.") {
- val clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"localhost","hashAlgorithm":"SHA-256","type":"webauthn.get"}"""
+ val clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","hashAlgorithm":"SHA-256","type":"webauthn.get"}"""
val steps = finishAssertion(
callerTokenBindingId = Some(ByteArray.fromBase64Url("YELLOWSUBMARINE")),
clientDataJson = clientDataJson
@@ -606,7 +827,7 @@ class RelyingPartyAssertionSpec extends FunSpec with Matchers with GeneratorDriv
}
it("Verification fails if RP specifies token binding ID but client does not use it.") {
- val clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"localhost","hashAlgorithm":"SHA-256","tokenBinding":{"status":"supported"},"type":"webauthn.get"}"""
+ val clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","hashAlgorithm":"SHA-256","tokenBinding":{"status":"supported"},"type":"webauthn.get"}"""
val steps = finishAssertion(
callerTokenBindingId = Some(ByteArray.fromBase64Url("YELLOWSUBMARINE")),
clientDataJson = clientDataJson
@@ -619,7 +840,7 @@ class RelyingPartyAssertionSpec extends FunSpec with Matchers with GeneratorDriv
}
it("Verification fails if client data and RP specify different token binding IDs.") {
- val clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"localhost","hashAlgorithm":"SHA-256","tokenBinding":{"status":"present","id":"YELLOWSUBMARINE"},"type":"webauthn.get"}"""
+ val clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","hashAlgorithm":"SHA-256","tokenBinding":{"status":"present","id":"YELLOWSUBMARINE"},"type":"webauthn.get"}"""
val steps = finishAssertion(
callerTokenBindingId = Some(ByteArray.fromBase64Url("ORANGESUBMARINE")),
clientDataJson = clientDataJson
@@ -635,7 +856,10 @@ class RelyingPartyAssertionSpec extends FunSpec with Matchers with GeneratorDriv
describe("11. Verify that the rpIdHash in authData is the SHA-256 hash of the RP ID expected by the Relying Party.") {
it("Fails if RP ID is different.") {
- val steps = finishAssertion(rpId = Defaults.rpId.toBuilder.id("root.evil").build())
+ val steps = finishAssertion(
+ rpId = Defaults.rpId.toBuilder.id("root.evil").build(),
+ origins = Some(Set("https://localhost"))
+ )
val step: FinishAssertionSteps#Step11 = steps.begin.next.next.next.next.next.next.next.next.next.next.next
step.validations shouldBe a [Failure[_]]
@@ -886,7 +1110,8 @@ class RelyingPartyAssertionSpec extends FunSpec with Matchers with GeneratorDriv
val rpIdHash: ByteArray = crypto.hash(rpId)
val steps = finishAssertion(
authenticatorData = new ByteArray((rpIdHash.getBytes.toVector ++ Defaults.authenticatorData.getBytes.toVector.drop(32)).toArray),
- rpId = Defaults.rpId.toBuilder.id(rpId).build()
+ rpId = Defaults.rpId.toBuilder.id(rpId).build(),
+ origins = Some(Set("https://localhost"))
)
val step: FinishAssertionSteps#Step16 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next
@@ -1268,6 +1493,66 @@ class RelyingPartyAssertionSpec extends FunSpec with Matchers with GeneratorDriv
result.getUserHandle should equal (registrationRequest.getUser.getId)
result.getCredentialId should equal (credId)
}
+
+ it("a generated Ed25519 key.") {
+ val registrationTestData = RegistrationTestData.Packed.BasicAttestationEdDsa
+ val testData = registrationTestData.assertion.get
+
+ val rp = RelyingParty.builder()
+ .identity(RelyingPartyIdentity.builder().id("localhost").name("Test RP").build())
+ .credentialRepository(credRepoWithUser(registrationTestData.userId, RegisteredCredential.builder()
+ .credentialId(registrationTestData.response.getId)
+ .userHandle(registrationTestData.userId.getId)
+ .publicKeyCose(registrationTestData.response.getResponse.getParsedAuthenticatorData.getAttestedCredentialData.get.getCredentialPublicKey)
+ .signatureCount(0)
+ .build()))
+ .build()
+
+ val result = rp.finishAssertion(FinishAssertionOptions.builder()
+ .request(testData.request)
+ .response(testData.response)
+ .build()
+ )
+
+ result.isSuccess should be (true)
+ result.getUserHandle should equal (registrationTestData.userId.getId)
+ result.getCredentialId should equal (registrationTestData.response.getId)
+ result.getCredentialId should equal (testData.response.getId)
+ }
+
+ describe("an RS1 key") {
+ def test(registrationTestData: RegistrationTestData): Unit = {
+ val testData = registrationTestData.assertion.get
+
+ val rp = RelyingParty.builder()
+ .identity(RelyingPartyIdentity.builder().id("localhost").name("Test RP").build())
+ .credentialRepository(credRepoWithUser(registrationTestData.userId, RegisteredCredential.builder()
+ .credentialId(registrationTestData.response.getId)
+ .userHandle(registrationTestData.userId.getId)
+ .publicKeyCose(registrationTestData.response.getResponse.getParsedAuthenticatorData.getAttestedCredentialData.get.getCredentialPublicKey)
+ .signatureCount(0)
+ .build()))
+ .build()
+
+ val result = rp.finishAssertion(FinishAssertionOptions.builder()
+ .request(testData.request)
+ .response(testData.response)
+ .build()
+ )
+
+ result.isSuccess should be (true)
+ result.getUserHandle should equal (registrationTestData.userId.getId)
+ result.getCredentialId should equal (registrationTestData.response.getId)
+ result.getCredentialId should equal (testData.response.getId)
+ }
+
+ it("with basic attestation.") {
+ test(RegistrationTestData.Packed.BasicAttestationRs1)
+ }
+ it("with self attestation.") {
+ test(RegistrationTestData.Packed.SelfAttestationRs1)
+ }
+ }
}
}
diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyRegistrationSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyRegistrationSpec.scala
index 4bf2e811d..afe70789d 100644
--- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyRegistrationSpec.scala
+++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyRegistrationSpec.scala
@@ -24,14 +24,14 @@
package com.yubico.webauthn
-import java.util
import java.io.IOException
import java.nio.charset.Charset
-import java.security.MessageDigest
import java.security.KeyPair
+import java.security.MessageDigest
import java.security.PrivateKey
import java.security.SignatureException
import java.security.cert.X509Certificate
+import java.security.interfaces.RSAPublicKey
import java.util.Optional
import com.fasterxml.jackson.databind.JsonNode
@@ -40,22 +40,27 @@ import com.fasterxml.jackson.databind.node.ObjectNode
import com.upokecenter.cbor.CBORObject
import com.yubico.internal.util.scala.JavaConverters._
import com.yubico.internal.util.JacksonCodecs
-import com.yubico.webauthn.attestation.MetadataService
import com.yubico.webauthn.attestation.Attestation
-import com.yubico.webauthn.data.RelyingPartyIdentity
-import com.yubico.webauthn.data.AuthenticatorSelectionCriteria
+import com.yubico.webauthn.attestation.MetadataService
import com.yubico.webauthn.data.AttestationObject
-import com.yubico.webauthn.data.AuthenticatorData
-import com.yubico.webauthn.data.UserVerificationRequirement
import com.yubico.webauthn.data.AttestationType
-import com.yubico.webauthn.data.CollectedClientData
+import com.yubico.webauthn.data.AuthenticatorData
+import com.yubico.webauthn.data.AuthenticatorSelectionCriteria
import com.yubico.webauthn.data.ByteArray
-import com.yubico.webauthn.data.RegistrationExtensionInputs
+import com.yubico.webauthn.data.CollectedClientData
+import com.yubico.webauthn.data.COSEAlgorithmIdentifier
+import com.yubico.webauthn.data.Generators._
import com.yubico.webauthn.data.PublicKeyCredentialDescriptor
+import com.yubico.webauthn.data.PublicKeyCredentialParameters
+import com.yubico.webauthn.data.RegistrationExtensionInputs
+import com.yubico.webauthn.data.RelyingPartyIdentity
import com.yubico.webauthn.data.UserIdentity
-import com.yubico.webauthn.data.AttestationConveyancePreference
-import com.yubico.webauthn.data.Generators._
+import com.yubico.webauthn.data.UserVerificationRequirement
+import com.yubico.webauthn.exception.RegistrationFailedException
import com.yubico.webauthn.test.Util.toStepWithUtilities
+import com.yubico.webauthn.TestAuthenticator.AttestationCert
+import com.yubico.webauthn.TestAuthenticator.AttestationMaker
+import com.yubico.webauthn.data.PublicKeyCredentialParameters
import javax.security.auth.x500.X500Principal
import org.bouncycastle.asn1.DEROctetString
import org.bouncycastle.asn1.x500.X500Name
@@ -94,29 +99,38 @@ class RelyingPartyRegistrationSpec extends FunSpec with Matchers with GeneratorD
}
private val unimplementedCredentialRepository = new CredentialRepository {
- override def getCredentialIdsForUsername(username: String): util.Set[PublicKeyCredentialDescriptor] = ???
+ override def getCredentialIdsForUsername(username: String): java.util.Set[PublicKeyCredentialDescriptor] = ???
override def getUserHandleForUsername(username: String): Optional[ByteArray] = ???
override def getUsernameForUserHandle(userHandleBase64: ByteArray): Optional[String] = ???
override def lookup(credentialId: ByteArray, userHandle: ByteArray): Optional[RegisteredCredential] = ???
- override def lookupAll(credentialId: ByteArray): util.Set[RegisteredCredential] = ???
+ override def lookupAll(credentialId: ByteArray): java.util.Set[RegisteredCredential] = ???
}
private def finishRegistration(
+ allowOriginPort: Boolean = false,
+ allowOriginSubdomain: Boolean = false,
allowUntrustedAttestation: Boolean = false,
callerTokenBindingId: Option[ByteArray] = None,
credentialId: Option[ByteArray] = None,
- credentialRepository: Option[CredentialRepository] = None,
+ credentialRepository: CredentialRepository = unimplementedCredentialRepository,
metadataService: Option[MetadataService] = None,
+ origins: Option[Set[String]] = None,
+ preferredPubkeyParams: List[PublicKeyCredentialParameters] = Nil,
rp: RelyingPartyIdentity = RelyingPartyIdentity.builder().id("localhost").name("Test party").build(),
testData: RegistrationTestData
): FinishRegistrationSteps = {
- RelyingParty.builder()
+ val builder = RelyingParty.builder()
.identity(rp)
- .credentialRepository(credentialRepository.getOrElse(unimplementedCredentialRepository))
- .preferredPubkeyParams(Nil.asJava)
- .origins(Set("https://" + rp.getId).asJava)
+ .credentialRepository(credentialRepository)
+ .preferredPubkeyParams(preferredPubkeyParams.asJava)
+ .allowOriginPort(allowOriginPort)
+ .allowOriginSubdomain(allowOriginSubdomain)
.allowUntrustedAttestation(allowUntrustedAttestation)
.metadataService(metadataService.asJava)
+
+ origins.map(_.asJava).foreach(builder.origins _)
+
+ builder
.build()
._finishRegistration(testData.request, testData.response, callerTokenBindingId.asJava)
}
@@ -211,15 +225,201 @@ class RelyingPartyRegistrationSpec extends FunSpec with Matchers with GeneratorD
step.tryNext shouldBe a [Failure[_]]
}
- it("5. Verify that the value of C.origin matches the Relying Party's origin.") {
- val steps = finishRegistration(
- testData = RegistrationTestData.FidoU2f.BasicAttestation.editClientData("origin", "https://root.evil")
- )
- val step: FinishRegistrationSteps#Step5 = steps.begin.next.next.next.next
+ describe("5. Verify that the value of C.origin matches the Relying Party's origin.") {
- step.validations shouldBe a [Failure[_]]
- step.validations.failed.get shouldBe an [IllegalArgumentException]
- step.tryNext shouldBe a [Failure[_]]
+ def checkAccepted(
+ origin: String,
+ origins: Option[Set[String]] = None,
+ allowOriginPort: Boolean = false,
+ allowOriginSubdomain: Boolean = false
+ ): Unit = {
+ val steps = finishRegistration(
+ testData = RegistrationTestData.FidoU2f.BasicAttestation.editClientData("origin", origin),
+ origins = origins,
+ allowOriginPort = allowOriginPort,
+ allowOriginSubdomain = allowOriginSubdomain
+ )
+ val step: FinishRegistrationSteps#Step5 = steps.begin.next.next.next.next
+
+ step.validations shouldBe a [Success[_]]
+ step.tryNext shouldBe a [Success[_]]
+ }
+
+ def checkRejected(
+ origin: String,
+ origins: Option[Set[String]] = None,
+ allowOriginPort: Boolean = false,
+ allowOriginSubdomain: Boolean = false
+ ): Unit = {
+ val steps = finishRegistration(
+ testData = RegistrationTestData.FidoU2f.BasicAttestation.editClientData("origin", origin),
+ origins = origins,
+ allowOriginPort = allowOriginPort,
+ allowOriginSubdomain = allowOriginSubdomain
+ )
+ val step: FinishRegistrationSteps#Step5 = steps.begin.next.next.next.next
+
+ step.validations shouldBe a [Failure[_]]
+ step.validations.failed.get shouldBe an [IllegalArgumentException]
+ step.tryNext shouldBe a [Failure[_]]
+ }
+
+ it("Fails if origin is different.") {
+ checkRejected(origin = "https://root.evil")
+ }
+
+ describe("Explicit ports are") {
+ val origin = "https://localhost:8080"
+ it("by default not allowed.") {
+ checkRejected(origin = origin)
+ }
+
+ it("allowed if RP opts in to it.") {
+ checkAccepted(origin = origin, allowOriginPort = true)
+ }
+ }
+
+ describe("Subdomains are") {
+ val origin = "https://foo.localhost"
+
+ it("by default not allowed.") {
+ checkRejected(origin = origin)
+ }
+
+ it("allowed if RP opts in to it.") {
+ checkAccepted(origin = origin, allowOriginSubdomain = true)
+ }
+ }
+
+ describe("Subdomains and explicit ports at the same time are") {
+ val origin = "https://foo.localhost:8080"
+
+ it("by default not allowed.") {
+ checkRejected(origin = origin)
+ }
+
+ it("not allowed if only subdomains are allowed.") {
+ checkRejected(origin = origin, allowOriginPort = false, allowOriginSubdomain = true)
+ }
+
+ it("not allowed if only explicit ports are allowed.") {
+ checkRejected(origin = origin, allowOriginPort = true, allowOriginSubdomain = false)
+ }
+
+ it("allowed if RP opts in to both.") {
+ checkAccepted(origin = origin, allowOriginPort = true, allowOriginSubdomain = true)
+ }
+ }
+
+ describe("The examples in JavaDoc are correct:") {
+ def check(
+ origins: Set[String],
+ acceptOrigins: Iterable[String],
+ rejectOrigins: Iterable[String],
+ allowOriginPort: Boolean = false,
+ allowOriginSubdomain: Boolean = false
+ ): Unit = {
+ for { origin <- acceptOrigins } {
+ it(s"${origin} is accepted.") {
+ checkAccepted(
+ origin = origin,
+ origins = Some(origins),
+ allowOriginPort = allowOriginPort,
+ allowOriginSubdomain = allowOriginSubdomain
+ )
+ }
+ }
+
+ for { origin <- rejectOrigins } {
+ it(s"${origin} is rejected.") {
+ checkRejected(
+ origin = origin,
+ origins = Some(origins),
+ allowOriginPort = allowOriginPort,
+ allowOriginSubdomain = allowOriginSubdomain
+ )
+ }
+ }
+ }
+
+ describe("For allowOriginPort:") {
+ val origins = Set("https://example.org", "https://accounts.example.org", "https://acme.com:8443")
+
+ describe("false,") {
+ check(
+ origins = origins,
+ acceptOrigins = List(
+ "https://example.org",
+ "https://accounts.example.org",
+ "https://acme.com:8443"
+ ),
+ rejectOrigins = List(
+ "https://example.org:8443",
+ "https://shop.example.org",
+ "https://acme.com",
+ "https://acme.com:9000"
+ ),
+ allowOriginPort = false
+ )
+ }
+
+ describe("true,") {
+ check(
+ origins = origins,
+ acceptOrigins = List(
+ "https://example.org",
+ "https://example.org:8443",
+ "https://accounts.example.org",
+ "https://acme.com",
+ "https://acme.com:8443",
+ "https://acme.com:9000"
+ ),
+ rejectOrigins = List(
+ "https://shop.example.org"
+ ),
+ allowOriginPort = true
+ )
+ }
+ }
+
+ describe("For allowOriginSubdomain:") {
+ val origins = Set("https://example.org", "https://acme.com:8443")
+
+ describe("false,") {
+ check(
+ origins = origins,
+ acceptOrigins = List(
+ "https://example.org",
+ "https://acme.com:8443"
+ ),
+ rejectOrigins = List(
+ "https://example.org:8443",
+ "https://accounts.example.org",
+ "https://acme.com",
+ "https://shop.acme.com:8443"
+ ),
+ allowOriginSubdomain = false
+ )
+ }
+
+ describe("true,") {
+ check(
+ origins = origins,
+ acceptOrigins = List(
+ "https://example.org",
+ "https://accounts.example.org",
+ "https://acme.com:8443",
+ "https://shop.acme.com:8443"
+ ),
+ rejectOrigins = List(
+ "https://example.org:8443",
+ "https://acme.com"
+ ),
+ allowOriginSubdomain = true
+ )
+ }
+ }
+ }
}
describe("6. Verify that the value of C.tokenBinding.status matches the state of Token Binding for the TLS connection over which the assertion was obtained.") {
@@ -717,11 +917,12 @@ class RelyingPartyRegistrationSpec extends FunSpec with Matchers with GeneratorD
describe("if x5c is not a certificate for an ECDSA public key over the P-256 curve, stop verification and return an error.") {
val testAuthenticator = TestAuthenticator
- def checkRejected(keypair: KeyPair): Unit = {
- val ((credential, _), _) = testAuthenticator.createBasicAttestedCredential(attestationCertAndKey = Some(testAuthenticator.generateAttestationCertificate(keypair)))
+ def checkRejected(attestationAlg: COSEAlgorithmIdentifier, keypair: KeyPair): Unit = {
+ val (credential, _) = testAuthenticator.createBasicAttestedCredential(attestationMaker = AttestationMaker.fidoU2f(new AttestationCert(attestationAlg, testAuthenticator.generateAttestationCertificate(attestationAlg, Some(keypair)))))
val steps = finishRegistration(
testData = RegistrationTestData(
+ alg = COSEAlgorithmIdentifier.ES256,
attestationObject = credential.getResponse.getAttestationObject,
clientDataJson = new String(credential.getResponse.getClientDataJSON.getBytes, "UTF-8")
),
@@ -744,11 +945,12 @@ class RelyingPartyRegistrationSpec extends FunSpec with Matchers with GeneratorD
standaloneVerification.failed.get shouldBe an [IllegalArgumentException]
}
- def checkAccepted(keypair: KeyPair): Unit = {
- val ((credential, _), _) = testAuthenticator.createBasicAttestedCredential(attestationCertAndKey = Some(testAuthenticator.generateAttestationCertificate(keypair)))
+ def checkAccepted(attestationAlg: COSEAlgorithmIdentifier, keypair: KeyPair): Unit = {
+ val (credential, _) = testAuthenticator.createBasicAttestedCredential(attestationMaker = AttestationMaker.fidoU2f(new AttestationCert(attestationAlg, testAuthenticator.generateAttestationCertificate(attestationAlg, Some(keypair)))))
val steps = finishRegistration(
testData = RegistrationTestData(
+ alg = COSEAlgorithmIdentifier.ES256,
attestationObject = credential.getResponse.getAttestationObject,
clientDataJson = new String(credential.getResponse.getClientDataJSON.getBytes, "UTF-8")
),
@@ -770,19 +972,19 @@ class RelyingPartyRegistrationSpec extends FunSpec with Matchers with GeneratorD
}
it("An RSA attestation certificate is rejected.") {
- checkRejected(testAuthenticator.generateRsaKeypair())
+ checkRejected(COSEAlgorithmIdentifier.RS256, testAuthenticator.generateRsaKeypair())
}
it("A secp256r1 attestation certificate is accepted.") {
- checkAccepted(testAuthenticator.generateEcKeypair(curve = "secp256r1"))
+ checkAccepted(COSEAlgorithmIdentifier.ES256, testAuthenticator.generateEcKeypair(curve = "secp256r1"))
}
it("A secp256k1 attestation certificate is rejected.") {
- checkRejected(testAuthenticator.generateEcKeypair(curve = "secp256k1"))
+ checkRejected(COSEAlgorithmIdentifier.ES256, testAuthenticator.generateEcKeypair(curve = "secp256k1"))
}
it("A P-256 attestation certificate is accepted.") {
- checkAccepted(testAuthenticator.generateEcKeypair(curve = "P-256"))
+ checkAccepted(COSEAlgorithmIdentifier.ES256, testAuthenticator.generateEcKeypair(curve = "P-256"))
}
}
}
@@ -884,6 +1086,16 @@ class RelyingPartyRegistrationSpec extends FunSpec with Matchers with GeneratorD
result should equal (Success(true))
}
+ it("Succeeds for an RS1 test case.") {
+ val testData = RegistrationTestData.Packed.BasicAttestationRs1
+
+ val result = verifier.verifyAttestationSignature(
+ new AttestationObject(testData.attestationObject),
+ testData.clientDataJsonHash
+ )
+ result should equal (true)
+ }
+
it("Fail if the default test case is mutated.") {
val testData = RegistrationTestData.Packed.BasicAttestation
@@ -904,12 +1116,13 @@ class RelyingPartyRegistrationSpec extends FunSpec with Matchers with GeneratorD
describe("2. Verify that attestnCert meets the requirements in §8.2.1 Packed Attestation Statement Certificate Requirements.") {
it("Fails for an attestation signature with an invalid country code.") {
val authenticator = TestAuthenticator
+ val alg = COSEAlgorithmIdentifier.ES256
val (badCert, key): (X509Certificate, PrivateKey) = authenticator.generateAttestationCertificate(
+ alg = alg,
name = new X500Name("O=Yubico, C=AA, OU=Authenticator Attestation")
)
- val ((credential, _), _) = authenticator.createBasicAttestedCredential(
- attestationCertAndKey = Some(badCert, key),
- attestationStatementFormat = "packed"
+ val (credential, _) = authenticator.createBasicAttestedCredential(
+ attestationMaker = AttestationMaker.packed(new AttestationCert(alg, (badCert, key))),
)
val result = Try(verifier.verifyAttestationSignature(credential.getResponse.getAttestation, sha256(credential.getResponse.getClientDataJSON)))
@@ -978,7 +1191,7 @@ class RelyingPartyRegistrationSpec extends FunSpec with Matchers with GeneratorD
step.tryNext shouldBe a [Success[_]]
step.attestationType should be (AttestationType.BASIC)
step.attestationTrustPath.asScala should not be empty
- step.attestationTrustPath.get.asScala should be (List(testData.packedAttestationCert))
+ step.attestationTrustPath.get.asScala should be (List(testData.packedAttestationCert, testData.attestationCaCert.get))
}
}
@@ -1017,14 +1230,33 @@ class RelyingPartyRegistrationSpec extends FunSpec with Matchers with GeneratorD
}
it("Fails if the alg is a different value.") {
- val testData = RegistrationTestData.Packed.SelfAttestationWithWrongAlgValue
+ def modifyAuthdataPubkeyAlg(authDataBytes: Array[Byte]): Array[Byte] = {
+ val authData = new AuthenticatorData(new ByteArray(authDataBytes))
+ val key = WebAuthnCodecs.importCosePublicKey(authData.getAttestedCredentialData.get.getCredentialPublicKey).asInstanceOf[RSAPublicKey]
+ val reencodedKey = WebAuthnTestCodecs.rsaPublicKeyToCose(key, COSEAlgorithmIdentifier.RS256)
+ new ByteArray(java.util.Arrays.copyOfRange(authDataBytes, 0, 32 + 1 + 4 + 16 + 2))
+ .concat(authData.getAttestedCredentialData.get.getCredentialId)
+ .concat(reencodedKey)
+ .getBytes
+ }
+ def modifyAttobjPubkeyAlg(attObjBytes: ByteArray): ByteArray = {
+ val attObj = JacksonCodecs.cbor.readTree(attObjBytes.getBytes)
+ new ByteArray(JacksonCodecs.cbor.writeValueAsBytes(
+ attObj.asInstanceOf[ObjectNode]
+ .set("authData", jsonFactory.binaryNode(modifyAuthdataPubkeyAlg(attObj.get("authData").binaryValue())))
+ ))
+ }
+
+ val testData = RegistrationTestData.Packed.SelfAttestationRs1
+ val attObj = new AttestationObject(modifyAttobjPubkeyAlg(testData.response.getResponse.getAttestationObject))
+
val result = Try(verifier.verifyAttestationSignature(
- new AttestationObject(testData.attestationObject),
+ attObj,
testData.clientDataJsonHash
))
- CBORObject.DecodeFromBytes(new AttestationObject(testData.attestationObject).getAuthenticatorData.getAttestedCredentialData.get.getCredentialPublicKey.getBytes).get(CBORObject.FromObject(3)).AsInt64 should equal (-7)
- new AttestationObject(testData.attestationObject).getAttestationStatement.get("alg").longValue should equal (-257)
+ CBORObject.DecodeFromBytes(attObj.getAuthenticatorData.getAttestedCredentialData.get.getCredentialPublicKey.getBytes).get(CBORObject.FromObject(3)).AsInt64 should equal (-257)
+ attObj.getAttestationStatement.get("alg").longValue should equal (-65535)
result shouldBe a [Failure[_]]
result.failed.get shouldBe an [IllegalArgumentException]
}
@@ -1039,6 +1271,18 @@ class RelyingPartyRegistrationSpec extends FunSpec with Matchers with GeneratorD
result should equal (true)
}
+ it("Succeeds for an RS1 test case.") {
+ val testData = RegistrationTestData.Packed.SelfAttestationRs1
+ val alg = WebAuthnCodecs.getCoseKeyAlg(testData.response.getResponse.getParsedAuthenticatorData.getAttestedCredentialData.get.getCredentialPublicKey).get
+ alg should be (COSEAlgorithmIdentifier.RS1)
+
+ val result = verifier.verifyAttestationSignature(
+ new AttestationObject(testData.attestationObject),
+ testData.clientDataJsonHash
+ )
+ result should equal (true)
+ }
+
it("Fails if the attestation object is mutated.") {
val testData = testDataBase.editAuthenticatorData { authData: ByteArray => new ByteArray(authData.getBytes.updated(16, if (authData.getBytes()(16) == 0) 1: Byte else 0: Byte)) }
val result = verifier.verifyAttestationSignature(
@@ -1578,7 +1822,7 @@ class RelyingPartyRegistrationSpec extends FunSpec with Matchers with GeneratorD
val steps = finishRegistration(
allowUntrustedAttestation = true,
testData = testData,
- credentialRepository = Some(credentialRepository)
+ credentialRepository = credentialRepository
)
val step: FinishRegistrationSteps#Step17 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next
@@ -1599,7 +1843,7 @@ class RelyingPartyRegistrationSpec extends FunSpec with Matchers with GeneratorD
val steps = finishRegistration(
allowUntrustedAttestation = true,
testData = testData,
- credentialRepository = Some(credentialRepository)
+ credentialRepository = credentialRepository
)
val step: FinishRegistrationSteps#Step17 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next
@@ -1614,7 +1858,7 @@ class RelyingPartyRegistrationSpec extends FunSpec with Matchers with GeneratorD
val steps = finishRegistration(
testData = testData,
metadataService = Some(new TestMetadataService(Some(Attestation.builder().trusted(true).build()))),
- credentialRepository = Some(emptyCredentialRepository)
+ credentialRepository = emptyCredentialRepository
)
steps.run.getKeyId.getId should be (testData.response.getId)
steps.run.isAttestationTrusted should be (true)
@@ -1627,7 +1871,7 @@ class RelyingPartyRegistrationSpec extends FunSpec with Matchers with GeneratorD
val steps = finishRegistration(
testData = testData,
allowUntrustedAttestation = true,
- credentialRepository = Some(emptyCredentialRepository)
+ credentialRepository = emptyCredentialRepository
)
steps.run.getKeyId.getId should be (testData.response.getId)
steps.run.isAttestationTrusted should be (false)
@@ -1638,7 +1882,7 @@ class RelyingPartyRegistrationSpec extends FunSpec with Matchers with GeneratorD
val steps = finishRegistration(
testData = testData,
allowUntrustedAttestation = true,
- credentialRepository = Some(emptyCredentialRepository)
+ credentialRepository = emptyCredentialRepository
)
val result = Try(steps.run)
result.failed.get shouldBe an [IllegalArgumentException]
@@ -1656,7 +1900,7 @@ class RelyingPartyRegistrationSpec extends FunSpec with Matchers with GeneratorD
testData = testData,
metadataService = None,
allowUntrustedAttestation = true,
- credentialRepository = Some(emptyCredentialRepository)
+ credentialRepository = emptyCredentialRepository
)
steps.run.getKeyId.getId should be (testData.response.getId)
steps.run.isAttestationTrusted should be (false)
@@ -1732,26 +1976,67 @@ class RelyingPartyRegistrationSpec extends FunSpec with Matchers with GeneratorD
result.getKeyId.getId should equal (RegistrationTestData.Tpm.PrivacyCa.response.getId)
}
- it("accept all test examples in the validExamples list.") {
- RegistrationTestData.validExamples.foreach { testData =>
- val rp = {
- val builder = RelyingParty.builder()
- .identity(testData.rpId)
- .credentialRepository(emptyCredentialRepository)
- testData.origin.foreach({ o => builder.origins(Set(o).asJava) })
- builder.build()
- }
+ describe("accept all test examples in the validExamples list.") {
+ RegistrationTestData.defaultSettingsValidExamples.zipWithIndex.foreach { case (testData, i) =>
+ it(s"Succeeds for example index ${i}.") {
+ val rp = {
+ val builder = RelyingParty.builder()
+ .identity(testData.rpId)
+ .credentialRepository(emptyCredentialRepository)
+ testData.origin.foreach({ o => builder.origins(Set(o).asJava) })
+ builder.build()
+ }
- val result = rp.finishRegistration(FinishRegistrationOptions.builder()
- .request(testData.request)
- .response(testData.response)
- .build()
- )
+ val result = rp.finishRegistration(FinishRegistrationOptions.builder()
+ .request(testData.request)
+ .response(testData.response)
+ .build()
+ )
- result.getKeyId.getId should equal (testData.response.getId)
+ result.getKeyId.getId should equal (testData.response.getId)
+ }
}
}
+ describe("generate pubKeyCredParams which") {
+ val rp = RelyingParty.builder()
+ .identity(RelyingPartyIdentity.builder().id("localhost").name("Test RP").build())
+ .credentialRepository(emptyCredentialRepository)
+ .build()
+ val pkcco = rp.startRegistration(StartRegistrationOptions.builder()
+ .user(UserIdentity.builder()
+ .name("foo")
+ .displayName("Foo")
+ .id(ByteArray.fromHex("aabbccdd"))
+ .build())
+ .build())
+
+ val pubKeyCredParams = pkcco.getPubKeyCredParams.asScala
+
+ describe("include") {
+ it("ES256.") {
+ pubKeyCredParams should contain (PublicKeyCredentialParameters.ES256)
+ pubKeyCredParams map (_.getAlg) should contain (COSEAlgorithmIdentifier.ES256)
+ }
+
+ it("EdDSA.") {
+ pubKeyCredParams should contain (PublicKeyCredentialParameters.EdDSA)
+ pubKeyCredParams map (_.getAlg) should contain (COSEAlgorithmIdentifier.EdDSA)
+ }
+
+ it("RS256.") {
+ pubKeyCredParams should contain (PublicKeyCredentialParameters.RS256)
+ pubKeyCredParams map (_.getAlg) should contain (COSEAlgorithmIdentifier.RS256)
+ }
+ }
+
+ describe("do not include") {
+ it("RS1.") {
+ pubKeyCredParams should not contain PublicKeyCredentialParameters.RS1
+ pubKeyCredParams map (_.getAlg) should not contain COSEAlgorithmIdentifier.RS1
+ }
+ }
+ }
}
describe("RelyingParty supports registering") {
@@ -1778,4 +2063,39 @@ class RelyingPartyRegistrationSpec extends FunSpec with Matchers with GeneratorD
}
+ describe("Additions in L2-WD02 editor's draft:") {
+
+ describe("Verify that the \"alg\" parameter in the credential public key in authData matches the alg attribute of one of the items in options.pubKeyCredParams.") {
+ it("An ES256 key succeeds if ES256 was a requested algorithm.") {
+ val testData = RegistrationTestData.FidoU2f.BasicAttestation
+ val result = finishRegistration(
+ testData = testData,
+ credentialRepository = emptyCredentialRepository,
+ allowUntrustedAttestation = true
+ ).run
+
+ result should not be null
+ result.getPublicKeyCose should not be null
+ }
+
+ it("An ES256 key fails if only RSA and EdDSA are allowed.") {
+ val testData = RegistrationTestData.FidoU2f.BasicAttestation
+ val result = Try(finishRegistration(
+ testData = testData.copy(
+ overrideRequest = Some(testData.request.toBuilder
+ .pubKeyCredParams(List(PublicKeyCredentialParameters.EdDSA, PublicKeyCredentialParameters.RS256).asJava)
+ .build()
+ )
+ ),
+ credentialRepository = emptyCredentialRepository,
+ allowUntrustedAttestation = true
+ ).run)
+
+ result shouldBe a [Failure[_]]
+ result.failed.get shouldBe an [IllegalArgumentException]
+ }
+ }
+
+ }
+
}
diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyUserIdentificationSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyUserIdentificationSpec.scala
index 7f1941a68..5d3aa898d 100644
--- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyUserIdentificationSpec.scala
+++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyUserIdentificationSpec.scala
@@ -133,7 +133,7 @@ class RelyingPartyUserIdentificationSpec extends FunSpec with Matchers {
Some(RegisteredCredential.builder()
.credentialId(Defaults.credentialId)
.userHandle(Defaults.userHandle)
- .publicKeyCose(WebAuthnCodecs.ecPublicKeyToCose(Defaults.credentialKey.getPublic.asInstanceOf[ECPublicKey]))
+ .publicKeyCose(WebAuthnTestCodecs.ecPublicKeyToCose(Defaults.credentialKey.getPublic.asInstanceOf[ECPublicKey]))
.signatureCount(0)
.build()
).asJava
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 91b52f918..c725e5387 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
@@ -45,7 +45,6 @@ import java.security.spec.ECPublicKeySpec
import java.security.spec.PKCS8EncodedKeySpec
import java.security.spec.X509EncodedKeySpec
import java.time.Instant
-import java.util.Base64
import java.util.Date
import com.fasterxml.jackson.databind.JsonNode
@@ -56,6 +55,7 @@ import com.yubico.internal.util.BinaryUtil
import com.yubico.internal.util.CertificateParser
import com.yubico.internal.util.JacksonCodecs
import com.yubico.internal.util.scala.JavaConverters._
+import com.yubico.webauthn.data.AttestationObject
import com.yubico.webauthn.data.AuthenticatorAssertionResponse
import com.yubico.webauthn.data.AuthenticatorAttestationResponse
import com.yubico.webauthn.data.AuthenticatorData
@@ -64,9 +64,7 @@ import com.yubico.webauthn.data.ClientAssertionExtensionOutputs
import com.yubico.webauthn.data.ClientRegistrationExtensionOutputs
import com.yubico.webauthn.data.COSEAlgorithmIdentifier
import com.yubico.webauthn.data.PublicKeyCredential
-import com.yubico.webauthn.data.PublicKeyCredentialCreationOptions
-import com.yubico.webauthn.data.PublicKeyCredentialParameters
-import com.yubico.webauthn.data.RelyingPartyIdentity
+import com.yubico.webauthn.data.PublicKeyCredentialRequestOptions
import com.yubico.webauthn.data.UserIdentity
import com.yubico.webauthn.test.Util
import org.bouncycastle.asn1.ASN1ObjectIdentifier
@@ -105,7 +103,7 @@ object TestAuthenticator {
println(generateAttestationCertificate())
- val ((credential, _), _) = createBasicAttestedCredential(attestationStatementFormat = "packed")
+ val (credential, _) = createBasicAttestedCredential(attestationMaker = AttestationMaker.packed(AttestationSigner.selfsigned(COSEAlgorithmIdentifier.ES256)))
println(credential)
println(s"Client data: ${new String(credential.getResponse.getClientDataJSON.getBytes, "UTF-8")}")
@@ -151,16 +149,101 @@ object TestAuthenticator {
private def sha256(s: String): ByteArray = sha256(toBytes(s))
private def sha256(b: ByteArray): ByteArray = new ByteArray(MessageDigest.getInstance("SHA-256", javaCryptoProvider).digest(b.getBytes))
+
+ sealed trait AttestationMaker {
+ val format: String
+ def makeAttestationStatement(authDataBytes: ByteArray, clientDataJson: String): JsonNode
+ def attestationCert: Option[X509Certificate] = ???
+
+ def makeAttestationObjectBytes(
+ authDataBytes: ByteArray,
+ clientDataJson: String,
+ ): ByteArray = {
+ val f = JsonNodeFactory.instance
+ val attObj = f.objectNode().setAll(Map(
+ "authData" -> f.binaryNode(authDataBytes.getBytes),
+ "fmt" -> f.textNode(format),
+ "attStmt" -> makeAttestationStatement(authDataBytes, clientDataJson),
+ ).asJava)
+ new ByteArray(JacksonCodecs.cbor.writeValueAsBytes(attObj))
+ }
+ }
+ object AttestationMaker {
+ def default(): AttestationMaker = packed(AttestationSigner.selfsigned(COSEAlgorithmIdentifier.ES256))
+
+ def packed(signer: AttestationSigner): AttestationMaker = new AttestationMaker {
+ override val format = "packed"
+ override def attestationCert: Option[X509Certificate] = Some(signer.cert)
+ override def makeAttestationStatement(authDataBytes: ByteArray, clientDataJson: String): JsonNode =
+ makePackedAttestationStatement(authDataBytes, clientDataJson, signer)
+ }
+ def fidoU2f(signer: AttestationSigner): AttestationMaker = new AttestationMaker {
+ override val format = "fido-u2f"
+ override def attestationCert: Option[X509Certificate] = Some(signer.cert)
+ override def makeAttestationStatement(authDataBytes: ByteArray, clientDataJson: String): JsonNode =
+ makeU2fAttestationStatement(authDataBytes, clientDataJson, signer)
+ }
+ def androidSafetynet(cert: AttestationCert, ctsProfileMatch: Boolean = true): AttestationMaker = new AttestationMaker {
+ override val format = "android-safetynet"
+ override def attestationCert: Option[X509Certificate] = Some(cert.cert)
+ override def makeAttestationStatement(authDataBytes: ByteArray, clientDataJson: String): JsonNode =
+ makeAndroidSafetynetAttestationStatement(authDataBytes, clientDataJson, cert, ctsProfileMatch = ctsProfileMatch)
+ }
+ def none(): AttestationMaker = new AttestationMaker {
+ override val format = "none"
+ override def attestationCert: Option[X509Certificate] = None
+ override def makeAttestationStatement(authDataBytes: ByteArray, clientDataJson: String): JsonNode =
+ makeNoneAttestationStatement()
+ }
+ }
+
+ sealed trait AttestationSigner { def key: PrivateKey; def alg: COSEAlgorithmIdentifier; def cert: X509Certificate }
+ case class SelfAttestation(keypair: KeyPair, alg: COSEAlgorithmIdentifier) extends AttestationSigner {
+ def key: PrivateKey = keypair.getPrivate
+ def cert: X509Certificate = generateAttestationCertificate(alg = alg, keypair = Some(keypair))._1
+ }
+ case class AttestationCert(cert: X509Certificate, key: PrivateKey, alg: COSEAlgorithmIdentifier, chain: List[X509Certificate]) extends AttestationSigner {
+ def this(alg: COSEAlgorithmIdentifier, keypair: (X509Certificate, PrivateKey)) = this(keypair._1, keypair._2, alg, Nil)
+ }
+ object AttestationSigner {
+ def ca(alg: COSEAlgorithmIdentifier, certSubject: X500Name = new X500Name("CN=Yubico WebAuthn unit tests CA, O=Yubico, OU=Authenticator Attestation, C=SE")): AttestationCert = {
+ val (caCert, caKey) = generateAttestationCaCertificate(signingAlg = alg, name = certSubject)
+ val (cert, key) = generateAttestationCertificate(alg, caCertAndKey = Some((caCert, caKey)), name = certSubject)
+ AttestationCert(cert, key, alg, List(caCert))
+ }
+
+ def selfsigned(alg: COSEAlgorithmIdentifier): AttestationCert = {
+ val (cert, key) = generateAttestationCertificate(alg = alg)
+ AttestationCert(cert, key, alg, Nil)
+ }
+ }
+
+
def makeCreateCredentialExample(publicKeyCredential: PublicKeyCredential[AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs]): String =
s"""Attestation object: ${publicKeyCredential.getResponse.getAttestationObject.getHex}
|Client data: ${publicKeyCredential.getResponse.getClientDataJSON.getHex}
""".stripMargin
+ def makeAssertionExample(alg: COSEAlgorithmIdentifier): String = {
+ val (credential, keypair) = createCredential(attestationMaker = AttestationMaker.default())
+ val assertion = createAssertion(alg, credentialKey = keypair)
+
+ s"""
+ |val keyAlgorithm: COSEAlgorithmIdentifier = COSEAlgorithmIdentifier.${alg.name}
+ |val authenticatorData: ByteArray = ByteArray.fromHex("${assertion.getResponse.getAuthenticatorData.getHex}")
+ |val clientDataJson: String = "\""${new String(assertion.getResponse.getClientDataJSON.getBytes, StandardCharsets.UTF_8)}""\"
+ |val credentialId: ByteArray = ByteArray.fromBase64Url("${assertion.getId.getBase64Url}")
+ |val credentialKey: KeyPair = TestAuthenticator.importEcKeypair(
+ | privateBytes = ByteArray.fromHex("${new ByteArray(keypair.getPrivate.getEncoded).getHex}"),
+ | publicBytes = ByteArray.fromHex("${new ByteArray(keypair.getPublic.getEncoded).getHex}")
+ |)
+ |val signature: ByteArray = ByteArray.fromHex("${assertion.getResponse.getSignature.getHex}")
+ """.stripMargin
+ }
+
private def createCredential(
aaguid: ByteArray = Defaults.aaguid,
- alg: Option[COSEAlgorithmIdentifier] = None,
- attestationCertAndKey: Option[(X509Certificate, PrivateKey)] = None,
- attestationStatementFormat: String = "fido-u2f",
+ attestationMaker: AttestationMaker,
authenticatorExtensions: Option[JsonNode] = None,
challenge: ByteArray = Defaults.challenge,
clientData: Option[JsonNode] = None,
@@ -169,20 +252,11 @@ object TestAuthenticator {
keyAlgorithm: COSEAlgorithmIdentifier = Defaults.keyAlgorithm,
origin: String = Defaults.origin,
rpId: String = Defaults.rpId,
- safetynetCtsProfileMatch: Boolean = true,
tokenBindingStatus: String = Defaults.TokenBinding.status,
tokenBindingId: Option[String] = Defaults.TokenBinding.id,
userId: UserIdentity = UserIdentity.builder().name("Test").displayName("Test").id(new ByteArray(Array(42, 13, 37))).build(),
- useSelfAttestation: Boolean = false
): (data.PublicKeyCredential[data.AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs], KeyPair) = {
- val options = PublicKeyCredentialCreationOptions.builder()
- .rp(RelyingPartyIdentity.builder().id(rpId).name("Test party").build())
- .user(userId)
- .challenge(challenge)
- .pubKeyCredParams(List(PublicKeyCredentialParameters.builder().alg(keyAlgorithm).build()).asJava)
- .build()
-
val clientDataJson: String = JacksonCodecs.json.writeValueAsString(clientData getOrElse {
val json: ObjectNode = jsonFactory.objectNode()
@@ -211,9 +285,9 @@ object TestAuthenticator {
val keypair = credentialKeypair.getOrElse(generateKeypair(algorithm = keyAlgorithm))
val publicKeyCose = keypair.getPublic match {
- case pub: ECPublicKey => WebAuthnCodecs.ecPublicKeyToCose(pub)
- case pub: BCEdDSAPublicKey => WebAuthnCodecs.eddsaPublicKeyToCose(pub)
- case pub: RSAPublicKey => WebAuthnCodecs.rsaPublicKeyToCose(pub)
+ case pub: ECPublicKey => WebAuthnTestCodecs.ecPublicKeyToCose(pub)
+ case pub: BCEdDSAPublicKey => WebAuthnTestCodecs.eddsaPublicKeyToCose(pub)
+ case pub: RSAPublicKey => WebAuthnTestCodecs.rsaPublicKeyToCose(pub, keyAlgorithm)
}
val authDataBytes: ByteArray = makeAuthDataBytes(
@@ -225,15 +299,7 @@ object TestAuthenticator {
))
)
- val attestationObjectBytes = makeAttestationObjectBytes(
- authDataBytes,
- attestationStatementFormat,
- clientDataJson,
- attestationCertAndKey,
- selfAttestationKey = if (useSelfAttestation) Some(credentialKeypair.get.getPrivate) else None,
- alg = alg,
- safetynetCtsProfileMatch = safetynetCtsProfileMatch
- )
+ val attestationObjectBytes = attestationMaker.makeAttestationObjectBytes(authDataBytes, clientDataJson)
val response = AuthenticatorAttestationResponse.builder()
.attestationObject(attestationObjectBytes)
@@ -252,71 +318,57 @@ object TestAuthenticator {
def createBasicAttestedCredential(
aaguid: ByteArray = Defaults.aaguid,
- attestationCertAndKey: Option[(X509Certificate, PrivateKey)] = None,
- attestationStatementFormat: String = "fido-u2f",
- generateRsaCert: Boolean = false,
- certSubject: Option[X500Name] = None,
+ attestationMaker: AttestationMaker,
keyAlgorithm: COSEAlgorithmIdentifier = Defaults.keyAlgorithm,
- safetynetCtsProfileMatch: Boolean = true
- ): ((data.PublicKeyCredential[data.AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs], KeyPair), Option[X509Certificate]) = {
- val (caCert, generatedAttestationCertAndKey) = attestationCertAndKey match {
- case None =>
- val (caCert, caKey) = generateAttestationCaCertificate()
- (
- Some(caCert),
- Some(
- certSubject match {
- case Some(name) => generateAttestationCertificate(caCertAndKey = Some((caCert, caKey)), generateRsa = generateRsaCert, name = name)
- case None => generateAttestationCertificate(caCertAndKey = Some((caCert, caKey)), generateRsa = generateRsaCert)
- }
- )
- )
- case Some(_) => (None, None)
- }
-
- (
- createCredential(
- aaguid = aaguid,
- attestationCertAndKey = attestationCertAndKey orElse generatedAttestationCertAndKey,
- attestationStatementFormat = attestationStatementFormat,
- keyAlgorithm = keyAlgorithm,
- safetynetCtsProfileMatch = safetynetCtsProfileMatch
- ),
- caCert
+ ): (data.PublicKeyCredential[data.AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs], KeyPair) =
+ createCredential(
+ aaguid = aaguid,
+ attestationMaker = attestationMaker,
+ keyAlgorithm = keyAlgorithm,
)
- }
def createSelfAttestedCredential(
- attestationStatementFormat: String = "fido-u2f",
- alg: Option[COSEAlgorithmIdentifier] = None
- ): ((data.PublicKeyCredential[data.AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs], KeyPair), Option[Nothing]) = {
- val keypair = generateEcKeypair()
- (
- attestationStatementFormat match {
- case "fido-u2f" =>
- createCredential(
- attestationCertAndKey = Some(generateAttestationCertificate (keypair) ),
- attestationStatementFormat = attestationStatementFormat,
- credentialKeypair = Some(keypair),
- alg = alg
- )
- case "packed" =>
- createCredential(
- attestationCertAndKey = None,
- attestationStatementFormat = attestationStatementFormat,
- credentialKeypair = Some(keypair),
- useSelfAttestation = true,
- alg = alg
- )
- },
- None
+ attestationMaker: SelfAttestation => AttestationMaker,
+ keyAlgorithm: COSEAlgorithmIdentifier = Defaults.keyAlgorithm,
+ ): (data.PublicKeyCredential[data.AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs], KeyPair) = {
+ val keypair = generateKeypair(keyAlgorithm)
+ val signer = SelfAttestation(keypair, keyAlgorithm)
+ createCredential(
+ attestationMaker = attestationMaker(signer),
+ credentialKeypair = Some(keypair),
+ keyAlgorithm = keyAlgorithm
)
}
- def createUnattestedCredential(): ((PublicKeyCredential[AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs], KeyPair), Option[X509Certificate]) =
- (createCredential(attestationStatementFormat = "none"), None)
+ def createUnattestedCredential(): (PublicKeyCredential[AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs], KeyPair) =
+ createCredential(attestationMaker = AttestationMaker.none())
+
+ def createAssertionFromTestData(
+ testData: RegistrationTestData,
+ request: PublicKeyCredentialRequestOptions,
+ origin: String = Defaults.origin,
+ tokenBindingStatus: String = Defaults.TokenBinding.status,
+ tokenBindingId: Option[String] = Defaults.TokenBinding.id,
+ withUserHandle: Boolean = false,
+ ): data.PublicKeyCredential[AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs] = {
+ createAssertion(
+ alg = testData.alg,
+ authenticatorExtensions = None,
+ challenge = request.getChallenge,
+ clientData = None,
+ clientExtensions = ClientAssertionExtensionOutputs.builder().build(),
+ credentialId = testData.response.getId,
+ credentialKey = testData.keypair.get,
+ origin = origin,
+ rpId = testData.rpId.getId,
+ tokenBindingStatus = tokenBindingStatus,
+ tokenBindingId = tokenBindingId,
+ userHandle = if (withUserHandle) Some(testData.userId.getId) else None,
+ )
+ }
def createAssertion(
+ alg: COSEAlgorithmIdentifier = COSEAlgorithmIdentifier.ES256,
authenticatorExtensions: Option[JsonNode] = None,
challenge: ByteArray = Defaults.challenge,
clientData: Option[JsonNode] = None,
@@ -327,7 +379,7 @@ object TestAuthenticator {
rpId: String = Defaults.rpId,
tokenBindingStatus: String = Defaults.TokenBinding.status,
tokenBindingId: Option[String] = Defaults.TokenBinding.id,
- userHandle: Option[ByteArray] = None
+ userHandle: Option[ByteArray] = None,
): data.PublicKeyCredential[data.AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs] = {
val clientDataJson: String = JacksonCodecs.json.writeValueAsString(clientData getOrElse {
@@ -365,7 +417,8 @@ object TestAuthenticator {
makeAssertionSignature(
authDataBytes,
crypto.hash(clientDataJsonBytes),
- credentialKey.getPrivate
+ credentialKey.getPrivate,
+ alg
)
)
.userHandle(userHandle.asJava)
@@ -378,40 +431,28 @@ object TestAuthenticator {
.build()
}
- def makeAttestationObjectBytes(
- authDataBytes: ByteArray,
- format: String,
- clientDataJson: String,
- certAndKey: Option[(X509Certificate, PrivateKey)],
- selfAttestationKey: Option[PrivateKey] = None,
- alg: Option[COSEAlgorithmIdentifier] = None,
- safetynetCtsProfileMatch: Boolean = true
- ): ByteArray = {
- val makeAttestationStatement: (ByteArray, String, Option[(X509Certificate, PrivateKey)]) => JsonNode = format match {
- case "android-safetynet" => makeAndroidSafetynetAttestationStatement(_, _, _, ctsProfileMatch = safetynetCtsProfileMatch)
- case "fido-u2f" => makeU2fAttestationStatement
- case "none" => makeNoneAttestationStatement
- case "packed" => makePackedAttestationStatement(_, _, _, selfAttestationKey = selfAttestationKey, alg = alg)
- }
-
- val f = JsonNodeFactory.instance
- val attObj = f.objectNode().setAll(Map(
- "authData" -> f.binaryNode(authDataBytes.getBytes),
- "fmt" -> f.textNode(format),
- "attStmt" -> makeAttestationStatement(authDataBytes, clientDataJson, certAndKey)
- ).asJava)
-
- new ByteArray(JacksonCodecs.cbor.writeValueAsBytes(attObj))
- }
-
def makeU2fAttestationStatement(
authDataBytes: ByteArray,
clientDataJson: String,
- attestationCertAndKey: Option[(X509Certificate, PrivateKey)] = None
+ signer: AttestationSigner,
): JsonNode = {
- val (cert, key) = attestationCertAndKey getOrElse generateAttestationCertificate()
val authData = new AuthenticatorData(authDataBytes)
- val signedData = makeU2fSignedData(
+
+ def makeSignedData(
+ rpIdHash: ByteArray,
+ clientDataJson: String,
+ credentialId: ByteArray,
+ credentialPublicKeyRawBytes: ByteArray
+ ): ByteArray = {
+ new ByteArray((Vector[Byte](0)
+ ++ rpIdHash.getBytes
+ ++ crypto.hash(clientDataJson).getBytes
+ ++ credentialId.getBytes
+ ++ credentialPublicKeyRawBytes.getBytes
+ ).toArray)
+ }
+
+ val signedData = makeSignedData(
authData.getRpIdHash,
clientDataJson,
authData.getAttestedCredentialData.get.getCredentialId,
@@ -420,61 +461,41 @@ object TestAuthenticator {
val f = JsonNodeFactory.instance
f.objectNode().setAll(Map(
- "x5c" -> f.arrayNode().add(f.binaryNode(cert.getEncoded)),
+ "x5c" -> f.arrayNode().add(f.binaryNode(signer.cert.getEncoded)),
"sig" -> f.binaryNode(
sign(
signedData,
- key
+ signer.key,
+ signer.alg
).getBytes
)
).asJava)
}
- def makeNoneAttestationStatement(
- authDataBytes: ByteArray,
- clientDataJson: String,
- attestationCertAndKey: Option[(X509Certificate, PrivateKey)] = None
- ): JsonNode = JsonNodeFactory.instance.objectNode()
-
- def makeU2fSignedData(
- rpIdHash: ByteArray,
- clientDataJson: String,
- credentialId: ByteArray,
- credentialPublicKeyRawBytes: ByteArray
- ): ByteArray = {
- new ByteArray((Vector[Byte](0)
- ++ rpIdHash.getBytes
- ++ crypto.hash(clientDataJson).getBytes
- ++ credentialId.getBytes
- ++ credentialPublicKeyRawBytes.getBytes
- ).toArray)
- }
+ def makeNoneAttestationStatement(): JsonNode = JsonNodeFactory.instance.objectNode()
def makePackedAttestationStatement(
authDataBytes: ByteArray,
clientDataJson: String,
- attestationCertAndKey: Option[(X509Certificate, PrivateKey)] = None,
- selfAttestationKey: Option[PrivateKey] = None,
- alg: Option[COSEAlgorithmIdentifier] = None
+ signer: AttestationSigner,
): JsonNode = {
- val (cert, key) = selfAttestationKey match {
- case Some(key) => (null, key)
- case None => attestationCertAndKey getOrElse generateAttestationCertificate()
- }
-
val signedData = new ByteArray(authDataBytes.getBytes ++ crypto.hash(clientDataJson).getBytes)
- val signature = sign(signedData, key)
+ val signature = signer match {
+ case SelfAttestation(keypair, alg) => sign(signedData, keypair.getPrivate, alg)
+ case AttestationCert(_, key, alg, _) => sign(signedData, key, alg)
+ }
val f = JsonNodeFactory.instance
f.objectNode().setAll(
(
- Map("sig" -> f.binaryNode(signature.getBytes))
- ++ (
- selfAttestationKey match {
- case Some(key) => Map("alg" -> f.numberNode((alg getOrElse coseAlgorithmOfJavaKey(key)).getId))
- case None => Map("x5c" -> f.arrayNode().add(f.binaryNode(cert.getEncoded)))
- }
- )
+ Map(
+ "alg" -> f.numberNode(signer.alg.getId),
+ "sig" -> f.binaryNode(signature.getBytes)
+ ) ++ (signer match {
+ case _: SelfAttestation => Map.empty
+ case AttestationCert(cert, _, _, chain) =>
+ Map("x5c" -> f.arrayNode().addAll((cert +: chain).map(crt => f.binaryNode(crt.getEncoded)).asJava))
+ })
).asJava
)
}
@@ -482,18 +503,17 @@ object TestAuthenticator {
def makeAndroidSafetynetAttestationStatement(
authDataBytes: ByteArray,
clientDataJson: String,
- attestationCertAndKey: Option[(X509Certificate, PrivateKey)] = None,
+ cert: AttestationCert,
ctsProfileMatch: Boolean = true
): JsonNode = {
- val (cert, key) = attestationCertAndKey getOrElse generateAttestationCertificate()
-
val nonce = crypto.hash(authDataBytes concat crypto.hash(clientDataJson))
val f = JsonNodeFactory.instance
val jwsHeader = f.objectNode().setAll(Map(
"alg" -> f.textNode("RS256"),
- "x5c" -> f.arrayNode().add(f.textNode(new ByteArray(cert.getEncoded).getBase64))
+ "x5c" -> f.arrayNode()
+ .addAll((cert.cert +: cert.chain).map(crt => f.textNode(new ByteArray(crt.getEncoded).getBase64)).asJava)
).asJava)
val jwsHeaderBase64 = new ByteArray(JacksonCodecs.json().writeValueAsBytes(jwsHeader)).getBase64Url
@@ -510,7 +530,7 @@ object TestAuthenticator {
val jwsSignedCompact = jwsHeaderBase64 + "." + jwsPayloadBase64
val jwsSignedBytes = new ByteArray(jwsSignedCompact.getBytes(StandardCharsets.UTF_8))
- val jwsSignature = sign(jwsSignedBytes, key)
+ val jwsSignature = sign(jwsSignedBytes, cert.key, cert.alg)
val jwsCompact = jwsSignedCompact + "." + jwsSignature.getBase64Url
@@ -552,11 +572,11 @@ object TestAuthenticator {
).toArray)
}
- def makeAssertionSignature(authenticatorData: ByteArray, clientDataHash: ByteArray, key: PrivateKey): ByteArray =
- sign(authenticatorData.concat(clientDataHash), key)
+ def makeAssertionSignature(authenticatorData: ByteArray, clientDataHash: ByteArray, key: PrivateKey, alg: COSEAlgorithmIdentifier = COSEAlgorithmIdentifier.ES256): ByteArray =
+ sign(authenticatorData.concat(clientDataHash), key, alg)
- def sign(data: ByteArray, key: PrivateKey): ByteArray = {
- val sig = Signature.getInstance("SHA256with" + key.getAlgorithm, javaCryptoProvider)
+ def sign(data: ByteArray, key: PrivateKey, alg: COSEAlgorithmIdentifier): ByteArray = {
+ val sig = Signature.getInstance(WebAuthnCodecs.getJavaAlgorithmName(alg), javaCryptoProvider)
sig.initSign(key)
sig.update(data.getBytes)
new ByteArray(sig.sign())
@@ -566,6 +586,7 @@ object TestAuthenticator {
case COSEAlgorithmIdentifier.EdDSA => generateEddsaKeypair()
case COSEAlgorithmIdentifier.ES256 => generateEcKeypair()
case COSEAlgorithmIdentifier.RS256 => generateRsaKeypair()
+ case COSEAlgorithmIdentifier.RS1 => generateRsaKeypair()
}
def generateEcKeypair(curve: String = "P-256"): KeyPair = {
@@ -594,7 +615,7 @@ object TestAuthenticator {
g.generateKeyPair()
}
- def verifySignature(
+ def verifyEcSignature(
pubKey: PublicKey,
signedDataBytes: ByteArray,
signatureBytes: ByteArray
@@ -604,7 +625,7 @@ object TestAuthenticator {
sig.update(signedDataBytes.getBytes)
sig.verify(signatureBytes.getBytes) &&
- crypto.verifySignature(pubKey, signedDataBytes, signatureBytes)
+ crypto.verifySignature(pubKey, signedDataBytes, signatureBytes, COSEAlgorithmIdentifier.ES256)
}
def verifyU2fExampleWithCert(
@@ -614,7 +635,7 @@ object TestAuthenticator {
): Unit = {
val attestationCert: X509Certificate = CertificateParser.parseDer(attestationCertBytes.getBytes)
val pubKey: PublicKey = attestationCert.getPublicKey
- verifySignature(pubKey, signedDataBytes, signatureBytes)
+ verifyEcSignature(pubKey, signedDataBytes, signatureBytes)
}
def verifyU2fExampleWithExplicitParams(
@@ -626,39 +647,40 @@ object TestAuthenticator {
val namedSpec = ECNamedCurveTable.getParameterSpec("P-256")
val curveSpec: ECNamedCurveSpec = new ECNamedCurveSpec("P-256", namedSpec.getCurve, namedSpec.getG, namedSpec.getN)
val pubKeySpec: ECPublicKeySpec = new ECPublicKeySpec(pubKeyPoint, curveSpec)
- val pubKey: PublicKey = KeyFactory.getInstance("ECDSA", javaCryptoProvider).generatePublic(pubKeySpec)
- verifySignature(pubKey, signedDataBytes, signatureBytes)
+ val pubKey: PublicKey = KeyFactory.getInstance("EC", javaCryptoProvider).generatePublic(pubKeySpec)
+ verifyEcSignature(pubKey, signedDataBytes, signatureBytes)
}
def generateAttestationCaCertificate(
- keypair: KeyPair = generateEcKeypair(),
+ keypair: Option[KeyPair] = None,
+ signingAlg: COSEAlgorithmIdentifier = COSEAlgorithmIdentifier.ES256,
name: X500Name = new X500Name("CN=Yubico WebAuthn unit tests CA, O=Yubico, OU=Authenticator Attestation, C=SE"),
superCa: Option[(X509Certificate, PrivateKey)] = None,
extensions: Iterable[(String, Boolean, ASN1Primitive)] = Nil
): (X509Certificate, PrivateKey) = {
+ val actualKeypair = keypair.getOrElse(generateKeypair(signingAlg))
(
buildCertificate(
- publicKey = keypair.getPublic,
+ publicKey = actualKeypair.getPublic,
issuerName = superCa map (_._1) map JcaX500NameUtil.getSubject getOrElse name,
subjectName = name,
- signingKey = superCa map (_._2) getOrElse keypair.getPrivate,
+ signingKey = superCa map (_._2) getOrElse actualKeypair.getPrivate,
+ signingAlg = signingAlg,
isCa = true,
extensions = extensions
),
- keypair.getPrivate
+ actualKeypair.getPrivate
)
}
def generateAttestationCertificate(
- keypair: KeyPair = generateEcKeypair(),
+ alg: COSEAlgorithmIdentifier = COSEAlgorithmIdentifier.ES256,
+ keypair: Option[KeyPair] = None,
name: X500Name = new X500Name("CN=Yubico WebAuthn unit tests, O=Yubico, OU=Authenticator Attestation, C=SE"),
extensions: Iterable[(String, Boolean, ASN1Primitive)] = List(("1.3.6.1.4.1.45724.1.1.4", false, new DEROctetString(Defaults.aaguid.getBytes))),
caCertAndKey: Option[(X509Certificate, PrivateKey)] = None,
- generateRsa: Boolean = false
): (X509Certificate, PrivateKey) = {
- val actualKeypair =
- if (generateRsa) generateRsaKeypair()
- else keypair
+ val actualKeypair = keypair.getOrElse(generateKeypair(alg))
(
buildCertificate(
@@ -666,6 +688,7 @@ object TestAuthenticator {
issuerName = caCertAndKey.map(_._1).map(JcaX500NameUtil.getSubject).getOrElse(name),
subjectName = name,
signingKey = caCertAndKey.map(_._2).getOrElse(actualKeypair.getPrivate),
+ signingAlg = alg,
isCa = false,
extensions = extensions
),
@@ -678,6 +701,7 @@ object TestAuthenticator {
issuerName: X500Name,
subjectName: X500Name,
signingKey: PrivateKey,
+ signingAlg: COSEAlgorithmIdentifier,
isCa: Boolean = false,
extensions: Iterable[(String, Boolean, ASN1Primitive)] = Nil
): X509Certificate = {
@@ -699,12 +723,12 @@ object TestAuthenticator {
builder.addExtension(Extension.basicConstraints, true, new BasicConstraints(true));
}
- builder.build(new JcaContentSignerBuilder("SHA256with" + signingKey.getAlgorithm).setProvider(javaCryptoProvider).build(signingKey)).getEncoded
+ builder.build(new JcaContentSignerBuilder(WebAuthnCodecs.getJavaAlgorithmName(signingAlg)).setProvider(javaCryptoProvider).build(signingKey)).getEncoded
})
}
def generateRsaCertificate(): (X509Certificate, PrivateKey) =
- generateAttestationCertificate(keypair = generateRsaKeypair())
+ generateAttestationCertificate(COSEAlgorithmIdentifier.RS256)
def importCertAndKeyFromPem(certPem: InputStream, keyPem: InputStream): (X509Certificate, PrivateKey) = {
val cert: X509Certificate = Util.importCertFromPem(certPem)
@@ -722,13 +746,6 @@ object TestAuthenticator {
(cert, key)
}
- def toPem(cert: X509Certificate): String = (
- "-----BEGIN CERTIFICATE-----\n"
- + Base64.getMimeEncoder(64, System.getProperty("line.separator").getBytes("UTF-8"))
- .encodeToString(cert.getEncoded)
- + "\n-----END CERTIFICATE-----\n"
- )
-
def coseAlgorithmOfJavaKey(key: PrivateKey): COSEAlgorithmIdentifier =
Try(COSEAlgorithmIdentifier.valueOf(key.getAlgorithm)) getOrElse
key match {
diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/WebAuthnCodecsSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/WebAuthnCodecsSpec.scala
index 0883e0aa5..9b5391778 100644
--- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/WebAuthnCodecsSpec.scala
+++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/WebAuthnCodecsSpec.scala
@@ -80,7 +80,7 @@ class WebAuthnCodecsSpec extends FunSpec with Matchers with GeneratorDrivenProp
forAll { originalPubkey: ECPublicKey =>
val rawKey = WebAuthnCodecs.ecPublicKeyToRaw(originalPubkey)
- val coseKey = WebAuthnCodecs.rawEcdaKeyToCose(rawKey)
+ val coseKey = WebAuthnTestCodecs.rawEcdaKeyToCose(rawKey)
val importedPubkey: ECPublicKey = WebAuthnCodecs.importCosePublicKey(coseKey).asInstanceOf[ECPublicKey]
val rawImportedPubkey = WebAuthnCodecs.ecPublicKeyToRaw(importedPubkey)
@@ -97,7 +97,7 @@ class WebAuthnCodecsSpec extends FunSpec with Matchers with GeneratorDrivenProp
forAll { originalPubkey: ECPublicKey =>
val rawKey = WebAuthnCodecs.ecPublicKeyToRaw(originalPubkey)
- val coseKey = WebAuthnCodecs.ecPublicKeyToCose(originalPubkey)
+ val coseKey = WebAuthnTestCodecs.ecPublicKeyToCose(originalPubkey)
val importedPubkey: ECPublicKey = WebAuthnCodecs.importCosePublicKey(coseKey).asInstanceOf[ECPublicKey]
val rawImportedPubkey = WebAuthnCodecs.ecPublicKeyToRaw(importedPubkey)
diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/WebAuthnTestCodecs.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/WebAuthnTestCodecs.scala
index a545f9ca5..cccfd66bb 100644
--- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/WebAuthnTestCodecs.scala
+++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/WebAuthnTestCodecs.scala
@@ -1,13 +1,95 @@
package com.yubico.webauthn
+import java.security.interfaces.ECPublicKey
+import java.security.interfaces.RSAPublicKey
+import java.security.KeyFactory
+import java.security.spec.PKCS8EncodedKeySpec
+import java.security.PrivateKey
+
+import com.upokecenter.cbor.CBORObject
+import com.yubico.webauthn.data.ByteArray
+import com.yubico.webauthn.data.COSEAlgorithmIdentifier
+import org.bouncycastle.jcajce.provider.asymmetric.edec.BCEdDSAPublicKey
+
/**
* Re-exports from [[WebAuthnCodecs]] so tests can use it
*/
object WebAuthnTestCodecs {
- def ecPublicKeyToCose = WebAuthnCodecs.ecPublicKeyToCose _
def ecPublicKeyToRaw = WebAuthnCodecs.ecPublicKeyToRaw _
def importCosePublicKey = WebAuthnCodecs.importCosePublicKey _
+ def ecPublicKeyToCose(key: ECPublicKey): ByteArray = rawEcdaKeyToCose(ecPublicKeyToRaw(key))
+
+ def rawEcdaKeyToCose(key: ByteArray): ByteArray = {
+ val keyBytes = key.getBytes
+ if (!(keyBytes.length == 64 || (keyBytes.length == 65 && keyBytes(0) == 0x04))) {
+ throw new IllegalArgumentException(
+ s"Raw key must be 64 bytes long or be 65 bytes long and start with 0x04, was ${keyBytes.length} bytes starting with ${keyBytes(0)}"
+ )
+ }
+ val start: Int =
+ if (keyBytes.length == 64) 0
+ else 1
+
+ val coseKey: java.util.Map[Long, Any] = new java.util.HashMap[Long, Any]
+ coseKey.put(1L, 2L) // Key type: EC
+
+ coseKey.put(3L, COSEAlgorithmIdentifier.ES256.getId)
+ coseKey.put(-1L, 1L) // Curve: P-256
+
+ coseKey.put(-2L, java.util.Arrays.copyOfRange(keyBytes, start, start + 32)) // x
+
+ coseKey.put(-3L, java.util.Arrays.copyOfRange(keyBytes, start + 32, start + 64)) // y
+
+ new ByteArray(CBORObject.FromObject(coseKey).EncodeToBytes)
+ }
+
+ def importPrivateKey(encodedKey: ByteArray, alg: COSEAlgorithmIdentifier): PrivateKey = alg match {
+ case COSEAlgorithmIdentifier.ES256 =>
+ val keyFactory: KeyFactory = KeyFactory.getInstance("ECDSA", new BouncyCastleCrypto().getProvider)
+ val spec = new PKCS8EncodedKeySpec(encodedKey.getBytes)
+ keyFactory.generatePrivate(spec)
+
+ case COSEAlgorithmIdentifier.EdDSA =>
+ val keyFactory: KeyFactory = KeyFactory.getInstance("EdDSA", new BouncyCastleCrypto().getProvider)
+ val spec = new PKCS8EncodedKeySpec(encodedKey.getBytes)
+ keyFactory.generatePrivate(spec)
+
+ case COSEAlgorithmIdentifier.RS256 | COSEAlgorithmIdentifier.RS1 =>
+ val keyFactory: KeyFactory = KeyFactory.getInstance("RSA", new BouncyCastleCrypto().getProvider)
+ val spec = new PKCS8EncodedKeySpec(encodedKey.getBytes)
+ keyFactory.generatePrivate(spec)
+ }
+
+ def importEcdsaPrivateKey(encodedKey: ByteArray): PrivateKey = {
+ val keyFactory: KeyFactory = KeyFactory.getInstance("ECDSA", new BouncyCastleCrypto().getProvider)
+ val spec = new PKCS8EncodedKeySpec(encodedKey.getBytes)
+ keyFactory.generatePrivate(spec)
+ }
+
+ def eddsaPublicKeyToCose(key: BCEdDSAPublicKey): ByteArray = {
+ val coseKey: java.util.Map[Long, Any] = new java.util.HashMap[Long, Any]
+ coseKey.put(1L, 1L) // Key type: octet key pair
+
+ coseKey.put(3L, COSEAlgorithmIdentifier.EdDSA.getId)
+ coseKey.put(-1L, 6L) // crv: Ed25519
+
+ coseKey.put(-2L, key.getEncoded.takeRight(32)) // Strip ASN.1 prefix
+ new ByteArray(CBORObject.FromObject(coseKey).EncodeToBytes)
+ }
+
+ def rsaPublicKeyToCose(key: RSAPublicKey, alg: COSEAlgorithmIdentifier): ByteArray = {
+ val coseKey: java.util.Map[Long, Any] = new java.util.HashMap[Long, Any]
+ coseKey.put(1L, 3L) // Key type: RSA
+
+ coseKey.put(3L, alg.getId)
+ coseKey.put(-1L, key.getModulus.toByteArray) // public modulus n
+
+ coseKey.put(-2L, key.getPublicExponent.toByteArray) // public exponent e
+
+ new ByteArray(CBORObject.FromObject(coseKey).EncodeToBytes)
+ }
+
}
diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/AuthenticatorTransportSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/AuthenticatorTransportSpec.scala
new file mode 100644
index 000000000..120860e05
--- /dev/null
+++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/AuthenticatorTransportSpec.scala
@@ -0,0 +1,82 @@
+// Copyright (c) 2019, 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.data
+
+import org.junit.runner.RunWith
+import org.scalatest.FunSpec
+import org.scalatest.Matchers
+import org.scalatest.junit.JUnitRunner
+import org.scalatest.prop.GeneratorDrivenPropertyChecks
+
+@RunWith(classOf[JUnitRunner])
+class AuthenticatorTransportSpec extends FunSpec with Matchers with GeneratorDrivenPropertyChecks {
+
+ describe("The AuthenticatorTransport type") {
+
+ describe("has the constant") {
+ it("USB.") {
+ AuthenticatorTransport.USB.getId should equal ("usb")
+ }
+ it("NFC.") {
+ AuthenticatorTransport.NFC.getId should equal ("nfc")
+ }
+ it("BLE.") {
+ AuthenticatorTransport.BLE.getId should equal ("ble")
+ }
+ it("INTERNAL.") {
+ AuthenticatorTransport.INTERNAL.getId should equal ("internal")
+ }
+ }
+
+ it("has a values() function.") {
+ AuthenticatorTransport.values().length should equal (4)
+ AuthenticatorTransport.values() should not be theSameInstanceAs (AuthenticatorTransport.values())
+ }
+
+ it("has a valueOf(name) function mimicking that of an enum type.") {
+ AuthenticatorTransport.valueOf("USB") should be theSameInstanceAs AuthenticatorTransport.USB
+ AuthenticatorTransport.valueOf("NFC") should be theSameInstanceAs AuthenticatorTransport.NFC
+ AuthenticatorTransport.valueOf("BLE") should be theSameInstanceAs AuthenticatorTransport.BLE
+ AuthenticatorTransport.valueOf("INTERNAL") should be theSameInstanceAs AuthenticatorTransport.INTERNAL
+ an[IllegalArgumentException] should be thrownBy {
+ AuthenticatorTransport.valueOf("foo")
+ }
+ }
+
+ it("can contain any value.") {
+ forAll { transport: String =>
+ AuthenticatorTransport.of(transport).getId should equal (transport)
+ }
+ }
+
+ it("has an of(id) function which returns the corresponding constant instance if applicable, and a new instance otherwise.") {
+ for { constant <- AuthenticatorTransport.values() } {
+ AuthenticatorTransport.of(constant.getId) should equal (constant)
+ AuthenticatorTransport.of(constant.getId) should be theSameInstanceAs constant
+ AuthenticatorTransport.of(constant.getId.toUpperCase) should not equal constant
+ }
+ }
+ }
+}
diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/Generators.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/Generators.scala
index e43b4aaf0..df273a557 100644
--- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/Generators.scala
+++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/Generators.scala
@@ -163,7 +163,7 @@ object Generators {
attestedCredentialDataBytes <- Gen.option(attestedCredentialDataBytes)
extensions <- arbitrary[Option[CBORObject]]
- extensionsBytes = extensions map { exts => new ByteArray(exts.EncodeToBytes(CBOREncodeOptions.NoDuplicateKeys.And(CBOREncodeOptions.NoIndefLengthStrings))) }
+ extensionsBytes = extensions map { exts => new ByteArray(exts.EncodeToBytes(CBOREncodeOptions.DefaultCtap2Canonical)) }
atFlag = attestedCredentialDataBytes.isDefined
edFlag = extensionsBytes.isDefined
flagsByte: Byte = setFlag(setFlag(fixedBytes.getBytes()(32), 0x40, atFlag), BinaryUtil.singleFromHex("80"), edFlag)
@@ -183,6 +183,12 @@ object Generators {
.userVerification(userVerification)
.build())
+ implicit val arbitraryAuthenticatorTransport: Arbitrary[AuthenticatorTransport] = Arbitrary(
+ Gen.oneOf(
+ Gen.oneOf(AuthenticatorTransport.values()),
+ arbitrary[String] map AuthenticatorTransport.of
+ ))
+
implicit val arbitraryByteArray: Arbitrary[ByteArray] = Arbitrary(arbitrary[Array[Byte]].map(new ByteArray(_)))
def byteArray(size: Int): Gen[ByteArray] = Gen.listOfN(size, arbitrary[Byte]).map(ba => new ByteArray(ba.toArray))
diff --git a/webauthn-server-demo/README b/webauthn-server-demo/README
index 0e59e33a4..d76e29327 100644
--- a/webauthn-server-demo/README
+++ b/webauthn-server-demo/README
@@ -13,6 +13,14 @@ https://github.com/Yubico/java-webauthn-server/blob/master/webauthn-server-demo/
class which provides the REST API on top of it.
+== Quick start
+
+```
+$ ./gradlew run
+$ $BROWSER https://localhost:8443/
+```
+
+
== Architecture
The example webapp is made up of three main layers, the bottom of which is the
diff --git a/webauthn-server-demo/build.gradle b/webauthn-server-demo/build.gradle
index fe66cfbce..56e3184e0 100644
--- a/webauthn-server-demo/build.gradle
+++ b/webauthn-server-demo/build.gradle
@@ -13,8 +13,6 @@ project.ext.dockerName = project.name
description = 'WebAuthn demo'
-apply from: 'gretty-2.2.0.plugin'
-
configurations {
forJdk10
}
@@ -26,38 +24,38 @@ dependencies {
project(':webauthn-server-core'),
project(':yubico-util'),
- 'com.fasterxml.jackson.core:jackson-databind:2.9.9.3',
- 'com.google.guava:guava:19.0',
- 'com.upokecenter:cbor:2.4.1',
- 'javax.ws.rs:javax.ws.rs-api:2.1',
- 'org.eclipse.jetty:jetty-server:9.4.9.v20180320',
- 'org.eclipse.jetty:jetty-servlet:9.4.9.v20180320',
- 'org.glassfish.jersey.containers:jersey-container-servlet-core:2.26',
- 'org.slf4j:slf4j-api:1.7.25',
+ addVersion('com.google.guava:guava'),
+ addVersion('com.fasterxml.jackson.core:jackson-databind'),
+ addVersion('com.upokecenter:cbor'),
+ addVersion('javax.ws.rs:javax.ws.rs-api'),
+ addVersion('org.eclipse.jetty:jetty-server'),
+ addVersion('org.eclipse.jetty:jetty-servlet'),
+ addVersion('org.glassfish.jersey.containers:jersey-container-servlet-core'),
+ addVersion('org.slf4j:slf4j-api'),
)
forJdk10(
- 'javax.activation:activation:1.1.1',
- 'javax.xml.bind:jaxb-api:2.3.0',
+ addVersion('javax.activation:activation'),
+ addVersion('javax.xml.bind:jaxb-api'),
)
runtimeOnly(
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',
+ addVersion('ch.qos.logback:logback-classic'),
+ addVersion('org.glassfish.jersey.containers:jersey-container-servlet'),
+ addVersion('org.glassfish.jersey.inject:jersey-hk2'),
)
testImplementation(
project(':webauthn-server-core').sourceSets.test.output,
project(':yubico-util-scala'),
- 'junit:junit:4.12',
- 'org.mockito:mockito-core:2.27.0',
- 'org.scala-lang:scala-library:2.12.8',
- 'org.scalacheck:scalacheck_2.12:1.14.0',
- 'org.scalatest:scalatest_2.12:3.0.4',
+ addVersion('junit:junit'),
+ addVersion('org.mockito:mockito-core'),
+ addVersion('org.scala-lang:scala-library'),
+ addVersion('org.scalacheck:scalacheck_2.12'),
+ addVersion('org.scalatest:scalatest_2.12'),
)
modules {
diff --git a/webauthn-server-demo/gretty-2.2.0.plugin b/webauthn-server-demo/gretty-2.2.0.plugin
deleted file mode 100644
index ff3af513a..000000000
--- a/webauthn-server-demo/gretty-2.2.0.plugin
+++ /dev/null
@@ -1,16 +0,0 @@
-buildscript {
- repositories {
- jcenter()
- }
-
- dependencies {
- classpath 'org.gretty:gretty:2.2.0'
- }
-}
-
-repositories {
- jcenter()
-}
-
-if (!project.plugins.findPlugin(org.akhikhl.gretty.GrettyPlugin))
- project.apply(plugin: org.akhikhl.gretty.GrettyPlugin)
diff --git a/webauthn-server-demo/src/main/java/demo/webauthn/RegistrationStorage.java b/webauthn-server-demo/src/main/java/demo/webauthn/RegistrationStorage.java
index 1022018a9..47a7a2c02 100644
--- a/webauthn-server-demo/src/main/java/demo/webauthn/RegistrationStorage.java
+++ b/webauthn-server-demo/src/main/java/demo/webauthn/RegistrationStorage.java
@@ -36,7 +36,7 @@ public interface RegistrationStorage extends CredentialRepository {
boolean addRegistrationByUsername(String username, CredentialRegistration reg);
Collection getRegistrationsByUsername(String username);
- Optional getRegistrationByUsernameAndCredentialId(String username, ByteArray userHandle);
+ Optional getRegistrationByUsernameAndCredentialId(String username, ByteArray credentialId);
Collection getRegistrationsByUserHandle(ByteArray userHandle);
boolean removeRegistrationByUsername(String username, CredentialRegistration credentialRegistration);
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 99cdb2290..ef5aa8a16 100644
--- a/webauthn-server-demo/src/main/java/demo/webauthn/WebAuthnServer.java
+++ b/webauthn-server-demo/src/main/java/demo/webauthn/WebAuthnServer.java
@@ -144,6 +144,8 @@ public WebAuthnServer(RegistrationStorage userStorage, Cache Set immutableSet(Set s) {
*
* @return A shallow copy of s
which cannot be modified
*/
- public static Set immutableSortedSet(SortedSet s) {
+ public static SortedSet immutableSortedSet(SortedSet s) {
return Collections.unmodifiableSortedSet(new TreeSet<>(s));
}
diff --git a/yubico-util/src/main/java/com/yubico/internal/util/EnumUtil.java b/yubico-util/src/main/java/com/yubico/internal/util/ComparableUtil.java
similarity index 67%
rename from yubico-util/src/main/java/com/yubico/internal/util/EnumUtil.java
rename to yubico-util/src/main/java/com/yubico/internal/util/ComparableUtil.java
index e3685f636..13c140f5f 100644
--- a/yubico-util/src/main/java/com/yubico/internal/util/EnumUtil.java
+++ b/yubico-util/src/main/java/com/yubico/internal/util/ComparableUtil.java
@@ -24,19 +24,33 @@
package com.yubico.internal.util;
-import java.util.Set;
+import java.util.Iterator;
+import java.util.SortedSet;
-public class EnumUtil {
+public class ComparableUtil {
- public static > int compareSets(Set a, Set b, Class clazz) {
- for (T value : clazz.getEnumConstants()) {
- if (a.contains(value) && !b.contains(value)) {
+ public static > int compareComparableSets(SortedSet a, SortedSet b) {
+ if (a.size() == b.size()) {
+ final Iterator as = a.iterator();
+ final Iterator bs = b.iterator();
+
+ while (as.hasNext() && bs.hasNext()) {
+ final int comp = as.next().compareTo(bs.next());
+ if (comp != 0) {
+ return comp;
+ }
+ }
+
+ if (as.hasNext()) {
return 1;
- } else if (!a.contains(value) && b.contains(value)) {
+ } else if (bs.hasNext()) {
return -1;
+ } else {
+ return 0;
}
+ } else {
+ return a.size() - b.size();
}
- return 0;
}
}
diff --git a/yubico-util/src/test/scala/com/yubico/internal/util/ComparableUtilSpec.scala b/yubico-util/src/test/scala/com/yubico/internal/util/ComparableUtilSpec.scala
new file mode 100644
index 000000000..072a2dd89
--- /dev/null
+++ b/yubico-util/src/test/scala/com/yubico/internal/util/ComparableUtilSpec.scala
@@ -0,0 +1,57 @@
+package com.yubico.internal.util
+
+import org.junit.runner.RunWith
+import org.scalacheck.Gen
+import org.scalacheck.Arbitrary.arbitrary
+import org.scalatest.junit.JUnitRunner
+import org.scalatest.prop.GeneratorDrivenPropertyChecks
+import org.scalatest.FunSpec
+import org.scalatest.Matchers
+
+import _root_.scala.collection.JavaConverters._
+
+
+@RunWith(classOf[JUnitRunner])
+class ComparableUtilSpec extends FunSpec with Matchers with GeneratorDrivenPropertyChecks {
+
+ def sameSizeSets[T](implicit gent: Gen[T]): Gen[(Set[T], Set[T])] = for {
+ n: Int <- Gen.chooseNum(0, 100)
+ a: Set[T] <- Gen.containerOfN[Set, T](n, gent)
+ b: Set[T] <- Gen.containerOfN[Set, T](n, gent)
+ } yield (a, b)
+
+ def toJava(s: Set[Int]): java.util.SortedSet[Integer] =
+ new java.util.TreeSet[Integer](s.map(new Integer(_)).asJava)
+
+ describe("compareComparableSets") {
+ it("sorts differently-sized sets in order of cardinality.") {
+ forAll { (a: Set[Int], b: Set[Int]) =>
+ whenever(a.size != b.size) {
+ val comp = ComparableUtil.compareComparableSets(toJava(a), toJava(b))
+ if (a.size < b.size) {
+ comp should be < 0
+ } else {
+ comp should be > 0
+ }
+ }
+ }
+ }
+
+ it("sorts same-sized sets like sorted lists.") {
+ forAll(sameSizeSets(arbitrary[Int])) { case (a, b) =>
+ whenever(a.size == b.size) {
+ val comp = ComparableUtil.compareComparableSets(toJava(a), toJava(b))
+
+ val aList = a.toList.sorted
+ val bList = b.toList.sorted
+ val firstDiff = aList.zip(bList).find({ case (a, b) => a != b })
+ firstDiff match {
+ case Some((a, b)) => comp should equal (a.compareTo(b))
+ case None => comp should equal (0)
+ }
+ }
+ }
+ }
+ }
+
+}