diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 27952e5eb..91a584fc1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,12 +24,34 @@ jobs: - name: Run tests run: ./gradlew cleanTest check - - name: Archive test report + - name: Archive HTML test report if: ${{ always() }} uses: actions/upload-artifact@v2 with: - name: test-reports + name: test-reports-java${{ matrix.java }}-html path: "*/build/reports/**" + - name: Archive JUnit test report + if: ${{ always() }} + uses: actions/upload-artifact@v2 + with: + name: test-reports-java${{ matrix.java }}-xml + path: "*/build/test-results/**/*.xml" + - name: Build JavaDoc run: ./gradlew assembleJavadoc + + publish-test-results: + name: Publish test results + needs: test + runs-on: ubuntu-latest + if: ${{ always() && github.event_name == 'pull_request' }} + + steps: + - name: Download artifacts + uses: actions/download-artifact@v2 + + - name: Publish test results + uses: EnricoMi/publish-unit-test-result-action@v1 + with: + files: "**/*.xml" diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 167a4e8c4..ced88f0f7 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -2,6 +2,7 @@ name: "Code scanning - action" on: push: + branches-ignore: 'dependabot/**' pull_request: schedule: - cron: '0 12 * * 2' diff --git a/.github/workflows/release-verify-signatures.yml b/.github/workflows/release-verify-signatures.yml index fe85b844f..a0b34ca5f 100644 --- a/.github/workflows/release-verify-signatures.yml +++ b/.github/workflows/release-verify-signatures.yml @@ -36,16 +36,20 @@ jobs: 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 + wget https://github.com/${GITHUB_REPOSITORY}/releases/download/${TAGNAME}/webauthn-server-core-minimal-${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 + gpg --no-default-keyring --keyring yubico --verify webauthn-server-core-${TAGNAME}.jar.asc webauthn-server-core-bundle/build/libs/webauthn-server-core-${TAGNAME}.jar + gpg --no-default-keyring --keyring yubico --verify webauthn-server-core-minimal-${TAGNAME}.jar.asc webauthn-server-core/build/libs/webauthn-server-core-minimal-${TAGNAME}.jar - name: Verify signatures from Maven Central run: | 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-core-minimal-${TAGNAME}.jar.mavencentral.asc https://repo1.maven.org/maven2/com/yubico/webauthn-server-core-minimal-/${TAGNAME}/webauthn-server-core-minimal-${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 + gpg --no-default-keyring --keyring yubico --verify webauthn-server-core-${TAGNAME}.jar.mavencentral.asc webauthn-server-core-bundle/build/libs/webauthn-server-core-${TAGNAME}.jar + gpg --no-default-keyring --keyring yubico --verify webauthn-server-core-minimal-${TAGNAME}.jar.mavencentral.asc webauthn-server-core/build/libs/webauthn-server-core-minimal-${TAGNAME}.jar diff --git a/NEWS b/NEWS index e28974c98..8048395a3 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,20 @@ +== Version 1.9.0 == + +webauthn-server-attestation: + +* Fixed that `SimpleAttestationResolver` would return empty transports when + transports are unknown. + +webauthn-server-core: + +* Added support for the `"apple"` attestation statement format. + +Other: + +* Dependency versions moved to new meta-module `webauthn-server-parent`. Users + should never need to depend on `webauthn-server-parent` directly. + + == Version 1.8.0 == Changes: diff --git a/README b/README index a9e9ade72..0e5bb18e9 100644 --- a/README +++ b/README @@ -25,7 +25,7 @@ Maven: com.yubico webauthn-server-core - 1.8.0 + 1.9.0 compile ---------- @@ -33,7 +33,7 @@ Maven: Gradle: ---------- -compile 'com.yubico:webauthn-server-core:1.8.0' +compile 'com.yubico:webauthn-server-core:1.9.0' ---------- === Semantic versioning diff --git a/build.gradle b/build.gradle index ed1b2eab9..d279295ad 100644 --- a/build.gradle +++ b/build.gradle @@ -4,10 +4,13 @@ buildscript { } dependencies { classpath 'com.cinnober.gradle:semver-git:2.5.0' + classpath 'com.diffplug.spotless:spotless-plugin-gradle:5.12.4' + classpath 'io.github.cosmicsilence:gradle-scalafix:0.1.8' } } plugins { - id 'com.github.kt3k.coveralls' version '2.11.0' + id 'java-platform' + id 'com.github.kt3k.coveralls' version '2.12.0' id 'io.codearte.nexus-staging' version '0.30.0' id 'io.franzbecker.gradle-lombok' version '4.0.0' } @@ -15,6 +18,8 @@ plugins { import io.franzbecker.gradle.lombok.LombokPlugin import io.franzbecker.gradle.lombok.task.DelombokTask +rootProject.description = "Metadata root for the com.yubico:webauthn-server-* module family" + project.ext.isCiBuild = System.env.CI == 'true' project.ext.publishEnabled = !isCiBuild && @@ -33,11 +38,36 @@ wrapper { gradleVersion = '6.8' } +dependencies { + constraints { + api('ch.qos.logback:logback-classic:[1.2.3,2)') + api('com.augustcellars.cose:cose-java:[1.0.0,2)') + api('com.fasterxml.jackson.core:jackson-databind:[2.11.0,3)') + api('com.google.guava:guava:[24.1.1,31)') + api('com.upokecenter:cbor:[4.0.1,5)') + api('javax.ws.rs:javax.ws.rs-api:[2.1,3)') + api('javax.xml.bind:jaxb-api:[2.3.0,3)') + api('junit:junit:[4.12,5)') + api('org.apache.httpcomponents:httpclient:[4.5.2,5)') + api('org.bouncycastle:bcpkix-jdk15on:[1.62,2)') + api('org.bouncycastle:bcprov-jdk15on:[1.62,2)') + api('org.eclipse.jetty:jetty-servlet:[9.4.9.v20180320,10)') + api('org.glassfish.jersey.containers:jersey-container-servlet-core:[2.26,3)') + api('org.glassfish.jersey.containers:jersey-container-servlet:[2.26,3)') + api('org.glassfish.jersey.inject:jersey-hk2:[2.26,3)') + api('org.mockito:mockito-core:[2.27.0,3)') + api('org.scalacheck:scalacheck_2.13:[1.14.0,2)') + api('org.scalatest:scalatest_2.13:[3.0.8,3.1)') + api('org.slf4j:slf4j-api:[1.7.25,2)') + } +} + allprojects { ext.snapshotSuffix = ".g-SNAPSHOT" ext.dirtyMarker = "-DIRTY" apply plugin: 'com.cinnober.gradle.semver-git' + apply plugin: 'com.diffplug.spotless' apply plugin: 'idea' group = 'com.yubico' @@ -48,34 +78,6 @@ allprojects { } } -Map dependencyVersions = [ - 'ch.qos.logback:logback-classic:[1.2.3,2)', - 'com.augustcellars.cose:cose-java:[1.0.0,2)', - 'com.fasterxml.jackson.core:jackson-databind:[2.11.0,3)', - 'com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:[2.11.0,3)', - 'com.fasterxml.jackson.datatype:jackson-datatype-jdk8:[2.11.0,3)', - 'com.google.guava:guava:[24.1.1,30)', - 'com.upokecenter:cbor:[4.0.1,5)', - 'javax.activation:activation:[1.1.1,2)', - 'javax.ws.rs:javax.ws.rs-api:[2.1,3)', - 'javax.xml.bind:jaxb-api:[2.3.0,3)', - 'junit:junit:[4.12,5)', - 'org.apache.httpcomponents:httpclient:[4.5.2,5)', - 'org.bouncycastle:bcpkix-jdk15on:[1.62,2)', - 'org.bouncycastle:bcprov-jdk15on:[1.62,2)', - 'org.eclipse.jetty:jetty-server:[9.4.9.v20180320,10)', - 'org.eclipse.jetty:jetty-servlet:[9.4.9.v20180320,10)', - 'org.glassfish.jersey.containers:jersey-container-servlet-core:[2.26,3)', - 'org.glassfish.jersey.containers:jersey-container-servlet:[2.26,3)', - 'org.glassfish.jersey.inject:jersey-hk2:[2.26,3)', - 'org.mockito:mockito-core:[2.27.0,3)', - 'org.scala-lang:scala-library:[2.13.1,3)', - 'org.scalacheck:scalacheck_2.13:[1.14.0,2)', - 'org.scalatest:scalatest_2.13:[3.0.8,3.1)', - 'org.slf4j:slf4j-api:[1.7.25,2)', -].collectEntries { [(it.split(':')[0..1].join(':')): it] } -rootProject.ext.addVersion = { dep -> dependencyVersions[dep] } - subprojects { apply plugin: LombokPlugin @@ -94,6 +96,16 @@ subprojects { maven { url "https://repo.maven.apache.org/maven2" } } + + spotless { + java { + googleJavaFormat() + } + scala { + scalafmt('2.6.3').configFile(rootProject.file('scalafmt.conf')) + } + } + tasks.check.dependsOn spotlessCheck } allprojects { @@ -111,20 +123,34 @@ 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 null } return proc.text.trim() } -subprojects { project -> +String getGitCommitOrUnknown() { + return getGitCommit() ?: 'UNKNOWN' +} - sourceCompatibility = 1.8 - targetCompatibility = 1.8 +subprojects { project -> + if (project.plugins.hasPlugin('scala')) { + project.scalafix { + configFile = rootProject.file('scalafix.conf') + } + dependencies.scalafix('com.github.liancheng:organize-imports_2.13:0.5.0') + project.tasks.spotlessApply.dependsOn(project.tasks.scalafix) + project.tasks.spotlessCheck.dependsOn(project.tasks.checkScalafix) + project.tasks.scalafix.finalizedBy(project.tasks.spotlessApply) + project.tasks.checkScalafix.finalizedBy(project.tasks.spotlessCheck) + } tasks.withType(JavaCompile) { options.encoding = 'UTF-8' } + tasks.withType(ScalaCompile) { + scalaCompileOptions.additionalParameters = ['-Wunused'] + } tasks.withType(AbstractArchiveTask) { from(rootProject.file('COPYING')) @@ -136,7 +162,7 @@ subprojects { project -> it.dependsOn check } - test { + tasks.withType(AbstractTestTask) { testLogging { showStandardStreams = isCiBuild } @@ -192,6 +218,10 @@ subprojects { project -> apply plugin: 'maven-publish' apply plugin: 'signing' + if (getGitCommit() == null) { + throw new RuntimeException("Failed to get git commit ID"); + } + publishing { publications { jars(MavenPublication) { @@ -248,10 +278,70 @@ subprojects { project -> } } +// The root project has no sources, but the dependency platform also needs to be published as an artifact +// See https://docs.gradle.org/current/userguide/java_platform_plugin.html +// See https://github.com/Yubico/java-webauthn-server/issues/93#issuecomment-822806951 +if (publishEnabled) { + apply plugin: 'maven-publish' + apply plugin: 'signing' + + publishing { + publications { + jars(MavenPublication) { + from components.javaPlatform + + pom { + name = project.name + description = project.description + url = 'https://developers.yubico.com/java-webauthn-server/' + + developers { + developer { + id = 'emil' + name = 'Emil Lundberg' + email = 'emil@yubico.com' + } + } + + licenses { + license { + name = 'BSD-license' + comments = 'Revised 2-clause BSD license' + } + } + + scm { + url = 'scm:git:git://github.com/Yubico/java-webauthn-server.git' + connection = 'scm:git:git://github.com/Yubico/java-webauthn-server.git' + developerConnection = 'scm:git:ssh://git@github.com/Yubico/java-webauthn-server.git' + tag = 'HEAD' + } + } + } + } + + repositories { + maven { + name = "sonatypeNexus" + url = "https://oss.sonatype.org/service/local/staging/deploy/maven2/" + credentials { + username ossrhUsername + password ossrhPassword + } + } + } + } + + signing { + useGpgCmd() + sign publishing.publications.jars + } +} + task pitestMerge(type: com.yubico.gradle.pitest.tasks.PitestMergeTask) coveralls { - sourceDirs = subprojects.sourceSets.main.allSource.srcDirs.flatten() + sourceDirs = subprojects.findAll({ project.hasProperty('sourceSets') }).sourceSets.main.allSource.srcDirs.flatten() } tasks.coveralls { inputs.files pitestMerge.outputs.files diff --git a/doc/development.md b/doc/development.md index 323298d64..bf5f4af57 100644 --- a/doc/development.md +++ b/doc/development.md @@ -14,3 +14,11 @@ and the `webauthn-server-core-minimal` module is hosted in `webauthn-server-core We intend to eliminate the `webauthn-server-core-bundle` subproject in the next major version release, and return the current `webauthn-server-core-minimal` module to the `webauthn-server-core` module name. This naming inconsistency should be fixed along with this. + + +Code formatting +--- + +Use `./gradlew spotlessApply` to run the automatic code formatter. +You can also run it in continuous mode as `./gradlew --continuous spotlessApply` +to reformat whenever a file changes. diff --git a/doc/releasing.md b/doc/releasing.md index be0906f68..29cb85188 100644 --- a/doc/releasing.md +++ b/doc/releasing.md @@ -29,8 +29,8 @@ Release candidate versions ``` 6. Wait for the artifacts to become downloadable at - https://repo1.maven.org/maven2/com/yubico/webauthn-server-core/1.4.0/ . This - is needed for one of the GitHub Actions release workflows and usually takes + https://repo1.maven.org/maven2/com/yubico/webauthn-server-core/ . This is + needed for one of the GitHub Actions release workflows and usually takes less than 30 minutes (long before the artifacts become searchable on the main Maven Central website). @@ -48,9 +48,10 @@ Release candidate versions 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` + `webauthn-server-attestation/build/libs/webauthn-server-attestation-X.Y.Z-RCN.jar.asc`, + `webauthn-server-core/build/libs/webauthn-server-core-minimal-X.Y.Z-RCN.jar.asc` and - `webauthn-server-core/build/libs/webauthn-server-core-X.Y.Z-RCN.jar.asc`. + `webauthn-server-core-bundle/build/libs/webauthn-server-core-X.Y.Z-RCN.jar.asc`. - Note which JDK version was used to build the artifacts. @@ -115,8 +116,8 @@ Release versions ``` 10. Wait for the artifacts to become downloadable at - https://repo1.maven.org/maven2/com/yubico/webauthn-server-core/1.4.0/ . This - is needed for one of the GitHub Actions release workflows and usually takes + https://repo1.maven.org/maven2/com/yubico/webauthn-server-core/ . This is + needed for one of the GitHub Actions release workflows and usually takes less than 30 minutes (long before the artifacts become searchable on the main Maven Central website). @@ -133,6 +134,9 @@ Release versions 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`. + `webauthn-server-attestation/build/libs/webauthn-server-attestation-X.Y.Z.jar.asc`, + `webauthn-server-core/build/libs/webauthn-server-core-minimal-X.Y.Z.jar.asc` + and + `webauthn-server-core-bundle/build/libs/webauthn-server-core-X.Y.Z.jar.asc`. + - Note which JDK version was used to build the artifacts. diff --git a/scalafix.conf b/scalafix.conf new file mode 100644 index 000000000..a8aa67c1b --- /dev/null +++ b/scalafix.conf @@ -0,0 +1,8 @@ +rules = [ + OrganizeImports +] + +OrganizeImports { + expandRelative = true + removeUnused = true +} diff --git a/scalafmt.conf b/scalafmt.conf new file mode 100644 index 000000000..9883d3ff8 --- /dev/null +++ b/scalafmt.conf @@ -0,0 +1,3 @@ +trailingCommas = multiple +rewrite.rules = [ExpandImportSelectors, SortModifiers] +newlines.avoidForSimpleOverflow = [tooLong] 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 index 4320448a1..2287b9f41 100644 --- 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 @@ -7,18 +7,19 @@ 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; - } - }; - } + 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 index afb20d93c..f910d3c3c 100644 --- 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 @@ -1,5 +1,8 @@ package com.yubico.webauthn.attestation; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + import java.io.IOException; import java.net.URL; import java.util.Enumeration; @@ -7,34 +10,33 @@ 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")); + 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); + } } - - @Test - public void customImplementationPropertiesAreSet() throws IOException { - assertTrue(lookup("Git-Commit").matches("^[a-f0-9]{40}$")); - } - + 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}$") || lookup("Git-Commit").equals("UNKNOWN")); + } } diff --git a/test-dependent-projects/java-dep-webauthn-server-core-minimal/src/test/java/com/yubico/webauthn/BouncyCastleProviderPresenceTest.java b/test-dependent-projects/java-dep-webauthn-server-core-minimal/src/test/java/com/yubico/webauthn/BouncyCastleProviderPresenceTest.java index d9ca840d2..e3dc68d5a 100644 --- a/test-dependent-projects/java-dep-webauthn-server-core-minimal/src/test/java/com/yubico/webauthn/BouncyCastleProviderPresenceTest.java +++ b/test-dependent-projects/java-dep-webauthn-server-core-minimal/src/test/java/com/yubico/webauthn/BouncyCastleProviderPresenceTest.java @@ -1,78 +1,75 @@ package com.yubico.webauthn; +import static org.junit.Assert.assertTrue; + import COSE.CoseException; import com.yubico.webauthn.data.AttestationObject; import com.yubico.webauthn.data.RelyingPartyIdentity; import java.io.IOException; import java.security.NoSuchAlgorithmException; -import java.security.PublicKey; +import java.security.Security; import java.security.spec.InvalidKeySpecException; +import java.util.Arrays; import org.junit.Test; import org.mockito.Mockito; -import java.security.Security; -import java.util.Arrays; - -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - /** - * Test that the BouncyCastle provider is not loaded by default - * when depending on the webauthn-server-core-minimal package. + * Test that the BouncyCastle provider is not loaded by default when depending on the + * webauthn-server-core-minimal package. * - * Motivation: https://github.com/Yubico/java-webauthn-server/issues/97 + *

Motivation: https://github.com/Yubico/java-webauthn-server/issues/97 */ public class BouncyCastleProviderPresenceTest { - @Test(expected = ClassNotFoundException.class) - public void bouncyCastleProviderIsNotInClasspath() throws ClassNotFoundException { - Class.forName("org.bouncycastle.jce.provider.BouncyCastleProvider"); - } + @Test(expected = ClassNotFoundException.class) + public void bouncyCastleProviderIsNotInClasspath() throws ClassNotFoundException { + Class.forName("org.bouncycastle.jce.provider.BouncyCastleProvider"); + } - @Test - public void bouncyCastleProviderIsNotLoadedByDefault() { - assertTrue( - Arrays.stream(Security.getProviders()) - .noneMatch(prov -> prov.getName().toLowerCase().contains("bouncy")) - ); - } + @Test + public void bouncyCastleProviderIsNotLoadedByDefault() { + assertTrue( + Arrays.stream(Security.getProviders()) + .noneMatch(prov -> prov.getName().toLowerCase().contains("bouncy"))); + } - @Test - public void bouncyCastleProviderIsNotLoadedAfterInstantiatingRelyingParty() { - // The RelyingParty constructor has the possible side-effect of loading the BouncyCastle provider - RelyingParty.builder() - .identity(RelyingPartyIdentity.builder().id("foo").name("foo").build()) - .credentialRepository(Mockito.mock(CredentialRepository.class)) - .build(); + @Test + public void bouncyCastleProviderIsNotLoadedAfterInstantiatingRelyingParty() { + // The RelyingParty constructor has the possible side-effect of loading the BouncyCastle + // provider + RelyingParty.builder() + .identity(RelyingPartyIdentity.builder().id("foo").name("foo").build()) + .credentialRepository(Mockito.mock(CredentialRepository.class)) + .build(); - assertTrue( - Arrays.stream(Security.getProviders()) - .noneMatch(prov -> + assertTrue( + Arrays.stream(Security.getProviders()) + .noneMatch( + prov -> prov.getName().equals("BC") - || prov.getClass().getCanonicalName().contains("bouncy") - )); - } + || prov.getClass().getCanonicalName().contains("bouncy"))); + } - @Test - public void bouncyCastleProviderIsNotLoadedAfterAttemptingToLoadEddsaKey() throws IOException, CoseException, NoSuchAlgorithmException, InvalidKeySpecException { - try { - WebAuthnCodecs.importCosePublicKey( - new AttestationObject(RegistrationTestData.Packed$.MODULE$.BasicAttestationEdDsa().attestationObject()) - .getAuthenticatorData() - .getAttestedCredentialData() - .get() - .getCredentialPublicKey() - ); - } catch (NoSuchAlgorithmException e) { - // OK - } - - assertTrue( - Arrays.stream(Security.getProviders()) - .noneMatch(prov -> - prov.getName().equals("BC") - || prov.getClass().getCanonicalName().contains("bouncy") - )); + @Test + public void bouncyCastleProviderIsNotLoadedAfterAttemptingToLoadEddsaKey() + throws IOException, CoseException, NoSuchAlgorithmException, InvalidKeySpecException { + try { + WebAuthnCodecs.importCosePublicKey( + new AttestationObject( + RegistrationTestData.Packed$.MODULE$.BasicAttestationEdDsa().attestationObject()) + .getAuthenticatorData() + .getAttestedCredentialData() + .get() + .getCredentialPublicKey()); + } catch (NoSuchAlgorithmException e) { + // OK } + assertTrue( + Arrays.stream(Security.getProviders()) + .noneMatch( + prov -> + prov.getName().equals("BC") + || prov.getClass().getCanonicalName().contains("bouncy"))); + } } 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 index 4c1a8b761..5201a6409 100644 --- 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 @@ -12,33 +12,50 @@ 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; - } + 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/CryptoAlgorithmsTest.java b/test-dependent-projects/java-dep-webauthn-server-core/src/test/java/com/yubico/webauthn/CryptoAlgorithmsTest.java index ef87ce9f3..d3b38f338 100644 --- a/test-dependent-projects/java-dep-webauthn-server-core/src/test/java/com/yubico/webauthn/CryptoAlgorithmsTest.java +++ b/test-dependent-projects/java-dep-webauthn-server-core/src/test/java/com/yubico/webauthn/CryptoAlgorithmsTest.java @@ -1,5 +1,8 @@ package com.yubico.webauthn; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + import COSE.CoseException; import com.yubico.webauthn.data.AttestationObject; import com.yubico.webauthn.data.RelyingPartyIdentity; @@ -17,66 +20,71 @@ import org.junit.Test; import org.mockito.Mockito; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - public class CryptoAlgorithmsTest { - private List providersBefore; + private List providersBefore; - @Before - public void setUp() { - providersBefore = Stream.of(Security.getProviders()).collect(Collectors.toList()); + @Before + public void setUp() { + providersBefore = Stream.of(Security.getProviders()).collect(Collectors.toList()); - // The RelyingParty constructor has the possible side-effect of loading the BouncyCastle provider - RelyingParty.builder() - .identity(RelyingPartyIdentity.builder().id("foo").name("foo").build()) - .credentialRepository(Mockito.mock(CredentialRepository.class)) - .build(); - } + // The RelyingParty constructor has the possible side-effect of loading the BouncyCastle + // provider + RelyingParty.builder() + .identity(RelyingPartyIdentity.builder().id("foo").name("foo").build()) + .credentialRepository(Mockito.mock(CredentialRepository.class)) + .build(); + } - @After - public void tearDown() { - for (Provider prov : Security.getProviders()) { - Security.removeProvider(prov.getName()); - } - providersBefore.forEach(Security::addProvider); + @After + public void tearDown() { + for (Provider prov : Security.getProviders()) { + Security.removeProvider(prov.getName()); } + providersBefore.forEach(Security::addProvider); + } - @Test - public void importRsa() throws IOException, CoseException, NoSuchAlgorithmException, InvalidKeySpecException { - PublicKey key = WebAuthnCodecs.importCosePublicKey( - new AttestationObject(RegistrationTestData.Packed$.MODULE$.BasicAttestationRsa().attestationObject()) + @Test + public void importRsa() + throws IOException, CoseException, NoSuchAlgorithmException, InvalidKeySpecException { + PublicKey key = + WebAuthnCodecs.importCosePublicKey( + new AttestationObject( + RegistrationTestData.Packed$.MODULE$.BasicAttestationRsa().attestationObject()) .getAuthenticatorData() .getAttestedCredentialData() .get() - .getCredentialPublicKey() - ); - assertEquals(key.getAlgorithm(), "RSA"); - } + .getCredentialPublicKey()); + assertEquals(key.getAlgorithm(), "RSA"); + } - @Test - public void importEcdsa() throws IOException, CoseException, NoSuchAlgorithmException, InvalidKeySpecException { - PublicKey key = WebAuthnCodecs.importCosePublicKey( - new AttestationObject(RegistrationTestData.Packed$.MODULE$.BasicAttestation().attestationObject()) + @Test + public void importEcdsa() + throws IOException, CoseException, NoSuchAlgorithmException, InvalidKeySpecException { + PublicKey key = + WebAuthnCodecs.importCosePublicKey( + new AttestationObject( + RegistrationTestData.Packed$.MODULE$.BasicAttestation().attestationObject()) .getAuthenticatorData() .getAttestedCredentialData() .get() - .getCredentialPublicKey() - ); - assertEquals(key.getAlgorithm(), "EC"); - } + .getCredentialPublicKey()); + assertEquals(key.getAlgorithm(), "EC"); + } - @Test - public void importEddsa() throws IOException, CoseException, NoSuchAlgorithmException, InvalidKeySpecException { - PublicKey key = WebAuthnCodecs.importCosePublicKey( - new AttestationObject(RegistrationTestData.Packed$.MODULE$.BasicAttestationEdDsa().attestationObject()) + @Test + public void importEddsa() + throws IOException, CoseException, NoSuchAlgorithmException, InvalidKeySpecException { + PublicKey key = + WebAuthnCodecs.importCosePublicKey( + new AttestationObject( + RegistrationTestData.Packed$.MODULE$ + .BasicAttestationEdDsa() + .attestationObject()) .getAuthenticatorData() .getAttestedCredentialData() .get() - .getCredentialPublicKey() - ); - assertTrue("EdDSA".equals(key.getAlgorithm()) || "Ed25519".equals(key.getAlgorithm())); - } - + .getCredentialPublicKey()); + assertTrue("EdDSA".equals(key.getAlgorithm()) || "Ed25519".equals(key.getAlgorithm())); + } } 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 index 4a4aefd6b..2c7cad123 100644 --- 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 @@ -1,5 +1,8 @@ package com.yubico.webauthn.meta; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + import com.yubico.webauthn.RelyingParty; import java.io.IOException; import java.net.URL; @@ -9,50 +12,49 @@ 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")); + 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); + } } - - - @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}$")); - } - + 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}$") || lookup("Git-Commit").equals("UNKNOWN")); + } } 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 index 3ef4f822c..e867eb214 100644 --- 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 @@ -1,42 +1,44 @@ 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; +import java.time.LocalDate; +import org.junit.Test; + /** - * 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. + * 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}$")); + 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}$") || impl.getGitCommit().equals("UNKNOWN")); + } + + @Test + public void majorVersionIsUnknownOrAtLeast1() { + final String version = versionInfo.getImplementation().getVersion(); + if (!"0.1.0-SNAPSHOT".equals(version)) { + String[] splits = version.split("\\."); + final int majorVersion = Integer.parseInt(splits[0]); + assertTrue(majorVersion >= 1); } - - @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/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 index 71f885e21..1a7effa38 100644 --- 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 @@ -5,8 +5,7 @@ public class ThisShouldCompile { - public String getEncodedValue() throws JsonProcessingException { - return JacksonCodecs.json().writeValueAsString("hej"); - } - + 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 c40388fdc..e59af0c86 100644 --- a/webauthn-server-attestation/build.gradle +++ b/webauthn-server-attestation/build.gradle @@ -1,12 +1,16 @@ plugins { id 'java-library' id 'scala' + id 'io.github.cosmicsilence.scalafix' } description = 'Yubico WebAuthn attestation subsystem' project.ext.publishMe = true +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + evaluationDependsOn(':webauthn-server-core-minimal') dependencies { @@ -17,28 +21,32 @@ dependencies { implementation( project(':yubico-util'), - addVersion('com.google.guava:guava'), - addVersion('com.fasterxml.jackson.core:jackson-databind'), - addVersion('org.bouncycastle:bcprov-jdk15on'), - addVersion('org.slf4j:slf4j-api'), + 'com.google.guava:guava', + 'com.fasterxml.jackson.core:jackson-databind', + 'org.bouncycastle:bcprov-jdk15on', + 'org.slf4j:slf4j-api', ) testImplementation( project(':webauthn-server-core-minimal').sourceSets.test.output, project(':yubico-util-scala'), - addVersion('junit:junit'), - addVersion('org.mockito:mockito-core'), - addVersion('org.scala-lang:scala-library'), - addVersion('org.scalacheck:scalacheck_2.13'), - addVersion('org.scalatest:scalatest_2.13'), + 'junit:junit', + 'org.mockito:mockito-core', + 'org.scala-lang:scala-library', + 'org.scalacheck:scalacheck_2.13', + 'org.scalatest:scalatest_2.13', ) testRuntimeOnly( - addVersion('ch.qos.logback:logback-classic'), + 'ch.qos.logback:logback-classic', ) testRuntimeOnly( // Transitive dependency from :webauthn-server-core:test - addVersion('org.bouncycastle:bcpkix-jdk15on'), + 'org.bouncycastle:bcpkix-jdk15on', + ) + + testRuntimeOnly( + 'ch.qos.logback:logback-classic', ) } @@ -50,7 +58,7 @@ jar { 'Implementation-Title': project.description, 'Implementation-Version': project.version, 'Implementation-Vendor': 'Yubico', - 'Git-Commit': getGitCommit(), + 'Git-Commit': getGitCommitOrUnknown(), ]) } } diff --git a/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/AttestationResolver.java b/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/AttestationResolver.java index fb0d622a0..69a08efad 100644 --- a/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/AttestationResolver.java +++ b/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/AttestationResolver.java @@ -31,14 +31,13 @@ public interface AttestationResolver { - /** - * Alias of resolve(attestationCertificate, Collections.emptyList()). - */ - default Optional resolve(X509Certificate attestationCertificate) { - return resolve(attestationCertificate, Collections.emptyList()); - } + /** Alias of resolve(attestationCertificate, Collections.emptyList()). */ + default Optional resolve(X509Certificate attestationCertificate) { + return resolve(attestationCertificate, Collections.emptyList()); + } - Optional resolve(X509Certificate attestationCertificate, List certificateChain); - Attestation untrustedFromCertificate(X509Certificate attestationCertificate); + Optional resolve( + X509Certificate attestationCertificate, List certificateChain); + Attestation untrustedFromCertificate(X509Certificate attestationCertificate); } diff --git a/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/DeviceMatcher.java b/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/DeviceMatcher.java index 163ebc14d..8fd5a8c3f 100644 --- a/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/DeviceMatcher.java +++ b/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/DeviceMatcher.java @@ -28,5 +28,5 @@ import java.security.cert.X509Certificate; public interface DeviceMatcher { - boolean matches(X509Certificate attestationCertificate, JsonNode parameters); + boolean matches(X509Certificate attestationCertificate, JsonNode parameters); } diff --git a/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/MetadataObject.java b/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/MetadataObject.java index 1faeb7c68..7d75da901 100644 --- a/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/MetadataObject.java +++ b/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/MetadataObject.java @@ -48,79 +48,82 @@ @Slf4j @JsonIgnoreProperties(ignoreUnknown = true) -@EqualsAndHashCode(of = { "data" }, callSuper = false) +@EqualsAndHashCode( + of = {"data"}, + callSuper = false) public final class MetadataObject { - private static final ObjectMapper OBJECT_MAPPER = JacksonCodecs.json(); - - private static final TypeReference> MAP_STRING_STRING_TYPE = new TypeReference>() { - }; - private static final TypeReference> LIST_STRING_TYPE = new TypeReference>() { - }; - private static final TypeReference> LIST_JSONNODE_TYPE = new TypeReference>() { - }; - - private final transient JsonNode data; - - private final String identifier; - private final long version; - private final Map vendorInfo; - private final List trustedCertificates; - private final List devices; - - @JsonCreator - public MetadataObject(JsonNode data) { - this.data = data; - try { - vendorInfo = OBJECT_MAPPER.readValue(data.get("vendorInfo").traverse(), MAP_STRING_STRING_TYPE); - trustedCertificates = OBJECT_MAPPER.readValue(data.get("trustedCertificates").traverse(), LIST_STRING_TYPE); - devices = OBJECT_MAPPER.readValue(data.get("devices").traverse(), LIST_JSONNODE_TYPE); - } catch (IOException e) { - throw new IllegalArgumentException("Invalid JSON data", e); - } - - identifier = data.get("identifier").asText(); - version = data.get("version").asLong(); - } - - public static MetadataObject readDefault() { - InputStream is = MetadataObject.class.getResourceAsStream("/metadata.json"); - try { - return JacksonCodecs.json().readValue(is, MetadataObject.class); - } catch (IOException e) { - throw ExceptionUtil.wrapAndLog(log, "Failed to read default metadata", e); - } finally { - Closeables.closeQuietly(is); - } - } - - public String getIdentifier() { - return identifier; - } - - public long getVersion() { - return version; - } - - public Map getVendorInfo() { - return vendorInfo; - } - - public List getTrustedCertificates() { - return trustedCertificates; + private static final ObjectMapper OBJECT_MAPPER = JacksonCodecs.json(); + + private static final TypeReference> MAP_STRING_STRING_TYPE = + new TypeReference>() {}; + private static final TypeReference> LIST_STRING_TYPE = + new TypeReference>() {}; + private static final TypeReference> LIST_JSONNODE_TYPE = + new TypeReference>() {}; + + private final transient JsonNode data; + + private final String identifier; + private final long version; + private final Map vendorInfo; + private final List trustedCertificates; + private final List devices; + + @JsonCreator + public MetadataObject(JsonNode data) { + this.data = data; + try { + vendorInfo = + OBJECT_MAPPER.readValue(data.get("vendorInfo").traverse(), MAP_STRING_STRING_TYPE); + trustedCertificates = + OBJECT_MAPPER.readValue(data.get("trustedCertificates").traverse(), LIST_STRING_TYPE); + devices = OBJECT_MAPPER.readValue(data.get("devices").traverse(), LIST_JSONNODE_TYPE); + } catch (IOException e) { + throw new IllegalArgumentException("Invalid JSON data", e); } - @JsonIgnore - public List getParsedTrustedCertificates() throws CertificateException { - List list = new ArrayList<>(); - for (String trustedCertificate : trustedCertificates) { - X509Certificate x509Certificate = CertificateParser.parsePem(trustedCertificate); - list.add(x509Certificate); - } - return list; + identifier = data.get("identifier").asText(); + version = data.get("version").asLong(); + } + + public static MetadataObject readDefault() { + InputStream is = MetadataObject.class.getResourceAsStream("/metadata.json"); + try { + return JacksonCodecs.json().readValue(is, MetadataObject.class); + } catch (IOException e) { + throw ExceptionUtil.wrapAndLog(log, "Failed to read default metadata", e); + } finally { + Closeables.closeQuietly(is); } - - public List getDevices() { - return MoreObjects.firstNonNull(devices, ImmutableList.of()); + } + + public String getIdentifier() { + return identifier; + } + + public long getVersion() { + return version; + } + + public Map getVendorInfo() { + return vendorInfo; + } + + public List getTrustedCertificates() { + return trustedCertificates; + } + + @JsonIgnore + public List getParsedTrustedCertificates() throws CertificateException { + List list = new ArrayList<>(); + for (String trustedCertificate : trustedCertificates) { + X509Certificate x509Certificate = CertificateParser.parsePem(trustedCertificate); + list.add(x509Certificate); } + return list; + } + public List getDevices() { + return MoreObjects.firstNonNull(devices, ImmutableList.of()); + } } diff --git a/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/StandardMetadataService.java b/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/StandardMetadataService.java index 129eb699e..501b09eb5 100644 --- a/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/StandardMetadataService.java +++ b/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/StandardMetadataService.java @@ -41,108 +41,94 @@ import org.slf4j.LoggerFactory; public final class StandardMetadataService implements MetadataService { - private static final Logger logger = LoggerFactory.getLogger(StandardMetadataService.class); + private static final Logger logger = LoggerFactory.getLogger(StandardMetadataService.class); - private final Attestation unknownAttestation = Attestation.empty(); - private final AttestationResolver attestationResolver; - private final Cache cache; + private final Attestation unknownAttestation = Attestation.empty(); + private final AttestationResolver attestationResolver; + private final Cache cache; - private StandardMetadataService( - @NonNull - AttestationResolver attestationResolver, - @NonNull - Cache cache - ) { - this.attestationResolver = attestationResolver; - this.cache = cache; - } + private StandardMetadataService( + @NonNull AttestationResolver attestationResolver, @NonNull Cache cache) { + this.attestationResolver = attestationResolver; + this.cache = cache; + } - public StandardMetadataService(AttestationResolver attestationResolver) { - this( - attestationResolver, - CacheBuilder.newBuilder().build() - ); - } + public StandardMetadataService(AttestationResolver attestationResolver) { + this(attestationResolver, CacheBuilder.newBuilder().build()); + } - public StandardMetadataService() throws CertificateException { - this(createDefaultAttestationResolver()); - } + public StandardMetadataService() throws CertificateException { + this(createDefaultAttestationResolver()); + } - public static TrustResolver createDefaultTrustResolver() throws CertificateException { - return SimpleTrustResolver.fromMetadata(Collections.singleton(MetadataObject.readDefault())); - } + public static TrustResolver createDefaultTrustResolver() throws CertificateException { + return SimpleTrustResolver.fromMetadata(Collections.singleton(MetadataObject.readDefault())); + } - public static AttestationResolver createDefaultAttestationResolver(TrustResolver trustResolver) throws CertificateException { - return new SimpleAttestationResolver( - Collections.singleton(MetadataObject.readDefault()), - trustResolver - ); - } + public static AttestationResolver createDefaultAttestationResolver(TrustResolver trustResolver) + throws CertificateException { + return new SimpleAttestationResolver( + Collections.singleton(MetadataObject.readDefault()), trustResolver); + } - public static AttestationResolver createDefaultAttestationResolver() throws CertificateException { - return createDefaultAttestationResolver(createDefaultTrustResolver()); - } + public static AttestationResolver createDefaultAttestationResolver() throws CertificateException { + return createDefaultAttestationResolver(createDefaultTrustResolver()); + } - public Attestation getCachedAttestation(String attestationCertificateFingerprint) { - return cache.getIfPresent(attestationCertificateFingerprint); - } + public Attestation getCachedAttestation(String attestationCertificateFingerprint) { + return cache.getIfPresent(attestationCertificateFingerprint); + } - /** - * Attempt to look up attestation for a chain of certificates - * - *

- * If there is a signature path from any trusted certificate to the first - * certificate in attestationCertificateChain, then the first - * certificate in attestationCertificateChain is matched - * against the metadata registry to look up metadata for the device. - *

- * - *

- * If the certificate chain is trusted but no metadata exists in the - * registry, the method returns a trusted attestation populated with - * information found embedded in the attestation certificate. - *

- * - *

- * If the certificate chain is not trusted, the method returns an untrusted - * attestation populated with {@link Attestation#getTransports() transports} - * information found embedded in the attestation certificate. - *

- * - *

- * If the certificate chain is empty, an untrusted empty attestation is - * returned. - *

- * - * @param attestationCertificateChain a certificate chain, where each - * certificate in the list should be signed by the following certificate. - * - * @throws CertificateEncodingException if computation of the fingerprint - * fails for any element of attestationCertificateChain that - * needs to be inspected - * - * @return An attestation as described above. - */ - @Override - public Attestation getAttestation(@NonNull List attestationCertificateChain) throws CertificateEncodingException { - if (attestationCertificateChain.isEmpty()) { - return unknownAttestation; - } + /** + * Attempt to look up attestation for a chain of certificates + * + *

If there is a signature path from any trusted certificate to the first certificate in + * attestationCertificateChain, then the first certificate in + * attestationCertificateChain is matched against the metadata registry to look up metadata + * for the device. + * + *

If the certificate chain is trusted but no metadata exists in the registry, the method + * returns a trusted attestation populated with information found embedded in the attestation + * certificate. + * + *

If the certificate chain is not trusted, the method returns an untrusted attestation + * populated with {@link Attestation#getTransports() transports} information found embedded in the + * attestation certificate. + * + *

If the certificate chain is empty, an untrusted empty attestation is returned. + * + * @param attestationCertificateChain a certificate chain, where each certificate in the list + * should be signed by the following certificate. + * @throws CertificateEncodingException if computation of the fingerprint fails for any element of + * attestationCertificateChain that needs to be inspected + * @return An attestation as described above. + */ + @Override + public Attestation getAttestation(@NonNull List attestationCertificateChain) + throws CertificateEncodingException { + if (attestationCertificateChain.isEmpty()) { + return unknownAttestation; + } - X509Certificate attestationCertificate = attestationCertificateChain.get(0); - List certificateChain = attestationCertificateChain.subList(1, attestationCertificateChain.size()); + X509Certificate attestationCertificate = attestationCertificateChain.get(0); + List certificateChain = + attestationCertificateChain.subList(1, attestationCertificateChain.size()); - try { - final String fingerprint = Hashing.sha1().hashBytes(attestationCertificate.getEncoded()).toString(); - return cache.get( - fingerprint, - () -> - attestationResolver.resolve(attestationCertificate, certificateChain) - .orElseGet(() -> attestationResolver.untrustedFromCertificate(attestationCertificate)) - ); - } catch (ExecutionException e) { - throw ExceptionUtil.wrapAndLog(logger, "Failed to look up attestation information for certificate: " + attestationCertificate, e); - } + try { + final String fingerprint = + Hashing.sha1().hashBytes(attestationCertificate.getEncoded()).toString(); + return cache.get( + fingerprint, + () -> + attestationResolver + .resolve(attestationCertificate, certificateChain) + .orElseGet( + () -> attestationResolver.untrustedFromCertificate(attestationCertificate))); + } catch (ExecutionException e) { + throw ExceptionUtil.wrapAndLog( + logger, + "Failed to look up attestation information for certificate: " + attestationCertificate, + e); } - + } } diff --git a/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/TrustResolver.java b/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/TrustResolver.java index 105fce61a..803d879a2 100644 --- a/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/TrustResolver.java +++ b/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/TrustResolver.java @@ -31,25 +31,25 @@ public interface TrustResolver { - /** - * Alias of resolveTrustAnchor(attestationCertificate, Collections.emptyList()). - * - * @see #resolveTrustAnchor(X509Certificate, List) - */ - default Optional resolveTrustAnchor(X509Certificate attestationCertificate) { - return resolveTrustAnchor(attestationCertificate, Collections.emptyList()); - } - - /** - * Resolve a trusted root anchor for the given attestation certificate and certificate chain - * - * @param attestationCertificate The attestation certificate - * @param caCertificateChain Zero or more certificates, of which the first - * has signed attestationCertificate and each of the - * remaining certificates has signed the certificate preceding it. - * @return A trusted root certificate from which there is a signature path - * to attestationCertificate, if one exists. - */ - Optional resolveTrustAnchor(X509Certificate attestationCertificate, List caCertificateChain); + /** + * Alias of resolveTrustAnchor(attestationCertificate, Collections.emptyList()). + * + * @see #resolveTrustAnchor(X509Certificate, List) + */ + default Optional resolveTrustAnchor(X509Certificate attestationCertificate) { + return resolveTrustAnchor(attestationCertificate, Collections.emptyList()); + } + /** + * Resolve a trusted root anchor for the given attestation certificate and certificate chain + * + * @param attestationCertificate The attestation certificate + * @param caCertificateChain Zero or more certificates, of which the first has signed + * attestationCertificate and each of the remaining certificates has signed the + * certificate preceding it. + * @return A trusted root certificate from which there is a signature path to + * attestationCertificate, if one exists. + */ + Optional resolveTrustAnchor( + X509Certificate attestationCertificate, List caCertificateChain); } diff --git a/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/matcher/ExtensionMatcher.java b/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/matcher/ExtensionMatcher.java index 276069944..fa6f0b269 100644 --- a/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/matcher/ExtensionMatcher.java +++ b/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/matcher/ExtensionMatcher.java @@ -37,101 +37,99 @@ @Slf4j public final class ExtensionMatcher implements DeviceMatcher { - private static final Charset CHARSET = Charset.forName("UTF-8"); + private static final Charset CHARSET = Charset.forName("UTF-8"); - public static final String SELECTOR_TYPE = "x509Extension"; + public static final String SELECTOR_TYPE = "x509Extension"; - private static final String EXTENSION_KEY = "key"; - private static final String EXTENSION_VALUE = "value"; - private static final String EXTENSION_VALUE_TYPE = "type"; - private static final String EXTENSION_VALUE_VALUE = "value"; - private static final String EXTENSION_VALUE_TYPE_HEX = "hex"; + private static final String EXTENSION_KEY = "key"; + private static final String EXTENSION_VALUE = "value"; + private static final String EXTENSION_VALUE_TYPE = "type"; + private static final String EXTENSION_VALUE_VALUE = "value"; + private static final String EXTENSION_VALUE_TYPE_HEX = "hex"; - @Override - public boolean matches(X509Certificate attestationCertificate, JsonNode parameters) { - String matchKey = parameters.get(EXTENSION_KEY).asText(); - JsonNode matchValue = parameters.get(EXTENSION_VALUE); - byte[] extensionValue = attestationCertificate.getExtensionValue(matchKey); - if (extensionValue != null) { - if (matchValue == null) { - return true; - } else { - try { - final ASN1Primitive value = ASN1Primitive.fromByteArray(extensionValue); + @Override + public boolean matches(X509Certificate attestationCertificate, JsonNode parameters) { + String matchKey = parameters.get(EXTENSION_KEY).asText(); + JsonNode matchValue = parameters.get(EXTENSION_VALUE); + byte[] extensionValue = attestationCertificate.getExtensionValue(matchKey); + if (extensionValue != null) { + if (matchValue == null) { + return true; + } else { + try { + final ASN1Primitive value = ASN1Primitive.fromByteArray(extensionValue); - if (matchValue.isObject()) { - if (matchTypedValue(matchKey, matchValue, value)) { - return true; - } - } else if (matchValue.isTextual()) { - if (matchStringValue(matchKey, matchValue, value)) return true; - } - } catch (IOException e) { - log.error("Failed to parse extension value as ASN1: {}", new ByteArray(extensionValue).getHex(), e); - } + if (matchValue.isObject()) { + if (matchTypedValue(matchKey, matchValue, value)) { + return true; } + } else if (matchValue.isTextual()) { + if (matchStringValue(matchKey, matchValue, value)) return true; + } + } catch (IOException e) { + log.error( + "Failed to parse extension value as ASN1: {}", + new ByteArray(extensionValue).getHex(), + e); } - return false; + } } + return false; + } - private boolean matchStringValue(String matchKey, JsonNode matchValue, ASN1Primitive value) { - if (value instanceof DEROctetString) { - final String readValue = new String(((DEROctetString) value).getOctets(), CHARSET); - return matchValue.asText().equals(readValue); - } else { - log.debug("Expected text string value for extension {}, was: {}", matchKey, value); - return false; - } + private boolean matchStringValue(String matchKey, JsonNode matchValue, ASN1Primitive value) { + if (value instanceof DEROctetString) { + final String readValue = new String(((DEROctetString) value).getOctets(), CHARSET); + return matchValue.asText().equals(readValue); + } else { + log.debug("Expected text string value for extension {}, was: {}", matchKey, value); + return false; } + } - private boolean matchTypedValue(String matchKey, JsonNode matchValue, ASN1Primitive value) { - final String extensionValueType = matchValue.get(EXTENSION_VALUE_TYPE).textValue(); - switch (extensionValueType) { - case EXTENSION_VALUE_TYPE_HEX: - return matchHex(matchKey, matchValue, value); + private boolean matchTypedValue(String matchKey, JsonNode matchValue, ASN1Primitive value) { + final String extensionValueType = matchValue.get(EXTENSION_VALUE_TYPE).textValue(); + switch (extensionValueType) { + case EXTENSION_VALUE_TYPE_HEX: + return matchHex(matchKey, matchValue, value); - default: - throw new IllegalArgumentException(String.format( - "Unknown extension value type \"%s\" for extension \"%s\"", - extensionValueType, - matchKey - )); - } + default: + throw new IllegalArgumentException( + String.format( + "Unknown extension value type \"%s\" for extension \"%s\"", + extensionValueType, matchKey)); } + } - private boolean matchHex(String matchKey, JsonNode matchValue, ASN1Primitive value) { - final String matchValueString = matchValue.get(EXTENSION_VALUE_VALUE).textValue(); - final ByteArray matchBytes; - try { - matchBytes = ByteArray.fromHex(matchValueString); - } catch (HexException e) { - throw new IllegalArgumentException(String.format( - "Bad hex value in extension %s: %s", - matchKey, - matchValueString - )); - } - - final ASN1Primitive innerValue; - if (value instanceof DEROctetString) { - try { - innerValue = ASN1Primitive.fromByteArray(((DEROctetString) value).getOctets()); - } catch (IOException e) { - log.debug("Failed to parse {} extension value as ASN1: {}", matchKey, value); - return false; - } - } else { - log.debug("Expected nested bit string value for extension {}, was: {}", matchKey, value); - return false; - } + private boolean matchHex(String matchKey, JsonNode matchValue, ASN1Primitive value) { + final String matchValueString = matchValue.get(EXTENSION_VALUE_VALUE).textValue(); + final ByteArray matchBytes; + try { + matchBytes = ByteArray.fromHex(matchValueString); + } catch (HexException e) { + throw new IllegalArgumentException( + String.format("Bad hex value in extension %s: %s", matchKey, matchValueString)); + } - if (innerValue instanceof DEROctetString) { - final ByteArray readBytes = new ByteArray(((DEROctetString) innerValue).getOctets()); - return matchBytes.equals(readBytes); - } else { - log.debug("Expected nested bit string value for extension {}, was: {}", matchKey, value); - return false; - } + final ASN1Primitive innerValue; + if (value instanceof DEROctetString) { + try { + innerValue = ASN1Primitive.fromByteArray(((DEROctetString) value).getOctets()); + } catch (IOException e) { + log.debug("Failed to parse {} extension value as ASN1: {}", matchKey, value); + return false; + } + } else { + log.debug("Expected nested bit string value for extension {}, was: {}", matchKey, value); + return false; } + if (innerValue instanceof DEROctetString) { + final ByteArray readBytes = new ByteArray(((DEROctetString) innerValue).getOctets()); + return matchBytes.equals(readBytes); + } else { + log.debug("Expected nested bit string value for extension {}, was: {}", matchKey, value); + return false; + } + } } diff --git a/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/matcher/FingerprintMatcher.java b/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/matcher/FingerprintMatcher.java index cdd7f3720..a057368c3 100644 --- a/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/matcher/FingerprintMatcher.java +++ b/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/matcher/FingerprintMatcher.java @@ -31,25 +31,26 @@ import java.security.cert.X509Certificate; public final class FingerprintMatcher implements DeviceMatcher { - public static final String SELECTOR_TYPE = "fingerprint"; + public static final String SELECTOR_TYPE = "fingerprint"; - private static final String FINGERPRINTS_KEY = "fingerprints"; + private static final String FINGERPRINTS_KEY = "fingerprints"; - @Override - public boolean matches(X509Certificate attestationCertificate, JsonNode parameters) { - JsonNode fingerprints = parameters.get(FINGERPRINTS_KEY); - if(fingerprints.isArray()) { - try { - String fingerprint = Hashing.sha1().hashBytes(attestationCertificate.getEncoded()).toString().toLowerCase(); - for(JsonNode candidate : fingerprints) { - if(fingerprint.equals(candidate.asText().toLowerCase())) { - return true; - } - } - } catch (CertificateEncodingException e) { - //Fall through to return false. - } + @Override + public boolean matches(X509Certificate attestationCertificate, JsonNode parameters) { + JsonNode fingerprints = parameters.get(FINGERPRINTS_KEY); + if (fingerprints.isArray()) { + try { + String fingerprint = + Hashing.sha1().hashBytes(attestationCertificate.getEncoded()).toString().toLowerCase(); + for (JsonNode candidate : fingerprints) { + if (fingerprint.equals(candidate.asText().toLowerCase())) { + return true; + } } - return false; + } catch (CertificateEncodingException e) { + // Fall through to return false. + } } + return false; + } } diff --git a/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/resolver/CompositeAttestationResolver.java b/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/resolver/CompositeAttestationResolver.java index e0bb0b5c7..85ff4a367 100644 --- a/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/resolver/CompositeAttestationResolver.java +++ b/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/resolver/CompositeAttestationResolver.java @@ -32,40 +32,37 @@ import java.util.Optional; /** - * An {@link AttestationResolver} whose {@link #resolve(X509Certificate, List)} - * method calls {@link AttestationResolver#resolve(X509Certificate, List)} on - * each of the subordinate {@link AttestationResolver}s in turn, and returns - * the first non-null result. + * An {@link AttestationResolver} whose {@link #resolve(X509Certificate, List)} method calls {@link + * AttestationResolver#resolve(X509Certificate, List)} on each of the subordinate {@link + * AttestationResolver}s in turn, and returns the first non-null result. */ public final class CompositeAttestationResolver implements AttestationResolver { - private final List resolvers; + private final List resolvers; - public CompositeAttestationResolver(List resolvers) { - this.resolvers = CollectionUtil.immutableList(resolvers); - } + public CompositeAttestationResolver(List resolvers) { + this.resolvers = CollectionUtil.immutableList(resolvers); + } - @Override - public Optional resolve(X509Certificate attestationCertificate, List certificateChain) { - for (AttestationResolver resolver : resolvers) { - Optional result = resolver.resolve(attestationCertificate, certificateChain); - if (result.isPresent()) { - return result; - } - } - return Optional.empty(); + @Override + public Optional resolve( + X509Certificate attestationCertificate, List certificateChain) { + for (AttestationResolver resolver : resolvers) { + Optional result = resolver.resolve(attestationCertificate, certificateChain); + if (result.isPresent()) { + return result; + } } + return Optional.empty(); + } - /** - * Delegates to the first subordinate resolver, or throws an exception if there is none. - */ - @Override - public Attestation untrustedFromCertificate(X509Certificate attestationCertificate) { - if (resolvers.isEmpty()) { - throw new UnsupportedOperationException("Cannot do this without any sub-resolver."); - } else { - return resolvers.get(0).untrustedFromCertificate(attestationCertificate); - } + /** Delegates to the first subordinate resolver, or throws an exception if there is none. */ + @Override + public Attestation untrustedFromCertificate(X509Certificate attestationCertificate) { + if (resolvers.isEmpty()) { + throw new UnsupportedOperationException("Cannot do this without any sub-resolver."); + } else { + return resolvers.get(0).untrustedFromCertificate(attestationCertificate); } - + } } diff --git a/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/resolver/CompositeTrustResolver.java b/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/resolver/CompositeTrustResolver.java index c1cc4be31..4578f29ea 100644 --- a/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/resolver/CompositeTrustResolver.java +++ b/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/resolver/CompositeTrustResolver.java @@ -31,27 +31,28 @@ import java.util.Optional; /** - * A {@link TrustResolver} whose {@link #resolveTrustAnchor(X509Certificate, - * List)} method calls {@link TrustResolver#resolveTrustAnchor(X509Certificate, - * List)} on each of the subordinate {@link TrustResolver}s in turn, and - * returns the first non-null result. + * A {@link TrustResolver} whose {@link #resolveTrustAnchor(X509Certificate, List)} method calls + * {@link TrustResolver#resolveTrustAnchor(X509Certificate, List)} on each of the subordinate {@link + * TrustResolver}s in turn, and returns the first non-null result. */ public final class CompositeTrustResolver implements TrustResolver { - private final List resolvers; + private final List resolvers; - public CompositeTrustResolver(List resolvers) { - this.resolvers = CollectionUtil.immutableList(resolvers); - } + public CompositeTrustResolver(List resolvers) { + this.resolvers = CollectionUtil.immutableList(resolvers); + } - @Override - public Optional resolveTrustAnchor(X509Certificate attestationCertificate, List certificateChain) { - for (TrustResolver resolver : resolvers) { - Optional result = resolver.resolveTrustAnchor(attestationCertificate, certificateChain); - if (result.isPresent()) { - return result; - } - } - return Optional.empty(); + @Override + public Optional resolveTrustAnchor( + X509Certificate attestationCertificate, List certificateChain) { + for (TrustResolver resolver : resolvers) { + Optional result = + resolver.resolveTrustAnchor(attestationCertificate, certificateChain); + if (result.isPresent()) { + return result; + } } + return Optional.empty(); + } } diff --git a/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/resolver/SimpleAttestationResolver.java b/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/resolver/SimpleAttestationResolver.java index 37878434a..a3e7a4d57 100644 --- a/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/resolver/SimpleAttestationResolver.java +++ b/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/resolver/SimpleAttestationResolver.java @@ -31,6 +31,7 @@ import com.yubico.internal.util.CertificateParser; import com.yubico.internal.util.CollectionUtil; import com.yubico.internal.util.ExceptionUtil; +import com.yubico.internal.util.OptionalUtil; import com.yubico.webauthn.attestation.Attestation; import com.yubico.webauthn.attestation.AttestationResolver; import com.yubico.webauthn.attestation.DeviceMatcher; @@ -49,144 +50,153 @@ import java.util.Optional; import lombok.NonNull; - public final class SimpleAttestationResolver implements AttestationResolver { - private static final String SELECTORS = "selectors"; - private static final String SELECTOR_TYPE = "type"; - private static final String SELECTOR_PARAMETERS = "parameters"; - - private static final String TRANSPORTS = "transports"; - private static final String TRANSPORTS_EXT_OID = "1.3.6.1.4.1.45724.2.1.1"; - - private static final Map DEFAULT_DEVICE_MATCHERS = ImmutableMap.of( - ExtensionMatcher.SELECTOR_TYPE, new ExtensionMatcher(), - FingerprintMatcher.SELECTOR_TYPE, new FingerprintMatcher() - ); - - private final Map metadata = new HashMap<>(); - private final TrustResolver trustResolver; - private final Map matchers; - - public SimpleAttestationResolver( - @NonNull Collection objects, - @NonNull TrustResolver trustResolver, - @NonNull Map matchers - ) throws CertificateException { - for (MetadataObject object : objects) { - for (String caPem : object.getTrustedCertificates()) { - X509Certificate trustAnchor = CertificateParser.parsePem(caPem); - metadata.put(trustAnchor, object); - } - } - - this.trustResolver = trustResolver; - this.matchers = CollectionUtil.immutableMap(matchers); - } - - public SimpleAttestationResolver(Collection objects, TrustResolver trustResolver) throws CertificateException { - this(objects, trustResolver, DEFAULT_DEVICE_MATCHERS); + private static final String SELECTORS = "selectors"; + private static final String SELECTOR_TYPE = "type"; + private static final String SELECTOR_PARAMETERS = "parameters"; + + private static final String TRANSPORTS = "transports"; + private static final String TRANSPORTS_EXT_OID = "1.3.6.1.4.1.45724.2.1.1"; + + private static final Map DEFAULT_DEVICE_MATCHERS = + ImmutableMap.of( + ExtensionMatcher.SELECTOR_TYPE, new ExtensionMatcher(), + FingerprintMatcher.SELECTOR_TYPE, new FingerprintMatcher()); + + private final Map metadata = new HashMap<>(); + private final TrustResolver trustResolver; + private final Map matchers; + + public SimpleAttestationResolver( + @NonNull Collection objects, + @NonNull TrustResolver trustResolver, + @NonNull Map matchers) + throws CertificateException { + for (MetadataObject object : objects) { + for (String caPem : object.getTrustedCertificates()) { + X509Certificate trustAnchor = CertificateParser.parsePem(caPem); + metadata.put(trustAnchor, object); + } } - private Optional lookupTrustAnchor(X509Certificate trustAnchor) { - return Optional.ofNullable(metadata.get(trustAnchor)); - } - - @Override - public Optional resolve(X509Certificate attestationCertificate, List certificateChain) { - Optional trustAnchor = trustResolver.resolveTrustAnchor(attestationCertificate, certificateChain); - - return trustAnchor.flatMap(this::lookupTrustAnchor).map(metadata -> { - Map vendorProperties; - Map deviceProperties = null; - String identifier; - int metadataTransports = 0; - - identifier = metadata.getIdentifier(); - vendorProperties = Maps.filterValues(metadata.getVendorInfo(), Objects::nonNull); - for (JsonNode device : metadata.getDevices()) { + this.trustResolver = trustResolver; + this.matchers = CollectionUtil.immutableMap(matchers); + } + + public SimpleAttestationResolver(Collection objects, TrustResolver trustResolver) + throws CertificateException { + this(objects, trustResolver, DEFAULT_DEVICE_MATCHERS); + } + + private Optional lookupTrustAnchor(X509Certificate trustAnchor) { + return Optional.ofNullable(metadata.get(trustAnchor)); + } + + @Override + public Optional resolve( + X509Certificate attestationCertificate, List certificateChain) { + Optional trustAnchor = + trustResolver.resolveTrustAnchor(attestationCertificate, certificateChain); + + return trustAnchor + .flatMap(this::lookupTrustAnchor) + .map( + metadata -> { + Map vendorProperties; + Map deviceProperties = null; + String identifier; + int metadataTransports = 0; + + identifier = metadata.getIdentifier(); + vendorProperties = Maps.filterValues(metadata.getVendorInfo(), Objects::nonNull); + for (JsonNode device : metadata.getDevices()) { if (deviceMatches(device.get(SELECTORS), attestationCertificate)) { - JsonNode transportNode = device.get(TRANSPORTS); - if (transportNode != null) { - metadataTransports |= transportNode.asInt(0); - } - ImmutableMap.Builder devicePropertiesBuilder = ImmutableMap.builder(); - for (Map.Entry deviceEntry : Lists.newArrayList(device.fields())) { - JsonNode value = deviceEntry.getValue(); - if (value.isTextual()) { - devicePropertiesBuilder.put(deviceEntry.getKey(), value.asText()); - } + JsonNode transportNode = device.get(TRANSPORTS); + if (transportNode != null) { + metadataTransports |= transportNode.asInt(0); + } + ImmutableMap.Builder devicePropertiesBuilder = + ImmutableMap.builder(); + for (Map.Entry deviceEntry : + Lists.newArrayList(device.fields())) { + JsonNode value = deviceEntry.getValue(); + if (value.isTextual()) { + devicePropertiesBuilder.put(deviceEntry.getKey(), value.asText()); } - deviceProperties = devicePropertiesBuilder.build(); - break; + } + deviceProperties = devicePropertiesBuilder.build(); + break; } - } - - return Attestation.builder() - .trusted(true) - .metadataIdentifier(Optional.ofNullable(identifier)) - .vendorProperties(Optional.of(vendorProperties)) - .deviceProperties(Optional.ofNullable(deviceProperties)) - .transports(Optional.of(Transport.fromInt(getTransports(attestationCertificate) | metadataTransports))) - .build(); - }); - } - - private boolean deviceMatches( - JsonNode selectors, - @NonNull X509Certificate attestationCertificate - ) { - if (selectors == null || selectors.isNull()) { - return true; - } else { - for (JsonNode selector : selectors) { - DeviceMatcher matcher = matchers.get(selector.get(SELECTOR_TYPE).asText()); - if (matcher != null && matcher.matches(attestationCertificate, selector.get(SELECTOR_PARAMETERS))) { - return true; - } - } - return false; + } + + return Attestation.builder() + .trusted(true) + .metadataIdentifier(Optional.ofNullable(identifier)) + .vendorProperties(Optional.of(vendorProperties)) + .deviceProperties(Optional.ofNullable(deviceProperties)) + .transports( + OptionalUtil.zipWith( + getTransports(attestationCertificate), + Optional.of(metadataTransports).filter(t -> t != 0), + (a, b) -> a | b) + .map(Transport::fromInt)) + .build(); + }); + } + + private boolean deviceMatches( + JsonNode selectors, @NonNull X509Certificate attestationCertificate) { + if (selectors == null || selectors.isNull()) { + return true; + } else { + for (JsonNode selector : selectors) { + DeviceMatcher matcher = matchers.get(selector.get(SELECTOR_TYPE).asText()); + if (matcher != null + && matcher.matches(attestationCertificate, selector.get(SELECTOR_PARAMETERS))) { + return true; } + } + return false; } + } - private static int getTransports(X509Certificate cert) { - byte[] extensionValue = cert.getExtensionValue(TRANSPORTS_EXT_OID); - - if(extensionValue == null) { - return 0; - } + private static Optional getTransports(X509Certificate cert) { + byte[] extensionValue = cert.getExtensionValue(TRANSPORTS_EXT_OID); - ExceptionUtil.assure( - extensionValue.length >= 4, - "Transports extension value must be at least 4 bytes (2 bytes octet string header, 2 bytes bit string header), was: %d", - extensionValue.length - ); + if (extensionValue == null) { + return Optional.empty(); + } - // Mask out unused bits (shouldn't be needed as they should already be 0). - int unusedBitMask = 0xff; - for(int i=0; i < extensionValue[3]; i++) { - unusedBitMask <<= 1; - } - extensionValue[extensionValue.length-1] &= unusedBitMask; - - int transports = 0; - for(int i=extensionValue.length - 1; i >= 5; i--) { - byte b = extensionValue[i]; - for(int bi=0; bi < 8; bi++) { - transports = (transports << 1) | (b & 1); - b >>= 1; - } - } + ExceptionUtil.assure( + extensionValue.length >= 4, + "Transports extension value must be at least 4 bytes (2 bytes octet string header, 2 bytes bit string header), was: %d", + extensionValue.length); - return transports; + // Mask out unused bits (shouldn't be needed as they should already be 0). + int unusedBitMask = 0xff; + for (int i = 0; i < extensionValue[3]; i++) { + unusedBitMask <<= 1; } - - @Override - public Attestation untrustedFromCertificate(X509Certificate attestationCertificate) { - return Attestation.builder() - .trusted(false) - .transports(Optional.of(Transport.fromInt(getTransports(attestationCertificate)))) - .build(); + extensionValue[extensionValue.length - 1] &= unusedBitMask; + + int transports = 0; + for (int i = extensionValue.length - 1; i >= 5; i--) { + byte b = extensionValue[i]; + for (int bi = 0; bi < 8; bi++) { + transports = (transports << 1) | (b & 1); + b >>= 1; + } } + return Optional.of(transports); + } + + @Override + public Attestation untrustedFromCertificate(X509Certificate attestationCertificate) { + return Attestation.builder() + .trusted(false) + .transports(getTransports(attestationCertificate).map(Transport::fromInt)) + .build(); + } } diff --git a/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/resolver/SimpleTrustResolver.java b/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/resolver/SimpleTrustResolver.java index 8020eb505..e7552a7cb 100644 --- a/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/resolver/SimpleTrustResolver.java +++ b/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/resolver/SimpleTrustResolver.java @@ -46,79 +46,97 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; - /** - * Assesses whether an argument certificate can be trusted, and if so, by what - * trusted root certificate. + * Assesses whether an argument certificate can be trusted, and if so, by what trusted root + * certificate. */ public final class SimpleTrustResolver implements TrustResolver { - private static final Logger logger = LoggerFactory.getLogger(SimpleTrustResolver.class); + private static final Logger logger = LoggerFactory.getLogger(SimpleTrustResolver.class); - private final Multimap trustedCerts = ArrayListMultimap.create(); + private final Multimap trustedCerts = ArrayListMultimap.create(); - public SimpleTrustResolver(Iterable trustedCertificates) { - for (X509Certificate cert : trustedCertificates) { - trustedCerts.put(cert.getSubjectDN().getName(), cert); - } + public SimpleTrustResolver(Iterable trustedCertificates) { + for (X509Certificate cert : trustedCertificates) { + trustedCerts.put(cert.getSubjectDN().getName(), cert); } - - public static SimpleTrustResolver fromMetadata(Iterable metadataObjects) throws CertificateException { - Set certs = new HashSet<>(); - for (MetadataObject metadata : metadataObjects) { - for (String encodedCert : metadata.getTrustedCertificates()) { - certs.add(CertificateParser.parsePem(encodedCert)); - } - } - return new SimpleTrustResolver(certs); + } + + public static SimpleTrustResolver fromMetadata(Iterable metadataObjects) + throws CertificateException { + Set certs = new HashSet<>(); + for (MetadataObject metadata : metadataObjects) { + for (String encodedCert : metadata.getTrustedCertificates()) { + certs.add(CertificateParser.parsePem(encodedCert)); + } } - - public static SimpleTrustResolver fromMetadataJson(String metadataObjectJson) throws IOException, CertificateException { - return fromMetadata(Collections.singleton(JacksonCodecs.json().readValue(metadataObjectJson, MetadataObject.class))); - } - - @Override - public Optional resolveTrustAnchor(X509Certificate attestationCertificate, List caCertificateChain) { - final List certChain = new ArrayList<>(); - certChain.add(attestationCertificate); - certChain.addAll(caCertificateChain); - - X509Certificate lastTriedCert = null; - - for (X509Certificate untrustedCert : certChain) { - if (lastTriedCert != null) { - logger.trace("No trusted certificate has signed certificate [{}] - trying next element in certificate chain.", lastTriedCert); - - try { - lastTriedCert.verify(untrustedCert.getPublicKey()); - } catch (CertificateException | NoSuchAlgorithmException | InvalidKeyException | NoSuchProviderException e) { - logger.error("Failed to verify that certificate [{}] was signed by [{}]", lastTriedCert, untrustedCert, e); - throw new RuntimeException("Resolve failed", e); - } catch (SignatureException e) { - logger.debug("Certificate chain broken - certificate [{}] was not signed by certificate [{}]", lastTriedCert, untrustedCert); - return Optional.empty(); - } - } - - final String issuer = untrustedCert.getIssuerDN().getName(); - for (X509Certificate trustedCert : trustedCerts.get(issuer)) { - try { - untrustedCert.verify(trustedCert.getPublicKey()); - logger.debug("Found signature from trusted certificate [{}]", trustedCert); - return Optional.of(trustedCert); - } catch (CertificateException | NoSuchAlgorithmException | InvalidKeyException | NoSuchProviderException e) { - logger.error("Resolve failed", e); - throw new RuntimeException("Resolve failed", e); - } catch (SignatureException e) { - // Not signed by the trusted cert - } - } - - lastTriedCert = untrustedCert; + return new SimpleTrustResolver(certs); + } + + public static SimpleTrustResolver fromMetadataJson(String metadataObjectJson) + throws IOException, CertificateException { + return fromMetadata( + Collections.singleton( + JacksonCodecs.json().readValue(metadataObjectJson, MetadataObject.class))); + } + + @Override + public Optional resolveTrustAnchor( + X509Certificate attestationCertificate, List caCertificateChain) { + final List certChain = new ArrayList<>(); + certChain.add(attestationCertificate); + certChain.addAll(caCertificateChain); + + X509Certificate lastTriedCert = null; + + for (X509Certificate untrustedCert : certChain) { + if (lastTriedCert != null) { + logger.trace( + "No trusted certificate has signed certificate [{}] - trying next element in certificate chain.", + lastTriedCert); + + try { + lastTriedCert.verify(untrustedCert.getPublicKey()); + } catch (CertificateException + | NoSuchAlgorithmException + | InvalidKeyException + | NoSuchProviderException e) { + logger.error( + "Failed to verify that certificate [{}] was signed by [{}]", + lastTriedCert, + untrustedCert, + e); + throw new RuntimeException("Resolve failed", e); + } catch (SignatureException e) { + logger.debug( + "Certificate chain broken - certificate [{}] was not signed by certificate [{}]", + lastTriedCert, + untrustedCert); + return Optional.empty(); + } + } + + final String issuer = untrustedCert.getIssuerDN().getName(); + for (X509Certificate trustedCert : trustedCerts.get(issuer)) { + try { + untrustedCert.verify(trustedCert.getPublicKey()); + logger.debug("Found signature from trusted certificate [{}]", trustedCert); + return Optional.of(trustedCert); + } catch (CertificateException + | NoSuchAlgorithmException + | InvalidKeyException + | NoSuchProviderException e) { + logger.error("Resolve failed", e); + throw new RuntimeException("Resolve failed", e); + } catch (SignatureException e) { + // Not signed by the trusted cert } + } - logger.debug("No trusted certificate has signed certificate chain {}", certChain); - return Optional.empty(); + lastTriedCert = untrustedCert; } + logger.debug("No trusted certificate has signed certificate chain {}", certChain); + return Optional.empty(); + } } diff --git a/webauthn-server-attestation/src/test/java/com/yubico/webauthn/attestation/MetadataObjectTest.java b/webauthn-server-attestation/src/test/java/com/yubico/webauthn/attestation/MetadataObjectTest.java index bfca8d5dc..445dced2c 100644 --- a/webauthn-server-attestation/src/test/java/com/yubico/webauthn/attestation/MetadataObjectTest.java +++ b/webauthn-server-attestation/src/test/java/com/yubico/webauthn/attestation/MetadataObjectTest.java @@ -24,49 +24,50 @@ package com.yubico.webauthn.attestation; +import static org.junit.Assert.assertEquals; + import com.fasterxml.jackson.databind.ObjectMapper; import com.yubico.internal.util.JacksonCodecs; import org.junit.Test; -import static org.junit.Assert.assertEquals; - public class MetadataObjectTest { - public static final String JSON = "{" - + "\"identifier\":\"foobar\"," - + "\"version\":1," - + "\"vendorInfo\":{\"name\":\"Yubico\",\"url\":\"https://yubico.com\",\"imageUrl\":\"https://developers.yubico.com/U2F/Images/yubico.png\"}," - + "\"trustedCertificates\":[\"-----BEGIN CERTIFICATE-----\\nMIIDHjCCAgagAwIBAgIEG1BT9zANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZ\\ndWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAw\\nMDBaGA8yMDUwMDkwNDAwMDAwMFowLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290\\nIENBIFNlcmlhbCA0NTcyMDA2MzEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK\\nAoIBAQC/jwYuhBVlqaiYWEMsrWFisgJ+PtM91eSrpI4TK7U53mwCIawSDHy8vUmk\\n5N2KAj9abvT9NP5SMS1hQi3usxoYGonXQgfO6ZXyUA9a+KAkqdFnBnlyugSeCOep\\n8EdZFfsaRFtMjkwz5Gcz2Py4vIYvCdMHPtwaz0bVuzneueIEz6TnQjE63Rdt2zbw\\nnebwTG5ZybeWSwbzy+BJ34ZHcUhPAY89yJQXuE0IzMZFcEBbPNRbWECRKgjq//qT\\n9nmDOFVlSRCt2wiqPSzluwn+v+suQEBsUjTGMEd25tKXXTkNW21wIWbxeSyUoTXw\\nLvGS6xlwQSgNpk2qXYwf8iXg7VWZAgMBAAGjQjBAMB0GA1UdDgQWBBQgIvz0bNGJ\\nhjgpToksyKpP9xv9oDAPBgNVHRMECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIBBjAN\\nBgkqhkiG9w0BAQsFAAOCAQEAjvjuOMDSa+JXFCLyBKsycXtBVZsJ4Ue3LbaEsPY4\\nMYN/hIQ5ZM5p7EjfcnMG4CtYkNsfNHc0AhBLdq45rnT87q/6O3vUEtNMafbhU6kt\\nhX7Y+9XFN9NpmYxr+ekVY5xOxi8h9JDIgoMP4VB1uS0aunL1IGqrNooL9mmFnL2k\\nLVVee6/VR6C5+KSTCMCWppMuJIZII2v9o4dkoZ8Y7QRjQlLfYzd3qGtKbw7xaF1U\\nsG/5xUb/Btwb2X2g4InpiB/yt/3CpQXpiWX/K4mBvUKiGn05ZsqeY1gx4g0xLBqc\\nU9psmyPzK+Vsgw2jeRQ5JlKDyqE0hebfC1tvFu0CCrJFcw==\\n-----END CERTIFICATE-----\"]," - + "\"devices\":[{" - + "\"deviceId\":\"1.3.6.1.4.1.41482.1.2\"," - + "\"deviceUrl\":\"https://www.yubico.com/products/yubikey-hardware/yubikey-neo/\"," - + "\"displayName\":\"YubiKey NEO/NEO-n\"," - + "\"imageUrl\":\"https://developers.yubico.com/U2F/Images/NEO.png\"," - + "\"selectors\":[{" - + "\"type\":\"x509Extension\"," - + "\"parameters\":{" - + "\"key\":\"1.3.6.1.4.1.41482.1.2\"" - + "}" - + "}]" - + "}]" - + "}"; - - private final ObjectMapper objectMapper = JacksonCodecs.json(); + public static final String JSON = + "{" + + "\"identifier\":\"foobar\"," + + "\"version\":1," + + "\"vendorInfo\":{\"name\":\"Yubico\",\"url\":\"https://yubico.com\",\"imageUrl\":\"https://developers.yubico.com/U2F/Images/yubico.png\"}," + + "\"trustedCertificates\":[\"-----BEGIN CERTIFICATE-----\\nMIIDHjCCAgagAwIBAgIEG1BT9zANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZ\\ndWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAw\\nMDBaGA8yMDUwMDkwNDAwMDAwMFowLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290\\nIENBIFNlcmlhbCA0NTcyMDA2MzEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK\\nAoIBAQC/jwYuhBVlqaiYWEMsrWFisgJ+PtM91eSrpI4TK7U53mwCIawSDHy8vUmk\\n5N2KAj9abvT9NP5SMS1hQi3usxoYGonXQgfO6ZXyUA9a+KAkqdFnBnlyugSeCOep\\n8EdZFfsaRFtMjkwz5Gcz2Py4vIYvCdMHPtwaz0bVuzneueIEz6TnQjE63Rdt2zbw\\nnebwTG5ZybeWSwbzy+BJ34ZHcUhPAY89yJQXuE0IzMZFcEBbPNRbWECRKgjq//qT\\n9nmDOFVlSRCt2wiqPSzluwn+v+suQEBsUjTGMEd25tKXXTkNW21wIWbxeSyUoTXw\\nLvGS6xlwQSgNpk2qXYwf8iXg7VWZAgMBAAGjQjBAMB0GA1UdDgQWBBQgIvz0bNGJ\\nhjgpToksyKpP9xv9oDAPBgNVHRMECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIBBjAN\\nBgkqhkiG9w0BAQsFAAOCAQEAjvjuOMDSa+JXFCLyBKsycXtBVZsJ4Ue3LbaEsPY4\\nMYN/hIQ5ZM5p7EjfcnMG4CtYkNsfNHc0AhBLdq45rnT87q/6O3vUEtNMafbhU6kt\\nhX7Y+9XFN9NpmYxr+ekVY5xOxi8h9JDIgoMP4VB1uS0aunL1IGqrNooL9mmFnL2k\\nLVVee6/VR6C5+KSTCMCWppMuJIZII2v9o4dkoZ8Y7QRjQlLfYzd3qGtKbw7xaF1U\\nsG/5xUb/Btwb2X2g4InpiB/yt/3CpQXpiWX/K4mBvUKiGn05ZsqeY1gx4g0xLBqc\\nU9psmyPzK+Vsgw2jeRQ5JlKDyqE0hebfC1tvFu0CCrJFcw==\\n-----END CERTIFICATE-----\"]," + + "\"devices\":[{" + + "\"deviceId\":\"1.3.6.1.4.1.41482.1.2\"," + + "\"deviceUrl\":\"https://www.yubico.com/products/yubikey-hardware/yubikey-neo/\"," + + "\"displayName\":\"YubiKey NEO/NEO-n\"," + + "\"imageUrl\":\"https://developers.yubico.com/U2F/Images/NEO.png\"," + + "\"selectors\":[{" + + "\"type\":\"x509Extension\"," + + "\"parameters\":{" + + "\"key\":\"1.3.6.1.4.1.41482.1.2\"" + + "}" + + "}]" + + "}]" + + "}"; - @Test - public void testToAndFromJson() throws Exception { - MetadataObject metadata = objectMapper.readValue(JSON, MetadataObject.class); - ObjectMapper objectMapper = new ObjectMapper(); - MetadataObject metadata2 = objectMapper.readValue(objectMapper.writeValueAsString(metadata), MetadataObject.class); + private final ObjectMapper objectMapper = JacksonCodecs.json(); - assertEquals("foobar", metadata.getIdentifier()); - assertEquals(1, metadata.getVersion()); - assertEquals(1, metadata.getTrustedCertificates().size()); + @Test + public void testToAndFromJson() throws Exception { + MetadataObject metadata = objectMapper.readValue(JSON, MetadataObject.class); + ObjectMapper objectMapper = new ObjectMapper(); + MetadataObject metadata2 = + objectMapper.readValue(objectMapper.writeValueAsString(metadata), MetadataObject.class); - assertEquals("Yubico", metadata.getVendorInfo().get("name")); - assertEquals("1.3.6.1.4.1.41482.1.2", metadata.getDevices().get(0).get("deviceId").asText()); + assertEquals("foobar", metadata.getIdentifier()); + assertEquals(1, metadata.getVersion()); + assertEquals(1, metadata.getTrustedCertificates().size()); - assertEquals(metadata, metadata2); - assertEquals(JSON, objectMapper.writeValueAsString(metadata)); - } + assertEquals("Yubico", metadata.getVendorInfo().get("name")); + assertEquals("1.3.6.1.4.1.41482.1.2", metadata.getDevices().get(0).get("deviceId").asText()); + assertEquals(metadata, metadata2); + assertEquals(JSON, objectMapper.writeValueAsString(metadata)); + } } diff --git a/webauthn-server-attestation/src/test/java/com/yubico/webauthn/attestation/StandardMetadataServiceTest.java b/webauthn-server-attestation/src/test/java/com/yubico/webauthn/attestation/StandardMetadataServiceTest.java index 6582a848e..df57edccf 100644 --- a/webauthn-server-attestation/src/test/java/com/yubico/webauthn/attestation/StandardMetadataServiceTest.java +++ b/webauthn-server-attestation/src/test/java/com/yubico/webauthn/attestation/StandardMetadataServiceTest.java @@ -24,6 +24,10 @@ package com.yubico.webauthn.attestation; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + import com.google.common.hash.Hashing; import com.yubico.internal.util.CertificateParser; import java.security.cert.CertificateException; @@ -33,75 +37,75 @@ import java.util.Optional; import org.junit.Test; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; - public class StandardMetadataServiceTest { - private static final String ATTESTATION_CERT = "MIICGzCCAQWgAwIBAgIEdaP2dTALBgkqhkiG9w0BAQswLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290IENBIFNlcmlhbCA0NTcyMDA2MzEwIBcNMTQwODAxMDAwMDAwWhgPMjA1MDA5MDQwMDAwMDBaMCoxKDAmBgNVBAMMH1l1YmljbyBVMkYgRUUgU2VyaWFsIDE5NzM2Nzk3MzMwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQZo35Damtpl81YdmcbhEuXKAr7xDcQzAy5n3ftAAhtBbu8EeGU4ynfSgLonckqX6J2uXLBppTNE3v2bt+Yf8MLoxIwEDAOBgorBgEEAYLECgECBAAwCwYJKoZIhvcNAQELA4IBAQG9LbiNPgs0sQYOHAJcg+lMk+HCsiWRlYVnbT4I/5lnqU907vY17XYAORd432bU3Nnhsbkvjz76kQJGXeNAF4DPANGGlz8JU+LNEVE2PWPGgEM0GXgB7mZN5Sinfy1AoOdO+3c3bfdJQuXlUxHbo+nDpxxKpzq9gr++RbokF1+0JBkMbaA/qLYL4WdhY5NvaOyMvYpO3sBxlzn6FcP67hlotGH1wU7qhCeh+uur7zDeAWVh7c4QtJOXHkLJQfV3Z7ZMvhkIA6jZJAX99hisABU/SSa5DtgX7AfsHwa04h69AAAWDUzSk3HgOXbUd1FaSOPdlVFkG2N2JllFHykyO3zO"; - private static final String ATTESTATION_CERT2 = "MIICLzCCARmgAwIBAgIEQvUaTTALBgkqhkiG9w0BAQswLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290IENBIFNlcmlhbCA0NTcyMDA2MzEwIBcNMTQwODAxMDAwMDAwWhgPMjA1MDA5MDQwMDAwMDBaMCoxKDAmBgNVBAMMH1l1YmljbyBVMkYgRUUgU2VyaWFsIDExMjMzNTkzMDkwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQphQ+PJYiZjZEVHtrx5QGE3/LE1+OytZPTwzrpWBKywji/3qmg22mwmVFl32PO269TxY+yVN4jbfVf5uX0EWJWoyYwJDAiBgkrBgEEAYLECgIEFTEuMy42LjEuNC4xLjQxNDgyLjEuNDALBgkqhkiG9w0BAQsDggEBALSc3YwTRbLwXhePj/imdBOhWiqh6ssS2ONgp5tphJCHR5Agjg2VstLBRsJzyJnLgy7bGZ0QbPOyh/J0hsvgBfvjByXOu1AwCW+tcoJ+pfxESojDLDn8hrFph6eWZoCtBsWMDh6vMqPENeP6grEAECWx4fTpBL9Bm7F+0Rp/d1/l66g4IhF/ZvuRFhY+BUK94BfivuBHpEkMwxKENTas7VkxvlVstUvPqhPHGYOq7RdF1D/THsbNY8+tgCTgvTziEG+bfDeY6zIz5h7bxb1rpajNVTpUDWtVYL7/w44e1KCoErqdS+kEbmmkmm7KvDE8kuyg42Fmb5DTMsbY2jxMlMU="; - private static final String ATTESTATION_CERT_WITH_TRANSPORTS = "MIICIjCCAQygAwIBAgIEIHHwozALBgkqhkiG9w0BAQswDzENMAsGA1UEAxMEdGVzdDAeFw0xNTA4MTEwOTAwMzNaFw0xNjA4MTAwOTAwMzNaMCkxJzAlBgNVBAMTHll1YmljbyBVMkYgRUUgU2VyaWFsIDU0NDMzODA4MzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABPdFG1pBjBBQVhLrD39Qg1vKjuR2kRdBZnwLI/zgzztQpf4ffpkrkB/3E0TXj5zg8gN9sgMkX48geBe+tBEpvMmjOzA5MCIGCSsGAQQBgsQKAgQVMS4zLjYuMS40LjEuNDE0ODIuMS4yMBMGCysGAQQBguUcAgEBBAQDAgQwMAsGCSqGSIb3DQEBCwOCAQEAb3YpnmHHduNuWEXlLqlnww9034ZeZaojhPAYSLR8d5NPk9gc0hkjQKmIaaBM7DsaHbcHMKpXoMGTQSC++NCZTcKvZ0Lt12mp5HRnM1NNBPol8Hte5fLmvW4tQ9EzLl4gkz7LSlORxTuwTbae1eQqNdxdeB+0ilMFCEUc+3NGCNM0RWd+sP5+gzMXBDQAI1Sc9XaPIg8t3du5JChAl1ifpu/uERZ2WQgtxeBDO6z1Xoa5qz4svf5oURjPZjxS0WUKht48Z2rIjk5lZzERSaY3RrX3UtrnZEIzCmInXOrcRPeAD4ZutpiwuHe62ABsjuMRnKbATbOUiLdknNyPYYQz2g=="; - - @Test - public void testGetAttestation_x509extension_key() throws Exception { - StandardMetadataService service = new StandardMetadataService(); + private static final String ATTESTATION_CERT = + "MIICGzCCAQWgAwIBAgIEdaP2dTALBgkqhkiG9w0BAQswLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290IENBIFNlcmlhbCA0NTcyMDA2MzEwIBcNMTQwODAxMDAwMDAwWhgPMjA1MDA5MDQwMDAwMDBaMCoxKDAmBgNVBAMMH1l1YmljbyBVMkYgRUUgU2VyaWFsIDE5NzM2Nzk3MzMwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQZo35Damtpl81YdmcbhEuXKAr7xDcQzAy5n3ftAAhtBbu8EeGU4ynfSgLonckqX6J2uXLBppTNE3v2bt+Yf8MLoxIwEDAOBgorBgEEAYLECgECBAAwCwYJKoZIhvcNAQELA4IBAQG9LbiNPgs0sQYOHAJcg+lMk+HCsiWRlYVnbT4I/5lnqU907vY17XYAORd432bU3Nnhsbkvjz76kQJGXeNAF4DPANGGlz8JU+LNEVE2PWPGgEM0GXgB7mZN5Sinfy1AoOdO+3c3bfdJQuXlUxHbo+nDpxxKpzq9gr++RbokF1+0JBkMbaA/qLYL4WdhY5NvaOyMvYpO3sBxlzn6FcP67hlotGH1wU7qhCeh+uur7zDeAWVh7c4QtJOXHkLJQfV3Z7ZMvhkIA6jZJAX99hisABU/SSa5DtgX7AfsHwa04h69AAAWDUzSk3HgOXbUd1FaSOPdlVFkG2N2JllFHykyO3zO"; + private static final String ATTESTATION_CERT2 = + "MIICLzCCARmgAwIBAgIEQvUaTTALBgkqhkiG9w0BAQswLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290IENBIFNlcmlhbCA0NTcyMDA2MzEwIBcNMTQwODAxMDAwMDAwWhgPMjA1MDA5MDQwMDAwMDBaMCoxKDAmBgNVBAMMH1l1YmljbyBVMkYgRUUgU2VyaWFsIDExMjMzNTkzMDkwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQphQ+PJYiZjZEVHtrx5QGE3/LE1+OytZPTwzrpWBKywji/3qmg22mwmVFl32PO269TxY+yVN4jbfVf5uX0EWJWoyYwJDAiBgkrBgEEAYLECgIEFTEuMy42LjEuNC4xLjQxNDgyLjEuNDALBgkqhkiG9w0BAQsDggEBALSc3YwTRbLwXhePj/imdBOhWiqh6ssS2ONgp5tphJCHR5Agjg2VstLBRsJzyJnLgy7bGZ0QbPOyh/J0hsvgBfvjByXOu1AwCW+tcoJ+pfxESojDLDn8hrFph6eWZoCtBsWMDh6vMqPENeP6grEAECWx4fTpBL9Bm7F+0Rp/d1/l66g4IhF/ZvuRFhY+BUK94BfivuBHpEkMwxKENTas7VkxvlVstUvPqhPHGYOq7RdF1D/THsbNY8+tgCTgvTziEG+bfDeY6zIz5h7bxb1rpajNVTpUDWtVYL7/w44e1KCoErqdS+kEbmmkmm7KvDE8kuyg42Fmb5DTMsbY2jxMlMU="; + private static final String ATTESTATION_CERT_WITH_TRANSPORTS = + "MIICIjCCAQygAwIBAgIEIHHwozALBgkqhkiG9w0BAQswDzENMAsGA1UEAxMEdGVzdDAeFw0xNTA4MTEwOTAwMzNaFw0xNjA4MTAwOTAwMzNaMCkxJzAlBgNVBAMTHll1YmljbyBVMkYgRUUgU2VyaWFsIDU0NDMzODA4MzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABPdFG1pBjBBQVhLrD39Qg1vKjuR2kRdBZnwLI/zgzztQpf4ffpkrkB/3E0TXj5zg8gN9sgMkX48geBe+tBEpvMmjOzA5MCIGCSsGAQQBgsQKAgQVMS4zLjYuMS40LjEuNDE0ODIuMS4yMBMGCysGAQQBguUcAgEBBAQDAgQwMAsGCSqGSIb3DQEBCwOCAQEAb3YpnmHHduNuWEXlLqlnww9034ZeZaojhPAYSLR8d5NPk9gc0hkjQKmIaaBM7DsaHbcHMKpXoMGTQSC++NCZTcKvZ0Lt12mp5HRnM1NNBPol8Hte5fLmvW4tQ9EzLl4gkz7LSlORxTuwTbae1eQqNdxdeB+0ilMFCEUc+3NGCNM0RWd+sP5+gzMXBDQAI1Sc9XaPIg8t3du5JChAl1ifpu/uERZ2WQgtxeBDO6z1Xoa5qz4svf5oURjPZjxS0WUKht48Z2rIjk5lZzERSaY3RrX3UtrnZEIzCmInXOrcRPeAD4ZutpiwuHe62ABsjuMRnKbATbOUiLdknNyPYYQz2g=="; - X509Certificate attestationCert = CertificateParser.parsePem(ATTESTATION_CERT); - Attestation attestation = service.getAttestation(Collections.singletonList(attestationCert)); + @Test + public void testGetAttestation_x509extension_key() throws Exception { + StandardMetadataService service = new StandardMetadataService(); - assertTrue(attestation.isTrusted()); - assertEquals("Yubico", attestation.getVendorProperties().get().get("name")); - assertEquals("1.3.6.1.4.1.41482.1.2", attestation.getDeviceProperties().get().get("deviceId")); - } + X509Certificate attestationCert = CertificateParser.parsePem(ATTESTATION_CERT); + Attestation attestation = service.getAttestation(Collections.singletonList(attestationCert)); - @Test - public void testGetAttestation_x509extension_key_value() throws Exception { - StandardMetadataService service = new StandardMetadataService(); + assertTrue(attestation.isTrusted()); + assertEquals("Yubico", attestation.getVendorProperties().get().get("name")); + assertEquals("1.3.6.1.4.1.41482.1.2", attestation.getDeviceProperties().get().get("deviceId")); + } - X509Certificate attestationCert = CertificateParser.parsePem(ATTESTATION_CERT2); - Attestation attestation = service.getAttestation(Collections.singletonList(attestationCert)); + @Test + public void testGetAttestation_x509extension_key_value() throws Exception { + StandardMetadataService service = new StandardMetadataService(); - assertTrue(attestation.isTrusted()); - assertEquals("Yubico", attestation.getVendorProperties().get().get("name")); - assertEquals("1.3.6.1.4.1.41482.1.4", attestation.getDeviceProperties().get().get("deviceId")); - } + X509Certificate attestationCert = CertificateParser.parsePem(ATTESTATION_CERT2); + Attestation attestation = service.getAttestation(Collections.singletonList(attestationCert)); - @Test - public void testGetTransportsFromCertificate() throws CertificateException { - StandardMetadataService service = new StandardMetadataService(); + assertTrue(attestation.isTrusted()); + assertEquals("Yubico", attestation.getVendorProperties().get().get("name")); + assertEquals("1.3.6.1.4.1.41482.1.4", attestation.getDeviceProperties().get().get("deviceId")); + } - X509Certificate attestationCert = CertificateParser.parsePem(ATTESTATION_CERT_WITH_TRANSPORTS); - Attestation attestation = service.getAttestation(Collections.singletonList(attestationCert)); + @Test + public void testGetTransportsFromCertificate() throws CertificateException { + StandardMetadataService service = new StandardMetadataService(); - assertEquals(Optional.of(EnumSet.of(Transport.USB, Transport.NFC)), attestation.getTransports()); - } + X509Certificate attestationCert = CertificateParser.parsePem(ATTESTATION_CERT_WITH_TRANSPORTS); + Attestation attestation = service.getAttestation(Collections.singletonList(attestationCert)); - @Test - public void testGetTransportsFromMetadata() throws CertificateException { - StandardMetadataService service = new StandardMetadataService(); + assertEquals( + Optional.of(EnumSet.of(Transport.USB, Transport.NFC)), attestation.getTransports()); + } - X509Certificate attestationCert = CertificateParser.parsePem(ATTESTATION_CERT2); - Attestation attestation = service.getAttestation(Collections.singletonList(attestationCert)); + @Test + public void testGetTransportsFromMetadata() throws CertificateException { + StandardMetadataService service = new StandardMetadataService(); - assertEquals(Optional.of(EnumSet.of(Transport.USB)), attestation.getTransports()); - } + X509Certificate attestationCert = CertificateParser.parsePem(ATTESTATION_CERT2); + Attestation attestation = service.getAttestation(Collections.singletonList(attestationCert)); - @Test - public void getCachedAttestationReturnsCertIfPresent() throws Exception { - StandardMetadataService service = new StandardMetadataService(); + assertEquals(Optional.of(EnumSet.of(Transport.USB)), attestation.getTransports()); + } - final X509Certificate attestationCert = CertificateParser.parsePem(ATTESTATION_CERT); - final String certFingerprint = Hashing.sha1().hashBytes(attestationCert.getEncoded()).toString(); + @Test + public void getCachedAttestationReturnsCertIfPresent() throws Exception { + StandardMetadataService service = new StandardMetadataService(); - assertNull(service.getCachedAttestation(certFingerprint)); + final X509Certificate attestationCert = CertificateParser.parsePem(ATTESTATION_CERT); + final String certFingerprint = + Hashing.sha1().hashBytes(attestationCert.getEncoded()).toString(); - service.getAttestation(Collections.singletonList(attestationCert)); + assertNull(service.getCachedAttestation(certFingerprint)); - Attestation attestation = service.getCachedAttestation(certFingerprint); + service.getAttestation(Collections.singletonList(attestationCert)); - assertTrue(attestation.isTrusted()); - assertEquals("Yubico", attestation.getVendorProperties().get().get("name")); - assertEquals("1.3.6.1.4.1.41482.1.2", attestation.getDeviceProperties().get().get("deviceId")); - } + Attestation attestation = service.getCachedAttestation(certFingerprint); + assertTrue(attestation.isTrusted()); + assertEquals("Yubico", attestation.getVendorProperties().get().get("name")); + assertEquals("1.3.6.1.4.1.41482.1.2", attestation.getDeviceProperties().get().get("deviceId")); + } } diff --git a/webauthn-server-attestation/src/test/java/com/yubico/webauthn/attestation/matcher/FingerprintMatcherTest.java b/webauthn-server-attestation/src/test/java/com/yubico/webauthn/attestation/matcher/FingerprintMatcherTest.java index abfe9be42..3e9fea7cf 100644 --- a/webauthn-server-attestation/src/test/java/com/yubico/webauthn/attestation/matcher/FingerprintMatcherTest.java +++ b/webauthn-server-attestation/src/test/java/com/yubico/webauthn/attestation/matcher/FingerprintMatcherTest.java @@ -24,6 +24,11 @@ package com.yubico.webauthn.attestation.matcher; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.BooleanNode; @@ -35,50 +40,45 @@ import java.security.cert.X509Certificate; import org.junit.Test; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - public class FingerprintMatcherTest { - private static final String ATTESTATION_CERT = "MIICGzCCAQWgAwIBAgIEdaP2dTALBgkqhkiG9w0BAQswLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290IENBIFNlcmlhbCA0NTcyMDA2MzEwIBcNMTQwODAxMDAwMDAwWhgPMjA1MDA5MDQwMDAwMDBaMCoxKDAmBgNVBAMMH1l1YmljbyBVMkYgRUUgU2VyaWFsIDE5NzM2Nzk3MzMwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQZo35Damtpl81YdmcbhEuXKAr7xDcQzAy5n3ftAAhtBbu8EeGU4ynfSgLonckqX6J2uXLBppTNE3v2bt+Yf8MLoxIwEDAOBgorBgEEAYLECgECBAAwCwYJKoZIhvcNAQELA4IBAQG9LbiNPgs0sQYOHAJcg+lMk+HCsiWRlYVnbT4I/5lnqU907vY17XYAORd432bU3Nnhsbkvjz76kQJGXeNAF4DPANGGlz8JU+LNEVE2PWPGgEM0GXgB7mZN5Sinfy1AoOdO+3c3bfdJQuXlUxHbo+nDpxxKpzq9gr++RbokF1+0JBkMbaA/qLYL4WdhY5NvaOyMvYpO3sBxlzn6FcP67hlotGH1wU7qhCeh+uur7zDeAWVh7c4QtJOXHkLJQfV3Z7ZMvhkIA6jZJAX99hisABU/SSa5DtgX7AfsHwa04h69AAAWDUzSk3HgOXbUd1FaSOPdlVFkG2N2JllFHykyO3zO"; - - @Test - public void matchesIsFalseForNonArrayFingerprints() { - JsonNode parameters = mock(JsonNode.class); - when(parameters.get("fingerprints")).thenReturn(BooleanNode.TRUE); + private static final String ATTESTATION_CERT = + "MIICGzCCAQWgAwIBAgIEdaP2dTALBgkqhkiG9w0BAQswLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290IENBIFNlcmlhbCA0NTcyMDA2MzEwIBcNMTQwODAxMDAwMDAwWhgPMjA1MDA5MDQwMDAwMDBaMCoxKDAmBgNVBAMMH1l1YmljbyBVMkYgRUUgU2VyaWFsIDE5NzM2Nzk3MzMwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQZo35Damtpl81YdmcbhEuXKAr7xDcQzAy5n3ftAAhtBbu8EeGU4ynfSgLonckqX6J2uXLBppTNE3v2bt+Yf8MLoxIwEDAOBgorBgEEAYLECgECBAAwCwYJKoZIhvcNAQELA4IBAQG9LbiNPgs0sQYOHAJcg+lMk+HCsiWRlYVnbT4I/5lnqU907vY17XYAORd432bU3Nnhsbkvjz76kQJGXeNAF4DPANGGlz8JU+LNEVE2PWPGgEM0GXgB7mZN5Sinfy1AoOdO+3c3bfdJQuXlUxHbo+nDpxxKpzq9gr++RbokF1+0JBkMbaA/qLYL4WdhY5NvaOyMvYpO3sBxlzn6FcP67hlotGH1wU7qhCeh+uur7zDeAWVh7c4QtJOXHkLJQfV3Z7ZMvhkIA6jZJAX99hisABU/SSa5DtgX7AfsHwa04h69AAAWDUzSk3HgOXbUd1FaSOPdlVFkG2N2JllFHykyO3zO"; - assertFalse(new FingerprintMatcher().matches(mock(X509Certificate.class), parameters)); - } + @Test + public void matchesIsFalseForNonArrayFingerprints() { + JsonNode parameters = mock(JsonNode.class); + when(parameters.get("fingerprints")).thenReturn(BooleanNode.TRUE); - @Test - public void matchesIsFalseIfNoFingerprintMatches() throws CertificateException { - final X509Certificate cert = CertificateParser.parsePem(ATTESTATION_CERT); + assertFalse(new FingerprintMatcher().matches(mock(X509Certificate.class), parameters)); + } - ArrayNode fingerprints = new ArrayNode(JsonNodeFactory.instance); - fingerprints.add(new TextNode("foo")); - fingerprints.add(new TextNode("bar")); + @Test + public void matchesIsFalseIfNoFingerprintMatches() throws CertificateException { + final X509Certificate cert = CertificateParser.parsePem(ATTESTATION_CERT); - JsonNode parameters = mock(JsonNode.class); - when(parameters.get("fingerprints")).thenReturn(fingerprints); + ArrayNode fingerprints = new ArrayNode(JsonNodeFactory.instance); + fingerprints.add(new TextNode("foo")); + fingerprints.add(new TextNode("bar")); - assertFalse(new FingerprintMatcher().matches(cert, parameters)); - } + JsonNode parameters = mock(JsonNode.class); + when(parameters.get("fingerprints")).thenReturn(fingerprints); - @Test - public void matchesIsTrueIfSomeFingerprintMatches() throws CertificateException { - final X509Certificate cert = CertificateParser.parsePem(ATTESTATION_CERT); - final String fingerprint = Hashing.sha1().hashBytes(cert.getEncoded()).toString().toLowerCase(); + assertFalse(new FingerprintMatcher().matches(cert, parameters)); + } - ArrayNode fingerprints = new ArrayNode(JsonNodeFactory.instance); - fingerprints.add(new TextNode("foo")); - fingerprints.add(new TextNode(fingerprint)); + @Test + public void matchesIsTrueIfSomeFingerprintMatches() throws CertificateException { + final X509Certificate cert = CertificateParser.parsePem(ATTESTATION_CERT); + final String fingerprint = Hashing.sha1().hashBytes(cert.getEncoded()).toString().toLowerCase(); - JsonNode parameters = mock(JsonNode.class); - when(parameters.get("fingerprints")).thenReturn(fingerprints); + ArrayNode fingerprints = new ArrayNode(JsonNodeFactory.instance); + fingerprints.add(new TextNode("foo")); + fingerprints.add(new TextNode(fingerprint)); - assertTrue(new FingerprintMatcher().matches(cert, parameters)); - } + JsonNode parameters = mock(JsonNode.class); + when(parameters.get("fingerprints")).thenReturn(fingerprints); + assertTrue(new FingerprintMatcher().matches(cert, parameters)); + } } diff --git a/webauthn-server-attestation/src/test/java/com/yubico/webauthn/attestation/resolver/SimpleAttestationResolverTest.java b/webauthn-server-attestation/src/test/java/com/yubico/webauthn/attestation/resolver/SimpleAttestationResolverTest.java index 8cfa27db9..8f74df544 100644 --- a/webauthn-server-attestation/src/test/java/com/yubico/webauthn/attestation/resolver/SimpleAttestationResolverTest.java +++ b/webauthn-server-attestation/src/test/java/com/yubico/webauthn/attestation/resolver/SimpleAttestationResolverTest.java @@ -24,6 +24,9 @@ package com.yubico.webauthn.attestation.resolver; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + import com.yubico.internal.util.CertificateParser; import com.yubico.internal.util.JacksonCodecs; import com.yubico.webauthn.attestation.Attestation; @@ -35,45 +38,43 @@ import java.util.Optional; import org.junit.Test; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; - - public class SimpleAttestationResolverTest { - private static final String METADATA_JSON = "{\"identifier\":\"foobar\",\"version\":1,\"trustedCertificates\":[\"-----BEGIN CERTIFICATE-----\\nMIIDHjCCAgagAwIBAgIEG1BT9zANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZ\\ndWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAw\\nMDBaGA8yMDUwMDkwNDAwMDAwMFowLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290\\nIENBIFNlcmlhbCA0NTcyMDA2MzEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK\\nAoIBAQC/jwYuhBVlqaiYWEMsrWFisgJ+PtM91eSrpI4TK7U53mwCIawSDHy8vUmk\\n5N2KAj9abvT9NP5SMS1hQi3usxoYGonXQgfO6ZXyUA9a+KAkqdFnBnlyugSeCOep\\n8EdZFfsaRFtMjkwz5Gcz2Py4vIYvCdMHPtwaz0bVuzneueIEz6TnQjE63Rdt2zbw\\nnebwTG5ZybeWSwbzy+BJ34ZHcUhPAY89yJQXuE0IzMZFcEBbPNRbWECRKgjq//qT\\n9nmDOFVlSRCt2wiqPSzluwn+v+suQEBsUjTGMEd25tKXXTkNW21wIWbxeSyUoTXw\\nLvGS6xlwQSgNpk2qXYwf8iXg7VWZAgMBAAGjQjBAMB0GA1UdDgQWBBQgIvz0bNGJ\\nhjgpToksyKpP9xv9oDAPBgNVHRMECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIBBjAN\\nBgkqhkiG9w0BAQsFAAOCAQEAjvjuOMDSa+JXFCLyBKsycXtBVZsJ4Ue3LbaEsPY4\\nMYN/hIQ5ZM5p7EjfcnMG4CtYkNsfNHc0AhBLdq45rnT87q/6O3vUEtNMafbhU6kt\\nhX7Y+9XFN9NpmYxr+ekVY5xOxi8h9JDIgoMP4VB1uS0aunL1IGqrNooL9mmFnL2k\\nLVVee6/VR6C5+KSTCMCWppMuJIZII2v9o4dkoZ8Y7QRjQlLfYzd3qGtKbw7xaF1U\\nsG/5xUb/Btwb2X2g4InpiB/yt/3CpQXpiWX/K4mBvUKiGn05ZsqeY1gx4g0xLBqc\\nU9psmyPzK+Vsgw2jeRQ5JlKDyqE0hebfC1tvFu0CCrJFcw==\\n-----END CERTIFICATE-----\"],\"vendorInfo\":{\"name\":\"Yubico\",\"url\":\"https://yubico.com\",\"imageUrl\":\"https://developers.yubico.com/U2F/Images/yubico.png\"},\"devices\":[{\"displayName\":\"YubiKey NEO/NEO-n\",\"deviceId\":\"1.3.6.1.4.1.41482.1.2\",\"deviceUrl\":\"https://www.yubico.com/products/yubikey-hardware/yubikey-neo/\",\"imageUrl\":\"https://developers.yubico.com/U2F/Images/NEO.png\",\"selectors\":[{\"type\":\"x509Extension\",\"parameters\":{\"key\":\"1.3.6.1.4.1.41482.1.2\"}}]}] }"; - private static final String ATTESTATION_CERT = "MIICGzCCAQWgAwIBAgIEdaP2dTALBgkqhkiG9w0BAQswLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290IENBIFNlcmlhbCA0NTcyMDA2MzEwIBcNMTQwODAxMDAwMDAwWhgPMjA1MDA5MDQwMDAwMDBaMCoxKDAmBgNVBAMMH1l1YmljbyBVMkYgRUUgU2VyaWFsIDE5NzM2Nzk3MzMwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQZo35Damtpl81YdmcbhEuXKAr7xDcQzAy5n3ftAAhtBbu8EeGU4ynfSgLonckqX6J2uXLBppTNE3v2bt+Yf8MLoxIwEDAOBgorBgEEAYLECgECBAAwCwYJKoZIhvcNAQELA4IBAQG9LbiNPgs0sQYOHAJcg+lMk+HCsiWRlYVnbT4I/5lnqU907vY17XYAORd432bU3Nnhsbkvjz76kQJGXeNAF4DPANGGlz8JU+LNEVE2PWPGgEM0GXgB7mZN5Sinfy1AoOdO+3c3bfdJQuXlUxHbo+nDpxxKpzq9gr++RbokF1+0JBkMbaA/qLYL4WdhY5NvaOyMvYpO3sBxlzn6FcP67hlotGH1wU7qhCeh+uur7zDeAWVh7c4QtJOXHkLJQfV3Z7ZMvhkIA6jZJAX99hisABU/SSa5DtgX7AfsHwa04h69AAAWDUzSk3HgOXbUd1FaSOPdlVFkG2N2JllFHykyO3zO"; + private static final String METADATA_JSON = + "{\"identifier\":\"foobar\",\"version\":1,\"trustedCertificates\":[\"-----BEGIN CERTIFICATE-----\\nMIIDHjCCAgagAwIBAgIEG1BT9zANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZ\\ndWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAw\\nMDBaGA8yMDUwMDkwNDAwMDAwMFowLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290\\nIENBIFNlcmlhbCA0NTcyMDA2MzEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK\\nAoIBAQC/jwYuhBVlqaiYWEMsrWFisgJ+PtM91eSrpI4TK7U53mwCIawSDHy8vUmk\\n5N2KAj9abvT9NP5SMS1hQi3usxoYGonXQgfO6ZXyUA9a+KAkqdFnBnlyugSeCOep\\n8EdZFfsaRFtMjkwz5Gcz2Py4vIYvCdMHPtwaz0bVuzneueIEz6TnQjE63Rdt2zbw\\nnebwTG5ZybeWSwbzy+BJ34ZHcUhPAY89yJQXuE0IzMZFcEBbPNRbWECRKgjq//qT\\n9nmDOFVlSRCt2wiqPSzluwn+v+suQEBsUjTGMEd25tKXXTkNW21wIWbxeSyUoTXw\\nLvGS6xlwQSgNpk2qXYwf8iXg7VWZAgMBAAGjQjBAMB0GA1UdDgQWBBQgIvz0bNGJ\\nhjgpToksyKpP9xv9oDAPBgNVHRMECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIBBjAN\\nBgkqhkiG9w0BAQsFAAOCAQEAjvjuOMDSa+JXFCLyBKsycXtBVZsJ4Ue3LbaEsPY4\\nMYN/hIQ5ZM5p7EjfcnMG4CtYkNsfNHc0AhBLdq45rnT87q/6O3vUEtNMafbhU6kt\\nhX7Y+9XFN9NpmYxr+ekVY5xOxi8h9JDIgoMP4VB1uS0aunL1IGqrNooL9mmFnL2k\\nLVVee6/VR6C5+KSTCMCWppMuJIZII2v9o4dkoZ8Y7QRjQlLfYzd3qGtKbw7xaF1U\\nsG/5xUb/Btwb2X2g4InpiB/yt/3CpQXpiWX/K4mBvUKiGn05ZsqeY1gx4g0xLBqc\\nU9psmyPzK+Vsgw2jeRQ5JlKDyqE0hebfC1tvFu0CCrJFcw==\\n-----END CERTIFICATE-----\"],\"vendorInfo\":{\"name\":\"Yubico\",\"url\":\"https://yubico.com\",\"imageUrl\":\"https://developers.yubico.com/U2F/Images/yubico.png\"},\"devices\":[{\"displayName\":\"YubiKey NEO/NEO-n\",\"deviceId\":\"1.3.6.1.4.1.41482.1.2\",\"deviceUrl\":\"https://www.yubico.com/products/yubikey-hardware/yubikey-neo/\",\"imageUrl\":\"https://developers.yubico.com/U2F/Images/NEO.png\",\"selectors\":[{\"type\":\"x509Extension\",\"parameters\":{\"key\":\"1.3.6.1.4.1.41482.1.2\"}}]}] }"; + private static final String ATTESTATION_CERT = + "MIICGzCCAQWgAwIBAgIEdaP2dTALBgkqhkiG9w0BAQswLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290IENBIFNlcmlhbCA0NTcyMDA2MzEwIBcNMTQwODAxMDAwMDAwWhgPMjA1MDA5MDQwMDAwMDBaMCoxKDAmBgNVBAMMH1l1YmljbyBVMkYgRUUgU2VyaWFsIDE5NzM2Nzk3MzMwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQZo35Damtpl81YdmcbhEuXKAr7xDcQzAy5n3ftAAhtBbu8EeGU4ynfSgLonckqX6J2uXLBppTNE3v2bt+Yf8MLoxIwEDAOBgorBgEEAYLECgECBAAwCwYJKoZIhvcNAQELA4IBAQG9LbiNPgs0sQYOHAJcg+lMk+HCsiWRlYVnbT4I/5lnqU907vY17XYAORd432bU3Nnhsbkvjz76kQJGXeNAF4DPANGGlz8JU+LNEVE2PWPGgEM0GXgB7mZN5Sinfy1AoOdO+3c3bfdJQuXlUxHbo+nDpxxKpzq9gr++RbokF1+0JBkMbaA/qLYL4WdhY5NvaOyMvYpO3sBxlzn6FcP67hlotGH1wU7qhCeh+uur7zDeAWVh7c4QtJOXHkLJQfV3Z7ZMvhkIA6jZJAX99hisABU/SSa5DtgX7AfsHwa04h69AAAWDUzSk3HgOXbUd1FaSOPdlVFkG2N2JllFHykyO3zO"; - private final MetadataObject metadata = JacksonCodecs.json().readValue(METADATA_JSON, MetadataObject.class); - private final X509Certificate attestationCertificate = CertificateParser.parseDer(ATTESTATION_CERT); + private final MetadataObject metadata = + JacksonCodecs.json().readValue(METADATA_JSON, MetadataObject.class); + private final X509Certificate attestationCertificate = + CertificateParser.parseDer(ATTESTATION_CERT); - public SimpleAttestationResolverTest() throws IOException, CertificateException { - } + public SimpleAttestationResolverTest() throws IOException, CertificateException {} - private static SimpleAttestationResolver createAttestationResolver(MetadataObject metadata) throws CertificateException { - return new SimpleAttestationResolver( - Collections.singleton(metadata), - SimpleTrustResolver.fromMetadata(Collections.singleton(metadata)) - ); - } + private static SimpleAttestationResolver createAttestationResolver(MetadataObject metadata) + throws CertificateException { + return new SimpleAttestationResolver( + Collections.singleton(metadata), + SimpleTrustResolver.fromMetadata(Collections.singleton(metadata))); + } - @Test - public void testResolve() throws Exception { - final SimpleAttestationResolver resolver = createAttestationResolver(metadata); - Attestation metadata = resolver.resolve(attestationCertificate).orElse(null); + @Test + public void testResolve() throws Exception { + final SimpleAttestationResolver resolver = createAttestationResolver(metadata); + Attestation metadata = resolver.resolve(attestationCertificate).orElse(null); - assertNotNull(metadata); - assertEquals("foobar", metadata.getMetadataIdentifier().get()); - } + assertNotNull(metadata); + assertEquals("foobar", metadata.getMetadataIdentifier().get()); + } - @Test - public void resolveReturnsEmptyOnUntrustedSignature() throws Exception { - final SimpleAttestationResolver resolver = new SimpleAttestationResolver( + @Test + public void resolveReturnsEmptyOnUntrustedSignature() throws Exception { + final SimpleAttestationResolver resolver = + new SimpleAttestationResolver( Collections.singletonList(metadata), - SimpleTrustResolver.fromMetadata(Collections.emptyList()) - ); - - assertEquals(Optional.empty(), resolver.resolve(attestationCertificate)); - } + SimpleTrustResolver.fromMetadata(Collections.emptyList())); + assertEquals(Optional.empty(), resolver.resolve(attestationCertificate)); + } } diff --git a/webauthn-server-attestation/src/test/java/com/yubico/webauthn/attestation/resolver/SimpleTrustResolverTest.java b/webauthn-server-attestation/src/test/java/com/yubico/webauthn/attestation/resolver/SimpleTrustResolverTest.java index f84119ed6..e2e4570ad 100644 --- a/webauthn-server-attestation/src/test/java/com/yubico/webauthn/attestation/resolver/SimpleTrustResolverTest.java +++ b/webauthn-server-attestation/src/test/java/com/yubico/webauthn/attestation/resolver/SimpleTrustResolverTest.java @@ -24,6 +24,12 @@ package com.yubico.webauthn.attestation.resolver; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + import com.yubico.internal.util.CertificateParser; import java.io.IOException; import java.security.InvalidKeyException; @@ -37,71 +43,66 @@ import org.junit.Test; import org.mockito.ArgumentMatchers; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - public class SimpleTrustResolverTest { - private static final String METADATA_JSON = "{\"identifier\":\"foobar\",\"version\":1,\"trustedCertificates\":[\"-----BEGIN CERTIFICATE-----\\nMIIDHjCCAgagAwIBAgIEG1BT9zANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZ\\ndWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAw\\nMDBaGA8yMDUwMDkwNDAwMDAwMFowLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290\\nIENBIFNlcmlhbCA0NTcyMDA2MzEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK\\nAoIBAQC/jwYuhBVlqaiYWEMsrWFisgJ+PtM91eSrpI4TK7U53mwCIawSDHy8vUmk\\n5N2KAj9abvT9NP5SMS1hQi3usxoYGonXQgfO6ZXyUA9a+KAkqdFnBnlyugSeCOep\\n8EdZFfsaRFtMjkwz5Gcz2Py4vIYvCdMHPtwaz0bVuzneueIEz6TnQjE63Rdt2zbw\\nnebwTG5ZybeWSwbzy+BJ34ZHcUhPAY89yJQXuE0IzMZFcEBbPNRbWECRKgjq//qT\\n9nmDOFVlSRCt2wiqPSzluwn+v+suQEBsUjTGMEd25tKXXTkNW21wIWbxeSyUoTXw\\nLvGS6xlwQSgNpk2qXYwf8iXg7VWZAgMBAAGjQjBAMB0GA1UdDgQWBBQgIvz0bNGJ\\nhjgpToksyKpP9xv9oDAPBgNVHRMECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIBBjAN\\nBgkqhkiG9w0BAQsFAAOCAQEAjvjuOMDSa+JXFCLyBKsycXtBVZsJ4Ue3LbaEsPY4\\nMYN/hIQ5ZM5p7EjfcnMG4CtYkNsfNHc0AhBLdq45rnT87q/6O3vUEtNMafbhU6kt\\nhX7Y+9XFN9NpmYxr+ekVY5xOxi8h9JDIgoMP4VB1uS0aunL1IGqrNooL9mmFnL2k\\nLVVee6/VR6C5+KSTCMCWppMuJIZII2v9o4dkoZ8Y7QRjQlLfYzd3qGtKbw7xaF1U\\nsG/5xUb/Btwb2X2g4InpiB/yt/3CpQXpiWX/K4mBvUKiGn05ZsqeY1gx4g0xLBqc\\nU9psmyPzK+Vsgw2jeRQ5JlKDyqE0hebfC1tvFu0CCrJFcw==\\n-----END CERTIFICATE-----\"],\"vendorInfo\":{\"name\":\"Yubico\",\"url\":\"https://yubico.com\",\"imageUrl\":\"https://developers.yubico.com/U2F/Images/yubico.png\"},\"devices\":[{\"displayName\":\"YubiKey NEO/NEO-n\",\"deviceId\":\"1.3.6.1.4.1.41482.1.2\",\"deviceUrl\":\"https://www.yubico.com/products/yubikey-hardware/yubikey-neo/\",\"imageUrl\":\"https://developers.yubico.com/U2F/Images/NEO.png\",\"selectors\":[{\"type\":\"x509Extension\",\"parameters\":{\"key\":\"1.3.6.1.4.1.41482.1.2\"}}]}] }"; - private static final String ATTESTATION_CERT = "MIICGzCCAQWgAwIBAgIEdaP2dTALBgkqhkiG9w0BAQswLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290IENBIFNlcmlhbCA0NTcyMDA2MzEwIBcNMTQwODAxMDAwMDAwWhgPMjA1MDA5MDQwMDAwMDBaMCoxKDAmBgNVBAMMH1l1YmljbyBVMkYgRUUgU2VyaWFsIDE5NzM2Nzk3MzMwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQZo35Damtpl81YdmcbhEuXKAr7xDcQzAy5n3ftAAhtBbu8EeGU4ynfSgLonckqX6J2uXLBppTNE3v2bt+Yf8MLoxIwEDAOBgorBgEEAYLECgECBAAwCwYJKoZIhvcNAQELA4IBAQG9LbiNPgs0sQYOHAJcg+lMk+HCsiWRlYVnbT4I/5lnqU907vY17XYAORd432bU3Nnhsbkvjz76kQJGXeNAF4DPANGGlz8JU+LNEVE2PWPGgEM0GXgB7mZN5Sinfy1AoOdO+3c3bfdJQuXlUxHbo+nDpxxKpzq9gr++RbokF1+0JBkMbaA/qLYL4WdhY5NvaOyMvYpO3sBxlzn6FcP67hlotGH1wU7qhCeh+uur7zDeAWVh7c4QtJOXHkLJQfV3Z7ZMvhkIA6jZJAX99hisABU/SSa5DtgX7AfsHwa04h69AAAWDUzSk3HgOXbUd1FaSOPdlVFkG2N2JllFHykyO3zO"; - - private final SimpleTrustResolver resolver = SimpleTrustResolver.fromMetadataJson(METADATA_JSON); - - public SimpleTrustResolverTest() throws IOException, CertificateException { - } - - @Test - public void testResolve() throws Exception { - X509Certificate certificate = CertificateParser.parseDer(ATTESTATION_CERT); - - Optional trustAnchor = resolver.resolveTrustAnchor(certificate); - - assertTrue(trustAnchor.isPresent()); - assertEquals("CN=Yubico U2F Root CA Serial 457200631", trustAnchor.get().getSubjectDN().getName()); - } - - @Test - public void resolveReturnsEmptyOnUntrustedSignature() throws Exception { - X509Certificate cert = mock(X509Certificate.class); - doThrow(new SignatureException("Forced failure")).when(cert).verify(ArgumentMatchers.any()); - Principal issuerDN = mock(Principal.class); - when(issuerDN.getName()).thenReturn("CN=Yubico U2F Root CA Serial 457200631"); - when(cert.getIssuerDN()).thenReturn(issuerDN); - - assertEquals(Optional.empty(), resolver.resolveTrustAnchor(cert)); - } - - private void resolveThrowsExceptionOnUnexpectedError(Exception thrownException) throws Exception { - X509Certificate cert = mock(X509Certificate.class); - doThrow(thrownException).when(cert).verify(ArgumentMatchers.any()); - Principal issuerDN = mock(Principal.class); - when(issuerDN.getName()).thenReturn("CN=Yubico U2F Root CA Serial 457200631"); - when(cert.getIssuerDN()).thenReturn(issuerDN); - - resolver.resolveTrustAnchor(cert); - } - - @Test(expected = RuntimeException.class) - public void resolveThrowsExceptionOnCertificateException() throws Exception { - resolveThrowsExceptionOnUnexpectedError(new CertificateException("Forced failure")); - } - - @Test(expected = RuntimeException.class) - public void resolveThrowsExceptionOnNoSuchAlgorithmException() throws Exception { - resolveThrowsExceptionOnUnexpectedError(new NoSuchAlgorithmException("Forced failure")); - } - - @Test(expected = RuntimeException.class) - public void resolveThrowsExceptionOnInvalidKeyException() throws Exception { - resolveThrowsExceptionOnUnexpectedError(new InvalidKeyException("Forced failure")); - } - - @Test(expected = RuntimeException.class) - public void resolveThrowsExceptionOnNoSuchProviderException() throws Exception { - resolveThrowsExceptionOnUnexpectedError(new NoSuchProviderException("Forced failure")); - } - + private static final String METADATA_JSON = + "{\"identifier\":\"foobar\",\"version\":1,\"trustedCertificates\":[\"-----BEGIN CERTIFICATE-----\\nMIIDHjCCAgagAwIBAgIEG1BT9zANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZ\\ndWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAw\\nMDBaGA8yMDUwMDkwNDAwMDAwMFowLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290\\nIENBIFNlcmlhbCA0NTcyMDA2MzEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK\\nAoIBAQC/jwYuhBVlqaiYWEMsrWFisgJ+PtM91eSrpI4TK7U53mwCIawSDHy8vUmk\\n5N2KAj9abvT9NP5SMS1hQi3usxoYGonXQgfO6ZXyUA9a+KAkqdFnBnlyugSeCOep\\n8EdZFfsaRFtMjkwz5Gcz2Py4vIYvCdMHPtwaz0bVuzneueIEz6TnQjE63Rdt2zbw\\nnebwTG5ZybeWSwbzy+BJ34ZHcUhPAY89yJQXuE0IzMZFcEBbPNRbWECRKgjq//qT\\n9nmDOFVlSRCt2wiqPSzluwn+v+suQEBsUjTGMEd25tKXXTkNW21wIWbxeSyUoTXw\\nLvGS6xlwQSgNpk2qXYwf8iXg7VWZAgMBAAGjQjBAMB0GA1UdDgQWBBQgIvz0bNGJ\\nhjgpToksyKpP9xv9oDAPBgNVHRMECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIBBjAN\\nBgkqhkiG9w0BAQsFAAOCAQEAjvjuOMDSa+JXFCLyBKsycXtBVZsJ4Ue3LbaEsPY4\\nMYN/hIQ5ZM5p7EjfcnMG4CtYkNsfNHc0AhBLdq45rnT87q/6O3vUEtNMafbhU6kt\\nhX7Y+9XFN9NpmYxr+ekVY5xOxi8h9JDIgoMP4VB1uS0aunL1IGqrNooL9mmFnL2k\\nLVVee6/VR6C5+KSTCMCWppMuJIZII2v9o4dkoZ8Y7QRjQlLfYzd3qGtKbw7xaF1U\\nsG/5xUb/Btwb2X2g4InpiB/yt/3CpQXpiWX/K4mBvUKiGn05ZsqeY1gx4g0xLBqc\\nU9psmyPzK+Vsgw2jeRQ5JlKDyqE0hebfC1tvFu0CCrJFcw==\\n-----END CERTIFICATE-----\"],\"vendorInfo\":{\"name\":\"Yubico\",\"url\":\"https://yubico.com\",\"imageUrl\":\"https://developers.yubico.com/U2F/Images/yubico.png\"},\"devices\":[{\"displayName\":\"YubiKey NEO/NEO-n\",\"deviceId\":\"1.3.6.1.4.1.41482.1.2\",\"deviceUrl\":\"https://www.yubico.com/products/yubikey-hardware/yubikey-neo/\",\"imageUrl\":\"https://developers.yubico.com/U2F/Images/NEO.png\",\"selectors\":[{\"type\":\"x509Extension\",\"parameters\":{\"key\":\"1.3.6.1.4.1.41482.1.2\"}}]}] }"; + private static final String ATTESTATION_CERT = + "MIICGzCCAQWgAwIBAgIEdaP2dTALBgkqhkiG9w0BAQswLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290IENBIFNlcmlhbCA0NTcyMDA2MzEwIBcNMTQwODAxMDAwMDAwWhgPMjA1MDA5MDQwMDAwMDBaMCoxKDAmBgNVBAMMH1l1YmljbyBVMkYgRUUgU2VyaWFsIDE5NzM2Nzk3MzMwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQZo35Damtpl81YdmcbhEuXKAr7xDcQzAy5n3ftAAhtBbu8EeGU4ynfSgLonckqX6J2uXLBppTNE3v2bt+Yf8MLoxIwEDAOBgorBgEEAYLECgECBAAwCwYJKoZIhvcNAQELA4IBAQG9LbiNPgs0sQYOHAJcg+lMk+HCsiWRlYVnbT4I/5lnqU907vY17XYAORd432bU3Nnhsbkvjz76kQJGXeNAF4DPANGGlz8JU+LNEVE2PWPGgEM0GXgB7mZN5Sinfy1AoOdO+3c3bfdJQuXlUxHbo+nDpxxKpzq9gr++RbokF1+0JBkMbaA/qLYL4WdhY5NvaOyMvYpO3sBxlzn6FcP67hlotGH1wU7qhCeh+uur7zDeAWVh7c4QtJOXHkLJQfV3Z7ZMvhkIA6jZJAX99hisABU/SSa5DtgX7AfsHwa04h69AAAWDUzSk3HgOXbUd1FaSOPdlVFkG2N2JllFHykyO3zO"; + + private final SimpleTrustResolver resolver = SimpleTrustResolver.fromMetadataJson(METADATA_JSON); + + public SimpleTrustResolverTest() throws IOException, CertificateException {} + + @Test + public void testResolve() throws Exception { + X509Certificate certificate = CertificateParser.parseDer(ATTESTATION_CERT); + + Optional trustAnchor = resolver.resolveTrustAnchor(certificate); + + assertTrue(trustAnchor.isPresent()); + assertEquals( + "CN=Yubico U2F Root CA Serial 457200631", trustAnchor.get().getSubjectDN().getName()); + } + + @Test + public void resolveReturnsEmptyOnUntrustedSignature() throws Exception { + X509Certificate cert = mock(X509Certificate.class); + doThrow(new SignatureException("Forced failure")).when(cert).verify(ArgumentMatchers.any()); + Principal issuerDN = mock(Principal.class); + when(issuerDN.getName()).thenReturn("CN=Yubico U2F Root CA Serial 457200631"); + when(cert.getIssuerDN()).thenReturn(issuerDN); + + assertEquals(Optional.empty(), resolver.resolveTrustAnchor(cert)); + } + + private void resolveThrowsExceptionOnUnexpectedError(Exception thrownException) throws Exception { + X509Certificate cert = mock(X509Certificate.class); + doThrow(thrownException).when(cert).verify(ArgumentMatchers.any()); + Principal issuerDN = mock(Principal.class); + when(issuerDN.getName()).thenReturn("CN=Yubico U2F Root CA Serial 457200631"); + when(cert.getIssuerDN()).thenReturn(issuerDN); + + resolver.resolveTrustAnchor(cert); + } + + @Test(expected = RuntimeException.class) + public void resolveThrowsExceptionOnCertificateException() throws Exception { + resolveThrowsExceptionOnUnexpectedError(new CertificateException("Forced failure")); + } + + @Test(expected = RuntimeException.class) + public void resolveThrowsExceptionOnNoSuchAlgorithmException() throws Exception { + resolveThrowsExceptionOnUnexpectedError(new NoSuchAlgorithmException("Forced failure")); + } + + @Test(expected = RuntimeException.class) + public void resolveThrowsExceptionOnInvalidKeyException() throws Exception { + resolveThrowsExceptionOnUnexpectedError(new InvalidKeyException("Forced failure")); + } + + @Test(expected = RuntimeException.class) + public void resolveThrowsExceptionOnNoSuchProviderException() throws Exception { + resolveThrowsExceptionOnUnexpectedError(new NoSuchProviderException("Forced failure")); + } } diff --git a/webauthn-server-attestation/src/test/scala/com/yubico/webauthn/attestation/DeviceIdentificationSpec.scala b/webauthn-server-attestation/src/test/scala/com/yubico/webauthn/attestation/DeviceIdentificationSpec.scala index 1e06cd6b6..acaa74e8f 100644 --- a/webauthn-server-attestation/src/test/scala/com/yubico/webauthn/attestation/DeviceIdentificationSpec.scala +++ b/webauthn-server-attestation/src/test/scala/com/yubico/webauthn/attestation/DeviceIdentificationSpec.scala @@ -24,65 +24,89 @@ package com.yubico.webauthn.attestation -import java.util.Collections - import com.yubico.internal.util.CertificateParser import com.yubico.internal.util.JacksonCodecs -import com.yubico.webauthn.attestation.resolver.SimpleAttestationResolver -import com.yubico.webauthn.attestation.resolver.SimpleTrustResolver -import com.yubico.webauthn.test.RealExamples import com.yubico.webauthn.FinishRegistrationOptions import com.yubico.webauthn.RelyingParty import com.yubico.webauthn.attestation.Transport.LIGHTNING import com.yubico.webauthn.attestation.Transport.NFC import com.yubico.webauthn.attestation.Transport.USB +import com.yubico.webauthn.attestation.resolver.SimpleAttestationResolver +import com.yubico.webauthn.attestation.resolver.SimpleTrustResolver import com.yubico.webauthn.data.PublicKeyCredentialCreationOptions import com.yubico.webauthn.data.PublicKeyCredentialParameters import com.yubico.webauthn.test.Helpers +import com.yubico.webauthn.test.RealExamples import org.junit.runner.RunWith import org.scalatest.FunSpec import org.scalatest.Matchers import org.scalatestplus.junit.JUnitRunner +import java.util.Collections import scala.jdk.CollectionConverters._ - @RunWith(classOf[JUnitRunner]) class DeviceIdentificationSpec extends FunSpec with Matchers { def metadataService(metadataJson: String): StandardMetadataService = { - val metadata = Collections.singleton(JacksonCodecs.json().readValue(metadataJson, classOf[MetadataObject])) + val metadata = Collections.singleton( + JacksonCodecs.json().readValue(metadataJson, classOf[MetadataObject]) + ) new StandardMetadataService( - new SimpleAttestationResolver(metadata, SimpleTrustResolver.fromMetadata(metadata)) + new SimpleAttestationResolver( + metadata, + SimpleTrustResolver.fromMetadata(metadata), + ) ) } describe("A RelyingParty with the default StandardMetadataService") { describe("correctly identifies") { - def check(expectedName: String, testData: RealExamples.Example, transports: Set[Transport]): Unit = { - val rp = RelyingParty.builder() + def check( + expectedName: String, + testData: RealExamples.Example, + transports: Set[Transport], + ): Unit = { + val rp = RelyingParty + .builder() .identity(testData.rp) .credentialRepository(Helpers.CredentialRepository.empty) .metadataService(new StandardMetadataService()) .build() - val result = rp.finishRegistration(FinishRegistrationOptions.builder() - .request(PublicKeyCredentialCreationOptions.builder() - .rp(testData.rp) - .user(testData.user) - .challenge(testData.attestation.challenge) - .pubKeyCredParams(List(PublicKeyCredentialParameters.ES256).asJava) - .build()) - .response(testData.attestation.credential) - .build()); + val result = rp.finishRegistration( + FinishRegistrationOptions + .builder() + .request( + PublicKeyCredentialCreationOptions + .builder() + .rp(testData.rp) + .user(testData.user) + .challenge(testData.attestation.challenge) + .pubKeyCredParams( + List(PublicKeyCredentialParameters.ES256).asJava + ) + .build() + ) + .response(testData.attestation.credential) + .build() + ); - result.isAttestationTrusted should be (true) - result.getAttestationMetadata.isPresent should be (true) - result.getAttestationMetadata.get.getDeviceProperties.isPresent should be (true) - result.getAttestationMetadata.get.getDeviceProperties.get().get("displayName") should equal (expectedName) - result.getAttestationMetadata.get.getTransports.isPresent should be (true) - result.getAttestationMetadata.get.getTransports.get.asScala should equal (transports) + result.isAttestationTrusted should be(true) + result.getAttestationMetadata.isPresent should be(true) + result.getAttestationMetadata.get.getDeviceProperties.isPresent should be( + true + ) + result.getAttestationMetadata.get.getDeviceProperties + .get() + .get("displayName") should equal(expectedName) + result.getAttestationMetadata.get.getTransports.isPresent should be( + true + ) + result.getAttestationMetadata.get.getTransports.get.asScala should equal( + transports + ) } it("a YubiKey NEO.") { @@ -98,7 +122,11 @@ class DeviceIdentificationSpec extends FunSpec with Matchers { check("YubiKey 5 NFC", RealExamples.YubiKey5Nfc, Set(USB, NFC)) } it("a newer YubiKey 5 NFC.") { - check("YubiKey 5/5C NFC", RealExamples.YubiKey5NfcPost5cNfc, Set(USB, NFC)) + check( + "YubiKey 5/5C NFC", + RealExamples.YubiKey5NfcPost5cNfc, + Set(USB, NFC), + ) } it("a YubiKey 5C NFC.") { check("YubiKey 5/5C NFC", RealExamples.YubiKey5cNfc, Set(USB, NFC)) @@ -116,21 +144,78 @@ class DeviceIdentificationSpec extends FunSpec with Matchers { check("Security Key by Yubico", RealExamples.SecurityKey2, Set(USB)) } it("a Security Key NFC by Yubico.") { - check("Security Key NFC by Yubico", RealExamples.SecurityKeyNfc, Set(USB, NFC)) + check( + "Security Key NFC by Yubico", + RealExamples.SecurityKeyNfc, + Set(USB, NFC), + ) + } + } + + describe("fails to identify") { + def check(testData: RealExamples.Example): Unit = { + val rp = RelyingParty + .builder() + .identity(testData.rp) + .credentialRepository(Helpers.CredentialRepository.empty) + .metadataService(new StandardMetadataService()) + .build() + + val result = rp.finishRegistration( + FinishRegistrationOptions + .builder() + .request( + PublicKeyCredentialCreationOptions + .builder() + .rp(testData.rp) + .user(testData.user) + .challenge(testData.attestation.challenge) + .pubKeyCredParams( + List(PublicKeyCredentialParameters.ES256).asJava + ) + .build() + ) + .response(testData.attestation.credential) + .build() + ); + + result.isAttestationTrusted should be(false) + result.getAttestationMetadata.isPresent should be(true) + result.getAttestationMetadata.get.getDeviceProperties.isPresent should be( + false + ) + result.getAttestationMetadata.get.getVendorProperties.isPresent should be( + false + ) + result.getAttestationMetadata.get.getTransports.isPresent should be( + false + ) + } + + it("an Apple iOS device.") { + check(RealExamples.AppleAttestationIos) } } } describe("The default AttestationResolver") { describe("successfully identifies") { - def check(expectedName: String, testData: RealExamples.Example, transports: Set[Transport]): Unit = { + def check( + expectedName: String, + testData: RealExamples.Example, + transports: Set[Transport], + ): Unit = { val cert = CertificateParser.parseDer(testData.attestationCert.getBytes) - val resolved = StandardMetadataService.createDefaultAttestationResolver().resolve(cert) - resolved.isPresent should be (true) - resolved.get.getDeviceProperties.isPresent should be (true) - resolved.get.getDeviceProperties.get.get("displayName") should equal (expectedName) - resolved.get.getTransports.isPresent should be (true) - resolved.get.getTransports.get.asScala should equal (transports) + val resolved = StandardMetadataService + .createDefaultAttestationResolver() + .resolve(cert) + resolved.isPresent should be(true) + resolved.get.getDeviceProperties.isPresent should be(true) + resolved.get.getDeviceProperties.get.get("displayName") should equal( + expectedName + ) + resolved.get.getTransports.isPresent should be(true) + resolved.get.getTransports.get.asScala should equal(transports) } it("a YubiKey NEO.") { @@ -146,7 +231,11 @@ class DeviceIdentificationSpec extends FunSpec with Matchers { check("YubiKey 5 NFC", RealExamples.YubiKey5Nfc, Set(USB, NFC)) } it("a newer YubiKey 5 NFC.") { - check("YubiKey 5/5C NFC", RealExamples.YubiKey5NfcPost5cNfc, Set(USB, NFC)) + check( + "YubiKey 5/5C NFC", + RealExamples.YubiKey5NfcPost5cNfc, + Set(USB, NFC), + ) } it("a YubiKey 5C NFC.") { check("YubiKey 5/5C NFC", RealExamples.YubiKey5cNfc, Set(USB, NFC)) @@ -164,7 +253,141 @@ class DeviceIdentificationSpec extends FunSpec with Matchers { check("Security Key by Yubico", RealExamples.SecurityKey2, Set(USB)) } it("a Security Key NFC by Yubico.") { - check("Security Key NFC by Yubico", RealExamples.SecurityKeyNfc, Set(USB, NFC)) + check( + "Security Key NFC by Yubico", + RealExamples.SecurityKeyNfc, + Set(USB, NFC), + ) + } + } + } + + describe( + "A StandardMetadataService configured with an Apple root certificate" + ) { + // Apple WebAuthn Root CA cert downloaded from https://www.apple.com/certificateauthority/private/ on 2021-04-12 + // https://www.apple.com/certificateauthority/Apple_WebAuthn_Root_CA.pem + val mds = metadataService("""{ + | "identifier": "98cf2729-e2b9-4633-8b6a-b295cda99ccf", + | "version": 1, + | "vendorInfo": { + | "name": "Apple Inc. (Metadata file by Yubico)" + | }, + | "trustedCertificates": [ + | "-----BEGIN CERTIFICATE-----\nMIICEjCCAZmgAwIBAgIQaB0BbHo84wIlpQGUKEdXcTAKBggqhkjOPQQDAzBLMR8w\nHQYDVQQDDBZBcHBsZSBXZWJBdXRobiBSb290IENBMRMwEQYDVQQKDApBcHBsZSBJ\nbmMuMRMwEQYDVQQIDApDYWxpZm9ybmlhMB4XDTIwMDMxODE4MjEzMloXDTQ1MDMx\nNTAwMDAwMFowSzEfMB0GA1UEAwwWQXBwbGUgV2ViQXV0aG4gUm9vdCBDQTETMBEG\nA1UECgwKQXBwbGUgSW5jLjETMBEGA1UECAwKQ2FsaWZvcm5pYTB2MBAGByqGSM49\nAgEGBSuBBAAiA2IABCJCQ2pTVhzjl4Wo6IhHtMSAzO2cv+H9DQKev3//fG59G11k\nxu9eI0/7o6V5uShBpe1u6l6mS19S1FEh6yGljnZAJ+2GNP1mi/YK2kSXIuTHjxA/\npcoRf7XkOtO4o1qlcaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUJtdk\n2cV4wlpn0afeaxLQG2PxxtcwDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2cA\nMGQCMFrZ+9DsJ1PW9hfNdBywZDsWDbWFp28it1d/5w2RPkRX3Bbn/UbDTNLx7Jr3\njAGGiQIwHFj+dJZYUJR786osByBelJYsVZd2GbHQu209b5RCmGQ21gpSAk9QZW4B\n1bWeT0vT\n-----END CERTIFICATE-----" + | ], + | "devices": [ + | { + | "displayName": "Apple device", + | "selectors": [ + | { + | "type": "x509Extension", + | "parameters": { + | "key": "1.2.840.113635.100.8.2" + | } + | } + | ] + | } + | ] + |}""".stripMargin) + + describe("successfully identifies") { + def check( + expectedName: String, + testData: RealExamples.Example, + ): Unit = { + val rp = RelyingParty + .builder() + .identity(testData.rp) + .credentialRepository(Helpers.CredentialRepository.empty) + .metadataService(mds) + .build() + + val result = rp.finishRegistration( + FinishRegistrationOptions + .builder() + .request( + PublicKeyCredentialCreationOptions + .builder() + .rp(testData.rp) + .user(testData.user) + .challenge(testData.attestation.challenge) + .pubKeyCredParams( + List(PublicKeyCredentialParameters.ES256).asJava + ) + .build() + ) + .response(testData.attestation.credential) + .build() + ) + + result.isAttestationTrusted should be(true) + result.getAttestationMetadata.isPresent should be(true) + result.getAttestationMetadata.get.getDeviceProperties.isPresent should be( + true + ) + result.getAttestationMetadata.get.getDeviceProperties + .get() + .get("displayName") should equal(expectedName) + result.getAttestationMetadata.get.getTransports.isPresent should be( + false + ) + } + + it("an Apple iOS device.") { + check( + "Apple device", + RealExamples.AppleAttestationIos, + ) + } + + it("an Apple MacOS device.") { + check( + "Apple device", + RealExamples.AppleAttestationMacos, + ) + } + } + + describe("fails to identify") { + def check(testData: RealExamples.Example): Unit = { + val rp = RelyingParty + .builder() + .identity(testData.rp) + .credentialRepository(Helpers.CredentialRepository.empty) + .metadataService(mds) + .build() + + val result = rp.finishRegistration( + FinishRegistrationOptions + .builder() + .request( + PublicKeyCredentialCreationOptions + .builder() + .rp(testData.rp) + .user(testData.user) + .challenge(testData.attestation.challenge) + .pubKeyCredParams( + List(PublicKeyCredentialParameters.ES256).asJava + ) + .build() + ) + .response(testData.attestation.credential) + .build() + ) + + result.isAttestationTrusted should be(false) + result.getAttestationMetadata.isPresent should be(true) + result.getAttestationMetadata.get.getVendorProperties.isPresent should be( + false + ) + result.getAttestationMetadata.get.getDeviceProperties.isPresent should be( + false + ) + } + + it("a YubiKey 5 NFC.") { + check(RealExamples.YubiKey5) } } } 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 c1d362627..c47ae6a75 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,13 +24,9 @@ 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 -import com.yubico.internal.util.scala.JavaConverters._ import com.yubico.internal.util.JacksonCodecs +import com.yubico.internal.util.scala.JavaConverters._ import com.yubico.webauthn.TestAuthenticator import com.yubico.webauthn.attestation.resolver.SimpleAttestationResolver import com.yubico.webauthn.attestation.resolver.SimpleTrustResolver @@ -42,9 +38,11 @@ import org.scalatest.FunSpec import org.scalatest.Matchers import org.scalatestplus.junit.JUnitRunner +import java.security.cert.X509Certificate +import java.util.Base64 +import java.util.Collections import scala.jdk.CollectionConverters._ - @RunWith(classOf[JUnitRunner]) class StandardMetadataServiceSpec extends FunSpec with Matchers { @@ -56,17 +54,27 @@ class StandardMetadataServiceSpec extends FunSpec with Matchers { private val ooidB = "1.3.6.1.4.1.41482.1.2" def metadataService(metadataJson: String): StandardMetadataService = { - val metadata = Collections.singleton(JacksonCodecs.json().readValue(metadataJson, classOf[MetadataObject])) + val metadata = Collections.singleton( + JacksonCodecs.json().readValue(metadataJson, classOf[MetadataObject]) + ) new StandardMetadataService( - new SimpleAttestationResolver(metadata, SimpleTrustResolver.fromMetadata(metadata)) + new SimpleAttestationResolver( + metadata, + SimpleTrustResolver.fromMetadata(metadata), + ) ) } - 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 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") { @@ -75,17 +83,17 @@ class StandardMetadataServiceSpec extends FunSpec with Matchers { val cacaca = TestAuthenticator.generateAttestationCaCertificate( name = new X500Name("CN=CA CA CA"), - extensions = List((ooidB, false, new DEROctetString(Array[Byte]()))) + extensions = List((ooidB, false, new DEROctetString(Array[Byte]()))), ) val caca = TestAuthenticator.generateAttestationCaCertificate( name = new X500Name("CN=CA CA"), superCa = Some(cacaca), - extensions = List((ooidB, false, new DEROctetString(Array[Byte]()))) + extensions = List((ooidB, false, new DEROctetString(Array[Byte]()))), ) val (caCert, caKey) = TestAuthenticator.generateAttestationCaCertificate( name = new X500Name("CN=CA"), superCa = Some(caca), - extensions = List((ooidB, false, new DEROctetString(Array[Byte]()))) + extensions = List((ooidB, false, new DEROctetString(Array[Byte]()))), ) val (certA, _) = TestAuthenticator.generateAttestationCertificate( @@ -93,24 +101,26 @@ class StandardMetadataServiceSpec extends FunSpec with Matchers { caCertAndKey = Some((caCert, caKey)), extensions = List( (ooidA, false, new DEROctetString(Array[Byte]())), - (TRANSPORTS_EXT_OID, false, new DERBitString(Array[Byte](0x60))) - ) + (TRANSPORTS_EXT_OID, false, new DERBitString(Array[Byte](0x60))), + ), ) val (certB, _) = TestAuthenticator.generateAttestationCertificate( name = new X500Name("CN=Cert B"), caCertAndKey = Some((caCert, caKey)), - extensions = List((ooidB, false, new DEROctetString(Array[Byte]()))) + extensions = List((ooidB, false, new DEROctetString(Array[Byte]()))), ) val (unknownCert, _) = TestAuthenticator.generateAttestationCertificate( name = new X500Name("CN=Unknown Cert"), - extensions = List((ooidA, false, new DEROctetString(Array[Byte]()))) + extensions = List((ooidA, false, new DEROctetString(Array[Byte]()))), ) val metadataJson = s"""{ "identifier": "44c87ead-4455-423e-88eb-9248e0ebe847", "version": 1, - "trustedCertificates": ["${toPem(caCert).linesIterator.mkString(raw"\n")}"], + "trustedCertificates": ["${toPem(caCert).linesIterator.mkString( + raw"\n" + )}"], "vendorInfo": {}, "devices": [ { @@ -139,28 +149,32 @@ class StandardMetadataServiceSpec extends FunSpec with Matchers { } ] }""" - val service = metadataService(metadataJson) + val service = metadataService(metadataJson) it("returns the trusted attestation matching the single cert passed, if it is signed by a trusted certificate.") { - val attestationA: Attestation = service.getAttestation(List(certA).asJava) - val attestationB: Attestation = service.getAttestation(List(certB).asJava) + val attestationA: Attestation = + service.getAttestation(List(certA).asJava) + val attestationB: Attestation = + service.getAttestation(List(certB).asJava) - attestationA.isTrusted should be (true) - attestationA.getDeviceProperties.get.get("deviceId") should be ("DevA") + attestationA.isTrusted should be(true) + attestationA.getDeviceProperties.get.get("deviceId") should be("DevA") - attestationB.isTrusted should be (true) - attestationB.getDeviceProperties.get.get("deviceId") should be ("DevB") + attestationB.isTrusted should be(true) + attestationB.getDeviceProperties.get.get("deviceId") should be("DevB") } it("returns the trusted attestation matching the first cert in the chain if it is signed by a trusted certificate.") { - val attestationA: Attestation = service.getAttestation(List(certA, certB).asJava) - val attestationB: Attestation = service.getAttestation(List(certB, certA).asJava) + val attestationA: Attestation = + service.getAttestation(List(certA, certB).asJava) + val attestationB: Attestation = + service.getAttestation(List(certB, certA).asJava) - attestationA.isTrusted should be (true) - attestationA.getDeviceProperties.get.get("deviceId") should be ("DevA") + attestationA.isTrusted should be(true) + attestationA.getDeviceProperties.get.get("deviceId") should be("DevA") - attestationB.isTrusted should be (true) - attestationB.getDeviceProperties.get.get("deviceId") should be ("DevB") + attestationB.isTrusted should be(true) + attestationB.getDeviceProperties.get.get("deviceId") should be("DevB") } it("returns a trusted best-effort attestation if the certificate is trusted but matches no known metadata.") { @@ -168,17 +182,22 @@ class StandardMetadataServiceSpec extends FunSpec with Matchers { s"""{ "identifier": "44c87ead-4455-423e-88eb-9248e0ebe847", "version": 1, - "trustedCertificates": ["${toPem(caCert).linesIterator.mkString(raw"\n")}"], + "trustedCertificates": ["${toPem(caCert).linesIterator.mkString( + raw"\n" + )}"], "vendorInfo": {}, "devices": [] }""" val service = metadataService(metadataJson) - val attestation: Attestation = service.getAttestation(List(certA).asJava) + val attestation: Attestation = + service.getAttestation(List(certA).asJava) - attestation.isTrusted should be (true) + attestation.isTrusted should be(true) attestation.getDeviceProperties.asScala shouldBe empty - attestation.getTransports.get.asScala should equal (Set(Transport.BLE, Transport.USB)) + attestation.getTransports.get.asScala should equal( + Set(Transport.BLE, Transport.USB) + ) } it("returns an untrusted attestation with transports if the certificate is not trusted.") { @@ -192,13 +211,16 @@ class StandardMetadataServiceSpec extends FunSpec with Matchers { }""" val service = metadataService(metadataJson) - val attestation: Attestation = service.getAttestation(List(certA).asJava) + val attestation: Attestation = + service.getAttestation(List(certA).asJava) - attestation.isTrusted should be (false) + attestation.isTrusted should be(false) attestation.getMetadataIdentifier.asScala shouldBe empty attestation.getVendorProperties.asScala shouldBe empty attestation.getDeviceProperties.asScala shouldBe empty - attestation.getTransports.get.asScala should equal (Set(Transport.BLE, Transport.USB)) + attestation.getTransports.get.asScala should equal( + Set(Transport.BLE, Transport.USB) + ) } it("returns the trusted attestation matching the first cert in the chain if the chain resolves to a trusted certificate.") { @@ -206,7 +228,8 @@ class StandardMetadataServiceSpec extends FunSpec with Matchers { s"""{ "identifier": "44c87ead-4455-423e-88eb-9248e0ebe847", "version": 1, - "trustedCertificates": ["${toPem(cacaca._1).linesIterator.mkString(raw"\n")}"], + "trustedCertificates": ["${toPem(cacaca._1).linesIterator + .mkString(raw"\n")}"], "vendorInfo": {}, "devices": [ { @@ -225,10 +248,11 @@ class StandardMetadataServiceSpec extends FunSpec with Matchers { }""" val service = metadataService(metadataJson) - val attestation: Attestation = service.getAttestation(List(certA, caCert, caca._1).asJava) + val attestation: Attestation = + service.getAttestation(List(certA, caCert, caca._1).asJava) - attestation.isTrusted should be (true) - attestation.getDeviceProperties.get.get("deviceId") should be ("DevA") + attestation.isTrusted should be(true) + attestation.getDeviceProperties.get.get("deviceId") should be("DevA") } it("matches any certificate to a device with no selectors.") { @@ -236,7 +260,9 @@ class StandardMetadataServiceSpec extends FunSpec with Matchers { s"""{ "identifier": "44c87ead-4455-423e-88eb-9248e0ebe847", "version": 1, - "trustedCertificates": ["${toPem(caCert).linesIterator.mkString(raw"\n")}"], + "trustedCertificates": ["${toPem(caCert).linesIterator.mkString( + raw"\n" + )}"], "vendorInfo": {}, "devices": [ { @@ -249,8 +275,8 @@ class StandardMetadataServiceSpec extends FunSpec with Matchers { val resultA = service.getAttestation(List(certA).asJava) val resultB = service.getAttestation(List(certB).asJava) - resultA.getDeviceProperties.get.get("deviceId") should be ("DevA") - resultB.getDeviceProperties.get.get("deviceId") should be ("DevA") + resultA.getDeviceProperties.get.get("deviceId") should be("DevA") + resultB.getDeviceProperties.get.get("deviceId") should be("DevA") } } diff --git a/webauthn-server-core-bundle/build.gradle b/webauthn-server-core-bundle/build.gradle index c9b07ac7b..0e037c025 100644 --- a/webauthn-server-core-bundle/build.gradle +++ b/webauthn-server-core-bundle/build.gradle @@ -6,13 +6,16 @@ description = 'Yubico WebAuthn server core API' project.ext.publishMe = true +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + dependencies { api( project(':webauthn-server-core-minimal'), ) implementation( - addVersion('org.bouncycastle:bcprov-jdk15on'), + 'org.bouncycastle:bcprov-jdk15on', ) } @@ -24,7 +27,7 @@ jar { 'Implementation-Version': project.version, 'Implementation-Vendor': 'Yubico', 'Implementation-Source-Url': 'https://github.com/Yubico/java-webauthn-server', - 'Git-Commit': getGitCommit(), + 'Git-Commit': getGitCommitOrUnknown(), ]) } } diff --git a/webauthn-server-core/build.gradle b/webauthn-server-core/build.gradle index b921144c9..f990b2511 100644 --- a/webauthn-server-core/build.gradle +++ b/webauthn-server-core/build.gradle @@ -2,49 +2,54 @@ plugins { id 'java-library' id 'scala' id 'info.solidsoft.pitest' + id 'io.github.cosmicsilence.scalafix' } description = 'Yubico WebAuthn server core API (fewer dependencies)' project.ext.publishMe = true +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + dependencies { + api(platform(rootProject)) api( project(':yubico-util'), ) compileOnly( - addVersion('org.bouncycastle:bcprov-jdk15on'), + platform(rootProject), + 'org.bouncycastle:bcprov-jdk15on', ) 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.slf4j:slf4j-api'), + 'com.augustcellars.cose:cose-java', + 'com.google.guava:guava', + 'com.fasterxml.jackson.core:jackson-databind', + 'com.upokecenter:cbor', + 'org.apache.httpcomponents:httpclient', + 'org.slf4j:slf4j-api', ) testImplementation( project(':yubico-util-scala'), - addVersion('com.fasterxml.jackson.datatype:jackson-datatype-jdk8'), - addVersion('junit:junit'), - addVersion('org.bouncycastle:bcpkix-jdk15on'), - addVersion('org.bouncycastle:bcprov-jdk15on'), - addVersion('org.mockito:mockito-core'), - addVersion('org.scala-lang:scala-library'), - addVersion('org.scalacheck:scalacheck_2.13'), - addVersion('org.scalatest:scalatest_2.13'), + 'com.fasterxml.jackson.datatype:jackson-datatype-jdk8', + 'junit:junit', + 'org.bouncycastle:bcpkix-jdk15on', + 'org.bouncycastle:bcprov-jdk15on', + 'org.mockito:mockito-core', + 'org.scala-lang:scala-library', + 'org.scalacheck:scalacheck_2.13', + 'org.scalatest:scalatest_2.13', ) testRuntimeOnly( - addVersion('ch.qos.logback:logback-classic'), + 'ch.qos.logback:logback-classic', ) } - jar { manifest { attributes([ @@ -62,7 +67,7 @@ jar { 'Implementation-Version': project.version, 'Implementation-Vendor': 'Yubico', 'Implementation-Source-Url': 'https://github.com/Yubico/java-webauthn-server', - 'Git-Commit': getGitCommit(), + 'Git-Commit': getGitCommitOrUnknown(), ]) } } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/AndroidSafetynetAttestationStatementVerifier.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/AndroidSafetynetAttestationStatementVerifier.java index 4495bf58f..d728bb24f 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/AndroidSafetynetAttestationStatementVerifier.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/AndroidSafetynetAttestationStatementVerifier.java @@ -1,7 +1,5 @@ package com.yubico.webauthn; -import javax.net.ssl.SSLException; - import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; @@ -23,167 +21,179 @@ import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.List; +import javax.net.ssl.SSLException; import lombok.Value; import lombok.extern.slf4j.Slf4j; import org.apache.http.conn.ssl.DefaultHostnameVerifier; @Slf4j -class AndroidSafetynetAttestationStatementVerifier implements AttestationStatementVerifier, X5cAttestationStatementVerifier { - - private static final DefaultHostnameVerifier HOSTNAME_VERIFIER = new DefaultHostnameVerifier(); - - @Override - public AttestationType getAttestationType(AttestationObject attestation) { - return AttestationType.BASIC; +class AndroidSafetynetAttestationStatementVerifier + implements AttestationStatementVerifier, X5cAttestationStatementVerifier { + + private static final DefaultHostnameVerifier HOSTNAME_VERIFIER = new DefaultHostnameVerifier(); + + @Override + public AttestationType getAttestationType(AttestationObject attestation) { + return AttestationType.BASIC; + } + + @Override + public JsonNode getX5cArray(AttestationObject attestationObject) { + JsonNodeFactory jsonFactory = JsonNodeFactory.instance; + ArrayNode array = jsonFactory.arrayNode(); + for (JsonNode cert : parseJws(attestationObject).getHeader().get("x5c")) { + array.add(jsonFactory.binaryNode(ByteArray.fromBase64(cert.textValue()).getBytes())); } - - @Override - public JsonNode getX5cArray(AttestationObject attestationObject) { - JsonNodeFactory jsonFactory = JsonNodeFactory.instance; - ArrayNode array = jsonFactory.arrayNode(); - for (JsonNode cert : parseJws(attestationObject).getHeader().get("x5c")) { - array.add(jsonFactory.binaryNode(ByteArray.fromBase64(cert.textValue()).getBytes())); - } - return array; + return array; + } + + @Override + public boolean verifyAttestationSignature( + AttestationObject attestationObject, ByteArray clientDataJsonHash) { + final JsonNode ver = attestationObject.getAttestationStatement().get("ver"); + + if (ver == null || !ver.isTextual()) { + throw new IllegalArgumentException( + "Property \"ver\" of android-safetynet attestation statement must be a string, was: " + + ver); } - @Override - public boolean verifyAttestationSignature(AttestationObject attestationObject, ByteArray clientDataJsonHash) { - final JsonNode ver = attestationObject.getAttestationStatement().get("ver"); + JsonWebSignatureCustom jws = parseJws(attestationObject); - if (ver == null || !ver.isTextual()) { - throw new IllegalArgumentException("Property \"ver\" of android-safetynet attestation statement must be a string, was: " + ver); - } - - JsonWebSignatureCustom jws = parseJws(attestationObject); + if (!verifySignature(jws)) { + return false; + } - if (!verifySignature(jws)) { - return false; - } + JsonNode payload = jws.getPayload(); + + ByteArray signedData = + attestationObject.getAuthenticatorData().getBytes().concat(clientDataJsonHash); + ByteArray hashSignedData = Crypto.sha256(signedData); + ByteArray nonceByteArray = ByteArray.fromBase64(payload.get("nonce").textValue()); + ExceptionUtil.assure( + hashSignedData.equals(nonceByteArray), + "Nonce does not equal authenticator data + client data. Expected nonce: %s, was nonce: %s", + hashSignedData.getBase64Url(), + nonceByteArray.getBase64Url()); + + ExceptionUtil.assure( + payload.get("ctsProfileMatch").booleanValue(), + "Expected ctsProfileMatch to be true, was: %s", + payload.get("ctsProfileMatch")); + + return true; + } + + private static JsonWebSignatureCustom parseJws(AttestationObject attestationObject) { + return new JsonWebSignatureCustom( + new String(getResponseBytes(attestationObject).getBytes(), StandardCharsets.UTF_8)); + } + + private static ByteArray getResponseBytes(AttestationObject attestationObject) { + final JsonNode response = attestationObject.getAttestationStatement().get("response"); + if (response == null || !response.isBinary()) { + throw new IllegalArgumentException( + "Property \"response\" of android-safetynet attestation statement must be a binary value, was: " + + response); + } - JsonNode payload = jws.getPayload(); + try { + return new ByteArray(response.binaryValue()); + } catch (IOException ioe) { + throw ExceptionUtil.wrapAndLog( + log, "response.isBinary() was true but response.binaryValue failed: " + response, ioe); + } + } - ByteArray signedData = attestationObject.getAuthenticatorData().getBytes().concat(clientDataJsonHash); - ByteArray hashSignedData = Crypto.hash(signedData); - ByteArray nonceByteArray = ByteArray.fromBase64(payload.get("nonce").textValue()); - ExceptionUtil.assure( - hashSignedData.equals(nonceByteArray), - "Nonce does not equal authenticator data + client data. Expected nonce: %s, was nonce: %s", - hashSignedData.getBase64Url(), - nonceByteArray.getBase64Url() - ); + private boolean verifySignature(JsonWebSignatureCustom jws) { + // Verify the signature of the JWS and retrieve the signature certificate. + X509Certificate attestationCertificate = jws.getX5c().get(0); - ExceptionUtil.assure( - payload.get("ctsProfileMatch").booleanValue(), - "Expected ctsProfileMatch to be true, was: %s", - payload.get("ctsProfileMatch") - ); + String signatureAlgorithmName = + WebAuthnCodecs.jwsAlgorithmNameToJavaAlgorithmName(jws.getAlgorithm()); - return true; + Signature signatureVerifier; + try { + signatureVerifier = Crypto.getSignature(signatureAlgorithmName); + } catch (NoSuchAlgorithmException e) { + throw ExceptionUtil.wrapAndLog( + log, "Failed to get a Signature instance for " + signatureAlgorithmName, e); } - - private static JsonWebSignatureCustom parseJws(AttestationObject attestationObject) { - return new JsonWebSignatureCustom(new String(getResponseBytes(attestationObject).getBytes(), StandardCharsets.UTF_8)); + try { + signatureVerifier.initVerify(attestationCertificate.getPublicKey()); + } catch (InvalidKeyException e) { + throw ExceptionUtil.wrapAndLog( + log, "Attestation key is invalid: " + attestationCertificate, e); } - - private static ByteArray getResponseBytes(AttestationObject attestationObject) { - final JsonNode response = attestationObject.getAttestationStatement().get("response"); - if (response == null || !response.isBinary()) { - throw new IllegalArgumentException("Property \"response\" of android-safetynet attestation statement must be a binary value, was: " + response); - } - - try { - return new ByteArray(response.binaryValue()); - } catch (IOException ioe) { - throw ExceptionUtil.wrapAndLog(log, "response.isBinary() was true but response.binaryValue failed: " + response, ioe); - } + try { + signatureVerifier.update(jws.getSignedBytes().getBytes()); + } catch (SignatureException e) { + throw ExceptionUtil.wrapAndLog( + log, "Signature object in invalid state: " + signatureVerifier, e); } - private boolean verifySignature(JsonWebSignatureCustom jws) { - // Verify the signature of the JWS and retrieve the signature certificate. - X509Certificate attestationCertificate = jws.getX5c().get(0); - - String signatureAlgorithmName = WebAuthnCodecs.jwsAlgorithmNameToJavaAlgorithmName(jws.getAlgorithm()); - - Signature signatureVerifier; - try { - signatureVerifier = Crypto.getSignature(signatureAlgorithmName); - } catch (NoSuchAlgorithmException e) { - throw ExceptionUtil.wrapAndLog(log, "Failed to get a Signature instance for " + signatureAlgorithmName, e); - } - try { - signatureVerifier.initVerify(attestationCertificate.getPublicKey()); - } catch (InvalidKeyException e) { - throw ExceptionUtil.wrapAndLog(log, "Attestation key is invalid: " + attestationCertificate, e); - } - try { - signatureVerifier.update(jws.getSignedBytes().getBytes()); - } catch (SignatureException e) { - throw ExceptionUtil.wrapAndLog(log, "Signature object in invalid state: " + signatureVerifier, e); - } - - // Verify the hostname of the certificate. - ExceptionUtil.assure( - verifyHostname(attestationCertificate), - "Certificate isn't issued for the hostname attest.android.com: %s", - attestationCertificate - ); - - try { - return signatureVerifier.verify(jws.getSignature().getBytes()); - } catch (SignatureException e) { - throw ExceptionUtil.wrapAndLog(log, "Failed to verify signature of JWS: " + jws, e); - } - } + // Verify the hostname of the certificate. + ExceptionUtil.assure( + verifyHostname(attestationCertificate), + "Certificate isn't issued for the hostname attest.android.com: %s", + attestationCertificate); - @Value - private static class JsonWebSignatureCustom { - public final JsonNode header; - public final JsonNode payload; - public final ByteArray signedBytes; - public final ByteArray signature; - public final List x5c; - public final String algorithm; - - JsonWebSignatureCustom(String jwsCompact) { - String[] parts = jwsCompact.split("\\."); - ObjectMapper json = JacksonCodecs.json(); - - try { - final ByteArray header = ByteArray.fromBase64Url(parts[0]); - final ByteArray payload = ByteArray.fromBase64Url(parts[1]); - - this.header = json.readTree(header.getBytes()); - this.payload = json.readTree(payload.getBytes()); - this.signedBytes = new ByteArray((parts[0] + "." + parts[1]).getBytes(StandardCharsets.UTF_8)); - this.signature = ByteArray.fromBase64Url(parts[2]); - this.x5c = getX5c(this.header); - this.algorithm = this.header.get("alg").textValue(); - } catch (IOException | Base64UrlException e) { - throw ExceptionUtil.wrapAndLog(log, "Failed to parse JWS: " + jwsCompact, e); - } catch (CertificateException e) { - throw ExceptionUtil.wrapAndLog(log, "Failed to parse attestation certificates in JWS header: " + jwsCompact, e); - } - } - - private static List getX5c(JsonNode header) throws IOException, CertificateException { - List result = new ArrayList<>(); - for (JsonNode jsonNode : header.get("x5c")) { - result.add(CertificateParser.parseDer(jsonNode.binaryValue())); - } - return result; - } + try { + return signatureVerifier.verify(jws.getSignature().getBytes()); + } catch (SignatureException e) { + throw ExceptionUtil.wrapAndLog(log, "Failed to verify signature of JWS: " + jws, e); + } + } + + @Value + private static class JsonWebSignatureCustom { + public final JsonNode header; + public final JsonNode payload; + public final ByteArray signedBytes; + public final ByteArray signature; + public final List x5c; + public final String algorithm; + + JsonWebSignatureCustom(String jwsCompact) { + String[] parts = jwsCompact.split("\\."); + ObjectMapper json = JacksonCodecs.json(); + + try { + final ByteArray header = ByteArray.fromBase64Url(parts[0]); + final ByteArray payload = ByteArray.fromBase64Url(parts[1]); + + this.header = json.readTree(header.getBytes()); + this.payload = json.readTree(payload.getBytes()); + this.signedBytes = + new ByteArray((parts[0] + "." + parts[1]).getBytes(StandardCharsets.UTF_8)); + this.signature = ByteArray.fromBase64Url(parts[2]); + this.x5c = getX5c(this.header); + this.algorithm = this.header.get("alg").textValue(); + } catch (IOException | Base64UrlException e) { + throw ExceptionUtil.wrapAndLog(log, "Failed to parse JWS: " + jwsCompact, e); + } catch (CertificateException e) { + throw ExceptionUtil.wrapAndLog( + log, "Failed to parse attestation certificates in JWS header: " + jwsCompact, e); + } } - /** - * Verifies that the certificate matches the hostname "attest.android.com". - */ - private static boolean verifyHostname(X509Certificate leafCert) { - try { - HOSTNAME_VERIFIER.verify("attest.android.com", leafCert); - return true; - } catch (SSLException e) { - return false; - } + private static List getX5c(JsonNode header) + throws IOException, CertificateException { + List result = new ArrayList<>(); + for (JsonNode jsonNode : header.get("x5c")) { + result.add(CertificateParser.parseDer(jsonNode.binaryValue())); + } + return result; + } + } + + /** Verifies that the certificate matches the hostname "attest.android.com". */ + private static boolean verifyHostname(X509Certificate leafCert) { + try { + HOSTNAME_VERIFIER.verify("attest.android.com", leafCert); + return true; + } catch (SSLException e) { + return false; } + } } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/AppleAttestationStatementVerifier.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/AppleAttestationStatementVerifier.java new file mode 100644 index 000000000..efb3f5d93 --- /dev/null +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/AppleAttestationStatementVerifier.java @@ -0,0 +1,126 @@ +// Copyright (c) 2018, Yubico AB +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package com.yubico.webauthn; + +import com.yubico.internal.util.ExceptionUtil; +import com.yubico.webauthn.data.AttestationObject; +import com.yubico.webauthn.data.AttestationType; +import com.yubico.webauthn.data.ByteArray; +import java.security.PublicKey; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Optional; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +final class AppleAttestationStatementVerifier + implements AttestationStatementVerifier, X5cAttestationStatementVerifier { + + private static final String NONCE_EXTENSION_OID = "1.2.840.113635.100.8.2"; + + @Override + public AttestationType getAttestationType(AttestationObject attestation) { + return AttestationType.ANONYMIZATION_CA; + } + + @Override + public boolean verifyAttestationSignature( + AttestationObject attestationObject, ByteArray clientDataJsonHash) { + final Optional attestationCert; + try { + attestationCert = getX5cAttestationCertificate(attestationObject); + } catch (CertificateException e) { + throw ExceptionUtil.wrapAndLog( + log, + String.format( + "Failed to parse X.509 certificate from attestation object: %s", attestationObject), + e); + } + + return attestationCert + .map( + attestationCertificate -> { + final ByteArray nonceToHash = + attestationObject.getAuthenticatorData().getBytes().concat(clientDataJsonHash); + + final ByteArray nonce = Crypto.sha256(nonceToHash); + + byte[] nonceExtension = attestationCertificate.getExtensionValue(NONCE_EXTENSION_OID); + if (nonceExtension == null) { + throw new IllegalArgumentException( + "Apple anonymous attestation certificate must contain extension OID: " + + NONCE_EXTENSION_OID); + } + + // X.509 extension values is a DER octet string: 0x0426 + // Then the extension contains a 1-element sequence: 0x3024 + // The element has context-specific tag "[1]": 0xa122 + // Then the sequence contains a 32-byte octet string: 0x0420 + final ByteArray expectedExtensionValue = + new ByteArray( + new byte[] { + 0x04, 0x26, 0x30, 0x24, (-128) + (0xa1 - 128), 0x22, 0x04, 0x20 + }) + .concat(nonce); + + if (!expectedExtensionValue.equals(new ByteArray(nonceExtension))) { + throw new IllegalArgumentException( + String.format( + "Apple anonymous attestation certificate extension %s must equal nonceToHash. Expected: %s, was: %s", + NONCE_EXTENSION_OID, + expectedExtensionValue, + new ByteArray(nonceExtension))); + } + + final PublicKey credentialPublicKey; + try { + credentialPublicKey = + WebAuthnCodecs.importCosePublicKey( + attestationObject + .getAuthenticatorData() + .getAttestedCredentialData() + .get() + .getCredentialPublicKey()); + } catch (Exception e) { + throw ExceptionUtil.wrapAndLog(log, "Failed to import credential public key", e); + } + + final PublicKey certPublicKey = attestationCertificate.getPublicKey(); + + if (!credentialPublicKey.equals(certPublicKey)) { + throw new IllegalArgumentException( + String.format( + "Apple anonymous attestation certificate subject public key must equal credential public key. Expected: %s, was: %s", + credentialPublicKey, certPublicKey)); + } + + return true; + }) + .orElseThrow( + () -> + new IllegalArgumentException( + "Failed to parse attestation certificate from \"apple\" attestation statement.")); + } +} diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/AssertionRequest.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/AssertionRequest.java index 77d2a2357..b44ba8377 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/AssertionRequest.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/AssertionRequest.java @@ -32,95 +32,95 @@ import lombok.NonNull; import lombok.Value; - /** - * A combination of a {@link PublicKeyCredentialRequestOptions} and, optionally, a {@link #getUsername() username}. + * A combination of a {@link PublicKeyCredentialRequestOptions} and, optionally, a {@link + * #getUsername() username}. */ @Value @Builder(toBuilder = true) public class AssertionRequest { - /** - * An object that can be serialized to JSON and passed as the publicKey argument to - * navigator.credentials.get(). - */ - @NonNull - private final PublicKeyCredentialRequestOptions publicKeyCredentialRequestOptions; + /** + * An object that can be serialized to JSON and passed as the publicKey argument to + * navigator.credentials.get(). + */ + @NonNull private final PublicKeyCredentialRequestOptions publicKeyCredentialRequestOptions; + + /** + * The username of the user to authenticate, if the user has already been identified. + * + *

If this is absent, this indicates that this is a request for an assertion by a client-side-resident + * credential, and identification of the user has been deferred until the response is + * received. + */ + private final String username; + + @JsonCreator + private AssertionRequest( + @NonNull @JsonProperty("publicKeyCredentialRequestOptions") + PublicKeyCredentialRequestOptions publicKeyCredentialRequestOptions, + @JsonProperty("username") String username) { + this.publicKeyCredentialRequestOptions = publicKeyCredentialRequestOptions; + this.username = username; + } + + /** + * The username of the user to authenticate, if the user has already been identified. + * + *

If this is absent, this indicates that this is a request for an assertion by a client-side-resident + * credential, and identification of the user has been deferred until the response is + * received. + */ + public Optional getUsername() { + return Optional.ofNullable(username); + } + + public static AssertionRequestBuilder.MandatoryStages builder() { + return new AssertionRequestBuilder.MandatoryStages(); + } + + public static class AssertionRequestBuilder { + private String username = null; + + public static class MandatoryStages { + private final AssertionRequestBuilder builder = new AssertionRequestBuilder(); + + /** + * {@link + * AssertionRequestBuilder#publicKeyCredentialRequestOptions(PublicKeyCredentialRequestOptions) + * publicKeyCredentialRequestOptions} is a required parameter. + */ + public AssertionRequestBuilder publicKeyCredentialRequestOptions( + PublicKeyCredentialRequestOptions publicKeyCredentialRequestOptions) { + return builder.publicKeyCredentialRequestOptions(publicKeyCredentialRequestOptions); + } + } /** * The username of the user to authenticate, if the user has already been identified. - *

- * If this is absent, this indicates that this is a request for an assertion by a If this is absent, this indicates that this is a request for an assertion by a client-side-resident - * credential, and identification of the user has been deferred until the response is received. - *

+ * credential, and identification of the user has been deferred until the response is + * received. */ - private final String username; - - @JsonCreator - private AssertionRequest( - @NonNull @JsonProperty("publicKeyCredentialRequestOptions") PublicKeyCredentialRequestOptions publicKeyCredentialRequestOptions, - @JsonProperty("username") String username - ) { - this.publicKeyCredentialRequestOptions = publicKeyCredentialRequestOptions; - this.username = username; + public AssertionRequestBuilder username(@NonNull Optional username) { + return this.username(username.orElse(null)); } /** * The username of the user to authenticate, if the user has already been identified. - *

- * If this is absent, this indicates that this is a request for an assertion by a If this is absent, this indicates that this is a request for an assertion by a client-side-resident - * credential, and identification of the user has been deferred until the response is received. - *

+ * credential, and identification of the user has been deferred until the response is + * received. */ - public Optional getUsername() { - return Optional.ofNullable(username); - } - - public static AssertionRequestBuilder.MandatoryStages builder() { - return new AssertionRequestBuilder.MandatoryStages(); - } - - public static class AssertionRequestBuilder { - private String username = null; - - public static class MandatoryStages { - private final AssertionRequestBuilder builder = new AssertionRequestBuilder(); - - /** - * {@link AssertionRequestBuilder#publicKeyCredentialRequestOptions(PublicKeyCredentialRequestOptions) - * publicKeyCredentialRequestOptions} is a required parameter. - */ - public AssertionRequestBuilder publicKeyCredentialRequestOptions(PublicKeyCredentialRequestOptions publicKeyCredentialRequestOptions) { - return builder.publicKeyCredentialRequestOptions(publicKeyCredentialRequestOptions); - } - } - - /** - * The username of the user to authenticate, if the user has already been identified. - *

- * If this is absent, this indicates that this is a request for an assertion by a client-side-resident - * credential, and identification of the user has been deferred until the response is received. - *

- */ - public AssertionRequestBuilder username(@NonNull Optional username) { - return this.username(username.orElse(null)); - } - - /** - * The username of the user to authenticate, if the user has already been identified. - *

- * If this is absent, this indicates that this is a request for an assertion by a client-side-resident - * credential, and identification of the user has been deferred until the response is received. - *

- */ - public AssertionRequestBuilder username(String username) { - this.username = username; - return this; - } + public AssertionRequestBuilder username(String username) { + this.username = username; + return this; } - + } } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/AssertionResult.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/AssertionResult.java index 1f889f6f2..a2cec70dd 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/AssertionResult.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/AssertionResult.java @@ -27,166 +27,153 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import com.yubico.internal.util.CollectionUtil; +import com.yubico.webauthn.data.AuthenticatorData; import com.yubico.webauthn.data.ByteArray; import com.yubico.webauthn.data.PublicKeyCredentialRequestOptions; import com.yubico.webauthn.data.UserIdentity; -import com.yubico.webauthn.data.AuthenticatorData; import java.util.List; import lombok.Builder; import lombok.NonNull; import lombok.Value; - -/** - * The result of a call to {@link RelyingParty#finishAssertion(FinishAssertionOptions)}. - */ +/** The result of a call to {@link RelyingParty#finishAssertion(FinishAssertionOptions)}. */ @Value @Builder(toBuilder = true) public class AssertionResult { - /** - * true if the assertion was verified successfully. - */ - private final boolean success; - - /** - * The credential ID of the credential - * used for the assertion. - * - * @see Credential ID - * @see PublicKeyCredentialRequestOptions#getAllowCredentials() - */ - @NonNull - private final ByteArray credentialId; - - /** - * The user handle of the authenticated - * user. - * - * @see User Handle - * @see UserIdentity#getId() - * @see #getUsername() - */ - @NonNull - private final ByteArray userHandle; - - /** - * The username of the authenticated user. - * - * @see #getUserHandle() - */ - @NonNull - private final String username; - - /** - * The new signature count of the - * credential used for the assertion. - * - *

- * You should update this value in your database. - *

- * - * @see AuthenticatorData#getSignatureCounter() - */ - private final long signatureCount; - - /** - * true if and only if at least one of the following is true: - *
    - *
  • The {@link AuthenticatorData#getSignatureCounter() signature counter value} in the assertion was strictly - * greater than {@link RegisteredCredential#getSignatureCount() the stored one}.
  • - *
  • The {@link AuthenticatorData#getSignatureCounter() signature counter value} in the assertion and - * {@link RegisteredCredential#getSignatureCount() the stored one} were both zero.
  • - *
- * - * @see §6.1. Authenticator - * Data - * @see AuthenticatorData#getSignatureCounter() - * @see RegisteredCredential#getSignatureCount() - * @see com.yubico.webauthn.RelyingParty.RelyingPartyBuilder#validateSignatureCounter(boolean) - */ - private final boolean signatureCounterValid; - - /** - * Zero or more human-readable messages about non-critical issues. - */ - @NonNull - private final List warnings; - - @JsonCreator - private AssertionResult( - @JsonProperty("success") boolean success, - @NonNull @JsonProperty("credentialId") ByteArray credentialId, - @NonNull @JsonProperty("userHandle") ByteArray userHandle, - @NonNull @JsonProperty("username") String username, - @JsonProperty("signatureCount") long signatureCount, - @JsonProperty("signatureCounterValid") boolean signatureCounterValid, - @NonNull @JsonProperty("warnings") List warnings - ) { - this.success = success; - this.credentialId = credentialId; - this.userHandle = userHandle; - this.username = username; - this.signatureCount = signatureCount; - this.signatureCounterValid = signatureCounterValid; - this.warnings = CollectionUtil.immutableList(warnings); - } + /** true if the assertion was verified successfully. */ + private final boolean success; + + /** + * The credential ID + * of the credential used for the assertion. + * + * @see Credential ID + * @see PublicKeyCredentialRequestOptions#getAllowCredentials() + */ + @NonNull private final ByteArray credentialId; + + /** + * The user handle of + * the authenticated user. + * + * @see User Handle + * @see UserIdentity#getId() + * @see #getUsername() + */ + @NonNull private final ByteArray userHandle; + + /** + * The username of the authenticated user. + * + * @see #getUserHandle() + */ + @NonNull private final String username; + + /** + * The new signature + * count of the credential used for the assertion. + * + *

You should update this value in your database. + * + * @see AuthenticatorData#getSignatureCounter() + */ + private final long signatureCount; + + /** + * true if and only if at least one of the following is true: + * + *

    + *
  • The {@link AuthenticatorData#getSignatureCounter() signature counter value} in the + * assertion was strictly greater than {@link RegisteredCredential#getSignatureCount() the + * stored one}. + *
  • The {@link AuthenticatorData#getSignatureCounter() signature counter value} in the + * assertion and {@link RegisteredCredential#getSignatureCount() the stored one} were both + * zero. + *
+ * + * @see §6.1. + * Authenticator Data + * @see AuthenticatorData#getSignatureCounter() + * @see RegisteredCredential#getSignatureCount() + * @see com.yubico.webauthn.RelyingParty.RelyingPartyBuilder#validateSignatureCounter(boolean) + */ + private final boolean signatureCounterValid; + + /** Zero or more human-readable messages about non-critical issues. */ + @NonNull private final List warnings; + + @JsonCreator + private AssertionResult( + @JsonProperty("success") boolean success, + @NonNull @JsonProperty("credentialId") ByteArray credentialId, + @NonNull @JsonProperty("userHandle") ByteArray userHandle, + @NonNull @JsonProperty("username") String username, + @JsonProperty("signatureCount") long signatureCount, + @JsonProperty("signatureCounterValid") boolean signatureCounterValid, + @NonNull @JsonProperty("warnings") List warnings) { + this.success = success; + this.credentialId = credentialId; + this.userHandle = userHandle; + this.username = username; + this.signatureCount = signatureCount; + this.signatureCounterValid = signatureCounterValid; + this.warnings = CollectionUtil.immutableList(warnings); + } + + static AssertionResultBuilder.MandatoryStages builder() { + return new AssertionResultBuilder.MandatoryStages(); + } + + static class AssertionResultBuilder { + public static class MandatoryStages { + private final AssertionResultBuilder builder = new AssertionResultBuilder(); + + public Step2 success(boolean success) { + builder.success(success); + return new Step2(); + } + + public class Step2 { + public Step3 credentialId(ByteArray credentialId) { + builder.credentialId(credentialId); + return new Step3(); + } + } - static AssertionResultBuilder.MandatoryStages builder() { - return new AssertionResultBuilder.MandatoryStages(); - } + public class Step3 { + public Step4 userHandle(ByteArray userHandle) { + builder.userHandle(userHandle); + return new Step4(); + } + } - static class AssertionResultBuilder { - public static class MandatoryStages { - private final AssertionResultBuilder builder = new AssertionResultBuilder(); - - public Step2 success(boolean success) { - builder.success(success); - return new Step2(); - } - - public class Step2 { - public Step3 credentialId(ByteArray credentialId) { - builder.credentialId(credentialId); - return new Step3(); - } - } - - public class Step3 { - public Step4 userHandle(ByteArray userHandle) { - builder.userHandle(userHandle); - return new Step4(); - } - } - - public class Step4 { - public Step5 username(String username) { - builder.username(username); - return new Step5(); - } - } - - public class Step5 { - public Step6 signatureCount(long signatureCount) { - builder.signatureCount(signatureCount); - return new Step6(); - } - } - - public class Step6 { - public Step7 signatureCounterValid(boolean signatureCounterValid) { - builder.signatureCounterValid(signatureCounterValid); - return new Step7(); - } - } - - public class Step7 { - public AssertionResultBuilder warnings(List warnings) { - return builder.warnings(warnings); - } - } + public class Step4 { + public Step5 username(String username) { + builder.username(username); + return new Step5(); } - } + } -} + public class Step5 { + public Step6 signatureCount(long signatureCount) { + builder.signatureCount(signatureCount); + return new Step6(); + } + } + + public class Step6 { + public Step7 signatureCounterValid(boolean signatureCounterValid) { + builder.signatureCounterValid(signatureCounterValid); + return new Step7(); + } + } + public class Step7 { + public AssertionResultBuilder warnings(List warnings) { + return builder.warnings(warnings); + } + } + } + } +} diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/AttestationStatementVerifier.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/AttestationStatementVerifier.java index 3c1e18f31..a962164e3 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/AttestationStatementVerifier.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/AttestationStatementVerifier.java @@ -25,17 +25,17 @@ package com.yubico.webauthn; import COSE.CoseException; -import com.yubico.webauthn.data.ByteArray; import com.yubico.webauthn.data.AttestationObject; import com.yubico.webauthn.data.AttestationType; +import com.yubico.webauthn.data.ByteArray; import java.io.IOException; import java.security.cert.CertificateException; - interface AttestationStatementVerifier { - AttestationType getAttestationType(AttestationObject attestation) throws IOException, CoseException, CertificateException; - - boolean verifyAttestationSignature(AttestationObject attestationObject, ByteArray clientDataJsonHash); + AttestationType getAttestationType(AttestationObject attestation) + throws IOException, CoseException, CertificateException; + boolean verifyAttestationSignature( + AttestationObject attestationObject, ByteArray clientDataJsonHash); } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/AttestationTrustResolver.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/AttestationTrustResolver.java index 23932e164..ac87a2af2 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/AttestationTrustResolver.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/AttestationTrustResolver.java @@ -29,9 +29,8 @@ import java.security.cert.X509Certificate; import java.util.List; - interface AttestationTrustResolver { - Attestation resolveTrustAnchor(List certificateChain) throws CertificateEncodingException; - + Attestation resolveTrustAnchor(List certificateChain) + throws CertificateEncodingException; } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/CredentialRepository.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/CredentialRepository.java index 14dba8bb5..2a11022f8 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/CredentialRepository.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/CredentialRepository.java @@ -29,53 +29,45 @@ import java.util.Optional; import java.util.Set; - /** * An abstraction of the database lookups needed by this library. * - *

- * This is used by {@link RelyingParty} to look up credentials, usernames and user handles from usernames, user handles - * and credential IDs. - *

+ *

This is used by {@link RelyingParty} to look up credentials, usernames and user handles from + * usernames, user handles and credential IDs. */ public interface CredentialRepository { - /** - * Get the credential IDs of all credentials registered to the user with the given username. - */ - Set getCredentialIdsForUsername(String username); - - /** - * Get the user handle corresponding to the given username - the inverse of {@link - * #getUsernameForUserHandle(ByteArray)}. - */ - Optional getUserHandleForUsername(String username); + /** Get the credential IDs of all credentials registered to the user with the given username. */ + Set getCredentialIdsForUsername(String username); - /** - * Get the username corresponding to the given user handle - the inverse of {@link - * #getUserHandleForUsername(String)}. - */ - Optional getUsernameForUserHandle(ByteArray userHandle); + /** + * Get the user handle corresponding to the given username - the inverse of {@link + * #getUsernameForUserHandle(ByteArray)}. + */ + Optional getUserHandleForUsername(String username); - /** - * Look up the public key and stored signature count for the given credential registered to the given user. - * - *

- * The returned {@link RegisteredCredential} is not expected to be long-lived. It may be read directly from a - * database or assembled from other components. - *

- */ - Optional lookup(ByteArray credentialId, ByteArray userHandle); + /** + * Get the username corresponding to the given user handle - the inverse of {@link + * #getUserHandleForUsername(String)}. + */ + Optional getUsernameForUserHandle(ByteArray userHandle); - /** - * Look up all credentials with the given credential ID, regardless of what user they're registered to. - * - *

- * This is used to refuse registration of duplicate credential IDs. Therefore, under normal circumstances this - * method should only return zero or one credential (this is an expected consequence, not an interface - * requirement). - *

- */ - Set lookupAll(ByteArray credentialId); + /** + * Look up the public key and stored signature count for the given credential registered to the + * given user. + * + *

The returned {@link RegisteredCredential} is not expected to be long-lived. It may be read + * directly from a database or assembled from other components. + */ + Optional lookup(ByteArray credentialId, ByteArray userHandle); + /** + * Look up all credentials with the given credential ID, regardless of what user they're + * registered to. + * + *

This is used to refuse registration of duplicate credential IDs. Therefore, under normal + * circumstances this method should only return zero or one credential (this is an expected + * consequence, not an interface requirement). + */ + Set lookupAll(ByteArray credentialId); } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/Crypto.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/Crypto.java index b98af3a6c..9075f95f1 100755 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/Crypto.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/Crypto.java @@ -50,90 +50,99 @@ @UtilityClass @Slf4j -final class Crypto -{ - // Values from https://apps.nsa.gov/iaarchive/library/ia-guidance/ia-solutions-for-classified/algorithm-guidance/mathematical-routines-for-the-nist-prime-elliptic-curves.cfm - // cross-referenced with "secp256r1" in https://www.secg.org/sec2-v2.pdf - private static final EllipticCurve P256 = new EllipticCurve( - new ECFieldFp( - new BigInteger("115792089210356248762697446949407573530086143415290314195533631308867097853951", 10)), - new BigInteger("115792089210356248762697446949407573530086143415290314195533631308867097853948", 10), - new BigInteger("41058363725152142129326129780047268409114441015993725554835256314039467401291", 10)); +final class Crypto { + // Values from + // https://apps.nsa.gov/iaarchive/library/ia-guidance/ia-solutions-for-classified/algorithm-guidance/mathematical-routines-for-the-nist-prime-elliptic-curves.cfm + // cross-referenced with "secp256r1" in https://www.secg.org/sec2-v2.pdf + private static final EllipticCurve P256 = + new EllipticCurve( + new ECFieldFp( + new BigInteger( + "115792089210356248762697446949407573530086143415290314195533631308867097853951", + 10)), + new BigInteger( + "115792089210356248762697446949407573530086143415290314195533631308867097853948", 10), + new BigInteger( + "41058363725152142129326129780047268409114441015993725554835256314039467401291", 10)); - /* - * TODO: Delete this in the next major version release - */ - private static class BouncyCastleLoader { - private static Provider getProvider() { - return new BouncyCastleProvider(); - } + /* + * TODO: Delete this in the next major version release + */ + private static class BouncyCastleLoader { + private static Provider getProvider() { + return new BouncyCastleProvider(); } + } - /* - * TODO: Delete this in the next major version release - */ - public static KeyFactory getKeyFactory(String algorithm) throws NoSuchAlgorithmException { - try { - return KeyFactory.getInstance(algorithm); - } catch (NoSuchAlgorithmException e) { - log.debug("Caught {}. Attempting fallback to BouncyCastle...", e.toString()); - try { - return KeyFactory.getInstance(algorithm, BouncyCastleLoader.getProvider()); - } catch (NoSuchAlgorithmException | NoClassDefFoundError e2) { - throw e; - } - } + /* + * TODO: Delete this in the next major version release + */ + public static KeyFactory getKeyFactory(String algorithm) throws NoSuchAlgorithmException { + try { + return KeyFactory.getInstance(algorithm); + } catch (NoSuchAlgorithmException e) { + log.debug("Caught {}. Attempting fallback to BouncyCastle...", e.toString()); + try { + return KeyFactory.getInstance(algorithm, BouncyCastleLoader.getProvider()); + } catch (NoSuchAlgorithmException | NoClassDefFoundError e2) { + throw e; + } } + } - /* - * TODO: Delete this in the next major version release - */ - public static Signature getSignature(String algorithm) throws NoSuchAlgorithmException { - try { - return Signature.getInstance(algorithm); - } catch (NoSuchAlgorithmException e) { - log.debug("Caught {}. Attempting fallback to BouncyCastle...", e.toString()); - try { - return Signature.getInstance(algorithm, BouncyCastleLoader.getProvider()); - } catch (NoSuchAlgorithmException | NoClassDefFoundError e2) { - throw e; - } - } + /* + * TODO: Delete this in the next major version release + */ + public static Signature getSignature(String algorithm) throws NoSuchAlgorithmException { + try { + return Signature.getInstance(algorithm); + } catch (NoSuchAlgorithmException e) { + log.debug("Caught {}. Attempting fallback to BouncyCastle...", e.toString()); + try { + return Signature.getInstance(algorithm, BouncyCastleLoader.getProvider()); + } catch (NoSuchAlgorithmException | NoClassDefFoundError e2) { + throw e; + } } + } - static boolean isP256(ECParameterSpec params) { - return P256.equals(params.getCurve()); - } + static boolean isP256(ECParameterSpec params) { + return P256.equals(params.getCurve()); + } - public static boolean verifySignature(X509Certificate attestationCertificate, ByteArray signedBytes, ByteArray signature, COSEAlgorithmIdentifier alg) { - return verifySignature(attestationCertificate.getPublicKey(), signedBytes, signature, alg); - } + public static boolean verifySignature( + X509Certificate attestationCertificate, + ByteArray signedBytes, + ByteArray signature, + COSEAlgorithmIdentifier alg) { + return verifySignature(attestationCertificate.getPublicKey(), signedBytes, signature, alg); + } - public static boolean verifySignature(PublicKey publicKey, ByteArray signedBytes, ByteArray signatureBytes, COSEAlgorithmIdentifier alg) { - try { - Signature signature = Signature.getInstance(WebAuthnCodecs.getJavaAlgorithmName(alg)); - signature.initVerify(publicKey); - signature.update(signedBytes.getBytes()); - return signature.verify(signatureBytes.getBytes()); - } catch (GeneralSecurityException | IllegalArgumentException e) { - throw new RuntimeException( - String.format( - "Failed to verify signature. This could be a problem with your JVM environment, or a bug in webauthn-server-core. Public key: %s, signed data: %s , signature: %s", - publicKey, - signedBytes.getBase64Url(), - signatureBytes.getBase64Url() - ), - e - ); - } + public static boolean verifySignature( + PublicKey publicKey, + ByteArray signedBytes, + ByteArray signatureBytes, + COSEAlgorithmIdentifier alg) { + try { + Signature signature = Signature.getInstance(WebAuthnCodecs.getJavaAlgorithmName(alg)); + signature.initVerify(publicKey); + signature.update(signedBytes.getBytes()); + return signature.verify(signatureBytes.getBytes()); + } catch (GeneralSecurityException | IllegalArgumentException e) { + throw new RuntimeException( + String.format( + "Failed to verify signature. This could be a problem with your JVM environment, or a bug in webauthn-server-core. Public key: %s, signed data: %s , signature: %s", + publicKey, signedBytes.getBase64Url(), signatureBytes.getBase64Url()), + e); } + } - public static ByteArray hash(ByteArray bytes) { - //noinspection UnstableApiUsage - return new ByteArray(Hashing.sha256().hashBytes(bytes.getBytes()).asBytes()); - } + public static ByteArray sha256(ByteArray bytes) { + //noinspection UnstableApiUsage + return new ByteArray(Hashing.sha256().hashBytes(bytes.getBytes()).asBytes()); + } - public static ByteArray hash(String str) { - return hash(new ByteArray(str.getBytes(StandardCharsets.UTF_8))); - } + public static ByteArray sha256(String str) { + return sha256(new ByteArray(str.getBytes(StandardCharsets.UTF_8))); + } } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/ExtensionsValidation.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/ExtensionsValidation.java index 641a1f4b6..6c82e5357 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/ExtensionsValidation.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/ExtensionsValidation.java @@ -34,38 +34,43 @@ import java.util.stream.Collectors; import lombok.experimental.UtilityClass; - @UtilityClass class ExtensionsValidation { - static boolean validate(ExtensionInputs requested, PublicKeyCredential response) { - Set requestedExtensionIds = requested.getExtensionIds(); - Set clientExtensionIds = response.getClientExtensionResults().getExtensionIds(); + static boolean validate( + ExtensionInputs requested, + PublicKeyCredential + response) { + Set requestedExtensionIds = requested.getExtensionIds(); + Set clientExtensionIds = response.getClientExtensionResults().getExtensionIds(); - if (!requestedExtensionIds.containsAll(clientExtensionIds)) { - throw new IllegalArgumentException(String.format( - "Client extensions {%s} are not a subset of requested extensions {%s}.", - String.join(", ", clientExtensionIds), - String.join(", ", requestedExtensionIds) - )); - } + if (!requestedExtensionIds.containsAll(clientExtensionIds)) { + throw new IllegalArgumentException( + String.format( + "Client extensions {%s} are not a subset of requested extensions {%s}.", + String.join(", ", clientExtensionIds), String.join(", ", requestedExtensionIds))); + } - Set authenticatorExtensionIds = response.getResponse().getParsedAuthenticatorData().getExtensions() - .map(extensions -> extensions.getKeys().stream() - .map(CBORObject::AsString) - .collect(Collectors.toSet()) - ) + Set authenticatorExtensionIds = + response + .getResponse() + .getParsedAuthenticatorData() + .getExtensions() + .map( + extensions -> + extensions.getKeys().stream() + .map(CBORObject::AsString) + .collect(Collectors.toSet())) .orElseGet(HashSet::new); - if (!requestedExtensionIds.containsAll(authenticatorExtensionIds)) { - throw new IllegalArgumentException(String.format( - "Authenticator extensions {%s} are not a subset of requested extensions {%s}.", - String.join(", ", authenticatorExtensionIds), - String.join(", ", requestedExtensionIds) - )); - } - - return true; + if (!requestedExtensionIds.containsAll(authenticatorExtensionIds)) { + throw new IllegalArgumentException( + String.format( + "Authenticator extensions {%s} are not a subset of requested extensions {%s}.", + String.join(", ", authenticatorExtensionIds), + String.join(", ", requestedExtensionIds))); } + return true; + } } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/FidoU2fAttestationStatementVerifier.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/FidoU2fAttestationStatementVerifier.java index c0cc586be..616153e14 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/FidoU2fAttestationStatementVerifier.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/FidoU2fAttestationStatementVerifier.java @@ -24,6 +24,8 @@ package com.yubico.webauthn; +import static com.yubico.webauthn.Crypto.isP256; + import COSE.CoseException; import com.fasterxml.jackson.databind.JsonNode; import com.yubico.internal.util.ExceptionUtil; @@ -41,132 +43,155 @@ import java.util.Optional; import lombok.extern.slf4j.Slf4j; -import static com.yubico.webauthn.Crypto.isP256; - @Slf4j -final class FidoU2fAttestationStatementVerifier implements AttestationStatementVerifier, X5cAttestationStatementVerifier { - - private X509Certificate getAttestationCertificate(AttestationObject attestationObject) throws CertificateException { - return getX5cAttestationCertificate(attestationObject).map(attestationCertificate -> { - if ("EC".equals(attestationCertificate.getPublicKey().getAlgorithm()) - && isP256(((ECPublicKey) attestationCertificate.getPublicKey()).getParams()) - ) { +final class FidoU2fAttestationStatementVerifier + implements AttestationStatementVerifier, X5cAttestationStatementVerifier { + + private X509Certificate getAttestationCertificate(AttestationObject attestationObject) + throws CertificateException { + return getX5cAttestationCertificate(attestationObject) + .map( + attestationCertificate -> { + if ("EC".equals(attestationCertificate.getPublicKey().getAlgorithm()) + && isP256(((ECPublicKey) attestationCertificate.getPublicKey()).getParams())) { return attestationCertificate; - } else { - throw new IllegalArgumentException("Attestation certificate for fido-u2f must have an ECDSA P-256 public key."); - } - }).orElseThrow(() -> new IllegalArgumentException( - "fido-u2f attestation statement must have an \"x5c\" property set to an array of at least one DER encoded X.509 certificate." - )); + } else { + throw new IllegalArgumentException( + "Attestation certificate for fido-u2f must have an ECDSA P-256 public key."); + } + }) + .orElseThrow( + () -> + new IllegalArgumentException( + "fido-u2f attestation statement must have an \"x5c\" property set to an array of at least one DER encoded X.509 certificate.")); + } + + private static boolean validSelfSignature(X509Certificate cert) { + try { + cert.verify(cert.getPublicKey()); + return true; + } catch (Exception e) { + return false; + } + } + + private static ByteArray getRawUserPublicKey(AttestationObject attestationObject) + throws IOException, CoseException { + final ByteArray pubkeyCose = + attestationObject + .getAuthenticatorData() + .getAttestedCredentialData() + .get() + .getCredentialPublicKey(); + final PublicKey pubkey; + try { + pubkey = WebAuthnCodecs.importCosePublicKey(pubkeyCose); + } catch (InvalidKeySpecException | NoSuchAlgorithmException e) { + throw ExceptionUtil.wrapAndLog(log, "Failed to decode public key: " + pubkeyCose.getHex(), e); } - private static boolean validSelfSignature(X509Certificate cert) { - try { - cert.verify(cert.getPublicKey()); - return true; - } catch (Exception e) { - return false; - } + final ECPublicKey ecPubkey; + try { + ecPubkey = (ECPublicKey) pubkey; + } catch (ClassCastException e) { + throw new RuntimeException("U2F supports only EC keys, was: " + pubkey); } - private static ByteArray getRawUserPublicKey(AttestationObject attestationObject) throws IOException, CoseException { - final ByteArray pubkeyCose = attestationObject.getAuthenticatorData().getAttestedCredentialData().get().getCredentialPublicKey(); - final PublicKey pubkey; - try { - pubkey = WebAuthnCodecs.importCosePublicKey(pubkeyCose); - } catch (InvalidKeySpecException | NoSuchAlgorithmException e) { - throw ExceptionUtil.wrapAndLog(log, "Failed to decode public key: " + pubkeyCose.getHex(), e); - } - - final ECPublicKey ecPubkey; - try { - ecPubkey = (ECPublicKey) pubkey; - } catch (ClassCastException e) { - throw new RuntimeException( "U2F supports only EC keys, was: " + pubkey); - } - - return WebAuthnCodecs.ecPublicKeyToRaw(ecPubkey); + return WebAuthnCodecs.ecPublicKeyToRaw(ecPubkey); + } + + @Override + public AttestationType getAttestationType(AttestationObject attestationObject) + throws CoseException, IOException, CertificateException { + X509Certificate attestationCertificate = getAttestationCertificate(attestationObject); + + if (attestationCertificate.getPublicKey() instanceof ECPublicKey + && validSelfSignature(attestationCertificate) + && getRawUserPublicKey(attestationObject) + .equals( + WebAuthnCodecs.ecPublicKeyToRaw( + (ECPublicKey) attestationCertificate.getPublicKey()))) { + return AttestationType.SELF_ATTESTATION; + } else { + return AttestationType.BASIC; + } + } + + @Override + public boolean verifyAttestationSignature( + AttestationObject attestationObject, ByteArray clientDataJsonHash) { + final X509Certificate attestationCertificate; + try { + attestationCertificate = getAttestationCertificate(attestationObject); + } catch (CertificateException e) { + throw new IllegalArgumentException( + String.format( + "Failed to parse X.509 certificate from attestation object: %s", attestationObject)); } - @Override - public AttestationType getAttestationType(AttestationObject attestationObject) throws CoseException, IOException, CertificateException { - X509Certificate attestationCertificate = getAttestationCertificate(attestationObject); - - if (attestationCertificate.getPublicKey() instanceof ECPublicKey - && validSelfSignature(attestationCertificate) - && getRawUserPublicKey(attestationObject) - .equals( - WebAuthnCodecs.ecPublicKeyToRaw((ECPublicKey) attestationCertificate.getPublicKey()) - ) - ) { - return AttestationType.SELF_ATTESTATION; - } else { - return AttestationType.BASIC; - } + if (!("EC".equals(attestationCertificate.getPublicKey().getAlgorithm()) + && isP256(((ECPublicKey) attestationCertificate.getPublicKey()).getParams()))) { + throw new IllegalArgumentException( + "Attestation certificate for fido-u2f must have an ECDSA P-256 public key."); } - @Override - public boolean verifyAttestationSignature(AttestationObject attestationObject, ByteArray clientDataJsonHash) { - final X509Certificate attestationCertificate; - try { - attestationCertificate = getAttestationCertificate(attestationObject); - } catch (CertificateException e) { - throw new IllegalArgumentException(String.format( - "Failed to parse X.509 certificate from attestation object: %s", attestationObject)); - } - - if (!( - "EC".equals(attestationCertificate.getPublicKey().getAlgorithm()) - && isP256(((ECPublicKey) attestationCertificate.getPublicKey()).getParams()) - )) { - throw new IllegalArgumentException("Attestation certificate for fido-u2f must have an ECDSA P-256 public key."); - } - - final Optional attData = attestationObject.getAuthenticatorData().getAttestedCredentialData(); - - return attData.map(attestedCredentialData -> { - JsonNode signature = attestationObject.getAttestationStatement().get("sig"); - - if (signature == null) { - throw new IllegalArgumentException("fido-u2f attestation statement must have a \"sig\" property set to a DER encoded signature."); - } - - if (signature.isBinary()) { + final Optional attData = + attestationObject.getAuthenticatorData().getAttestedCredentialData(); + + return attData + .map( + attestedCredentialData -> { + JsonNode signature = attestationObject.getAttestationStatement().get("sig"); + + if (signature == null) { + throw new IllegalArgumentException( + "fido-u2f attestation statement must have a \"sig\" property set to a DER encoded signature."); + } + + if (signature.isBinary()) { final ByteArray userPublicKey; try { - userPublicKey = getRawUserPublicKey(attestationObject); + userPublicKey = getRawUserPublicKey(attestationObject); } catch (IOException | CoseException e) { - RuntimeException err = new RuntimeException(String.format("Failed to parse public key from attestation data %s", attestedCredentialData), e); - log.error(err.getMessage(), err); - throw err; + RuntimeException err = + new RuntimeException( + String.format( + "Failed to parse public key from attestation data %s", + attestedCredentialData), + e); + log.error(err.getMessage(), err); + throw err; } ByteArray keyHandle = attestedCredentialData.getCredentialId(); U2fRawRegisterResponse u2fRegisterResponse; try { - u2fRegisterResponse = new U2fRawRegisterResponse( - userPublicKey, - keyHandle, - attestationCertificate, - new ByteArray(signature.binaryValue()) - ); + u2fRegisterResponse = + new U2fRawRegisterResponse( + userPublicKey, + keyHandle, + attestationCertificate, + new ByteArray(signature.binaryValue())); } catch (IOException e) { - RuntimeException err = new RuntimeException("signature.isBinary() was true but signature.binaryValue() failed", e); - log.error(err.getMessage(), err); - throw err; + RuntimeException err = + new RuntimeException( + "signature.isBinary() was true but signature.binaryValue() failed", e); + log.error(err.getMessage(), err); + throw err; } return u2fRegisterResponse.verifySignature( - attestationObject.getAuthenticatorData().getRpIdHash(), - clientDataJsonHash - ); - } else { - throw new IllegalArgumentException("\"sig\" property of fido-u2f attestation statement must be a CBOR byte array value."); - } - - }).orElseThrow(() -> new IllegalArgumentException("Attestation object for credential creation must have attestation data.")); - } - + attestationObject.getAuthenticatorData().getRpIdHash(), clientDataJsonHash); + } else { + throw new IllegalArgumentException( + "\"sig\" property of fido-u2f attestation statement must be a CBOR byte array value."); + } + }) + .orElseThrow( + () -> + new IllegalArgumentException( + "Attestation object for credential creation must have attestation data.")); + } } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishAssertionOptions.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishAssertionOptions.java index a94ed4c3b..17cd9ae3d 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishAssertionOptions.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishAssertionOptions.java @@ -33,87 +33,87 @@ import lombok.NonNull; import lombok.Value; -/** - * Parameters for {@link RelyingParty#finishAssertion(FinishAssertionOptions)}. - */ +/** Parameters for {@link RelyingParty#finishAssertion(FinishAssertionOptions)}. */ @Value @Builder(toBuilder = true) public class FinishAssertionOptions { - /** - * The request that the {@link #getResponse() response} is a response to. - */ - @NonNull - private final AssertionRequest request; + /** The request that the {@link #getResponse() response} is a response to. */ + @NonNull private final AssertionRequest request; - /** - * The client's response to the {@link #getRequest() request}. - * - * @see navigator.credentials.get() - */ - @NonNull - private final PublicKeyCredential response; - - /** - * The token binding ID of the connection to the - * client, if any. - * - * @see The Token Binding Protocol Version 1.0 - */ - private final ByteArray callerTokenBindingId; + /** + * The client's response to the {@link #getRequest() request}. + * + * @see navigator.credentials.get() + */ + @NonNull + private final PublicKeyCredential + response; - /** - * The token binding ID of the connection to the - * client, if any. - * - * @see The Token Binding Protocol Version 1.0 - */ - public Optional getCallerTokenBindingId() { - return Optional.ofNullable(callerTokenBindingId); - } + /** + * The token binding ID of the + * connection to the client, if any. + * + * @see The Token Binding Protocol Version 1.0 + */ + private final ByteArray callerTokenBindingId; - public static FinishAssertionOptionsBuilder.MandatoryStages builder() { - return new FinishAssertionOptionsBuilder.MandatoryStages(); - } + /** + * The token binding ID of the + * connection to the client, if any. + * + * @see The Token Binding Protocol Version 1.0 + */ + public Optional getCallerTokenBindingId() { + return Optional.ofNullable(callerTokenBindingId); + } - public static class FinishAssertionOptionsBuilder { - private ByteArray callerTokenBindingId = null; + public static FinishAssertionOptionsBuilder.MandatoryStages builder() { + return new FinishAssertionOptionsBuilder.MandatoryStages(); + } - public static class MandatoryStages { - private final FinishAssertionOptionsBuilder builder = new FinishAssertionOptionsBuilder(); + public static class FinishAssertionOptionsBuilder { + private ByteArray callerTokenBindingId = null; - public Step2 request(AssertionRequest request) { - builder.request(request); - return new Step2(); - } + public static class MandatoryStages { + private final FinishAssertionOptionsBuilder builder = new FinishAssertionOptionsBuilder(); - public class Step2 { - public FinishAssertionOptionsBuilder response(PublicKeyCredential response) { - return builder.response(response); - } - } - } + public Step2 request(AssertionRequest request) { + builder.request(request); + return new Step2(); + } - /** - * The token binding ID of the connection to the - * client, if any. - * - * @see The Token Binding Protocol Version 1.0 - */ - public FinishAssertionOptionsBuilder callerTokenBindingId(@NonNull Optional callerTokenBindingId) { - this.callerTokenBindingId = callerTokenBindingId.orElse(null); - return this; + public class Step2 { + public FinishAssertionOptionsBuilder response( + PublicKeyCredential + response) { + return builder.response(response); } + } + } - /** - * The token binding ID of the connection to the - * client, if any. - * - * @see The Token Binding Protocol Version 1.0 - */ - public FinishAssertionOptionsBuilder callerTokenBindingId(@NonNull ByteArray callerTokenBindingId) { - return this.callerTokenBindingId(Optional.of(callerTokenBindingId)); - } + /** + * The token binding ID of the + * connection to the client, if any. + * + * @see The Token Binding Protocol Version 1.0 + */ + public FinishAssertionOptionsBuilder callerTokenBindingId( + @NonNull Optional callerTokenBindingId) { + this.callerTokenBindingId = callerTokenBindingId.orElse(null); + return this; } + /** + * The token binding ID of the + * connection to the client, if any. + * + * @see The Token Binding Protocol Version 1.0 + */ + public FinishAssertionOptionsBuilder callerTokenBindingId( + @NonNull ByteArray callerTokenBindingId) { + return this.callerTokenBindingId(Optional.of(callerTokenBindingId)); + } + } } 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 aa9c86a83..67d705bb6 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 @@ -24,6 +24,7 @@ package com.yubico.webauthn; +import static com.yubico.internal.util.ExceptionUtil.assure; import COSE.CoseException; import com.yubico.internal.util.CollectionUtil; @@ -50,617 +51,610 @@ import lombok.Value; import lombok.extern.slf4j.Slf4j; -import static com.yubico.internal.util.ExceptionUtil.assure; - - @Builder @Slf4j final class FinishAssertionSteps { - private static final String CLIENT_DATA_TYPE = "webauthn.get"; + private static final String CLIENT_DATA_TYPE = "webauthn.get"; - private final AssertionRequest request; - private final PublicKeyCredential response; - private final Optional callerTokenBindingId; - private final Set origins; - private final String rpId; - private final CredentialRepository credentialRepository; + private final AssertionRequest request; + private final PublicKeyCredential + response; + private final Optional callerTokenBindingId; + private final Set origins; + private final String rpId; + private final CredentialRepository credentialRepository; - @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; + @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(); - } + public Step0 begin() { + return new Step0(); + } - public AssertionResult run() throws InvalidSignatureCountException { - return begin().run(); - } + public AssertionResult run() throws InvalidSignatureCountException { + return begin().run(); + } - interface Step> { - Next nextStep(); + interface Step> { + Next nextStep(); - void validate() throws InvalidSignatureCountException; + void validate() throws InvalidSignatureCountException; - List getPrevWarnings(); + List getPrevWarnings(); - default Optional result() { - return Optional.empty(); - } + default Optional result() { + return Optional.empty(); + } - default List getWarnings() { - return Collections.emptyList(); - } + default List getWarnings() { + return Collections.emptyList(); + } - default List allWarnings() { - List result = new ArrayList<>(getPrevWarnings().size() + getWarnings().size()); - result.addAll(getPrevWarnings()); - result.addAll(getWarnings()); - return CollectionUtil.immutableList(result); - } + default List allWarnings() { + List result = new ArrayList<>(getPrevWarnings().size() + getWarnings().size()); + result.addAll(getPrevWarnings()); + result.addAll(getWarnings()); + return CollectionUtil.immutableList(result); + } - default Next next() throws InvalidSignatureCountException { - validate(); - return nextStep(); - } + default Next next() throws InvalidSignatureCountException { + validate(); + return nextStep(); + } - default AssertionResult run() throws InvalidSignatureCountException { - if (result().isPresent()) { - return result().get(); - } else { - return next().run(); - } - } + default AssertionResult run() throws InvalidSignatureCountException { + if (result().isPresent()) { + return result().get(); + } else { + return next().run(); + } } + } - @Value - class Step0 implements Step { - @Override - public Step1 nextStep() { - return new Step1(username().get(), userHandle().get(), allWarnings()); - } + @Value + class Step0 implements Step { + @Override + public Step1 nextStep() { + return new Step1(username().get(), userHandle().get(), allWarnings()); + } - @Override - public void validate() { - assure( - request.getUsername().isPresent() || response.getResponse().getUserHandle().isPresent(), - "At least one of username and user handle must be given; none was." - ); - assure( - userHandle().isPresent(), - "No user found for username: %s, userHandle: %s", - request.getUsername(), response.getResponse().getUserHandle() - ); - assure( - username().isPresent(), - "No user found for username: %s, userHandle: %s", - request.getUsername(), response.getResponse().getUserHandle() - ); - } + @Override + public void validate() { + assure( + request.getUsername().isPresent() || response.getResponse().getUserHandle().isPresent(), + "At least one of username and user handle must be given; none was."); + assure( + userHandle().isPresent(), + "No user found for username: %s, userHandle: %s", + request.getUsername(), + response.getResponse().getUserHandle()); + assure( + username().isPresent(), + "No user found for username: %s, userHandle: %s", + request.getUsername(), + response.getResponse().getUserHandle()); + } - @Override - public List getPrevWarnings() { - return Collections.emptyList(); - } + @Override + public List getPrevWarnings() { + return Collections.emptyList(); + } - private Optional userHandle() { - return response.getResponse().getUserHandle() - .map(Optional::of) - .orElseGet(() -> credentialRepository.getUserHandleForUsername(request.getUsername().get())); - } + private Optional userHandle() { + return response + .getResponse() + .getUserHandle() + .map(Optional::of) + .orElseGet( + () -> credentialRepository.getUserHandleForUsername(request.getUsername().get())); + } - private Optional username() { - return request.getUsername() - .map(Optional::of) - .orElseGet(() -> credentialRepository.getUsernameForUserHandle(response.getResponse().getUserHandle().get())); - } + private Optional username() { + return request + .getUsername() + .map(Optional::of) + .orElseGet( + () -> + credentialRepository.getUsernameForUserHandle( + response.getResponse().getUserHandle().get())); } + } - @Value - class Step1 implements Step { - private final String username; - private final ByteArray userHandle; - private final List prevWarnings; + @Value + class Step1 implements Step { + private final String username; + private final ByteArray userHandle; + private final List prevWarnings; - @Override - public Step2 nextStep() { - return new Step2(username, userHandle, allWarnings()); - } + @Override + public Step2 nextStep() { + return new Step2(username, userHandle, allWarnings()); + } - @Override - public void validate() { - request.getPublicKeyCredentialRequestOptions().getAllowCredentials().ifPresent(allowed -> { + @Override + public void validate() { + request + .getPublicKeyCredentialRequestOptions() + .getAllowCredentials() + .ifPresent( + allowed -> { assure( allowed.stream().anyMatch(allow -> allow.getId().equals(response.getId())), "Unrequested credential ID: %s", - response.getId() - ); - }); - } + response.getId()); + }); } + } - @Value - class Step2 implements Step { - private final String username; - private final ByteArray userHandle; - private final List prevWarnings; - - @Override - public Step3 nextStep() { - return new Step3(username, userHandle, allWarnings()); - } + @Value + class Step2 implements Step { + private final String username; + private final ByteArray userHandle; + private final List prevWarnings; - @Override - public void validate() { - Optional registration = credentialRepository.lookup(response.getId(), userHandle); - - assure( - registration.isPresent(), - "Unknown credential: %s", - response.getId() - ); - - assure( - userHandle.equals(registration.get().getUserHandle()), - "User handle %s does not own credential %s", - userHandle, response.getId() - ); - } + @Override + public Step3 nextStep() { + return new Step3(username, userHandle, allWarnings()); } - @Value - class Step3 implements Step { - private final String username; - private final ByteArray userHandle; - private final List prevWarnings; - - @Override - public Step4 nextStep() { - return new Step4(username, userHandle, credential(), allWarnings()); - } + @Override + public void validate() { + Optional registration = + credentialRepository.lookup(response.getId(), userHandle); - @Override - public void validate() { - assure( - maybeCredential().isPresent(), - "Unknown credential. Credential ID: %s, user handle: %s", - response.getId(), userHandle - ); - } + assure(registration.isPresent(), "Unknown credential: %s", response.getId()); - private Optional maybeCredential() { - return credentialRepository.lookup(response.getId(), userHandle); - } - - public RegisteredCredential credential() { - return maybeCredential().get(); - } + assure( + userHandle.equals(registration.get().getUserHandle()), + "User handle %s does not own credential %s", + userHandle, + response.getId()); } + } - @Value - class Step4 implements Step { - - private final String username; - private final ByteArray userHandle; - private final RegisteredCredential credential; - private final List prevWarnings; + @Value + class Step3 implements Step { + private final String username; + private final ByteArray userHandle; + private final List prevWarnings; - @Override - public void validate() { - assure(clientData() != null, "Missing client data."); - assure(authenticatorData() != null, "Missing authenticator data."); - assure(signature() != null, "Missing signature."); - } - - @Override - public Step5 nextStep() { - return new Step5(username, userHandle, credential, allWarnings()); - } + @Override + public Step4 nextStep() { + return new Step4(username, userHandle, credential(), allWarnings()); + } - public ByteArray authenticatorData() { - return response.getResponse().getAuthenticatorData(); - } + @Override + public void validate() { + assure( + maybeCredential().isPresent(), + "Unknown credential. Credential ID: %s, user handle: %s", + response.getId(), + userHandle); + } - public ByteArray clientData() { - return response.getResponse().getClientDataJSON(); - } + private Optional maybeCredential() { + return credentialRepository.lookup(response.getId(), userHandle); + } - public ByteArray signature() { - return response.getResponse().getSignature(); - } + public RegisteredCredential credential() { + return maybeCredential().get(); } + } - @Value - class Step5 implements Step { - private final String username; - private final ByteArray userHandle; - private final RegisteredCredential credential; - private final List prevWarnings; + @Value + class Step4 implements Step { - // Nothing to do - @Override - public void validate() { - } + private final String username; + private final ByteArray userHandle; + private final RegisteredCredential credential; + private final List prevWarnings; - @Override - public Step6 nextStep() { - return new Step6(username, userHandle, credential, allWarnings()); - } + @Override + public void validate() { + assure(clientData() != null, "Missing client data."); + assure(authenticatorData() != null, "Missing authenticator data."); + assure(signature() != null, "Missing signature."); } - @Value - class Step6 implements Step { - private final String username; - private final ByteArray userHandle; - private final RegisteredCredential credential; - private final List prevWarnings; + @Override + public Step5 nextStep() { + return new Step5(username, userHandle, credential, allWarnings()); + } - @Override - public void validate() { - assure(clientData() != null, "Missing client data."); - } + public ByteArray authenticatorData() { + return response.getResponse().getAuthenticatorData(); + } - @Override - public Step7 nextStep() { - return new Step7(username, userHandle, credential, clientData(), allWarnings()); - } + public ByteArray clientData() { + return response.getResponse().getClientDataJSON(); + } - public CollectedClientData clientData() { - return response.getResponse().getClientData(); - } + public ByteArray signature() { + return response.getResponse().getSignature(); + } + } + + @Value + class Step5 implements Step { + private final String username; + private final ByteArray userHandle; + private final RegisteredCredential credential; + private final List prevWarnings; + + // Nothing to do + @Override + public void validate() {} + + @Override + public Step6 nextStep() { + return new Step6(username, userHandle, credential, allWarnings()); + } + } + + @Value + class Step6 implements Step { + private final String username; + private final ByteArray userHandle; + private final RegisteredCredential credential; + private final List prevWarnings; + + @Override + public void validate() { + assure(clientData() != null, "Missing client data."); } - @Value - class Step7 implements Step { + @Override + public Step7 nextStep() { + return new Step7(username, userHandle, credential, clientData(), allWarnings()); + } - private final String username; - private final ByteArray userHandle; - private final RegisteredCredential credential; - private final CollectedClientData clientData; - private final List prevWarnings; + public CollectedClientData clientData() { + return response.getResponse().getClientData(); + } + } - private List warnings = new LinkedList<>(); + @Value + class Step7 implements Step { - @Override - public List getWarnings() { - return CollectionUtil.immutableList(warnings); - } + private final String username; + private final ByteArray userHandle; + private final RegisteredCredential credential; + private final CollectedClientData clientData; + private final List prevWarnings; - @Override - public void validate() { - assure(CLIENT_DATA_TYPE.equals(clientData.getType()), - "The \"type\" in the client data must be exactly \"%s\", was: %s", - CLIENT_DATA_TYPE, clientData.getType() - ); - } + private List warnings = new LinkedList<>(); - @Override - public Step8 nextStep() { - return new Step8(username, userHandle, credential, allWarnings()); - } + @Override + public List getWarnings() { + return CollectionUtil.immutableList(warnings); } - @Value - class Step8 implements Step { - private final String username; - private final ByteArray userHandle; - private final RegisteredCredential credential; - private final List prevWarnings; - - @Override - public void validate() { - assure( - request.getPublicKeyCredentialRequestOptions().getChallenge().equals(response.getResponse().getClientData().getChallenge()), - "Incorrect challenge." - ); - } - - @Override - public Step9 nextStep() { - return new Step9(username, userHandle, credential, allWarnings()); - } + @Override + public void validate() { + assure( + CLIENT_DATA_TYPE.equals(clientData.getType()), + "The \"type\" in the client data must be exactly \"%s\", was: %s", + CLIENT_DATA_TYPE, + clientData.getType()); } - @Value - class Step9 implements Step { - private final String username; - private final ByteArray userHandle; - private final RegisteredCredential credential; - private final List prevWarnings; - - @Override - public void validate() { - final String responseOrigin = response.getResponse().getClientData().getOrigin(); - assure( - OriginMatcher.isAllowed( - responseOrigin, - origins, - allowOriginPort, - allowOriginSubdomain - ), - "Incorrect origin: " + responseOrigin - ); - } - - @Override - public Step10 nextStep() { - return new Step10(username, userHandle, credential, allWarnings()); - } + @Override + public Step8 nextStep() { + return new Step8(username, userHandle, credential, allWarnings()); } - - @Value - class Step10 implements Step { - private final String username; - private final ByteArray userHandle; - private final RegisteredCredential credential; - private final List prevWarnings; - - @Override - public void validate() { - TokenBindingValidator.validate(response.getResponse().getClientData().getTokenBinding(), callerTokenBindingId); - } - - @Override - public Step11 nextStep() { - return new Step11(username, userHandle, credential, allWarnings()); - } + } + + @Value + class Step8 implements Step { + private final String username; + private final ByteArray userHandle; + private final RegisteredCredential credential; + private final List prevWarnings; + + @Override + public void validate() { + assure( + request + .getPublicKeyCredentialRequestOptions() + .getChallenge() + .equals(response.getResponse().getClientData().getChallenge()), + "Incorrect challenge."); } - @Value - class Step11 implements Step { - private final String username; - private final ByteArray userHandle; - private final RegisteredCredential credential; - private final List prevWarnings; - - @Override - public void validate() { - try { - assure( - Crypto.hash(rpId).equals(response.getResponse().getParsedAuthenticatorData().getRpIdHash()), - "Wrong RP ID hash." - ); - } catch (IllegalArgumentException e) { - Optional appid = request.getPublicKeyCredentialRequestOptions().getExtensions().getAppid(); - if (appid.isPresent()) { - assure( - Crypto.hash(appid.get().getId()).equals(response.getResponse().getParsedAuthenticatorData().getRpIdHash()), - "Wrong RP ID hash." - ); - } else { - throw e; - } - } - } - - @Override - public Step12 nextStep() { - return new Step12(username, userHandle, credential, allWarnings()); - } + @Override + public Step9 nextStep() { + return new Step9(username, userHandle, credential, allWarnings()); } - - @Value - class Step12 implements Step { - private final String username; - private final ByteArray userHandle; - private final RegisteredCredential credential; - private final List prevWarnings; - - @Override - public void validate() { - assure( - response.getResponse().getParsedAuthenticatorData().getFlags().UP, - "User Presence is required." - ); - } - - @Override - public Step13 nextStep() { - return new Step13(username, userHandle, credential, allWarnings()); - } + } + + @Value + class Step9 implements Step { + private final String username; + private final ByteArray userHandle; + private final RegisteredCredential credential; + private final List prevWarnings; + + @Override + public void validate() { + final String responseOrigin = response.getResponse().getClientData().getOrigin(); + assure( + OriginMatcher.isAllowed(responseOrigin, origins, allowOriginPort, allowOriginSubdomain), + "Incorrect origin: " + responseOrigin); } - @Value - class Step13 implements Step { - private final String username; - private final ByteArray userHandle; - private final RegisteredCredential credential; - private final List prevWarnings; - - @Override - public void validate() { - if (request.getPublicKeyCredentialRequestOptions().getUserVerification() == UserVerificationRequirement.REQUIRED) { - assure( - response.getResponse().getParsedAuthenticatorData().getFlags().UV, - "User Verification is required." - ); - } - } - - @Override - public Step14 nextStep() { - return new Step14(username, userHandle, credential, allWarnings()); - } + @Override + public Step10 nextStep() { + return new Step10(username, userHandle, credential, allWarnings()); } - - @Value - class Step14 implements Step { - private final String username; - private final ByteArray userHandle; - private final RegisteredCredential credential; - private final List prevWarnings; - - @Override - public void validate() { - if (!allowUnrequestedExtensions) { - ExtensionsValidation.validate(request.getPublicKeyCredentialRequestOptions().getExtensions(), response); - } - } - - @Override - public List getWarnings() { - try { - ExtensionsValidation.validate(request.getPublicKeyCredentialRequestOptions().getExtensions(), response); - return Collections.emptyList(); - } catch (Exception e) { - return CollectionUtil.immutableList(Collections.singletonList(e.getMessage())); - } - } - - @Override - public Step15 nextStep() { - return new Step15(username, userHandle, credential, allWarnings()); - } + } + + @Value + class Step10 implements Step { + private final String username; + private final ByteArray userHandle; + private final RegisteredCredential credential; + private final List prevWarnings; + + @Override + public void validate() { + TokenBindingValidator.validate( + response.getResponse().getClientData().getTokenBinding(), callerTokenBindingId); } - @Value - class Step15 implements Step { - private final String username; - private final ByteArray userHandle; - private final RegisteredCredential credential; - private final List prevWarnings; - - @Override - public void validate() { - assure(clientDataJsonHash().size() == 32, "Failed to compute hash of client data"); - } - - @Override - public Step16 nextStep() { - return new Step16(username, userHandle, credential, clientDataJsonHash(), allWarnings()); - } + @Override + public Step11 nextStep() { + return new Step11(username, userHandle, credential, allWarnings()); + } + } + + @Value + class Step11 implements Step { + private final String username; + private final ByteArray userHandle; + private final RegisteredCredential credential; + private final List prevWarnings; + + @Override + public void validate() { + try { + assure( + Crypto.sha256(rpId) + .equals(response.getResponse().getParsedAuthenticatorData().getRpIdHash()), + "Wrong RP ID hash."); + } catch (IllegalArgumentException e) { + Optional appid = + request.getPublicKeyCredentialRequestOptions().getExtensions().getAppid(); + if (appid.isPresent()) { + assure( + Crypto.sha256(appid.get().getId()) + .equals(response.getResponse().getParsedAuthenticatorData().getRpIdHash()), + "Wrong RP ID hash."); + } else { + throw e; + } + } + } - public ByteArray clientDataJsonHash() { - return Crypto.hash(response.getResponse().getClientDataJSON()); - } + @Override + public Step12 nextStep() { + return new Step12(username, userHandle, credential, allWarnings()); + } + } + + @Value + class Step12 implements Step { + private final String username; + private final ByteArray userHandle; + private final RegisteredCredential credential; + private final List prevWarnings; + + @Override + public void validate() { + assure( + response.getResponse().getParsedAuthenticatorData().getFlags().UP, + "User Presence is required."); } - @Value - class Step16 implements Step { - private final String username; - private final ByteArray userHandle; - private final RegisteredCredential credential; - private final ByteArray clientDataJsonHash; - private final List prevWarnings; - - @Override - public void validate() { - final ByteArray cose = credential.getPublicKeyCose(); - final PublicKey key; - - try { - key = WebAuthnCodecs.importCosePublicKey(cose); - } catch (CoseException | IOException | InvalidKeySpecException e) { - throw new IllegalArgumentException( - String.format( - "Failed to decode public key: Credential ID: %s COSE: %s", - credential.getCredentialId().getBase64Url(), - cose.getBase64Url() - ), - e - ); - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException(e); - } - - final COSEAlgorithmIdentifier alg = 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(), - alg - ) - ) { - throw new IllegalArgumentException("Invalid assertion signature."); - } - } + @Override + public Step13 nextStep() { + return new Step13(username, userHandle, credential, allWarnings()); + } + } + + @Value + class Step13 implements Step { + private final String username; + private final ByteArray userHandle; + private final RegisteredCredential credential; + private final List prevWarnings; + + @Override + public void validate() { + if (request.getPublicKeyCredentialRequestOptions().getUserVerification() + == UserVerificationRequirement.REQUIRED) { + assure( + response.getResponse().getParsedAuthenticatorData().getFlags().UV, + "User Verification is required."); + } + } - @Override - public Step17 nextStep() { - return new Step17(username, userHandle, allWarnings()); - } + @Override + public Step14 nextStep() { + return new Step14(username, userHandle, credential, allWarnings()); + } + } + + @Value + class Step14 implements Step { + private final String username; + private final ByteArray userHandle; + private final RegisteredCredential credential; + private final List prevWarnings; + + @Override + public void validate() { + if (!allowUnrequestedExtensions) { + ExtensionsValidation.validate( + request.getPublicKeyCredentialRequestOptions().getExtensions(), response); + } + } - public ByteArray signedBytes() { - return response.getResponse().getAuthenticatorData().concat(clientDataJsonHash); - } + @Override + public List getWarnings() { + try { + ExtensionsValidation.validate( + request.getPublicKeyCredentialRequestOptions().getExtensions(), response); + return Collections.emptyList(); + } catch (Exception e) { + return CollectionUtil.immutableList(Collections.singletonList(e.getMessage())); + } } - @Value - class Step17 implements Step { - private final String username; - private final ByteArray userHandle; - private final List prevWarnings; - - @Override - public void validate() throws InvalidSignatureCountException { - if (validateSignatureCounter - && ! - signatureCounterValid() - ) { - throw new InvalidSignatureCountException( - response.getId(), - storedSignatureCountBefore() + 1, - assertionSignatureCount() - ); - } - } + @Override + public Step15 nextStep() { + return new Step15(username, userHandle, credential, allWarnings()); + } + } + + @Value + class Step15 implements Step { + private final String username; + private final ByteArray userHandle; + private final RegisteredCredential credential; + private final List prevWarnings; + + @Override + public void validate() { + assure(clientDataJsonHash().size() == 32, "Failed to compute hash of client data"); + } - private boolean signatureCounterValid() { - return (assertionSignatureCount() == 0 && storedSignatureCountBefore() == 0) - || assertionSignatureCount() > storedSignatureCountBefore(); - } + @Override + public Step16 nextStep() { + return new Step16(username, userHandle, credential, clientDataJsonHash(), allWarnings()); + } - @Override - public Finished nextStep() { - return new Finished(username, userHandle, assertionSignatureCount(), signatureCounterValid(), allWarnings()); - } + public ByteArray clientDataJsonHash() { + return Crypto.sha256(response.getResponse().getClientDataJSON()); + } + } + + @Value + class Step16 implements Step { + private final String username; + private final ByteArray userHandle; + private final RegisteredCredential credential; + private final ByteArray clientDataJsonHash; + private final List prevWarnings; + + @Override + public void validate() { + final ByteArray cose = credential.getPublicKeyCose(); + final PublicKey key; + + try { + key = WebAuthnCodecs.importCosePublicKey(cose); + } catch (CoseException | IOException | InvalidKeySpecException e) { + throw new IllegalArgumentException( + String.format( + "Failed to decode public key: Credential ID: %s COSE: %s", + credential.getCredentialId().getBase64Url(), cose.getBase64Url()), + e); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + + final COSEAlgorithmIdentifier alg = + 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(), alg)) { + throw new IllegalArgumentException("Invalid assertion signature."); + } + } - private long storedSignatureCountBefore() { - return credentialRepository.lookup(response.getId(), userHandle) - .map(RegisteredCredential::getSignatureCount) - .orElse(0L); - } + @Override + public Step17 nextStep() { + return new Step17(username, userHandle, allWarnings()); + } - private long assertionSignatureCount() { - return response.getResponse().getParsedAuthenticatorData().getSignatureCounter(); - } + public ByteArray signedBytes() { + return response.getResponse().getAuthenticatorData().concat(clientDataJsonHash); + } + } + + @Value + class Step17 implements Step { + private final String username; + private final ByteArray userHandle; + private final List prevWarnings; + + @Override + public void validate() throws InvalidSignatureCountException { + if (validateSignatureCounter && !signatureCounterValid()) { + throw new InvalidSignatureCountException( + response.getId(), storedSignatureCountBefore() + 1, assertionSignatureCount()); + } } - @Value - class Finished implements Step { - private final String username; - private final ByteArray userHandle; - private final long assertionSignatureCount; - private final boolean signatureCounterValid; - private final List prevWarnings; + private boolean signatureCounterValid() { + return (assertionSignatureCount() == 0 && storedSignatureCountBefore() == 0) + || assertionSignatureCount() > storedSignatureCountBefore(); + } - @Override - public void validate() { /* No-op */ } + @Override + public Finished nextStep() { + return new Finished( + username, userHandle, assertionSignatureCount(), signatureCounterValid(), allWarnings()); + } - @Override - public Finished nextStep() { - return this; - } + private long storedSignatureCountBefore() { + return credentialRepository + .lookup(response.getId(), userHandle) + .map(RegisteredCredential::getSignatureCount) + .orElse(0L); + } - @Override - public Optional result() { - return Optional.of(AssertionResult.builder() - .success(true) - .credentialId(response.getId()) - .userHandle(userHandle) - .username(username) - .signatureCount(assertionSignatureCount) - .signatureCounterValid(signatureCounterValid) - .warnings(allWarnings()) - .build() - ); - } + private long assertionSignatureCount() { + return response.getResponse().getParsedAuthenticatorData().getSignatureCounter(); + } + } + + @Value + class Finished implements Step { + private final String username; + private final ByteArray userHandle; + private final long assertionSignatureCount; + private final boolean signatureCounterValid; + private final List prevWarnings; + + @Override + public void validate() { + /* No-op */ + } + @Override + public Finished nextStep() { + return this; } + @Override + public Optional result() { + return Optional.of( + AssertionResult.builder() + .success(true) + .credentialId(response.getId()) + .userHandle(userHandle) + .username(username) + .signatureCount(assertionSignatureCount) + .signatureCounterValid(signatureCounterValid) + .warnings(allWarnings()) + .build()); + } + } } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishRegistrationOptions.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishRegistrationOptions.java index ca5ff837c..36284b8ab 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishRegistrationOptions.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishRegistrationOptions.java @@ -34,86 +34,90 @@ import lombok.NonNull; import lombok.Value; -/** - * Parameters for {@link RelyingParty#finishRegistration(FinishRegistrationOptions)}. - */ +/** Parameters for {@link RelyingParty#finishRegistration(FinishRegistrationOptions)}. */ @Value @Builder(toBuilder = true) public class FinishRegistrationOptions { - /** - * The request that the {@link #getResponse() response} is a response to. - */ - @NonNull - private final PublicKeyCredentialCreationOptions request; + /** The request that the {@link #getResponse() response} is a response to. */ + @NonNull private final PublicKeyCredentialCreationOptions request; - /** - * The client's response to the {@link #getRequest() request}. - * - * navigator.credentials.create() - */ - @NonNull - private final PublicKeyCredential response; + /** + * The client's response to the {@link #getRequest() request}. + * + *

navigator.credentials.create() + */ + @NonNull + private final PublicKeyCredential< + AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs> + response; - /** - * The token binding ID of the connection to the - * client, if any. - * - * @see The Token Binding Protocol Version 1.0 - */ - private final ByteArray callerTokenBindingId; + /** + * The token binding ID of the + * connection to the client, if any. + * + * @see The Token Binding Protocol Version 1.0 + */ + private final ByteArray callerTokenBindingId; - /** - * The token binding ID of the connection to the - * client, if any. - * - * @see The Token Binding Protocol Version 1.0 - */ - public Optional getCallerTokenBindingId() { - return Optional.ofNullable(callerTokenBindingId); - } + /** + * The token binding ID of the + * connection to the client, if any. + * + * @see The Token Binding Protocol Version 1.0 + */ + public Optional getCallerTokenBindingId() { + return Optional.ofNullable(callerTokenBindingId); + } - public static FinishRegistrationOptionsBuilder.MandatoryStages builder() { - return new FinishRegistrationOptionsBuilder.MandatoryStages(); - } + public static FinishRegistrationOptionsBuilder.MandatoryStages builder() { + return new FinishRegistrationOptionsBuilder.MandatoryStages(); + } - public static class FinishRegistrationOptionsBuilder { - private ByteArray callerTokenBindingId = null; + public static class FinishRegistrationOptionsBuilder { + private ByteArray callerTokenBindingId = null; - public static class MandatoryStages { - private final FinishRegistrationOptionsBuilder builder = new FinishRegistrationOptionsBuilder(); + public static class MandatoryStages { + private final FinishRegistrationOptionsBuilder builder = + new FinishRegistrationOptionsBuilder(); - public Step2 request(PublicKeyCredentialCreationOptions request) { - builder.request(request); - return new Step2(); - } + public Step2 request(PublicKeyCredentialCreationOptions request) { + builder.request(request); + return new Step2(); + } - public class Step2 { - public FinishRegistrationOptionsBuilder response(PublicKeyCredential response) { - return builder.response(response); - } - } + public class Step2 { + public FinishRegistrationOptionsBuilder response( + PublicKeyCredential< + AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs> + response) { + return builder.response(response); } + } + } - /** - * The token binding ID of the connection to the - * client, if any. - * - * @see The Token Binding Protocol Version 1.0 - */ - public FinishRegistrationOptionsBuilder callerTokenBindingId(@NonNull Optional callerTokenBindingId) { - this.callerTokenBindingId = callerTokenBindingId.orElse(null); - return this; - } + /** + * The token binding ID of the + * connection to the client, if any. + * + * @see The Token Binding Protocol Version 1.0 + */ + public FinishRegistrationOptionsBuilder callerTokenBindingId( + @NonNull Optional callerTokenBindingId) { + this.callerTokenBindingId = callerTokenBindingId.orElse(null); + return this; + } - /** - * The token binding ID of the connection to the - * client, if any. - * - * @see The Token Binding Protocol Version 1.0 - */ - public FinishRegistrationOptionsBuilder callerTokenBindingId(@NonNull ByteArray callerTokenBindingId) { - return this.callerTokenBindingId(Optional.of(callerTokenBindingId)); - } + /** + * The token binding ID of the + * connection to the client, if any. + * + * @see The Token Binding Protocol Version 1.0 + */ + public FinishRegistrationOptionsBuilder callerTokenBindingId( + @NonNull ByteArray callerTokenBindingId) { + return this.callerTokenBindingId(Optional.of(callerTokenBindingId)); } + } } 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 60a96456e..374de13cb 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 @@ -24,6 +24,9 @@ package com.yubico.webauthn; +import static com.yubico.internal.util.ExceptionUtil.assure; +import static com.yubico.internal.util.ExceptionUtil.wrapAndLog; + import COSE.CoseException; import com.upokecenter.cbor.CBORObject; import com.yubico.internal.util.CollectionUtil; @@ -42,7 +45,6 @@ import com.yubico.webauthn.data.UserVerificationRequirement; import java.io.IOException; import java.security.NoSuchAlgorithmException; -import java.security.Security; import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; @@ -57,685 +59,707 @@ 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 final class FinishRegistrationSteps { - private static final String CLIENT_DATA_TYPE = "webauthn.create"; + private static final String CLIENT_DATA_TYPE = "webauthn.create"; - private final PublicKeyCredentialCreationOptions request; - private final PublicKeyCredential response; - private final Optional callerTokenBindingId; - private final Set origins; - private final String rpId; - private final boolean allowUntrustedAttestation; - private final Optional metadataService; - private final CredentialRepository credentialRepository; + private final PublicKeyCredentialCreationOptions request; + private final PublicKeyCredential< + AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs> + response; + private final Optional callerTokenBindingId; + private final Set origins; + private final String rpId; + private final boolean allowUntrustedAttestation; + private final Optional metadataService; + private final CredentialRepository credentialRepository; - @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 allowOriginPort = false; + @Builder.Default private final boolean allowOriginSubdomain = false; + @Builder.Default private final boolean allowUnrequestedExtensions = false; + public Step1 begin() { + return new Step1(); + } - public Step1 begin() { - return new Step1(); - } - - public RegistrationResult run() { - return begin().run(); - } + public RegistrationResult run() { + return begin().run(); + } - interface Step> { - Next nextStep(); + interface Step> { + Next nextStep(); - void validate(); + void validate(); - List getPrevWarnings(); - - default Optional result() { - return Optional.empty(); - } + List getPrevWarnings(); - default List getWarnings() { - return Collections.emptyList(); - } - - default List allWarnings() { - List result = new ArrayList<>(getPrevWarnings().size() + getWarnings().size()); - result.addAll(getPrevWarnings()); - result.addAll(getWarnings()); - return CollectionUtil.immutableList(result); - } - - default Next next() { - validate(); - return nextStep(); - } - - default RegistrationResult run() { - if (result().isPresent()) { - return result().get(); - } else { - return next().run(); - } - } + default Optional result() { + return Optional.empty(); } - @Value - class Step1 implements Step { - @Override - public void validate() {} - - @Override - public Step2 nextStep() { - return new Step2(); - } - - @Override - public List getPrevWarnings() { - return Collections.emptyList(); - } + default List getWarnings() { + return Collections.emptyList(); } - @Value - class Step2 implements Step { - @Override - public void validate() { - assure(clientData() != null, "Client data must not be null."); - } - - @Override - public Step3 nextStep() { - return new Step3(clientData()); - } - - @Override - public List getPrevWarnings() { - return Collections.emptyList(); - } - - public CollectedClientData clientData() { - return response.getResponse().getClientData(); - } + default List allWarnings() { + List result = new ArrayList<>(getPrevWarnings().size() + getWarnings().size()); + result.addAll(getPrevWarnings()); + result.addAll(getWarnings()); + return CollectionUtil.immutableList(result); } - @Value - class Step3 implements Step { - private final CollectedClientData clientData; - - private List warnings = new ArrayList<>(0); - - @Override - public void validate() { - assure(CLIENT_DATA_TYPE.equals(clientData.getType()), - "The \"type\" in the client data must be exactly \"%s\", was: %s", - CLIENT_DATA_TYPE, clientData.getType() - ); - } + default Next next() { + validate(); + return nextStep(); + } - @Override - public Step4 nextStep() { - return new Step4(clientData, allWarnings()); - } + default RegistrationResult run() { + if (result().isPresent()) { + return result().get(); + } else { + return next().run(); + } + } + } - @Override - public List getPrevWarnings() { - return Collections.emptyList(); - } + @Value + class Step1 implements Step { + @Override + public void validate() {} - @Override - public List getWarnings() { - return CollectionUtil.immutableList(warnings); - } + @Override + public Step2 nextStep() { + return new Step2(); } - @Value - class Step4 implements Step { - private final CollectedClientData clientData; - private final List prevWarnings; - - @Override - public void validate() { - assure( - request.getChallenge().equals(clientData.getChallenge()), - "Incorrect challenge." - ); - } + @Override + public List getPrevWarnings() { + return Collections.emptyList(); + } + } - @Override - public Step5 nextStep() { - return new Step5(clientData, allWarnings()); - } + @Value + class Step2 implements Step { + @Override + public void validate() { + assure(clientData() != null, "Client data must not be null."); } - @Value - class Step5 implements Step { - private final CollectedClientData clientData; - private final List prevWarnings; + @Override + public Step3 nextStep() { + return new Step3(clientData()); + } - @Override - public void validate() { - final String responseOrigin = clientData.getOrigin(); - assure( - OriginMatcher.isAllowed( - responseOrigin, - origins, - allowOriginPort, - allowOriginSubdomain - ), - "Incorrect origin: " + responseOrigin - ); - } + @Override + public List getPrevWarnings() { + return Collections.emptyList(); + } - @Override - public Step6 nextStep() { - return new Step6(clientData, allWarnings()); - } + public CollectedClientData clientData() { + return response.getResponse().getClientData(); } + } - @Value - class Step6 implements Step { - private final CollectedClientData clientData; - private final List prevWarnings; + @Value + class Step3 implements Step { + private final CollectedClientData clientData; - @Override - public void validate() { - TokenBindingValidator.validate(clientData.getTokenBinding(), callerTokenBindingId); - } + private List warnings = new ArrayList<>(0); - @Override - public Step7 nextStep() { - return new Step7(allWarnings()); - } + @Override + public void validate() { + assure( + CLIENT_DATA_TYPE.equals(clientData.getType()), + "The \"type\" in the client data must be exactly \"%s\", was: %s", + CLIENT_DATA_TYPE, + clientData.getType()); } - @Value - class Step7 implements Step { - private final List prevWarnings; - - @Override - public void validate() { - assure(clientDataJsonHash().size() == 32, "Failed to compute hash of client data"); - } - - @Override - public Step8 nextStep() { - return new Step8(clientDataJsonHash(), allWarnings()); - } + @Override + public Step4 nextStep() { + return new Step4(clientData, allWarnings()); + } - public ByteArray clientDataJsonHash() { - return Crypto.hash(response.getResponse().getClientDataJSON()); - } + @Override + public List getPrevWarnings() { + return Collections.emptyList(); } - @Value - class Step8 implements Step { - private final ByteArray clientDataJsonHash; - private final List prevWarnings; + @Override + public List getWarnings() { + return CollectionUtil.immutableList(warnings); + } + } - @Override - public void validate() { - assure(attestation() != null, "Malformed attestation object."); - } + @Value + class Step4 implements Step { + private final CollectedClientData clientData; + private final List prevWarnings; - @Override - public Step9 nextStep() { - return new Step9(clientDataJsonHash, attestation(), allWarnings()); - } + @Override + public void validate() { + assure(request.getChallenge().equals(clientData.getChallenge()), "Incorrect challenge."); + } - public AttestationObject attestation() { - return response.getResponse().getAttestation(); - } + @Override + public Step5 nextStep() { + return new Step5(clientData, allWarnings()); + } + } + + @Value + class Step5 implements Step { + private final CollectedClientData clientData; + private final List prevWarnings; + + @Override + public void validate() { + final String responseOrigin = clientData.getOrigin(); + assure( + OriginMatcher.isAllowed(responseOrigin, origins, allowOriginPort, allowOriginSubdomain), + "Incorrect origin: " + responseOrigin); } - @Value - class Step9 implements Step { - private final ByteArray clientDataJsonHash; - private final AttestationObject attestation; - private final List prevWarnings; + @Override + public Step6 nextStep() { + return new Step6(clientData, allWarnings()); + } + } - @Override - public void validate() { - assure( - Crypto.hash(rpId).equals(response.getResponse().getAttestation().getAuthenticatorData().getRpIdHash()), - "Wrong RP ID hash." - ); - } + @Value + class Step6 implements Step { + private final CollectedClientData clientData; + private final List prevWarnings; - @Override - public Step10 nextStep() { - return new Step10(clientDataJsonHash, attestation, allWarnings()); - } + @Override + public void validate() { + TokenBindingValidator.validate(clientData.getTokenBinding(), callerTokenBindingId); } - @Value - class Step10 implements Step { - private final ByteArray clientDataJsonHash; - private final AttestationObject attestation; - private final List prevWarnings; + @Override + public Step7 nextStep() { + return new Step7(allWarnings()); + } + } - @Override - public void validate() { - assure( - response.getResponse().getParsedAuthenticatorData().getFlags().UP, - "User Presence is required." - ); - } + @Value + class Step7 implements Step { + private final List prevWarnings; - @Override - public Step11 nextStep() { - return new Step11(clientDataJsonHash, attestation, allWarnings()); - } + @Override + public void validate() { + assure(clientDataJsonHash().size() == 32, "Failed to compute hash of client data"); } - @Value - class Step11 implements Step { - private final ByteArray clientDataJsonHash; - private final AttestationObject attestation; - private final List prevWarnings; - - @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." - ); - } - } - - @Override - public Step12 nextStep() { - return new Step12(clientDataJsonHash, attestation, allWarnings()); - } + @Override + public Step8 nextStep() { + return new Step8(clientDataJsonHash(), allWarnings()); } - @Value - class Step12 implements Step { - private final ByteArray clientDataJsonHash; - private final AttestationObject attestation; - private final List prevWarnings; + public ByteArray clientDataJsonHash() { + return Crypto.sha256(response.getResponse().getClientDataJSON()); + } + } - @Override - public void validate() { - if (!allowUnrequestedExtensions) { - ExtensionsValidation.validate(request.getExtensions(), response); - } - } + @Value + class Step8 implements Step { + private final ByteArray clientDataJsonHash; + private final List prevWarnings; - @Override - public List getWarnings() { - try { - ExtensionsValidation.validate(request.getExtensions(), response); - return Collections.emptyList(); - } catch (Exception e) { - return Collections.singletonList(e.getMessage()); - } - } + @Override + public void validate() { + assure(attestation() != null, "Malformed attestation object."); + } - @Override - public Step13 nextStep() { - return new Step13(clientDataJsonHash, attestation, allWarnings()); - } + @Override + public Step9 nextStep() { + return new Step9(clientDataJsonHash, attestation(), allWarnings()); } - @Value - class Step13 implements Step { - private final ByteArray clientDataJsonHash; - private final AttestationObject attestation; - private final List prevWarnings; + public AttestationObject attestation() { + return response.getResponse().getAttestation(); + } + } + + @Value + class Step9 implements Step { + private final ByteArray clientDataJsonHash; + private final AttestationObject attestation; + private final List prevWarnings; + + @Override + public void validate() { + assure( + Crypto.sha256(rpId) + .equals(response.getResponse().getAttestation().getAuthenticatorData().getRpIdHash()), + "Wrong RP ID hash."); + } - @Override - public void validate() {} + @Override + public Step10 nextStep() { + return new Step10(clientDataJsonHash, attestation, allWarnings()); + } + } + + @Value + class Step10 implements Step { + private final ByteArray clientDataJsonHash; + private final AttestationObject attestation; + private final List prevWarnings; + + @Override + public void validate() { + assure( + response.getResponse().getParsedAuthenticatorData().getFlags().UP, + "User Presence is required."); + } - @Override - public Step14 nextStep() { - return new Step14(clientDataJsonHash, attestation, attestationStatementVerifier(), allWarnings()); - } + @Override + public Step11 nextStep() { + return new Step11(clientDataJsonHash, attestation, allWarnings()); + } + } + + @Value + class Step11 implements Step { + private final ByteArray clientDataJsonHash; + private final AttestationObject attestation; + private final List prevWarnings; + + @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."); + } + } - public String format() { - return attestation.getFormat(); - } + @Override + public Step12 nextStep() { + return new Step12(clientDataJsonHash, attestation, allWarnings()); + } + } + + @Value + class Step12 implements Step { + private final ByteArray clientDataJsonHash; + private final AttestationObject attestation; + private final List prevWarnings; + + @Override + public void validate() { + if (!allowUnrequestedExtensions) { + ExtensionsValidation.validate(request.getExtensions(), response); + } + } - public Optional attestationStatementVerifier() { - switch (format()) { - case "fido-u2f": - return Optional.of(new FidoU2fAttestationStatementVerifier()); - case "none": - return Optional.of(new NoneAttestationStatementVerifier()); - case "packed": - return Optional.of(new PackedAttestationStatementVerifier()); - case "android-safetynet": - return Optional.of(new AndroidSafetynetAttestationStatementVerifier()); - default: - return Optional.empty(); - } - } + @Override + public List getWarnings() { + try { + ExtensionsValidation.validate(request.getExtensions(), response); + return Collections.emptyList(); + } catch (Exception e) { + return Collections.singletonList(e.getMessage()); + } } - @Value - class Step14 implements Step { - private final ByteArray clientDataJsonHash; - private final AttestationObject attestation; - private final Optional attestationStatementVerifier; - private final List prevWarnings; + @Override + public Step13 nextStep() { + return new Step13(clientDataJsonHash, attestation, allWarnings()); + } + } - @Override - public void validate() { - attestationStatementVerifier.ifPresent(verifier -> { - assure( - verifier.verifyAttestationSignature(attestation, clientDataJsonHash), - "Invalid attestation signature." - ); - }); + @Value + class Step13 implements Step { + private final ByteArray clientDataJsonHash; + private final AttestationObject attestation; + private final List prevWarnings; - assure(attestationType() != null, "Failed to determine attestation type"); - } + @Override + public void validate() {} - @Override - public Step15 nextStep() { - return new Step15(attestation, attestationType(), attestationTrustPath(), allWarnings()); - } + @Override + public Step14 nextStep() { + return new Step14( + clientDataJsonHash, attestation, attestationStatementVerifier(), allWarnings()); + } - public AttestationType attestationType() { - try { - if (attestationStatementVerifier.isPresent()) { - return attestationStatementVerifier.get().getAttestationType(attestation); - } else { - switch (attestation.getFormat()) { - case "android-key": - // TODO delete this once android-key attestation verification is implemented - return AttestationType.BASIC; - case "tpm": - // TODO delete this once tpm attestation verification is implemented - if (attestation.getAttestationStatement().has("x5c")) { - return AttestationType.ATTESTATION_CA; - } else { - return AttestationType.ECDAA; - } - default: - return AttestationType.UNKNOWN; - } - } - } catch (IOException | CoseException | CertificateException e) { - throw new IllegalArgumentException("Failed to resolve attestation type.", e); - } - } + public String format() { + return attestation.getFormat(); + } - public Optional> attestationTrustPath() { - if (attestationStatementVerifier.isPresent()) { - AttestationStatementVerifier verifier = attestationStatementVerifier.get(); - if (verifier instanceof X5cAttestationStatementVerifier) { - try { - return ((X5cAttestationStatementVerifier) verifier).getAttestationTrustPath(attestation); - } catch (CertificateException e) { - throw new IllegalArgumentException("Failed to resolve attestation trust path.", e); - } - } else { - return Optional.empty(); - } - } else { - return Optional.empty(); - } - } + public Optional attestationStatementVerifier() { + switch (format()) { + case "fido-u2f": + return Optional.of(new FidoU2fAttestationStatementVerifier()); + case "none": + return Optional.of(new NoneAttestationStatementVerifier()); + case "packed": + return Optional.of(new PackedAttestationStatementVerifier()); + case "android-safetynet": + return Optional.of(new AndroidSafetynetAttestationStatementVerifier()); + case "apple": + return Optional.of(new AppleAttestationStatementVerifier()); + default: + return Optional.empty(); + } } + } + + @Value + class Step14 implements Step { + private final ByteArray clientDataJsonHash; + private final AttestationObject attestation; + private final Optional attestationStatementVerifier; + private final List prevWarnings; + + @Override + public void validate() { + attestationStatementVerifier.ifPresent( + verifier -> { + assure( + verifier.verifyAttestationSignature(attestation, clientDataJsonHash), + "Invalid attestation signature."); + }); - @Value - class Step15 implements Step { - private final AttestationObject attestation; - private final AttestationType attestationType; - private final Optional> attestationTrustPath; - private final List prevWarnings; + assure(attestationType() != null, "Failed to determine attestation type"); + } - @Override - public void validate() { - } + @Override + public Step15 nextStep() { + return new Step15(attestation, attestationType(), attestationTrustPath(), allWarnings()); + } - @Override - public Step16 nextStep() { - return new Step16(attestation, attestationType, attestationTrustPath, trustResolver(), allWarnings()); - } + public AttestationType attestationType() { + try { + if (attestationStatementVerifier.isPresent()) { + return attestationStatementVerifier.get().getAttestationType(attestation); + } else { + switch (attestation.getFormat()) { + case "android-key": + // TODO delete this once android-key attestation verification is implemented + return AttestationType.BASIC; + case "tpm": + // TODO delete this once tpm attestation verification is implemented + if (attestation.getAttestationStatement().has("x5c")) { + return AttestationType.ATTESTATION_CA; + } else { + return AttestationType.ECDAA; + } + default: + return AttestationType.UNKNOWN; + } + } + } catch (IOException | CoseException | CertificateException e) { + throw new IllegalArgumentException("Failed to resolve attestation type.", e); + } + } - public Optional trustResolver() { - switch (attestationType) { - case NONE: - case SELF_ATTESTATION: - case UNKNOWN: - return Optional.empty(); - - case ATTESTATION_CA: - case BASIC: - switch (attestation.getFormat()) { - case "android-key": - case "android-safetynet": - case "fido-u2f": - case "packed": - case "tpm": - return metadataService.map(KnownX509TrustAnchorsTrustResolver::new); - default: - throw new UnsupportedOperationException(String.format( - "Attestation type %s is not supported for attestation statement format \"%s\".", - attestationType, attestation.getFormat() - )); - } - - default: - throw new UnsupportedOperationException("Attestation type not implemented: " + attestationType); - } - } + public Optional> attestationTrustPath() { + if (attestationStatementVerifier.isPresent()) { + AttestationStatementVerifier verifier = attestationStatementVerifier.get(); + if (verifier instanceof X5cAttestationStatementVerifier) { + try { + return ((X5cAttestationStatementVerifier) verifier) + .getAttestationTrustPath(attestation); + } catch (CertificateException e) { + throw new IllegalArgumentException("Failed to resolve attestation trust path.", e); + } + } else { + return Optional.empty(); + } + } else { + return Optional.empty(); + } + } + } + + @Value + class Step15 implements Step { + private final AttestationObject attestation; + private final AttestationType attestationType; + private final Optional> attestationTrustPath; + private final List prevWarnings; + + @Override + public void validate() {} + + @Override + public Step16 nextStep() { + return new Step16( + attestation, attestationType, attestationTrustPath, trustResolver(), allWarnings()); } - @Value - class Step16 implements Step { - private final AttestationObject attestation; - private final AttestationType attestationType; - private final Optional> attestationTrustPath; - private final Optional trustResolver; - private final List prevWarnings; + public Optional trustResolver() { + switch (attestationType) { + case NONE: + case SELF_ATTESTATION: + case UNKNOWN: + return Optional.empty(); + + case ANONYMIZATION_CA: + case ATTESTATION_CA: + case BASIC: + switch (attestation.getFormat()) { + case "android-key": + case "android-safetynet": + case "apple": + case "fido-u2f": + case "packed": + case "tpm": + return metadataService.map(KnownX509TrustAnchorsTrustResolver::new); + default: + throw new UnsupportedOperationException( + String.format( + "Attestation type %s is not supported for attestation statement format \"%s\".", + attestationType, attestation.getFormat())); + } + + default: + throw new UnsupportedOperationException( + "Attestation type not implemented: " + attestationType); + } + } + } + + @Value + class Step16 implements Step { + private final AttestationObject attestation; + private final AttestationType attestationType; + private final Optional> attestationTrustPath; + private final Optional trustResolver; + private final List prevWarnings; + + @Override + public void validate() { + assure( + trustResolver.isPresent() || allowUntrustedAttestation, + "Failed to obtain attestation trust anchors."); + + switch (attestationType) { + case SELF_ATTESTATION: + assure(allowUntrustedAttestation, "Self attestation is not allowed."); + break; + + case ANONYMIZATION_CA: + case ATTESTATION_CA: + case BASIC: + assure( + allowUntrustedAttestation || attestationTrusted(), + "Failed to derive trust for attestation key."); + break; + + case NONE: + assure(allowUntrustedAttestation, "No attestation is not allowed."); + break; + + case UNKNOWN: + assure( + allowUntrustedAttestation, "Unknown attestation statement formats are not allowed."); + break; + + default: + throw new UnsupportedOperationException( + "Attestation type not implemented: " + attestationType); + } + } - @Override - public void validate() { - assure( - trustResolver.isPresent() || allowUntrustedAttestation, - "Failed to obtain attestation trust anchors." - ); - - switch (attestationType) { - case SELF_ATTESTATION: - assure(allowUntrustedAttestation, "Self attestation is not allowed."); - break; - - case ATTESTATION_CA: - case BASIC: - assure(allowUntrustedAttestation || attestationTrusted(), "Failed to derive trust for attestation key."); - break; - - case NONE: - assure(allowUntrustedAttestation, "No attestation is not allowed."); - break; - - case UNKNOWN: - assure(allowUntrustedAttestation, "Unknown attestation statement formats are not allowed."); - break; - - default: - throw new UnsupportedOperationException("Attestation type not implemented: " + attestationType); - } - } + @Override + public Step17 nextStep() { + return new Step17( + attestationType, attestationMetadata(), attestationTrusted(), allWarnings()); + } - @Override - public Step17 nextStep() { - return new Step17(attestationType, attestationMetadata(), attestationTrusted(), allWarnings()); - } + public boolean attestationTrusted() { + switch (attestationType) { + case NONE: + case SELF_ATTESTATION: + case UNKNOWN: + return false; + + case ANONYMIZATION_CA: + case ATTESTATION_CA: + case BASIC: + return attestationMetadata().filter(Attestation::isTrusted).isPresent(); + default: + throw new UnsupportedOperationException( + "Attestation type not implemented: " + attestationType); + } + } - public boolean attestationTrusted() { - switch (attestationType) { - case NONE: - case SELF_ATTESTATION: - case UNKNOWN: - return false; - - case ATTESTATION_CA: - case BASIC: - return attestationMetadata().filter(Attestation::isTrusted).isPresent(); - default: - throw new UnsupportedOperationException("Attestation type not implemented: " + attestationType); + public Optional attestationMetadata() { + return trustResolver.flatMap( + tr -> { + try { + return Optional.of( + tr.resolveTrustAnchor(attestationTrustPath.orElseGet(Collections::emptyList))); + } catch (CertificateEncodingException e) { + log.debug("Failed to resolve trust anchor for attestation: {}", attestation, e); + return Optional.empty(); } - } - - public Optional attestationMetadata() { - return trustResolver.flatMap(tr -> { - try { - return Optional.of(tr.resolveTrustAnchor(attestationTrustPath.orElseGet(Collections::emptyList))); - } catch (CertificateEncodingException e) { - log.debug("Failed to resolve trust anchor for attestation: {}", attestation, e); - return Optional.empty(); - } - }); - } + }); + } - @Override - public List getWarnings() { - return trustResolver.map(tr -> { + @Override + public List getWarnings() { + return trustResolver + .map( + tr -> { try { - tr.resolveTrustAnchor(attestationTrustPath.orElseGet(Collections::emptyList)); - return Collections.emptyList(); + tr.resolveTrustAnchor(attestationTrustPath.orElseGet(Collections::emptyList)); + return Collections.emptyList(); } catch (CertificateEncodingException e) { - return Collections.singletonList("Failed to resolve trust anchor: " + e); + return Collections.singletonList("Failed to resolve trust anchor: " + e); } - }).orElseGet(Collections::emptyList); - } + }) + .orElseGet(Collections::emptyList); } - - @Value - class Step17 implements Step { - private final AttestationType attestationType; - private final Optional attestationMetadata; - private final boolean attestationTrusted; - private final List prevWarnings; - - @Override - public void validate() { - assure(credentialRepository.lookupAll(response.getId()).isEmpty(), "Credential ID is already registered: %s", response.getId()); - } - - @Override - public Step18 nextStep() { - return new Step18(attestationType, attestationMetadata, attestationTrusted, allWarnings()); - } + } + + @Value + class Step17 implements Step { + private final AttestationType attestationType; + private final Optional attestationMetadata; + private final boolean attestationTrusted; + private final List prevWarnings; + + @Override + public void validate() { + assure( + credentialRepository.lookupAll(response.getId()).isEmpty(), + "Credential ID is already registered: %s", + response.getId()); } - @Value - class Step18 implements Step { - private final AttestationType attestationType; - private final Optional attestationMetadata; - private final boolean attestationTrusted; - private final List prevWarnings; - - @Override - public void validate() { - } - - @Override - public Step19 nextStep() { - return new Step19(attestationType, attestationMetadata, attestationTrusted, allWarnings()); - } + @Override + public Step18 nextStep() { + return new Step18(attestationType, attestationMetadata, attestationTrusted, allWarnings()); } + } - @Value - class Step19 implements Step { - private final AttestationType attestationType; - private final Optional attestationMetadata; - private final boolean attestationTrusted; - private final List prevWarnings; + @Value + class Step18 implements Step { + private final AttestationType attestationType; + private final Optional attestationMetadata; + private final boolean attestationTrusted; + private final List prevWarnings; - @Override - public void validate() { - } + @Override + public void validate() {} - @Override - public CustomLastStep nextStep() { - return new CustomLastStep(attestationType, attestationMetadata, attestationTrusted, allWarnings()); - } + @Override + public Step19 nextStep() { + return new Step19(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; - private final List prevWarnings; - - @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 - public Finished nextStep() { - return new Finished(attestationType, attestationMetadata, attestationTrusted, allWarnings()); - } + } + + @Value + 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; + private final List prevWarnings; + + @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); + } } - @Value - class Finished implements Step { - private final AttestationType attestationType; - private final Optional attestationMetadata; - private final boolean attestationTrusted; - private final List prevWarnings; - - - @Override - public void validate() { /* No-op */ } - - @Override - public Finished nextStep() { - return this; - } + @Override + public Finished nextStep() { + return new Finished(attestationType, attestationMetadata, attestationTrusted, allWarnings()); + } + } + + @Value + class Finished implements Step { + private final AttestationType attestationType; + private final Optional attestationMetadata; + private final boolean attestationTrusted; + private final List prevWarnings; + + @Override + public void validate() { + /* No-op */ + } - @Override - public Optional result() { - return Optional.of(RegistrationResult.builder() - .keyId(keyId()) - .attestationTrusted(attestationTrusted) - .attestationType(attestationType) - .publicKeyCose(response.getResponse().getAttestation().getAuthenticatorData().getAttestedCredentialData().get().getCredentialPublicKey()) - .attestationMetadata(attestationMetadata) - .warnings(allWarnings()) - .build() - ); - } + @Override + public Finished nextStep() { + return this; + } - private PublicKeyCredentialDescriptor keyId() { - return PublicKeyCredentialDescriptor.builder() - .id(response.getId()) - .type(response.getType()) - .build(); - } + @Override + public Optional result() { + return Optional.of( + RegistrationResult.builder() + .keyId(keyId()) + .attestationTrusted(attestationTrusted) + .attestationType(attestationType) + .publicKeyCose( + response + .getResponse() + .getAttestation() + .getAuthenticatorData() + .getAttestedCredentialData() + .get() + .getCredentialPublicKey()) + .attestationMetadata(attestationMetadata) + .warnings(allWarnings()) + .build()); } + private PublicKeyCredentialDescriptor keyId() { + return PublicKeyCredentialDescriptor.builder() + .id(response.getId()) + .type(response.getType()) + .build(); + } + } } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/KnownX509TrustAnchorsTrustResolver.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/KnownX509TrustAnchorsTrustResolver.java index 9b1a2a3a5..407be8dc3 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/KnownX509TrustAnchorsTrustResolver.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/KnownX509TrustAnchorsTrustResolver.java @@ -32,17 +32,15 @@ import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; - @Slf4j @AllArgsConstructor final class KnownX509TrustAnchorsTrustResolver implements AttestationTrustResolver { - private final MetadataService metadataService; - - @Override - public Attestation resolveTrustAnchor(List certificateChain) throws CertificateEncodingException { - return metadataService.getAttestation(certificateChain); - } + private final MetadataService metadataService; + @Override + public Attestation resolveTrustAnchor(List certificateChain) + throws CertificateEncodingException { + return metadataService.getAttestation(certificateChain); + } } - diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/NoneAttestationStatementVerifier.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/NoneAttestationStatementVerifier.java index 202eb1bbd..fb87b12ba 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/NoneAttestationStatementVerifier.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/NoneAttestationStatementVerifier.java @@ -28,17 +28,16 @@ import com.yubico.webauthn.data.AttestationType; import com.yubico.webauthn.data.ByteArray; - final class NoneAttestationStatementVerifier implements AttestationStatementVerifier { - @Override - public AttestationType getAttestationType(AttestationObject attestation) { - return AttestationType.NONE; - } - - @Override - public boolean verifyAttestationSignature(AttestationObject attestationObject, ByteArray clientDataJsonHash) { - return true; - } + @Override + public AttestationType getAttestationType(AttestationObject attestation) { + return AttestationType.NONE; + } + @Override + public boolean verifyAttestationSignature( + AttestationObject attestationObject, ByteArray clientDataJsonHash) { + return true; + } } 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 index 576c962fa..d2fe7bbe5 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/OriginMatcher.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/OriginMatcher.java @@ -10,65 +10,65 @@ @UtilityClass class OriginMatcher { - static boolean isAllowed( - String origin, - Set allowedOrigins, - boolean allowPort, - boolean allowSubdomain - ) { - log.trace("isAllowed({}, {}, {}, {})", origin, allowedOrigins, allowPort, allowSubdomain); + 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; + 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)) { + return allowedOrigins.stream() + .anyMatch( + allowedOriginString -> { + if (allowedOriginString.equals(origin)) { log.debug("Exact match: {} == {}", origin, allowedOriginString); return true; - } else if (originUrl != null && (allowPort || allowSubdomain)) { + } else if (originUrl != null && (allowPort || allowSubdomain)) { final URL allowedOrigin; try { - allowedOrigin = new URL(allowedOriginString); + allowedOrigin = new URL(allowedOriginString); } catch (MalformedURLException e) { - log.error("Allowed origin is not a valid URL; skipping port/subdomain matching: {}", allowedOriginString); - return false; + 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); + final boolean domainAccepted = + isDomainAccepted(allowSubdomain, allowedOrigin, originUrl); log.debug("portAccepted: {}, domainAccepted: {}", portAccepted, domainAccepted); return portAccepted && domainAccepted; - } else { + } 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 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(); + 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); - } + 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 4b20cc938..8cf0ef490 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 @@ -24,10 +24,6 @@ package com.yubico.webauthn; -import javax.naming.InvalidNameException; -import javax.naming.ldap.LdapName; -import javax.naming.ldap.Rdn; - import COSE.CoseException; import com.fasterxml.jackson.databind.JsonNode; import com.upokecenter.cbor.CBORObject; @@ -53,246 +49,305 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; +import javax.naming.InvalidNameException; +import javax.naming.ldap.LdapName; +import javax.naming.ldap.Rdn; import lombok.extern.slf4j.Slf4j; import lombok.val; - @Slf4j -final class PackedAttestationStatementVerifier implements AttestationStatementVerifier, X5cAttestationStatementVerifier { - - @Override - public AttestationType getAttestationType(AttestationObject attestation) { - if (attestation.getAttestationStatement().hasNonNull("x5c")) { - return AttestationType.BASIC; - } else if (attestation.getAttestationStatement().hasNonNull("ecdaaKeyId")) { - return AttestationType.ECDAA; - } else { - return AttestationType.SELF_ATTESTATION; - } +final class PackedAttestationStatementVerifier + implements AttestationStatementVerifier, X5cAttestationStatementVerifier { + + @Override + public AttestationType getAttestationType(AttestationObject attestation) { + if (attestation.getAttestationStatement().hasNonNull("x5c")) { + return AttestationType.BASIC; + } else if (attestation.getAttestationStatement().hasNonNull("ecdaaKeyId")) { + return AttestationType.ECDAA; + } else { + return AttestationType.SELF_ATTESTATION; + } + } + + @Override + public boolean verifyAttestationSignature( + AttestationObject attestationObject, ByteArray clientDataJsonHash) { + val signatureNode = attestationObject.getAttestationStatement().get("sig"); + + if (signatureNode == null || !signatureNode.isBinary()) { + throw new IllegalArgumentException("attStmt.sig must be set to a binary value."); } - @Override - public boolean verifyAttestationSignature(AttestationObject attestationObject, ByteArray clientDataJsonHash) { - val signatureNode = attestationObject.getAttestationStatement().get("sig"); - - if (signatureNode == null || !signatureNode.isBinary()) { - throw new IllegalArgumentException("attStmt.sig must be set to a binary value."); - } - - if (attestationObject.getAttestationStatement().has("x5c")) { - return verifyX5cSignature(attestationObject, clientDataJsonHash); - } else if (attestationObject.getAttestationStatement().has("ecdaaKeyId")) { - return verifyEcdaaSignature(attestationObject, clientDataJsonHash); - } else { - return verifySelfAttestationSignature(attestationObject, clientDataJsonHash); - } + if (attestationObject.getAttestationStatement().has("x5c")) { + return verifyX5cSignature(attestationObject, clientDataJsonHash); + } else if (attestationObject.getAttestationStatement().has("ecdaaKeyId")) { + return verifyEcdaaSignature(attestationObject, clientDataJsonHash); + } else { + return verifySelfAttestationSignature(attestationObject, clientDataJsonHash); + } + } + + private boolean verifyEcdaaSignature( + AttestationObject attestationObject, ByteArray clientDataJsonHash) { + throw new UnsupportedOperationException( + "ECDAA signature verification is not (yet) implemented."); + } + + private boolean verifySelfAttestationSignature( + AttestationObject attestationObject, ByteArray clientDataJsonHash) { + final PublicKey pubkey; + try { + pubkey = + WebAuthnCodecs.importCosePublicKey( + attestationObject + .getAuthenticatorData() + .getAttestedCredentialData() + .get() + .getCredentialPublicKey()); + } catch (IOException | CoseException | InvalidKeySpecException e) { + throw ExceptionUtil.wrapAndLog( + log, + String.format( + "Failed to parse public key from attestation data %s", + attestationObject.getAuthenticatorData().getAttestedCredentialData()), + e); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); } - private boolean verifyEcdaaSignature(AttestationObject attestationObject, ByteArray clientDataJsonHash) { - throw new UnsupportedOperationException("ECDAA signature verification is not (yet) implemented."); + final Long keyAlgId = + CBORObject.DecodeFromBytes( + attestationObject + .getAuthenticatorData() + .getAttestedCredentialData() + .get() + .getCredentialPublicKey() + .getBytes()) + .get(CBORObject.FromObject(3)) + .AsInt64(); + final COSEAlgorithmIdentifier keyAlg = + COSEAlgorithmIdentifier.fromId(keyAlgId) + .orElseThrow( + () -> + new IllegalArgumentException( + "Unsupported COSE algorithm identifier: " + keyAlgId)); + + final Long sigAlgId = attestationObject.getAttestationStatement().get("alg").asLong(); + final COSEAlgorithmIdentifier sigAlg = + COSEAlgorithmIdentifier.fromId(sigAlgId) + .orElseThrow( + () -> + new IllegalArgumentException( + "Unsupported COSE algorithm identifier: " + sigAlgId)); + + if (!Objects.equals(keyAlg, sigAlg)) { + throw new IllegalArgumentException( + String.format( + "Key algorithm and signature algorithm must be equal, was: Key: %s, Sig: %s", + keyAlg, sigAlg)); } - private boolean verifySelfAttestationSignature(AttestationObject attestationObject, ByteArray clientDataJsonHash) { - final PublicKey pubkey; - try { - pubkey = WebAuthnCodecs.importCosePublicKey( - attestationObject.getAuthenticatorData().getAttestedCredentialData().get().getCredentialPublicKey() - ); - } catch (IOException | CoseException | InvalidKeySpecException e) { - throw ExceptionUtil.wrapAndLog( - log, - String.format("Failed to parse public key from attestation data %s", attestationObject.getAuthenticatorData().getAttestedCredentialData()), - e - ); - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException(e); - } - - final Long keyAlgId = CBORObject.DecodeFromBytes(attestationObject.getAuthenticatorData().getAttestedCredentialData().get().getCredentialPublicKey().getBytes()) - .get(CBORObject.FromObject(3)) - .AsInt64(); - final COSEAlgorithmIdentifier keyAlg = COSEAlgorithmIdentifier.fromId(keyAlgId) - .orElseThrow(() -> new IllegalArgumentException("Unsupported COSE algorithm identifier: " + keyAlgId)); - - final Long sigAlgId = attestationObject.getAttestationStatement().get("alg").asLong(); - final COSEAlgorithmIdentifier sigAlg = COSEAlgorithmIdentifier.fromId(sigAlgId) - .orElseThrow(() -> new IllegalArgumentException("Unsupported COSE algorithm identifier: " + sigAlgId)); - - if (!Objects.equals(keyAlg, sigAlg)) { - throw new IllegalArgumentException(String.format( - "Key algorithm and signature algorithm must be equal, was: Key: %s, Sig: %s", keyAlg, sigAlg)); - } - - ByteArray signedData = attestationObject.getAuthenticatorData().getBytes().concat(clientDataJsonHash); - ByteArray signature; - try { - signature = new ByteArray(attestationObject.getAttestationStatement().get("sig").binaryValue()); - } catch (IOException e) { - throw ExceptionUtil.wrapAndLog(log, ".binaryValue() of \"sig\" failed", e); - } - - return Crypto.verifySignature(pubkey, signedData, signature, keyAlg); + ByteArray signedData = + attestationObject.getAuthenticatorData().getBytes().concat(clientDataJsonHash); + ByteArray signature; + try { + signature = + new ByteArray(attestationObject.getAttestationStatement().get("sig").binaryValue()); + } catch (IOException e) { + throw ExceptionUtil.wrapAndLog(log, ".binaryValue() of \"sig\" failed", e); } - private boolean verifyX5cSignature(AttestationObject attestationObject, ByteArray clientDataHash) { - final Optional attestationCert; - try { - attestationCert = getX5cAttestationCertificate(attestationObject); - } catch (CertificateException e) { - throw ExceptionUtil.wrapAndLog( - log, - String.format("Failed to parse X.509 certificate from attestation object: %s", attestationObject), - e - ); - } - return attestationCert.map(attestationCertificate -> { - JsonNode signatureNode = attestationObject.getAttestationStatement().get("sig"); - if (signatureNode == null) { - throw new IllegalArgumentException("Packed attestation statement must have field \"sig\"."); - } - - if (signatureNode.isBinary()) { + return Crypto.verifySignature(pubkey, signedData, signature, keyAlg); + } + + private boolean verifyX5cSignature( + AttestationObject attestationObject, ByteArray clientDataHash) { + final Optional attestationCert; + try { + attestationCert = getX5cAttestationCertificate(attestationObject); + } catch (CertificateException e) { + throw ExceptionUtil.wrapAndLog( + log, + String.format( + "Failed to parse X.509 certificate from attestation object: %s", attestationObject), + e); + } + return attestationCert + .map( + attestationCertificate -> { + JsonNode signatureNode = attestationObject.getAttestationStatement().get("sig"); + if (signatureNode == null) { + throw new IllegalArgumentException( + "Packed attestation statement must have field \"sig\"."); + } + + if (signatureNode.isBinary()) { ByteArray signature; try { - signature = new ByteArray(signatureNode.binaryValue()); + signature = new ByteArray(signatureNode.binaryValue()); } catch (IOException e) { - throw ExceptionUtil.wrapAndLog(log, "signatureNode.isBinary() was true but signatureNode.binaryValue() failed", e); + 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\"."); + throw new IllegalArgumentException( + "Packed attestation statement must have field \"alg\"."); } - ExceptionUtil.assure(algNode.isIntegralNumber(), "Field \"alg\" in packed attestation statement must be a COSEAlgorithmIdentifier."); + 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)); + final COSEAlgorithmIdentifier sigAlg = + COSEAlgorithmIdentifier.fromId(sigAlgId) + .orElseThrow( + () -> + new IllegalArgumentException( + "Unsupported COSE algorithm identifier: " + sigAlgId)); - ByteArray signedData = attestationObject.getAuthenticatorData().getBytes().concat(clientDataHash); + ByteArray signedData = + attestationObject.getAuthenticatorData().getBytes().concat(clientDataHash); final String signatureAlgorithmName = WebAuthnCodecs.getJavaAlgorithmName(sigAlg); Signature signatureVerifier; try { - signatureVerifier = Crypto.getSignature(signatureAlgorithmName); + signatureVerifier = Crypto.getSignature(signatureAlgorithmName); } catch (NoSuchAlgorithmException e) { - throw ExceptionUtil.wrapAndLog(log, "Failed to get a Signature instance for " + signatureAlgorithmName, e); + throw ExceptionUtil.wrapAndLog( + log, "Failed to get a Signature instance for " + signatureAlgorithmName, e); } try { - signatureVerifier.initVerify(attestationCertificate.getPublicKey()); + signatureVerifier.initVerify(attestationCertificate.getPublicKey()); } catch (InvalidKeyException e) { - throw ExceptionUtil.wrapAndLog(log, "Attestation key is invalid: " + attestationCertificate, e); + throw ExceptionUtil.wrapAndLog( + log, "Attestation key is invalid: " + attestationCertificate, e); } try { - signatureVerifier.update(signedData.getBytes()); + signatureVerifier.update(signedData.getBytes()); } catch (SignatureException e) { - throw ExceptionUtil.wrapAndLog(log, "Signature object in invalid state: " + signatureVerifier, e); + throw ExceptionUtil.wrapAndLog( + log, "Signature object in invalid state: " + signatureVerifier, e); } try { - return (signatureVerifier.verify(signature.getBytes()) - && verifyX5cRequirements(attestationCertificate, attestationObject.getAuthenticatorData().getAttestedCredentialData().get().getAaguid()) - ); + return (signatureVerifier.verify(signature.getBytes()) + && verifyX5cRequirements( + attestationCertificate, + attestationObject + .getAuthenticatorData() + .getAttestedCredentialData() + .get() + .getAaguid())); } catch (SignatureException e) { - throw ExceptionUtil.wrapAndLog(log, "Failed to verify signature: " + attestationObject, e); + throw ExceptionUtil.wrapAndLog( + log, "Failed to verify signature: " + attestationObject, e); } - } else { - throw new IllegalArgumentException("Field \"sig\" in packed attestation statement must be a binary value."); - } - }).orElseThrow(() -> new IllegalArgumentException( - "If \"x5c\" property is present in \"packed\" attestation format it must be an array containing at least one DER encoded X.509 cerficicate.")); + } else { + throw new IllegalArgumentException( + "Field \"sig\" in packed attestation statement must be a binary value."); + } + }) + .orElseThrow( + () -> + new IllegalArgumentException( + "If \"x5c\" property is present in \"packed\" attestation format it must be an array containing at least one DER encoded X.509 cerficicate.")); + } + + private Optional getDnField(String field, X509Certificate cert) { + final LdapName ldap; + try { + ldap = new LdapName(cert.getSubjectX500Principal().getName()); + } catch (InvalidNameException e) { + throw ExceptionUtil.wrapAndLog( + log, + "X500Principal name was not accepted as an LdapName: " + + cert.getSubjectX500Principal().getName(), + e); } - - private Optional getDnField(String field, X509Certificate cert) { - final LdapName ldap; - try { - ldap = new LdapName(cert.getSubjectX500Principal().getName()); - } catch (InvalidNameException e) { - throw ExceptionUtil.wrapAndLog(log, "X500Principal name was not accepted as an LdapName: " + cert.getSubjectX500Principal().getName(), e); - } - return ldap.getRdns().stream() - .filter(rdn -> Objects.equals(rdn.getType(), field)) - .findAny() - .map(Rdn::getValue); + return ldap.getRdns().stream() + .filter(rdn -> Objects.equals(rdn.getType(), field)) + .findAny() + .map(Rdn::getValue); + } + + public boolean verifyX5cRequirements(X509Certificate cert, ByteArray aaguid) { + if (cert.getVersion() != 3) { + throw new IllegalArgumentException( + String.format( + "Wrong attestation certificate X509 version: %s, expected: 3", cert.getVersion())); } - public boolean verifyX5cRequirements(X509Certificate cert, ByteArray aaguid) { - if (cert.getVersion() != 3) { - throw new IllegalArgumentException(String.format("Wrong attestation certificate X509 version: %s, expected: 3", cert.getVersion())); - } - - final String ouValue = "Authenticator Attestation"; - final String idFidoGenCeAaguid = "1.3.6.1.4.1.45724.1.1.4"; - final Set countries = CollectionUtil.immutableSet(new HashSet<>(Arrays.asList(Locale.getISOCountries()))); - - ExceptionUtil.assure( - getDnField("C", cert).filter(countries::contains).isPresent(), - "Invalid attestation certificate country code: %s", - getDnField("C", cert) - ); - - ExceptionUtil.assure( - getDnField("O", cert).filter(o -> !((String) o).isEmpty()).isPresent(), - "Organization (O) field of attestation certificate DN must be present." - ); - - ExceptionUtil.assure( - getDnField("OU", cert).filter(ouValue::equals).isPresent(), - "Organization Unit (OU) field of attestation certificate DN must be exactly \"%s\", was: %s", - ouValue, getDnField("OU", cert) - ); - - Optional.ofNullable(cert.getExtensionValue(idFidoGenCeAaguid)) - .map(ext -> new ByteArray(parseAaguid(ext))) - .ifPresent((ByteArray value) -> { - ExceptionUtil.assure( - value.equals(aaguid), - "X.509 extension %s (id-fido-gen-ce-aaguid) is present but does not match the authenticator AAGUID.", - idFidoGenCeAaguid - ); - - ExceptionUtil.assure( - ! - cert.getCriticalExtensionOIDs().contains(idFidoGenCeAaguid), - "X.509 extension %s (id-fido-gen-ce-aaguid) must not be marked critical.", - idFidoGenCeAaguid - ); + final String ouValue = "Authenticator Attestation"; + final String idFidoGenCeAaguid = "1.3.6.1.4.1.45724.1.1.4"; + final Set countries = + CollectionUtil.immutableSet(new HashSet<>(Arrays.asList(Locale.getISOCountries()))); + + ExceptionUtil.assure( + getDnField("C", cert).filter(countries::contains).isPresent(), + "Invalid attestation certificate country code: %s", + getDnField("C", cert)); + + ExceptionUtil.assure( + getDnField("O", cert).filter(o -> !((String) o).isEmpty()).isPresent(), + "Organization (O) field of attestation certificate DN must be present."); + + ExceptionUtil.assure( + getDnField("OU", cert).filter(ouValue::equals).isPresent(), + "Organization Unit (OU) field of attestation certificate DN must be exactly \"%s\", was: %s", + ouValue, + getDnField("OU", cert)); + + Optional.ofNullable(cert.getExtensionValue(idFidoGenCeAaguid)) + .map(ext -> new ByteArray(parseAaguid(ext))) + .ifPresent( + (ByteArray value) -> { + ExceptionUtil.assure( + value.equals(aaguid), + "X.509 extension %s (id-fido-gen-ce-aaguid) is present but does not match the authenticator AAGUID.", + idFidoGenCeAaguid); + + ExceptionUtil.assure( + !cert.getCriticalExtensionOIDs().contains(idFidoGenCeAaguid), + "X.509 extension %s (id-fido-gen-ce-aaguid) must not be marked critical.", + idFidoGenCeAaguid); }); - ExceptionUtil.assure( - cert.getBasicConstraints() == -1, - "Attestation certificate must not be a CA certificate." - ); - - return true; + ExceptionUtil.assure( + cert.getBasicConstraints() == -1, "Attestation certificate must not be a CA certificate."); + + return true; + } + + /** + * Parses an AAGUID into bytes. Refer to Packed + * Attestation Statement Certificate Requirements on the W3C web site for details of the ASN.1 + * structure that this method parses. + * + * @param bytes the bytes making up value of the extension + * @return the bytes of the AAGUID + */ + private byte[] parseAaguid(byte[] bytes) { + + if (bytes != null && bytes.length == 20) { + ByteBuffer buffer = ByteBuffer.wrap(bytes); + + if (buffer.get() == (byte) 0x04 + && buffer.get() == (byte) 0x12 + && buffer.get() == (byte) 0x04 + && buffer.get() == (byte) 0x10) { + byte[] aaguidBytes = new byte[16]; + buffer.get(aaguidBytes); + + return aaguidBytes; + } } - /** - * Parses an AAGUID into bytes. Refer to Packed Attestation Statement - * Certificate Requirements on the W3C web site for details of the ASN.1 structure that this method parses. - * - * @param bytes the bytes making up value of the extension - * @return the bytes of the AAGUID - */ - private byte[] parseAaguid(byte[] bytes) { - - if (bytes != null && bytes.length == 20) - { - ByteBuffer buffer = ByteBuffer.wrap(bytes); - - if (buffer.get() == (byte) 0x04 && - buffer.get() == (byte) 0x12 && - buffer.get() == (byte) 0x04 && - buffer.get() == (byte) 0x10) - { - byte[] aaguidBytes = new byte[16]; - buffer.get(aaguidBytes); - - return aaguidBytes; - } - } - - throw new IllegalArgumentException( - "X.509 extension 1.3.6.1.4.1.45724.1.1.4 (id-fido-gen-ce-aaguid) is not valid."); - } + throw new IllegalArgumentException( + "X.509 extension 1.3.6.1.4.1.45724.1.1.4 (id-fido-gen-ce-aaguid) is not valid."); + } } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/RegisteredCredential.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/RegisteredCredential.java index 9503cab5d..de4fcad38 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/RegisteredCredential.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/RegisteredCredential.java @@ -36,108 +36,99 @@ import lombok.NonNull; import lombok.Value; - /** * An abstraction of a credential registered to a particular user. * - *

- * Instances of this class are not expected to be long-lived, and the library only needs to read them, never write them. - * You may at your discretion store them directly in your database, or assemble them from other components. - *

+ *

Instances of this class are not expected to be long-lived, and the library only needs to read + * them, never write them. You may at your discretion store them directly in your database, or + * assemble them from other components. */ @Value @Builder(toBuilder = true) public final class RegisteredCredential { - /** - * The credential ID of the - * credential. - * - * @see Credential ID - * @see RegistrationResult#getKeyId() - * @see PublicKeyCredentialDescriptor#getId() - */ - @NonNull - private final ByteArray credentialId; + /** + * The credential ID + * of the credential. + * + * @see Credential ID + * @see RegistrationResult#getKeyId() + * @see PublicKeyCredentialDescriptor#getId() + */ + @NonNull private final ByteArray credentialId; - /** - * The user handle of the user the - * credential is registered to. - * - * @see User Handle - * @see UserIdentity#getId() - */ - @NonNull - private final ByteArray userHandle; + /** + * The user handle of + * the user the credential is registered to. + * + * @see User Handle + * @see UserIdentity#getId() + */ + @NonNull private final ByteArray userHandle; - /** - * The credential public key encoded in COSE_Key format, as defined in Section 7 of RFC 8152. - * - *

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

- * - * @see AttestedCredentialData#getCredentialPublicKey() - * @see RegistrationResult#getPublicKeyCose() - */ - @NonNull - private final ByteArray publicKeyCose; + /** + * The credential public key encoded in COSE_Key format, as defined in Section 7 of RFC 8152. + * + *

This is used to verify the {@link AuthenticatorAssertionResponse#getSignature() signature} + * in authentication assertions. + * + * @see AttestedCredentialData#getCredentialPublicKey() + * @see RegistrationResult#getPublicKeyCose() + */ + @NonNull private final ByteArray publicKeyCose; - /** - * The stored signature count of the - * credential. - * - *

- * This is used to validate the {@link AuthenticatorData#getSignatureCounter() signature counter} in authentication - * assertions. - *

- * - * @see §6.1. Authenticator - * Data - * @see AuthenticatorData#getSignatureCounter() - * @see AssertionResult#getSignatureCount() - */ - @Builder.Default - private final long signatureCount = 0; + /** + * The stored signature + * count of the credential. + * + *

This is used to validate the {@link AuthenticatorData#getSignatureCounter() signature + * counter} in authentication assertions. + * + * @see §6.1. + * Authenticator Data + * @see AuthenticatorData#getSignatureCounter() + * @see AssertionResult#getSignatureCount() + */ + @Builder.Default private final long signatureCount = 0; - @JsonCreator - private RegisteredCredential( - @NonNull @JsonProperty("credentialId") ByteArray credentialId, - @NonNull @JsonProperty("userHandle") ByteArray userHandle, - @NonNull @JsonProperty("publicKeyCose") ByteArray publicKeyCose, - @JsonProperty("signatureCount") long signatureCount - ) { - this.credentialId = credentialId; - this.userHandle = userHandle; - this.publicKeyCose = publicKeyCose; - this.signatureCount = signatureCount; - } + @JsonCreator + private RegisteredCredential( + @NonNull @JsonProperty("credentialId") ByteArray credentialId, + @NonNull @JsonProperty("userHandle") ByteArray userHandle, + @NonNull @JsonProperty("publicKeyCose") ByteArray publicKeyCose, + @JsonProperty("signatureCount") long signatureCount) { + this.credentialId = credentialId; + this.userHandle = userHandle; + this.publicKeyCose = publicKeyCose; + this.signatureCount = signatureCount; + } - public static RegisteredCredentialBuilder.MandatoryStages builder() { - return new RegisteredCredentialBuilder.MandatoryStages(); - } + public static RegisteredCredentialBuilder.MandatoryStages builder() { + return new RegisteredCredentialBuilder.MandatoryStages(); + } + + public static class RegisteredCredentialBuilder { + public static class MandatoryStages { + private RegisteredCredentialBuilder builder = new RegisteredCredentialBuilder(); - public static class RegisteredCredentialBuilder { - public static class MandatoryStages { - private RegisteredCredentialBuilder builder = new RegisteredCredentialBuilder(); - public Step2 credentialId(ByteArray credentialId) { - builder.credentialId(credentialId); - return new Step2(); - } - public class Step2 { - public Step3 userHandle(ByteArray userHandle) { - builder.userHandle(userHandle); - return new Step3(); - } - } - public class Step3 { - public RegisteredCredentialBuilder publicKeyCose(ByteArray publicKeyCose) { - return builder.publicKeyCose(publicKeyCose); - } - } + public Step2 credentialId(ByteArray credentialId) { + builder.credentialId(credentialId); + return new Step2(); + } + + public class Step2 { + public Step3 userHandle(ByteArray userHandle) { + builder.userHandle(userHandle); + return new Step3(); } - } + } + public class Step3 { + public RegisteredCredentialBuilder publicKeyCose(ByteArray publicKeyCose) { + return builder.publicKeyCose(publicKeyCose); + } + } + } + } } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/RegistrationResult.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/RegistrationResult.java index 69a2660dc..fd127fa79 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/RegistrationResult.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/RegistrationResult.java @@ -39,148 +39,133 @@ import lombok.NonNull; import lombok.Value; - -/** - * The result of a call to {@link RelyingParty#finishRegistration(FinishRegistrationOptions)}. - */ +/** The result of a call to {@link RelyingParty#finishRegistration(FinishRegistrationOptions)}. */ @Value @Builder(toBuilder = true) public class RegistrationResult { - /** - * The credential ID of the created - * credential. - * - * @see Credential ID - * @see PublicKeyCredential#getId() - */ - @NonNull - private final PublicKeyCredentialDescriptor keyId; - - /** - * true if and only if the attestation signature was successfully linked to a trusted attestation - * root. - * - *

- * You can ignore this if authenticator attestation is not relevant to your application. - *

- */ - private final boolean attestationTrusted; - - /** - * The attestation type §6.4.3. - * Attestation Types that was used for the created credential. - * - *

- * You can ignore this if authenticator attestation is not relevant to your application. - *

- * - * @see §6.4.3. Attestation - * Types - */ - @NonNull - private final AttestationType attestationType; - - /** - * The public key of the created credential. - * - *

- * This is used in {@link RelyingParty#finishAssertion(FinishAssertionOptions)} to verify the authentication - * signatures. - *

- * - * @see RegisteredCredential#getPublicKeyCose() - */ - @NonNull - private final ByteArray publicKeyCose; - - /** - * Zero or more human-readable messages about non-critical issues. - */ - @NonNull - @Builder.Default - private final List warnings = Collections.emptyList(); - - /** - * Additional information about the authenticator, identified based on the attestation certificate. - * - *

- * This will be absent unless you set a {@link com.yubico.webauthn.RelyingParty.RelyingPartyBuilder#metadataService(Optional) - * metadataService} in {@link RelyingParty}. - *

- * - * @see §6.4. Attestation - * @see com.yubico.webauthn.RelyingParty.RelyingPartyBuilder#metadataService(Optional) - */ - private final Attestation attestationMetadata; - - @JsonCreator - private RegistrationResult( - @NonNull @JsonProperty("keyId") PublicKeyCredentialDescriptor keyId, - @JsonProperty("attestationTrusted") boolean attestationTrusted, - @NonNull @JsonProperty("attestationType") AttestationType attestationType, - @NonNull @JsonProperty("publicKeyCose") ByteArray publicKeyCose, - @NonNull @JsonProperty("warnings") List warnings, - @JsonProperty("attestationMetadata") Attestation attestationMetadata - ) { - this.keyId = keyId; - this.attestationTrusted = attestationTrusted; - this.attestationType = attestationType; - this.publicKeyCose = publicKeyCose; - this.warnings = CollectionUtil.immutableList(warnings); - this.attestationMetadata = attestationMetadata; - } - - public Optional getAttestationMetadata() { - return Optional.ofNullable(attestationMetadata); - } - - static RegistrationResultBuilder.MandatoryStages builder() { - return new RegistrationResultBuilder.MandatoryStages(); - } - - static class RegistrationResultBuilder { - static class MandatoryStages { - private RegistrationResultBuilder builder = new RegistrationResultBuilder(); - - Step2 keyId(PublicKeyCredentialDescriptor keyId) { - builder.keyId(keyId); - return new Step2(); - } - - class Step2 { - Step3 attestationTrusted(boolean attestationTrusted) { - builder.attestationTrusted(attestationTrusted); - return new Step3(); - } - } - - class Step3 { - Step4 attestationType(AttestationType attestationType) { - builder.attestationType(attestationType); - return new Step4(); - } - } - - class Step4 { - RegistrationResultBuilder publicKeyCose(ByteArray publicKeyCose) { - return builder.publicKeyCose(publicKeyCose); - } - } + /** + * The credential ID + * of the created credential. + * + * @see Credential ID + * @see PublicKeyCredential#getId() + */ + @NonNull private final PublicKeyCredentialDescriptor keyId; + + /** + * true if and only if the attestation signature was successfully linked to a trusted + * attestation root. + * + *

You can ignore this if authenticator attestation is not relevant to your application. + */ + private final boolean attestationTrusted; + + /** + * The attestation type §6.4.3. + * Attestation Types that was used for the created credential. + * + *

You can ignore this if authenticator attestation is not relevant to your application. + * + * @see §6.4.3. + * Attestation Types + */ + @NonNull private final AttestationType attestationType; + + /** + * The public key of the created credential. + * + *

This is used in {@link RelyingParty#finishAssertion(FinishAssertionOptions)} to verify the + * authentication signatures. + * + * @see RegisteredCredential#getPublicKeyCose() + */ + @NonNull private final ByteArray publicKeyCose; + + /** Zero or more human-readable messages about non-critical issues. */ + @NonNull @Builder.Default private final List warnings = Collections.emptyList(); + + /** + * Additional information about the authenticator, identified based on the attestation + * certificate. + * + *

This will be absent unless you set a {@link + * com.yubico.webauthn.RelyingParty.RelyingPartyBuilder#metadataService(Optional) metadataService} + * in {@link RelyingParty}. + * + * @see §6.4. + * Attestation + * @see com.yubico.webauthn.RelyingParty.RelyingPartyBuilder#metadataService(Optional) + */ + private final Attestation attestationMetadata; + + @JsonCreator + private RegistrationResult( + @NonNull @JsonProperty("keyId") PublicKeyCredentialDescriptor keyId, + @JsonProperty("attestationTrusted") boolean attestationTrusted, + @NonNull @JsonProperty("attestationType") AttestationType attestationType, + @NonNull @JsonProperty("publicKeyCose") ByteArray publicKeyCose, + @NonNull @JsonProperty("warnings") List warnings, + @JsonProperty("attestationMetadata") Attestation attestationMetadata) { + this.keyId = keyId; + this.attestationTrusted = attestationTrusted; + this.attestationType = attestationType; + this.publicKeyCose = publicKeyCose; + this.warnings = CollectionUtil.immutableList(warnings); + this.attestationMetadata = attestationMetadata; + } + + public Optional getAttestationMetadata() { + return Optional.ofNullable(attestationMetadata); + } + + static RegistrationResultBuilder.MandatoryStages builder() { + return new RegistrationResultBuilder.MandatoryStages(); + } + + static class RegistrationResultBuilder { + static class MandatoryStages { + private RegistrationResultBuilder builder = new RegistrationResultBuilder(); + + Step2 keyId(PublicKeyCredentialDescriptor keyId) { + builder.keyId(keyId); + return new Step2(); + } + + class Step2 { + Step3 attestationTrusted(boolean attestationTrusted) { + builder.attestationTrusted(attestationTrusted); + return new Step3(); } + } - RegistrationResultBuilder attestationMetadata(@NonNull Optional attestationMetadata) { - this.attestationMetadata = attestationMetadata.orElse(null); - return this; + class Step3 { + Step4 attestationType(AttestationType attestationType) { + builder.attestationType(attestationType); + return new Step4(); } + } - /* - * Workaround, see: https://github.com/rzwitserloot/lombok/issues/2623#issuecomment-714816001 - * Consider reverting this workaround if Lombok fixes that issue. - */ - private RegistrationResultBuilder attestationMetadata(Attestation attestationMetadata) { - return this.attestationMetadata(Optional.ofNullable(attestationMetadata)); + class Step4 { + RegistrationResultBuilder publicKeyCose(ByteArray publicKeyCose) { + return builder.publicKeyCose(publicKeyCose); } + } } + RegistrationResultBuilder attestationMetadata( + @NonNull Optional attestationMetadata) { + this.attestationMetadata = attestationMetadata.orElse(null); + return this; + } + + /* + * Workaround, see: https://github.com/rzwitserloot/lombok/issues/2623#issuecomment-714816001 + * Consider reverting this workaround if Lombok fixes that issue. + */ + private RegistrationResultBuilder attestationMetadata(Attestation attestationMetadata) { + return this.attestationMetadata(Optional.ofNullable(attestationMetadata)); + } + } } 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 869c7b70f..6b6f300e7 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 @@ -46,10 +46,6 @@ import com.yubico.webauthn.exception.InvalidSignatureCountException; import com.yubico.webauthn.exception.RegistrationFailedException; import com.yubico.webauthn.extension.appid.AppId; -import lombok.Builder; -import lombok.NonNull; -import lombok.Value; -import lombok.extern.slf4j.Slf4j; import java.net.MalformedURLException; import java.net.URL; import java.security.SecureRandom; @@ -59,656 +55,609 @@ import java.util.List; import java.util.Optional; import java.util.Set; - +import lombok.Builder; +import lombok.NonNull; +import lombok.Value; +import lombok.extern.slf4j.Slf4j; /** - * Encapsulates the four basic Web Authentication operations - start/finish registration, start/finish authentication - - * along with overall operational settings for them. - *

- * This class has no mutable state. An instance of this class may therefore be thought of as a container for specialized - * versions (function closures) of these four operations rather than a stateful object. - *

+ * Encapsulates the four basic Web Authentication operations - start/finish registration, + * start/finish authentication - along with overall operational settings for them. + * + *

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

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

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

If {@link 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() + */ + @NonNull private final Set origins; + + /** + * An abstract database which can look up credentials, usernames and user handles from usernames, + * user handles and credential IDs. This is a required parameter. + * + *

This is used to look up: + * + *

    + *
  • the user handle for a user logging in via user name + *
  • the user name for a user logging in via user handle + *
  • the credential IDs to include in {@link + * PublicKeyCredentialCreationOptions#getExcludeCredentials()} + *
  • the credential IDs to include in {@link + * PublicKeyCredentialRequestOptions#getAllowCredentials()} + *
  • that the correct user owns the credential when verifying an assertion + *
  • the public key to use to verify an assertion + *
  • the stored signature counter when verifying an assertion + *
+ */ + @NonNull private final CredentialRepository credentialRepository; + + /** + * The extension input to set for the appid extension when initiating authentication + * operations. + * + *

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

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

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

If you set this, you may want to explicitly set {@link + * RelyingPartyBuilder#allowUntrustedAttestation(boolean) allowUntrustedAttestation} and {@link + * RelyingPartyBuilder#metadataService(MetadataService) metadataService} too. + * + *

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

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

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

The default is the following list: + * + *

    + *
  1. {@link com.yubico.webauthn.data.PublicKeyCredentialParameters#ES256 ES256} + *
  2. {@link com.yubico.webauthn.data.PublicKeyCredentialParameters#EdDSA EdDSA} + *
  3. {@link com.yubico.webauthn.data.PublicKeyCredentialParameters#RS256 RS256} + *
+ * + * @see PublicKeyCredentialCreationOptions#getAttestation() + * @see §6.4. + * Attestation + */ + @Builder.Default @NonNull + private final List preferredPubkeyParams = + Collections.unmodifiableList( + Arrays.asList( + PublicKeyCredentialParameters.ES256, + PublicKeyCredentialParameters.EdDSA, + 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"] + * + *

    + *
  • + *

    allowOriginSubdomain: false + *

    Accepted: + *

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

    Rejected: + *

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

    allowOriginSubdomain: true + *

    Accepted: + *

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

    Rejected: + *

      + *
    • https://example.org:8443 + *
    • https://acme.com + *
    + *
+ */ + @Builder.Default private final boolean allowOriginSubdomain = false; + + /** + * If true, {@link #finishRegistration(FinishRegistrationOptions) finishRegistration} + * and {@link #finishAssertion(FinishAssertionOptions) finishAssertion} will accept responses + * containing extension outputs for which there was no extension input. + * + *

The default is false. + * + * @see §9. WebAuthn + * Extensions + */ + @Builder.Default private final boolean allowUnrequestedExtensions = false; + + /** + * If false, {@link #finishRegistration(FinishRegistrationOptions) + * finishRegistration} will only allow registrations where the attestation signature can be linked + * to a trusted attestation root. This excludes self attestation and none attestation. + * + *

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

The default is true. + */ + @Builder.Default private final boolean allowUntrustedAttestation = true; + + /** + * If true, {@link #finishAssertion(FinishAssertionOptions) finishAssertion} will + * fail if the {@link AuthenticatorData#getSignatureCounter() signature counter value} in the + * response is not strictly greater than the {@link RegisteredCredential#getSignatureCount() + * stored signature counter value}. + * + *

The default is true. + */ + @Builder.Default private final boolean validateSignatureCounter = true; + + private RelyingParty( + @NonNull RelyingPartyIdentity identity, + Set origins, + @NonNull CredentialRepository credentialRepository, + @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 + ? 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); + } + } - /** - * The {@link RelyingPartyIdentity} that will be set as the {@link PublicKeyCredentialCreationOptions#getRp() rp} - * parameter when initiating registration operations, and which {@link AuthenticatorData#getRpIdHash()} will be - * compared against. This is a required parameter. - * - *

- * A successful registration or authentication operation requires {@link AuthenticatorData#getRpIdHash()} to exactly - * equal the SHA-256 hash of this member's {@link RelyingPartyIdentity#getId() id} member. Alternatively, it may - * instead equal the SHA-256 hash of {@link #getAppId() appId} if the latter is present. - *

- * - * @see #startRegistration(StartRegistrationOptions) - * @see PublicKeyCredentialCreationOptions - */ - @NonNull - private final RelyingPartyIdentity identity; + 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; + } + + private static ByteArray generateChallenge() { + byte[] bytes = new byte[32]; + random.nextBytes(bytes); + return new ByteArray(bytes); + } + + public PublicKeyCredentialCreationOptions startRegistration( + StartRegistrationOptions startRegistrationOptions) { + PublicKeyCredentialCreationOptionsBuilder builder = + PublicKeyCredentialCreationOptions.builder() + .rp(identity) + .user(startRegistrationOptions.getUser()) + .challenge(generateChallenge()) + .pubKeyCredParams(preferredPubkeyParams) + .excludeCredentials( + credentialRepository.getCredentialIdsForUsername( + startRegistrationOptions.getUser().getName())) + .authenticatorSelection(startRegistrationOptions.getAuthenticatorSelection()) + .extensions(startRegistrationOptions.getExtensions()) + .timeout(startRegistrationOptions.getTimeout()); + attestationConveyancePreference.ifPresent(builder::attestation); + return builder.build(); + } + + public RegistrationResult finishRegistration(FinishRegistrationOptions finishRegistrationOptions) + throws RegistrationFailedException { + try { + return _finishRegistration( + finishRegistrationOptions.getRequest(), + finishRegistrationOptions.getResponse(), + finishRegistrationOptions.getCallerTokenBindingId()) + .run(); + } catch (IllegalArgumentException e) { + throw new RegistrationFailedException(e); + } + } + + /** + * This method is NOT part of the public API. + * + *

This method is called internally by {@link #finishRegistration(FinishRegistrationOptions)}. + * It is a separate method to facilitate testing; users should call {@link + * #finishRegistration(FinishRegistrationOptions)} instead of this method. + */ + FinishRegistrationSteps _finishRegistration( + PublicKeyCredentialCreationOptions request, + PublicKeyCredential + response, + Optional callerTokenBindingId) { + return FinishRegistrationSteps.builder() + .request(request) + .response(response) + .callerTokenBindingId(callerTokenBindingId) + .credentialRepository(credentialRepository) + .origins(origins) + .rpId(identity.getId()) + .allowOriginPort(allowOriginPort) + .allowOriginSubdomain(allowOriginSubdomain) + .allowUnrequestedExtensions(allowUnrequestedExtensions) + .allowUntrustedAttestation(allowUntrustedAttestation) + .metadataService(metadataService) + .build(); + } + + public AssertionRequest startAssertion(StartAssertionOptions startAssertionOptions) { + PublicKeyCredentialRequestOptionsBuilder pkcro = + PublicKeyCredentialRequestOptions.builder() + .challenge(generateChallenge()) + .rpId(identity.getId()) + .allowCredentials( + startAssertionOptions + .getUsername() + .map( + un -> + new ArrayList<>(credentialRepository.getCredentialIdsForUsername(un)))) + .extensions(startAssertionOptions.getExtensions().toBuilder().appid(appId).build()) + .timeout(startAssertionOptions.getTimeout()); + + startAssertionOptions.getUserVerification().ifPresent(pkcro::userVerification); + + return AssertionRequest.builder() + .publicKeyCredentialRequestOptions(pkcro.build()) + .username(startAssertionOptions.getUsername()) + .build(); + } + + /** + * @throws InvalidSignatureCountException if {@link + * RelyingPartyBuilder#validateSignatureCounter(boolean) validateSignatureCounter} is + * true, the {@link AuthenticatorData#getSignatureCounter() signature count} in the + * response is less than or equal to the {@link RegisteredCredential#getSignatureCount() + * stored signature count}, and at least one of the signature count values is nonzero. + * @throws AssertionFailedException if validation fails for any other reason. + */ + public AssertionResult finishAssertion(FinishAssertionOptions finishAssertionOptions) + throws AssertionFailedException { + try { + return _finishAssertion( + finishAssertionOptions.getRequest(), + finishAssertionOptions.getResponse(), + finishAssertionOptions.getCallerTokenBindingId()) + .run(); + } catch (IllegalArgumentException e) { + throw new AssertionFailedException(e); + } + } + + /** + * This method is NOT part of the public API. + * + *

This method is called internally by {@link #finishAssertion(FinishAssertionOptions)}. It is + * a separate method to facilitate testing; users should call {@link + * #finishAssertion(FinishAssertionOptions)} instead of this method. + */ + FinishAssertionSteps _finishAssertion( + AssertionRequest request, + PublicKeyCredential response, + Optional callerTokenBindingId // = None.asJava + ) { + return FinishAssertionSteps.builder() + .request(request) + .response(response) + .callerTokenBindingId(callerTokenBindingId) + .origins(origins) + .rpId(identity.getId()) + .credentialRepository(credentialRepository) + .allowOriginPort(allowOriginPort) + .allowOriginSubdomain(allowOriginSubdomain) + .allowUnrequestedExtensions(allowUnrequestedExtensions) + .validateSignatureCounter(validateSignatureCounter) + .build(); + } + + public static RelyingPartyBuilder.MandatoryStages builder() { + return new RelyingPartyBuilder.MandatoryStages(); + } + + public static class RelyingPartyBuilder { + private @NonNull Optional appId = Optional.empty(); + private @NonNull Optional attestationConveyancePreference = + Optional.empty(); + private @NonNull Optional metadataService = Optional.empty(); + + public static class MandatoryStages { + private final RelyingPartyBuilder builder = new RelyingPartyBuilder(); + + /** + * {@link RelyingPartyBuilder#identity(RelyingPartyIdentity) identity} is a required + * parameter. + * + * @see RelyingPartyBuilder#identity(RelyingPartyIdentity) + */ + public Step2 identity(RelyingPartyIdentity identity) { + builder.identity(identity); + return new Step2(); + } + + public class Step2 { + /** + * {@link RelyingPartyBuilder#credentialRepository(CredentialRepository) + * credentialRepository} is a required parameter. + * + * @see RelyingPartyBuilder#credentialRepository(CredentialRepository) + */ + public RelyingPartyBuilder credentialRepository(CredentialRepository credentialRepository) { + return builder.credentialRepository(credentialRepository); + } + } + } /** - * The allowed origins that returned authenticator responses will be compared against. + * The extension input to set for the appid extension when initiating + * authentication operations. * - *

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

+ *

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

- * 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. - *

+ *

By default, this is not set. * - *

- * 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() - */ - @NonNull - private final Set origins; - - - /** - * An abstract database which can look up credentials, usernames and user handles from usernames, user handles and - * credential IDs. This is a required parameter. - * - *

- * This is used to look up: - *

- * - *
    - *
  • the user handle for a user logging in via user name
  • - *
  • the user name for a user logging in via user handle
  • - *
  • the credential IDs to include in {@link PublicKeyCredentialCreationOptions#getExcludeCredentials()}
  • - *
  • the credential IDs to include in {@link PublicKeyCredentialRequestOptions#getAllowCredentials()}
  • - *
  • that the correct user owns the credential when verifying an assertion
  • - *
  • the public key to use to verify an assertion
  • - *
  • the stored signature counter when verifying an assertion
  • - *
+ * @see AssertionExtensionInputs#getAppid() + * @see §10.1. + * FIDO AppID Extension (appid) */ - @NonNull - private final CredentialRepository credentialRepository; + public RelyingPartyBuilder appId(@NonNull Optional appId) { + this.appId = appId; + return this; + } /** - * The extension input to set for the appid extension when initiating authentication operations. + * The extension input to set for the appid extension when initiating + * authentication operations. * - *

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

+ *

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

- * By default, this is not set. - *

+ *

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

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

+ * The argument for the {@link PublicKeyCredentialCreationOptions#getAttestation() attestation} + * parameter in registration operations. * - *

- * If you set this, you may want to explicitly set - * {@link RelyingPartyBuilder#allowUntrustedAttestation(boolean) allowUntrustedAttestation} and - * {@link RelyingPartyBuilder#metadataService(MetadataService) metadataService} too. - *

+ *

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

- * By default, this is not set. - *

- * - * @see PublicKeyCredentialCreationOptions#getAttestation() - * @see §6.4. Attestation - */ - @NonNull - private final Optional attestationConveyancePreference; - - /** - * A {@link MetadataService} instance to use for looking up device attestation metadata. This matters only if {@link - * #getAttestationConveyancePreference()} is non-empty and not set to {@link AttestationConveyancePreference#NONE}. + *

If you set this, you may want to explicitly set {@link + * RelyingPartyBuilder#allowUntrustedAttestation(boolean) allowUntrustedAttestation} and {@link + * RelyingPartyBuilder#metadataService(MetadataService) metadataService} too. * - *

- * By default, this is not set. - *

+ *

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

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

+ *

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

- * The default is the following list: - *

+ *

If you set this, you may want to explicitly set {@link + * RelyingPartyBuilder#allowUntrustedAttestation(boolean) allowUntrustedAttestation} and {@link + * RelyingPartyBuilder#metadataService(MetadataService) metadataService} too. * - *

    - *
  1. {@link com.yubico.webauthn.data.PublicKeyCredentialParameters#ES256 ES256}
  2. - *
  3. {@link com.yubico.webauthn.data.PublicKeyCredentialParameters#EdDSA EdDSA}
  4. - *
  5. {@link com.yubico.webauthn.data.PublicKeyCredentialParameters#RS256 RS256}
  6. - *
+ *

By default, this is not set. * * @see PublicKeyCredentialCreationOptions#getAttestation() - * @see §6.4. Attestation - */ - @Builder.Default - @NonNull - private final List preferredPubkeyParams = Collections.unmodifiableList(Arrays.asList( - PublicKeyCredentialParameters.ES256, - PublicKeyCredentialParameters.EdDSA, - 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"] - *

- * - *
    - *
  • - *

    allowOriginSubdomain: false

    - * - *

    Accepted:

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

    Rejected:

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

    allowOriginSubdomain: true

    - * - *

    Accepted:

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

    Rejected:

    - *
      - *
    • https://example.org:8443
    • - *
    • https://acme.com
    • - *
    - *
  • - *
+ * @see §6.4. + * Attestation */ - @Builder.Default - private final boolean allowOriginSubdomain = false; + public RelyingPartyBuilder attestationConveyancePreference( + @NonNull AttestationConveyancePreference attestationConveyancePreference) { + return this.attestationConveyancePreference(Optional.of(attestationConveyancePreference)); + } /** - * If true, {@link #finishRegistration(FinishRegistrationOptions) finishRegistration} and {@link - * #finishAssertion(FinishAssertionOptions) finishAssertion} will accept responses containing extension outputs for - * which there was no extension input. + * A {@link MetadataService} instance to use for looking up device attestation metadata. This + * matters only if {@link #getAttestationConveyancePreference()} is non-empty and not set to + * {@link AttestationConveyancePreference#NONE}. * - *

- * The default is false. - *

+ *

By default, this is not set. * - * @see §9. WebAuthn Extensions + * @see PublicKeyCredentialCreationOptions#getAttestation() + * @see §6.4. + * Attestation */ - @Builder.Default - private final boolean allowUnrequestedExtensions = false; + public RelyingPartyBuilder metadataService(@NonNull Optional metadataService) { + this.metadataService = metadataService; + return this; + } /** - * If false, {@link #finishRegistration(FinishRegistrationOptions) finishRegistration} will only allow - * registrations where the attestation signature can be linked to a trusted attestation root. This excludes self - * attestation and none attestation. - * - *

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

+ * A {@link MetadataService} instance to use for looking up device attestation metadata. This + * matters only if {@link #getAttestationConveyancePreference()} is non-empty and not set to + * {@link AttestationConveyancePreference#NONE}. * - *

- * The default is true. - *

- */ - @Builder.Default - private final boolean allowUntrustedAttestation = true; - - /** - * If true, {@link #finishAssertion(FinishAssertionOptions) finishAssertion} will fail if the {@link - * AuthenticatorData#getSignatureCounter() signature counter value} in the response is not strictly greater than the - * {@link RegisteredCredential#getSignatureCount() stored signature counter value}. + *

By default, this is not set. * - *

- * The default is true. - *

- */ - @Builder.Default - private final boolean validateSignatureCounter = true; - - private RelyingParty( - @NonNull RelyingPartyIdentity identity, - Set origins, - @NonNull CredentialRepository credentialRepository, - @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 ? 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; - } - - private static ByteArray generateChallenge() { - byte[] bytes = new byte[32]; - random.nextBytes(bytes); - return new ByteArray(bytes); - } - - public PublicKeyCredentialCreationOptions startRegistration(StartRegistrationOptions startRegistrationOptions) { - PublicKeyCredentialCreationOptionsBuilder builder = PublicKeyCredentialCreationOptions.builder() - .rp(identity) - .user(startRegistrationOptions.getUser()) - .challenge(generateChallenge()) - .pubKeyCredParams(preferredPubkeyParams) - .excludeCredentials( - credentialRepository.getCredentialIdsForUsername(startRegistrationOptions.getUser().getName()) - ) - .authenticatorSelection(startRegistrationOptions.getAuthenticatorSelection()) - .extensions(startRegistrationOptions.getExtensions()) - .timeout(startRegistrationOptions.getTimeout()) - ; - attestationConveyancePreference.ifPresent(builder::attestation); - return builder.build(); - } - - public RegistrationResult finishRegistration(FinishRegistrationOptions finishRegistrationOptions) throws RegistrationFailedException { - try { - return _finishRegistration(finishRegistrationOptions.getRequest(), finishRegistrationOptions.getResponse(), finishRegistrationOptions.getCallerTokenBindingId()).run(); - } catch (IllegalArgumentException e) { - throw new RegistrationFailedException(e); - } - } - - /** - * This method is NOT part of the public API. - *

- * This method is called internally by {@link #finishRegistration(FinishRegistrationOptions)}. It is a separate - * method to facilitate testing; users should call {@link #finishRegistration(FinishRegistrationOptions)} instead of - * this method. - */ - FinishRegistrationSteps _finishRegistration( - PublicKeyCredentialCreationOptions request, - PublicKeyCredential response, - Optional callerTokenBindingId - ) { - return FinishRegistrationSteps.builder() - .request(request) - .response(response) - .callerTokenBindingId(callerTokenBindingId) - .credentialRepository(credentialRepository) - .origins(origins) - .rpId(identity.getId()) - .allowOriginPort(allowOriginPort) - .allowOriginSubdomain(allowOriginSubdomain) - .allowUnrequestedExtensions(allowUnrequestedExtensions) - .allowUntrustedAttestation(allowUntrustedAttestation) - .metadataService(metadataService) - .build(); - } - - public AssertionRequest startAssertion(StartAssertionOptions startAssertionOptions) { - PublicKeyCredentialRequestOptionsBuilder pkcro = PublicKeyCredentialRequestOptions.builder() - .challenge(generateChallenge()) - .rpId(identity.getId()) - .allowCredentials( - startAssertionOptions.getUsername().map(un -> - new ArrayList<>(credentialRepository.getCredentialIdsForUsername(un))) - ) - .extensions( - startAssertionOptions.getExtensions() - .toBuilder() - .appid(appId) - .build() - ) - .timeout(startAssertionOptions.getTimeout()) - ; - - startAssertionOptions.getUserVerification().ifPresent(pkcro::userVerification); - - return AssertionRequest.builder() - .publicKeyCredentialRequestOptions( - pkcro.build() - ) - .username(startAssertionOptions.getUsername()) - .build(); - } - - /** - * @throws InvalidSignatureCountException - * if {@link RelyingPartyBuilder#validateSignatureCounter(boolean) validateSignatureCounter} is - * true, the {@link AuthenticatorData#getSignatureCounter() signature count} in the response is - * less than or equal to the {@link RegisteredCredential#getSignatureCount() stored signature count}, and at - * least one of the signature count values is nonzero. - * @throws AssertionFailedException - * if validation fails for any other reason. - */ - public AssertionResult finishAssertion(FinishAssertionOptions finishAssertionOptions) throws AssertionFailedException { - try { - return _finishAssertion(finishAssertionOptions.getRequest(), finishAssertionOptions.getResponse(), finishAssertionOptions.getCallerTokenBindingId()).run(); - } catch (IllegalArgumentException e) { - throw new AssertionFailedException(e); - } - } - - /** - * This method is NOT part of the public API. - *

- * This method is called internally by {@link #finishAssertion(FinishAssertionOptions)}. It is a separate method to - * facilitate testing; users should call {@link #finishAssertion(FinishAssertionOptions)} instead of this method. + * @see PublicKeyCredentialCreationOptions#getAttestation() + * @see §6.4. + * Attestation */ - FinishAssertionSteps _finishAssertion( - AssertionRequest request, - PublicKeyCredential response, - Optional callerTokenBindingId // = None.asJava - ) { - return FinishAssertionSteps.builder() - .request(request) - .response(response) - .callerTokenBindingId(callerTokenBindingId) - .origins(origins) - .rpId(identity.getId()) - .credentialRepository(credentialRepository) - .allowOriginPort(allowOriginPort) - .allowOriginSubdomain(allowOriginSubdomain) - .allowUnrequestedExtensions(allowUnrequestedExtensions) - .validateSignatureCounter(validateSignatureCounter) - .build(); - } - - public static RelyingPartyBuilder.MandatoryStages builder() { - return new RelyingPartyBuilder.MandatoryStages(); - } - - public static class RelyingPartyBuilder { - private @NonNull Optional appId = Optional.empty(); - private @NonNull Optional attestationConveyancePreference = Optional.empty(); - private @NonNull Optional metadataService = Optional.empty(); - - public static class MandatoryStages { - private final RelyingPartyBuilder builder = new RelyingPartyBuilder(); - - /** - * {@link RelyingPartyBuilder#identity(RelyingPartyIdentity) identity} is a required parameter. - * - * @see RelyingPartyBuilder#identity(RelyingPartyIdentity) - */ - public Step2 identity(RelyingPartyIdentity identity) { - builder.identity(identity); - return new Step2(); - } - - public class Step2 { - /** - * {@link RelyingPartyBuilder#credentialRepository(CredentialRepository) credentialRepository} is a - * required parameter. - * - * @see RelyingPartyBuilder#credentialRepository(CredentialRepository) - */ - public RelyingPartyBuilder credentialRepository(CredentialRepository credentialRepository) { - return builder.credentialRepository(credentialRepository); - } - } - } - - /** - * The extension input to set for the appid extension when initiating authentication operations. - * - *

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

- * - *

- * By default, this is not set. - *

- * - * @see AssertionExtensionInputs#getAppid() - * @see §10.1. FIDO AppID Extension - * (appid) - */ - public RelyingPartyBuilder appId(@NonNull Optional appId) { - this.appId = appId; - return this; - } - - /** - * The extension input to set for the appid extension when initiating authentication operations. - * - *

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

- * - *

- * By default, this is not set. - *

- * - * @see AssertionExtensionInputs#getAppid() - * @see §10.1. FIDO AppID Extension - * (appid) - */ - public RelyingPartyBuilder appId(@NonNull AppId appId) { - return this.appId(Optional.of(appId)); - } - - /** - * The argument for the {@link PublicKeyCredentialCreationOptions#getAttestation() attestation} parameter in - * registration operations. - * - *

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

- * - *

- * If you set this, you may want to explicitly set - * {@link RelyingPartyBuilder#allowUntrustedAttestation(boolean) allowUntrustedAttestation} and - * {@link RelyingPartyBuilder#metadataService(MetadataService) metadataService} too. - *

- * - *

- * By default, this is not set. - *

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

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

- * - *

- * If you set this, you may want to explicitly set - * {@link RelyingPartyBuilder#allowUntrustedAttestation(boolean) allowUntrustedAttestation} and - * {@link RelyingPartyBuilder#metadataService(MetadataService) metadataService} too. - *

- * - *

- * By default, this is not set. - *

- * - * @see PublicKeyCredentialCreationOptions#getAttestation() - * @see §6.4. Attestation - */ - public RelyingPartyBuilder attestationConveyancePreference(@NonNull AttestationConveyancePreference attestationConveyancePreference) { - return this.attestationConveyancePreference(Optional.of(attestationConveyancePreference)); - } - - /** - * A {@link MetadataService} instance to use for looking up device attestation metadata. This matters only if {@link - * #getAttestationConveyancePreference()} is non-empty and not set to {@link AttestationConveyancePreference#NONE}. - * - *

- * By default, this is not set. - *

- * - * @see PublicKeyCredentialCreationOptions#getAttestation() - * @see §6.4. Attestation - */ - public RelyingPartyBuilder metadataService(@NonNull Optional metadataService) { - this.metadataService = metadataService; - return this; - } - - /** - * A {@link MetadataService} instance to use for looking up device attestation metadata. This matters only if {@link - * #getAttestationConveyancePreference()} is non-empty and not set to {@link AttestationConveyancePreference#NONE}. - * - *

- * By default, this is not set. - *

- * - * @see PublicKeyCredentialCreationOptions#getAttestation() - * @see §6.4. Attestation - */ - public RelyingPartyBuilder metadataService(@NonNull MetadataService metadataService) { - return this.metadataService(Optional.of(metadataService)); - } + public RelyingPartyBuilder metadataService(@NonNull MetadataService metadataService) { + return this.metadataService(Optional.of(metadataService)); } + } } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/StartAssertionOptions.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/StartAssertionOptions.java index f89207d16..d24ee202a 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/StartAssertionOptions.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/StartAssertionOptions.java @@ -32,203 +32,188 @@ import lombok.NonNull; import lombok.Value; -/** - * Parameters for {@link RelyingParty#startAssertion(StartAssertionOptions)}. - */ +/** Parameters for {@link RelyingParty#startAssertion(StartAssertionOptions)}. */ @Value @Builder(toBuilder = true) public class StartAssertionOptions { + /** + * The username of the user to authenticate, if the user has already been identified. + * + *

If this is absent, that implies a first-factor authentication operation - meaning + * identification of the user is deferred until after receiving the response from the client. + * + *

The default is empty (absent). + * + * @see Client-side-resident + * credential + */ + private final String username; + + /** + * Extension inputs for this authentication operation. + * + *

If {@link RelyingParty#getAppId()} is set, {@link + * RelyingParty#startAssertion(StartAssertionOptions)} will overwrite any {@link + * AssertionExtensionInputs#getAppid() appId} extension input set herein. + * + *

The default specifies no extension inputs. + */ + @NonNull @Builder.Default + private final AssertionExtensionInputs extensions = AssertionExtensionInputs.builder().build(); + + /** + * The value for {@link PublicKeyCredentialRequestOptions#getUserVerification()} for this + * authentication operation. + * + *

The default is {@link UserVerificationRequirement#PREFERRED}. + */ + private final UserVerificationRequirement userVerification; + + /** + * The value for {@link PublicKeyCredentialRequestOptions#getTimeout()} for this authentication + * operation. + * + *

This library does not take the timeout into account in any way, other than passing it + * through to the {@link PublicKeyCredentialRequestOptions} so it can be used as an argument to + * navigator.credentials.get() on the client side. + * + *

The default is empty. + */ + private final Long timeout; + + /** + * The username of the user to authenticate, if the user has already been identified. + * + *

If this is absent, that implies a first-factor authentication operation - meaning + * identification of the user is deferred until after receiving the response from the client. + * + *

The default is empty (absent). + * + * @see Client-side-resident + * credential + */ + public Optional getUsername() { + return Optional.ofNullable(username); + } + + /** + * The value for {@link PublicKeyCredentialRequestOptions#getUserVerification()} for this + * authentication operation. + * + *

The default is {@link UserVerificationRequirement#PREFERRED}. + */ + public Optional getUserVerification() { + return Optional.ofNullable(userVerification); + } + + /** + * The value for {@link PublicKeyCredentialRequestOptions#getTimeout()} for this authentication + * operation. + * + *

This library does not take the timeout into account in any way, other than passing it + * through to the {@link PublicKeyCredentialRequestOptions} so it can be used as an argument to + * navigator.credentials.get() on the client side. + * + *

The default is empty. + */ + public Optional getTimeout() { + return Optional.ofNullable(timeout); + } + + public static class StartAssertionOptionsBuilder { + private String username = null; + private UserVerificationRequirement userVerification = null; + private Long timeout = null; + /** * The username of the user to authenticate, if the user has already been identified. - *

- * If this is absent, that implies a first-factor authentication operation - meaning identification of the user is - * deferred until after receiving the response from the client. - *

* - *

- * The default is empty (absent). - *

+ *

If this is absent, that implies a first-factor authentication operation - meaning + * identification of the user is deferred until after receiving the response from the client. * - * @see Client-side-resident - * credential - */ - private final String username; - - /** - * Extension inputs for this authentication operation. - *

- * If {@link RelyingParty#getAppId()} is set, {@link RelyingParty#startAssertion(StartAssertionOptions)} will - * overwrite any {@link AssertionExtensionInputs#getAppid() appId} extension input set herein. - *

+ *

The default is empty (absent). * - *

- * The default specifies no extension inputs. - *

+ * @see Client-side-resident + * credential */ - @NonNull - @Builder.Default - private final AssertionExtensionInputs extensions = AssertionExtensionInputs.builder().build(); + public StartAssertionOptionsBuilder username(@NonNull Optional username) { + this.username = username.orElse(null); + return this; + } /** - * The value for {@link PublicKeyCredentialRequestOptions#getUserVerification()} for this authentication operation. - *

- * The default is {@link UserVerificationRequirement#PREFERRED}. - *

+ * The username of the user to authenticate, if the user has already been identified. + * + *

If this is absent, that implies a first-factor authentication operation - meaning + * identification of the user is deferred until after receiving the response from the client. + * + *

The default is empty (absent). + * + * @see Client-side-resident + * credential */ - private final UserVerificationRequirement userVerification; + public StartAssertionOptionsBuilder username(@NonNull String username) { + return this.username(Optional.of(username)); + } /** - * The value for {@link PublicKeyCredentialRequestOptions#getTimeout()} for this authentication operation. - *

- * This library does not take the timeout into account in any way, other than passing it through to the {@link - * PublicKeyCredentialRequestOptions} so it can be used as an argument to - * navigator.credentials.get() on the client side. - *

- *

- * The default is empty. - *

+ * The value for {@link PublicKeyCredentialRequestOptions#getUserVerification()} for this + * authentication operation. + * + *

The default is {@link UserVerificationRequirement#PREFERRED}. */ - private final Long timeout; + public StartAssertionOptionsBuilder userVerification( + @NonNull Optional userVerification) { + this.userVerification = userVerification.orElse(null); + return this; + } /** - * The username of the user to authenticate, if the user has already been identified. - *

- * If this is absent, that implies a first-factor authentication operation - meaning identification of the user is - * deferred until after receiving the response from the client. - *

+ * The value for {@link PublicKeyCredentialRequestOptions#getUserVerification()} for this + * authentication operation. * - *

- * The default is empty (absent). - *

- * - * @see Client-side-resident - * credential + *

The default is {@link UserVerificationRequirement#PREFERRED}. */ - public Optional getUsername() { - return Optional.ofNullable(username); + public StartAssertionOptionsBuilder userVerification( + @NonNull UserVerificationRequirement userVerification) { + return this.userVerification(Optional.of(userVerification)); } /** - * The value for {@link PublicKeyCredentialRequestOptions#getUserVerification()} for this authentication operation. - *

- * The default is {@link UserVerificationRequirement#PREFERRED}. - *

+ * The value for {@link PublicKeyCredentialRequestOptions#getTimeout()} for this authentication + * operation. + * + *

This library does not take the timeout into account in any way, other than passing it + * through to the {@link PublicKeyCredentialRequestOptions} so it can be used as an argument to + * navigator.credentials.get() on the client side. + * + *

The default is empty. */ - public Optional getUserVerification() { - return Optional.ofNullable(userVerification); + public StartAssertionOptionsBuilder timeout(@NonNull Optional timeout) { + if (timeout.isPresent() && timeout.get() <= 0) { + throw new IllegalArgumentException("timeout must be positive, was: " + timeout.get()); + } + this.timeout = timeout.orElse(null); + return this; } /** - * The value for {@link PublicKeyCredentialRequestOptions#getTimeout()} for this authentication operation. - *

- * This library does not take the timeout into account in any way, other than passing it through to the {@link - * PublicKeyCredentialRequestOptions} so it can be used as an argument to + * The value for {@link PublicKeyCredentialRequestOptions#getTimeout()} for this authentication + * operation. + * + *

This library does not take the timeout into account in any way, other than passing it + * through to the {@link PublicKeyCredentialRequestOptions} so it can be used as an argument to * navigator.credentials.get() on the client side. - *

- *

- * The default is empty. - *

+ * + *

The default is empty. */ - public Optional getTimeout() { - return Optional.ofNullable(timeout); - } - - public static class StartAssertionOptionsBuilder { - private String username = null; - private UserVerificationRequirement userVerification = null; - private Long timeout = null; - - /** - * The username of the user to authenticate, if the user has already been identified. - *

- * If this is absent, that implies a first-factor authentication operation - meaning identification of the user is - * deferred until after receiving the response from the client. - *

- * - *

- * The default is empty (absent). - *

- * - * @see Client-side-resident - * credential - */ - public StartAssertionOptionsBuilder username(@NonNull Optional username) { - this.username = username.orElse(null); - return this; - } - - /** - * The username of the user to authenticate, if the user has already been identified. - *

- * If this is absent, that implies a first-factor authentication operation - meaning identification of the user is - * deferred until after receiving the response from the client. - *

- * - *

- * The default is empty (absent). - *

- * - * @see Client-side-resident - * credential - */ - public StartAssertionOptionsBuilder username(@NonNull String username) { - return this.username(Optional.of(username)); - } - - /** - * The value for {@link PublicKeyCredentialRequestOptions#getUserVerification()} for this authentication operation. - *

- * The default is {@link UserVerificationRequirement#PREFERRED}. - *

- */ - public StartAssertionOptionsBuilder userVerification(@NonNull Optional userVerification) { - this.userVerification = userVerification.orElse(null); - return this; - } - - /** - * The value for {@link PublicKeyCredentialRequestOptions#getUserVerification()} for this authentication operation. - *

- * The default is {@link UserVerificationRequirement#PREFERRED}. - *

- */ - public StartAssertionOptionsBuilder userVerification(@NonNull UserVerificationRequirement userVerification) { - return this.userVerification(Optional.of(userVerification)); - } - - /** - * The value for {@link PublicKeyCredentialRequestOptions#getTimeout()} for this authentication operation. - *

- * This library does not take the timeout into account in any way, other than passing it through to the {@link - * PublicKeyCredentialRequestOptions} so it can be used as an argument to - * navigator.credentials.get() on the client side. - *

- *

- * The default is empty. - *

- */ - public StartAssertionOptionsBuilder timeout(@NonNull Optional timeout) { - if (timeout.isPresent() && timeout.get() <= 0) { - throw new IllegalArgumentException("timeout must be positive, was: " + timeout.get()); - } - this.timeout = timeout.orElse(null); - return this; - } - - /** - * The value for {@link PublicKeyCredentialRequestOptions#getTimeout()} for this authentication operation. - *

- * This library does not take the timeout into account in any way, other than passing it through to the {@link - * PublicKeyCredentialRequestOptions} so it can be used as an argument to - * navigator.credentials.get() on the client side. - *

- *

- * The default is empty. - *

- */ - public StartAssertionOptionsBuilder timeout(long timeout) { - return this.timeout(Optional.of(timeout)); - } + public StartAssertionOptionsBuilder timeout(long timeout) { + return this.timeout(Optional.of(timeout)); } + } } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/StartRegistrationOptions.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/StartRegistrationOptions.java index 979992917..665f78b6e 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/StartRegistrationOptions.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/StartRegistrationOptions.java @@ -33,130 +33,122 @@ import lombok.NonNull; import lombok.Value; -/** - * Parameters for {@link RelyingParty#startRegistration(StartRegistrationOptions)}. - */ +/** Parameters for {@link RelyingParty#startRegistration(StartRegistrationOptions)}. */ @Value @Builder(toBuilder = true) public class StartRegistrationOptions { - /** - * Identifiers for the user creating a credential. - */ - @NonNull - private final UserIdentity user; + /** Identifiers for the user creating a credential. */ + @NonNull private final UserIdentity user; + + /** + * Constraints on what kind of authenticator the user is allowed to use to create the credential. + */ + private final AuthenticatorSelectionCriteria authenticatorSelection; + + /** Extension inputs for this registration operation. */ + @NonNull @Builder.Default + private final RegistrationExtensionInputs extensions = + RegistrationExtensionInputs.builder().build(); + + /** + * The value for {@link PublicKeyCredentialCreationOptions#getTimeout()} for this registration + * operation. + * + *

This library does not take the timeout into account in any way, other than passing it + * through to the {@link PublicKeyCredentialCreationOptions} so it can be used as an argument to + * navigator.credentials.create() on the client side. + * + *

The default is empty. + */ + private final Long timeout; + + /** + * Constraints on what kind of authenticator the user is allowed to use to create the credential. + */ + public Optional getAuthenticatorSelection() { + return Optional.ofNullable(authenticatorSelection); + } + + /** + * The value for {@link PublicKeyCredentialCreationOptions#getTimeout()} for this registration + * operation. + * + *

This library does not take the timeout into account in any way, other than passing it + * through to the {@link PublicKeyCredentialCreationOptions} so it can be used as an argument to + * navigator.credentials.create() on the client side. + * + *

The default is empty. + */ + public Optional getTimeout() { + return Optional.ofNullable(timeout); + } + + public static StartRegistrationOptionsBuilder.MandatoryStages builder() { + return new StartRegistrationOptionsBuilder.MandatoryStages(); + } + + public static class StartRegistrationOptionsBuilder { + private AuthenticatorSelectionCriteria authenticatorSelection = null; + private Long timeout = null; + + public static class MandatoryStages { + private final StartRegistrationOptionsBuilder builder = new StartRegistrationOptionsBuilder(); + + public StartRegistrationOptionsBuilder user(UserIdentity user) { + return builder.user(user); + } + } /** - * Constraints on what kind of authenticator the user is allowed to use to create the credential. + * Constraints on what kind of authenticator the user is allowed to use to create the + * credential. */ - private final AuthenticatorSelectionCriteria authenticatorSelection; + public StartRegistrationOptionsBuilder authenticatorSelection( + @NonNull Optional authenticatorSelection) { + return this.authenticatorSelection(authenticatorSelection.orElse(null)); + } /** - * Extension inputs for this registration operation. + * Constraints on what kind of authenticator the user is allowed to use to create the + * credential. */ - @NonNull - @Builder.Default - private final RegistrationExtensionInputs extensions = RegistrationExtensionInputs.builder().build(); + public StartRegistrationOptionsBuilder authenticatorSelection( + AuthenticatorSelectionCriteria authenticatorSelection) { + this.authenticatorSelection = authenticatorSelection; + return this; + } /** - * The value for {@link PublicKeyCredentialCreationOptions#getTimeout()} for this registration operation. - *

- * This library does not take the timeout into account in any way, other than passing it through to the {@link - * PublicKeyCredentialCreationOptions} so it can be used as an argument to + * The value for {@link PublicKeyCredentialCreationOptions#getTimeout()} for this registration + * operation. + * + *

This library does not take the timeout into account in any way, other than passing it + * through to the {@link PublicKeyCredentialCreationOptions} so it can be used as an argument to * navigator.credentials.create() on the client side. - *

- *

- * The default is empty. - *

+ * + *

The default is empty. */ - private final Long timeout; - - /** - * Constraints on what kind of authenticator the user is allowed to use to create the credential. - */ - public Optional getAuthenticatorSelection() { - return Optional.ofNullable(authenticatorSelection); + public StartRegistrationOptionsBuilder timeout(@NonNull Optional timeout) { + if (timeout.isPresent() && timeout.get() <= 0) { + throw new IllegalArgumentException("timeout must be positive, was: " + timeout.get()); + } + this.timeout = timeout.orElse(null); + return this; } /** - * The value for {@link PublicKeyCredentialCreationOptions#getTimeout()} for this registration operation. - *

- * This library does not take the timeout into account in any way, other than passing it through to the {@link - * PublicKeyCredentialCreationOptions} so it can be used as an argument to + * The value for {@link PublicKeyCredentialCreationOptions#getTimeout()} for this registration + * operation. + * + *

This library does not take the timeout into account in any way, other than passing it + * through to the {@link PublicKeyCredentialCreationOptions} so it can be used as an argument to * navigator.credentials.create() on the client side. - *

- *

- * The default is empty. - *

+ * + *

The default is empty. */ - public Optional getTimeout() { - return Optional.ofNullable(timeout); - } - - public static StartRegistrationOptionsBuilder.MandatoryStages builder() { - return new StartRegistrationOptionsBuilder.MandatoryStages(); + public StartRegistrationOptionsBuilder timeout(long timeout) { + return this.timeout(Optional.of(timeout)); } - - public static class StartRegistrationOptionsBuilder { - private AuthenticatorSelectionCriteria authenticatorSelection = null; - private Long timeout = null; - - public static class MandatoryStages { - private final StartRegistrationOptionsBuilder builder = new StartRegistrationOptionsBuilder(); - - public StartRegistrationOptionsBuilder user(UserIdentity user) { - return builder.user(user); - } - } - - /** - * Constraints on what kind of authenticator the user is allowed to use to create the credential. - */ - public StartRegistrationOptionsBuilder authenticatorSelection(@NonNull Optional authenticatorSelection) { - return this.authenticatorSelection(authenticatorSelection.orElse(null)); - } - - /** - * Constraints on what kind of authenticator the user is allowed to use to create the credential. - */ - public StartRegistrationOptionsBuilder authenticatorSelection(AuthenticatorSelectionCriteria authenticatorSelection) { - this.authenticatorSelection = authenticatorSelection; - return this; - } - - /** - * The value for {@link PublicKeyCredentialCreationOptions#getTimeout()} for this registration operation. - *

- * This library does not take the timeout into account in any way, other than passing it through to the {@link - * PublicKeyCredentialCreationOptions} so it can be used as an argument to - * navigator.credentials.create() on the client side. - *

- *

- * The default is empty. - *

- */ - public StartRegistrationOptionsBuilder timeout(@NonNull Optional timeout) { - if (timeout.isPresent() && timeout.get() <= 0) { - throw new IllegalArgumentException("timeout must be positive, was: " + timeout.get()); - } - this.timeout = timeout.orElse(null); - return this; - } - - /** - * The value for {@link PublicKeyCredentialCreationOptions#getTimeout()} for this registration operation. - *

- * This library does not take the timeout into account in any way, other than passing it through to the {@link - * PublicKeyCredentialCreationOptions} so it can be used as an argument to - * navigator.credentials.create() on the client side. - *

- *

- * The default is empty. - *

- */ - public StartRegistrationOptionsBuilder timeout(long timeout) { - return this.timeout(Optional.of(timeout)); - } - } - + } } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/TokenBindingValidator.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/TokenBindingValidator.java index 1e492e20b..bfaf097be 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/TokenBindingValidator.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/TokenBindingValidator.java @@ -28,38 +28,58 @@ import com.yubico.webauthn.data.TokenBindingInfo; import java.util.Optional; - final class TokenBindingValidator { - static boolean validate(Optional clientTokenBinding, Optional rpTokenBindingId) { - return rpTokenBindingId.map(rpToken -> - clientTokenBinding.map(tbi -> { - switch (tbi.getStatus()) { - case SUPPORTED: - - case PRESENT: - return tbi.getId().map(id -> { - if (id.equals(rpToken)) { - return true; - } else { - throw new IllegalArgumentException("Incorrect token binding ID."); - } - }).orElseThrow(() -> new IllegalArgumentException("Property \"id\" missing from \"tokenBinding\" object.")); - } - throw new RuntimeException("Unknown token binding status: " + tbi.getStatus()); - }).orElseThrow(() -> new IllegalArgumentException("Token binding ID set by RP but not by client.")) - ).orElseGet(() -> - clientTokenBinding.map(tbi -> { - switch (tbi.getStatus()) { - case SUPPORTED: - return true; + static boolean validate( + Optional clientTokenBinding, Optional rpTokenBindingId) { + return rpTokenBindingId + .map( + rpToken -> + clientTokenBinding + .map( + tbi -> { + switch (tbi.getStatus()) { + case SUPPORTED: - case PRESENT: - throw new IllegalArgumentException("Token binding ID set by client but not by RP."); - } - throw new RuntimeException("Unknown token binding status: " + tbi.getStatus()); - }).orElse(true) - ); - } + case PRESENT: + return tbi.getId() + .map( + id -> { + if (id.equals(rpToken)) { + return true; + } else { + throw new IllegalArgumentException( + "Incorrect token binding ID."); + } + }) + .orElseThrow( + () -> + new IllegalArgumentException( + "Property \"id\" missing from \"tokenBinding\" object.")); + } + throw new RuntimeException( + "Unknown token binding status: " + tbi.getStatus()); + }) + .orElseThrow( + () -> + new IllegalArgumentException( + "Token binding ID set by RP but not by client."))) + .orElseGet( + () -> + clientTokenBinding + .map( + tbi -> { + switch (tbi.getStatus()) { + case SUPPORTED: + return true; + case PRESENT: + throw new IllegalArgumentException( + "Token binding ID set by client but not by RP."); + } + throw new RuntimeException( + "Unknown token binding status: " + tbi.getStatus()); + }) + .orElse(true)); + } } 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 f918e6f4f..7aee50fa7 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 @@ -36,53 +36,47 @@ import java.security.cert.X509Certificate; import lombok.Value; -/** - * The register response produced by the token/key - */ +/** The register response produced by the token/key */ @Value class U2fRawRegisterResponse { - private static final byte REGISTRATION_SIGNED_RESERVED_BYTE_VALUE = (byte) 0x00; + private static final byte REGISTRATION_SIGNED_RESERVED_BYTE_VALUE = (byte) 0x00; - /** - * The (uncompressed) x,y-representation of a curve point on the P-256 - * NIST elliptic curve. - */ - private final ByteArray userPublicKey; + /** The (uncompressed) x,y-representation of a curve point on the P-256 NIST elliptic curve. */ + private final ByteArray userPublicKey; - /** - * A handle that allows the U2F token to identify the generated key pair. - */ - private final ByteArray keyHandle; - private final X509Certificate attestationCertificate; + /** A handle that allows the U2F token to identify the generated key pair. */ + private final ByteArray keyHandle; - /** - * A ECDSA signature (on P-256) - */ - private final ByteArray signature; + private final X509Certificate attestationCertificate; - U2fRawRegisterResponse(ByteArray userPublicKey, - ByteArray keyHandle, - X509Certificate attestationCertificate, - ByteArray signature) { - this.userPublicKey = userPublicKey; - this.keyHandle = keyHandle; - this.attestationCertificate = attestationCertificate; - this.signature = signature; - } + /** A ECDSA signature (on P-256) */ + private final ByteArray signature; - boolean verifySignature(ByteArray appIdHash, ByteArray clientDataHash) { - ByteArray signedBytes = packBytesToSign(appIdHash, clientDataHash, keyHandle, userPublicKey); - return Crypto.verifySignature(attestationCertificate, signedBytes, signature, COSEAlgorithmIdentifier.ES256); - } + U2fRawRegisterResponse( + ByteArray userPublicKey, + ByteArray keyHandle, + X509Certificate attestationCertificate, + ByteArray signature) { + this.userPublicKey = userPublicKey; + this.keyHandle = keyHandle; + this.attestationCertificate = attestationCertificate; + this.signature = signature; + } - private static ByteArray packBytesToSign(ByteArray appIdHash, ByteArray clientDataHash, ByteArray keyHandle, ByteArray userPublicKey) { - ByteArrayDataOutput encoded = ByteStreams.newDataOutput(); - encoded.write(REGISTRATION_SIGNED_RESERVED_BYTE_VALUE); - encoded.write(appIdHash.getBytes()); - encoded.write(clientDataHash.getBytes()); - encoded.write(keyHandle.getBytes()); - encoded.write(userPublicKey.getBytes()); - return new ByteArray(encoded.toByteArray()); - } + boolean verifySignature(ByteArray appIdHash, ByteArray clientDataHash) { + ByteArray signedBytes = packBytesToSign(appIdHash, clientDataHash, keyHandle, userPublicKey); + return Crypto.verifySignature( + attestationCertificate, signedBytes, signature, COSEAlgorithmIdentifier.ES256); + } + private static ByteArray packBytesToSign( + ByteArray appIdHash, ByteArray clientDataHash, ByteArray keyHandle, ByteArray userPublicKey) { + ByteArrayDataOutput encoded = ByteStreams.newDataOutput(); + encoded.write(REGISTRATION_SIGNED_RESERVED_BYTE_VALUE); + encoded.write(appIdHash.getBytes()); + encoded.write(clientDataHash.getBytes()); + encoded.write(keyHandle.getBytes()); + encoded.write(userPublicKey.getBytes()); + return new ByteArray(encoded.toByteArray()); + } } 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 a11758516..6a784759a 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 @@ -42,99 +42,106 @@ import java.util.Arrays; import java.util.Optional; - final class WebAuthnCodecs { - private static final ByteArray ED25519_CURVE_OID = new ByteArray(new byte[]{0x30, 0x05, 0x06, 0x03, 0x2B, 0x65, 0x70}); - - 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)]; - byte[] yPadding = new byte[Math.max(0, 32 - y.length)]; - - Arrays.fill(xPadding, (byte) 0); - Arrays.fill(yPadding, (byte) 0); - - return new ByteArray(Bytes.concat( - new byte[]{ 0x04 }, - Bytes.concat( - xPadding, - Arrays.copyOfRange(x, Math.max(0, x.length - 32), x.length) - ), - Bytes.concat( - yPadding, - Arrays.copyOfRange(y, Math.max(0, y.length - 32), y.length) - ) - )); - } - - 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) { - case 1: return importCoseEdDsaPublicKey(cose); - case 2: return importCoseP256PublicKey(cose); - case 3: return importCoseRsaPublicKey(cose); - default: - throw new IllegalArgumentException("Unsupported key type: " + kty); - } + private static final ByteArray ED25519_CURVE_OID = + new ByteArray(new byte[] {0x30, 0x05, 0x06, 0x03, 0x2B, 0x65, 0x70}); + + 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)]; + byte[] yPadding = new byte[Math.max(0, 32 - y.length)]; + + Arrays.fill(xPadding, (byte) 0); + Arrays.fill(yPadding, (byte) 0); + + return new ByteArray( + Bytes.concat( + new byte[] {0x04}, + Bytes.concat(xPadding, Arrays.copyOfRange(x, Math.max(0, x.length - 32), x.length)), + Bytes.concat(yPadding, Arrays.copyOfRange(y, Math.max(0, y.length - 32), y.length)))); + } + + 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) { + case 1: + return importCoseEdDsaPublicKey(cose); + case 2: + return importCoseP256PublicKey(cose); + case 3: + return importCoseRsaPublicKey(cose); + default: + throw new IllegalArgumentException("Unsupported key type: " + kty); } + } - private static PublicKey importCoseRsaPublicKey(CBORObject cose) throws NoSuchAlgorithmException, InvalidKeySpecException { - RSAPublicKeySpec spec = new RSAPublicKeySpec( + private static PublicKey importCoseRsaPublicKey(CBORObject cose) + throws NoSuchAlgorithmException, InvalidKeySpecException { + RSAPublicKeySpec spec = + new RSAPublicKeySpec( new BigInteger(1, cose.get(CBORObject.FromObject(-1)).GetByteString()), - new BigInteger(1, cose.get(CBORObject.FromObject(-2)).GetByteString()) - ); - return Crypto.getKeyFactory("RSA").generatePublic(spec); - } - - private static ECPublicKey importCoseP256PublicKey(CBORObject cose) throws CoseException { - return (ECPublicKey) new OneKey(cose).AsPublicKey(); - } - - 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); - default: - throw new IllegalArgumentException("Unsupported EdDSA curve: " + curveId); - } + new BigInteger(1, cose.get(CBORObject.FromObject(-2)).GetByteString())); + return Crypto.getKeyFactory("RSA").generatePublic(spec); + } + + private static ECPublicKey importCoseP256PublicKey(CBORObject cose) throws CoseException { + return (ECPublicKey) new OneKey(cose).AsPublicKey(); + } + + 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); + default: + throw new IllegalArgumentException("Unsupported EdDSA curve: " + curveId); } + } - 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()) }) + 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(new ByteArray(new byte[] {0x03, (byte) (rawKey.size() + 1), 0})) .concat(rawKey); - KeyFactory kFact = Crypto.getKeyFactory("EdDSA"); - return kFact.generatePublic(new X509EncodedKeySpec(x509Key.getBytes())); - } - - 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); - } - } - - static String jwsAlgorithmNameToJavaAlgorithmName(String alg) { - switch (alg) { - case "RS256": - return "SHA256withRSA"; - } + KeyFactory kFact = Crypto.getKeyFactory("EdDSA"); + return kFact.generatePublic(new X509EncodedKeySpec(x509Key.getBytes())); + } + + 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); } + } + static String jwsAlgorithmNameToJavaAlgorithmName(String alg) { + switch (alg) { + case "RS256": + return "SHA256withRSA"; + } + throw new IllegalArgumentException("Unknown algorithm: " + alg); + } } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/X5cAttestationStatementVerifier.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/X5cAttestationStatementVerifier.java index 5eb5295c9..2a48257aa 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/X5cAttestationStatementVerifier.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/X5cAttestationStatementVerifier.java @@ -34,42 +34,43 @@ import java.util.List; import java.util.Optional; - interface X5cAttestationStatementVerifier { - default Optional getX5cAttestationCertificate(AttestationObject attestationObject) throws CertificateException { - return getAttestationTrustPath(attestationObject).flatMap(certs -> certs.stream().findFirst()); - } - - default Optional> getAttestationTrustPath(AttestationObject attestationObject) throws CertificateException { - JsonNode x5cNode = getX5cArray(attestationObject); + default Optional getX5cAttestationCertificate( + AttestationObject attestationObject) throws CertificateException { + return getAttestationTrustPath(attestationObject).flatMap(certs -> certs.stream().findFirst()); + } - if (x5cNode != null && x5cNode.isArray()) { - List certs = new ArrayList<>(x5cNode.size()); + default Optional> getAttestationTrustPath( + AttestationObject attestationObject) throws CertificateException { + JsonNode x5cNode = getX5cArray(attestationObject); - for (JsonNode binary : x5cNode) { - if (binary.isBinary()) { - try { - certs.add(CertificateParser.parseDer(binary.binaryValue())); - } catch (IOException e) { - throw new RuntimeException("binary.isBinary() was true but binary.binaryValue() failed", e); - } - } else { - throw new IllegalArgumentException(String.format( - "Each element of \"x5c\" property of attestation statement must be a binary value, was: %s", - binary.getNodeType() - )); - } - } + if (x5cNode != null && x5cNode.isArray()) { + List certs = new ArrayList<>(x5cNode.size()); - return Optional.of(certs); + for (JsonNode binary : x5cNode) { + if (binary.isBinary()) { + try { + certs.add(CertificateParser.parseDer(binary.binaryValue())); + } catch (IOException e) { + throw new RuntimeException( + "binary.isBinary() was true but binary.binaryValue() failed", e); + } } else { - return Optional.empty(); + throw new IllegalArgumentException( + String.format( + "Each element of \"x5c\" property of attestation statement must be a binary value, was: %s", + binary.getNodeType())); } - } + } - default JsonNode getX5cArray(AttestationObject attestationObject) { - return attestationObject.getAttestationStatement().get("x5c"); + return Optional.of(certs); + } else { + return Optional.empty(); } + } + default JsonNode getX5cArray(AttestationObject attestationObject) { + return attestationObject.getAttestationStatement().get("x5c"); + } } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/attestation/Attestation.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/attestation/Attestation.java index 79c59310a..382ac858c 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/attestation/Attestation.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/attestation/Attestation.java @@ -42,133 +42,117 @@ @Builder(toBuilder = true) public class Attestation implements Serializable { - /** - * true if and only if the contained information has been verified to be cryptographically supported by - * a trusted attestation root. - */ - private final boolean trusted; - - /** - * A unique identifier for a particular version of the data source of the data in this object. - */ - private final String metadataIdentifier; - - /** - * Free-form information about the authenticator vendor. - */ - private final Map vendorProperties; - - /** - * Free-form information about the authenticator model. - */ - private final Map deviceProperties; - - /** - * The set of communication modes supported by the authenticator. - */ - private final Set transports; - - @JsonCreator - private Attestation( - @JsonProperty("trusted") boolean trusted, - @JsonProperty("metadataIdentifier") String metadataIdentifier, - @JsonProperty("vendorProperties") Map vendorProperties, - @JsonProperty("deviceProperties") Map deviceProperties, - @JsonProperty("transports") Set transports - ) { - this.trusted = trusted; - this.metadataIdentifier = metadataIdentifier; - this.vendorProperties = vendorProperties; - this.deviceProperties = deviceProperties; - this.transports = transports == null ? null : new TreeSet<>(transports); + /** + * true if and only if the contained information has been verified to be + * cryptographically supported by a trusted attestation root. + */ + private final boolean trusted; + + /** A unique identifier for a particular version of the data source of the data in this object. */ + private final String metadataIdentifier; + + /** Free-form information about the authenticator vendor. */ + private final Map vendorProperties; + + /** Free-form information about the authenticator model. */ + private final Map deviceProperties; + + /** The set of communication modes supported by the authenticator. */ + private final Set transports; + + @JsonCreator + private Attestation( + @JsonProperty("trusted") boolean trusted, + @JsonProperty("metadataIdentifier") String metadataIdentifier, + @JsonProperty("vendorProperties") Map vendorProperties, + @JsonProperty("deviceProperties") Map deviceProperties, + @JsonProperty("transports") Set transports) { + this.trusted = trusted; + this.metadataIdentifier = metadataIdentifier; + this.vendorProperties = vendorProperties; + this.deviceProperties = deviceProperties; + this.transports = transports == null ? null : new TreeSet<>(transports); + } + + /** A unique identifier for a particular version of the data source of the data in this object. */ + public Optional getMetadataIdentifier() { + return Optional.ofNullable(metadataIdentifier); + } + + /** Free-form information about the authenticator vendor. */ + public Optional> getVendorProperties() { + return Optional.ofNullable(vendorProperties); + } + + /** Free-form information about the authenticator model. */ + public Optional> getDeviceProperties() { + return Optional.ofNullable(deviceProperties); + } + + /** The set of communication modes supported by the authenticator. */ + public Optional> getTransports() { + return Optional.ofNullable(transports); + } + + public static Attestation empty() { + return builder().trusted(false).build(); + } + + public static AttestationBuilder.MandatoryStages builder() { + return new AttestationBuilder.MandatoryStages(); + } + + public static class AttestationBuilder { + private boolean trusted; + private String metadataIdentifier; + private Map vendorProperties; + private Map deviceProperties; + private Set transports; + + public static class MandatoryStages { + private final AttestationBuilder builder = new AttestationBuilder(); + + public AttestationBuilder trusted(boolean trusted) { + return builder.trusted(trusted); + } } - /** - * A unique identifier for a particular version of the data source of the data in this object. - */ - public Optional getMetadataIdentifier() { - return Optional.ofNullable(metadataIdentifier); + public AttestationBuilder metadataIdentifier(@NonNull Optional metadataIdentifier) { + return this.metadataIdentifier(metadataIdentifier.orElse(null)); } - /** - * Free-form information about the authenticator vendor. - */ - public Optional> getVendorProperties() { - return Optional.ofNullable(vendorProperties); + public AttestationBuilder metadataIdentifier(String metadataIdentifier) { + this.metadataIdentifier = metadataIdentifier; + return this; } - /** - * Free-form information about the authenticator model. - */ - public Optional> getDeviceProperties() { - return Optional.ofNullable(deviceProperties); + public AttestationBuilder vendorProperties( + @NonNull Optional> vendorProperties) { + return this.vendorProperties(vendorProperties.orElse(null)); } - /** - * The set of communication modes supported by the authenticator. - */ - public Optional> getTransports() { - return Optional.ofNullable(transports); + public AttestationBuilder vendorProperties(Map vendorProperties) { + this.vendorProperties = vendorProperties; + return this; } - public static Attestation empty() { - return builder().trusted(false).build(); + public AttestationBuilder deviceProperties( + @NonNull Optional> deviceProperties) { + return this.deviceProperties(deviceProperties.orElse(null)); } - public static AttestationBuilder.MandatoryStages builder() { - return new AttestationBuilder.MandatoryStages(); + public AttestationBuilder deviceProperties(Map deviceProperties) { + this.deviceProperties = deviceProperties; + return this; } - public static class AttestationBuilder { - private boolean trusted; - private String metadataIdentifier; - private Map vendorProperties; - private Map deviceProperties; - private Set transports; - - public static class MandatoryStages { - private final AttestationBuilder builder = new AttestationBuilder(); - - public AttestationBuilder trusted(boolean trusted) { - return builder.trusted(trusted); - } - } - - public AttestationBuilder metadataIdentifier(@NonNull Optional metadataIdentifier) { - return this.metadataIdentifier(metadataIdentifier.orElse(null)); - } - - public AttestationBuilder metadataIdentifier(String metadataIdentifier) { - this.metadataIdentifier = metadataIdentifier; - return this; - } - - public AttestationBuilder vendorProperties(@NonNull Optional> vendorProperties) { - return this.vendorProperties(vendorProperties.orElse(null)); - } - - public AttestationBuilder vendorProperties(Map vendorProperties) { - this.vendorProperties = vendorProperties; - return this; - } - - public AttestationBuilder deviceProperties(@NonNull Optional> deviceProperties) { - return this.deviceProperties(deviceProperties.orElse(null)); - } - - public AttestationBuilder deviceProperties(Map deviceProperties) { - this.deviceProperties = deviceProperties; - return this; - } - - public AttestationBuilder transports(@NonNull Optional> transports) { - return this.transports(transports.orElse(null)); - } - - public AttestationBuilder transports(Set transports) { - this.transports = transports; - return this; - } + public AttestationBuilder transports(@NonNull Optional> transports) { + return this.transports(transports.orElse(null)); } + public AttestationBuilder transports(Set transports) { + this.transports = transports; + return this; + } + } } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/attestation/MetadataService.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/attestation/MetadataService.java index 17a0c1e18..d0593d0d4 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/attestation/MetadataService.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/attestation/MetadataService.java @@ -29,20 +29,20 @@ import java.util.List; /** - * Abstraction of a repository which can look up authenticator attestation metadata from an attestation certificate - * chain. + * Abstraction of a repository which can look up authenticator attestation metadata from an + * attestation certificate chain. */ public interface MetadataService { - /** - * Attempt to look up attestation for a chain of certificates - * - * @param attestationCertificateChain - * a certificate chain, where each certificate in the list should be signed by the following certificate. - * @return Attestation metadata, if any is available. If the certificate chain is empty, or if there is no signature - * path from a trusted attestation root to the first certificate in attestationCertificateChange, - * return {@link Attestation#empty()}. - */ - Attestation getAttestation(List attestationCertificateChain) throws CertificateEncodingException; - + /** + * Attempt to look up attestation for a chain of certificates + * + * @param attestationCertificateChain a certificate chain, where each certificate in the list + * should be signed by the following certificate. + * @return Attestation metadata, if any is available. If the certificate chain is empty, or if + * there is no signature path from a trusted attestation root to the first certificate in + * attestationCertificateChange, return {@link Attestation#empty()}. + */ + Attestation getAttestation(List attestationCertificateChain) + throws CertificateEncodingException; } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/attestation/Transport.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/attestation/Transport.java index 4d34b4c82..0154b76c2 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/attestation/Transport.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/attestation/Transport.java @@ -28,62 +28,49 @@ import java.util.EnumSet; import java.util.Set; -/** - * Representations of communication modes supported by an authenticator. - */ +/** Representations of communication modes supported by an authenticator. */ public enum Transport { - /** - * The authenticator supports communication via classic Bluetooth. - */ - BT_CLASSIC(1), + /** The authenticator supports communication via classic Bluetooth. */ + BT_CLASSIC(1), - /** - * The authenticator supports communication via Bluetooth Low Energy (BLE). - */ - BLE(2), + /** The authenticator supports communication via Bluetooth Low Energy (BLE). */ + BLE(2), - /** - * The authenticator supports communication via USB. - */ - USB(4), + /** The authenticator supports communication via USB. */ + USB(4), - /** - * The authenticator supports communication via Near Field Communication (NFC). - */ - NFC(8), + /** The authenticator supports communication via Near Field Communication (NFC). */ + NFC(8), - /** - * The authenticator supports communication via Lightning. - */ - LIGHTNING(16); + /** The authenticator supports communication via Lightning. */ + LIGHTNING(16); - private final int bitpos; + private final int bitpos; - Transport(int bitpos) { - this.bitpos = bitpos; - } - - public static Set fromInt(int bits) { - EnumSet transports = EnumSet.noneOf(Transport.class); - for(Transport transport : Transport.values()) { - if((transport.bitpos & bits) != 0) { - transports.add(transport); - } - } + Transport(int bitpos) { + this.bitpos = bitpos; + } - return transports; + public static Set fromInt(int bits) { + EnumSet transports = EnumSet.noneOf(Transport.class); + for (Transport transport : Transport.values()) { + if ((transport.bitpos & bits) != 0) { + transports.add(transport); + } } - public static int toInt(Iterable transports) { - int transportsInt = 0; - for(Transport transport : transports) { - transportsInt |= transport.bitpos; - } - return transportsInt; - } + return transports; + } - public static int toInt(Transport...transports) { - return toInt(Arrays.asList(transports)); + public static int toInt(Iterable transports) { + int transportsInt = 0; + for (Transport transport : transports) { + transportsInt |= transport.bitpos; } + return transportsInt; + } + public static int toInt(Transport... transports) { + return toInt(Arrays.asList(transports)); + } } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AssertionExtensionInputs.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AssertionExtensionInputs.java index eb0417062..7504c660c 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AssertionExtensionInputs.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AssertionExtensionInputs.java @@ -37,149 +37,129 @@ import lombok.Value; /** - * Contains client extension - * inputs to a - * navigator.credentials.get() operation. All members are optional. + * Contains client + * extension inputs to a navigator.credentials.get() operation. All members are + * optional. * - *

- * The authenticator extension inputs are derived from these client extension inputs. - *

+ *

The authenticator extension inputs are derived from these client extension inputs. * - * @see §9. WebAuthn Extensions + * @see §9. WebAuthn + * Extensions */ @Value @Builder(toBuilder = true) public class AssertionExtensionInputs implements ExtensionInputs { - /** - * The input to the FIDO AppID Extension (appid). - * - *

- * This extension allows WebAuthn Relying Parties that have previously registered a credential using the legacy FIDO - * JavaScript APIs to request an assertion. The FIDO APIs use an alternative identifier for Relying Parties called - * an AppID, - * and any credentials created using those APIs will be scoped to that identifier. Without this extension, they - * would need to be re-registered in order to be scoped to an RP ID. - *

- *

- * This extension does not allow FIDO-compatible credentials to be created. Thus, credentials created with WebAuthn - * are not backwards compatible with the FIDO JavaScript APIs. - *

- * - *

- * {@link RelyingParty#startAssertion(StartAssertionOptions)} sets this extension input automatically if the {@link - * RelyingParty.RelyingPartyBuilder#appId(Optional)} parameter is given when constructing the {@link RelyingParty} - * instance. - *

- * - * @see §10.1. FIDO AppID Extension - * (appid) - */ - private final AppId appid; - - @JsonCreator - private AssertionExtensionInputs( - @JsonProperty("appid") AppId appid - ) { - this.appid = appid; - } + /** + * The input to the FIDO AppID Extension (appid). + * + *

This extension allows WebAuthn Relying Parties that have previously registered a credential + * using the legacy FIDO JavaScript APIs to request an assertion. The FIDO APIs use an alternative + * identifier for Relying Parties called an AppID, + * and any credentials created using those APIs will be scoped to that identifier. Without this + * extension, they would need to be re-registered in order to be scoped to an RP ID. + * + *

This extension does not allow FIDO-compatible credentials to be created. Thus, credentials + * created with WebAuthn are not backwards compatible with the FIDO JavaScript APIs. + * + *

{@link RelyingParty#startAssertion(StartAssertionOptions)} sets this extension input + * automatically if the {@link RelyingParty.RelyingPartyBuilder#appId(Optional)} parameter is + * given when constructing the {@link RelyingParty} instance. + * + * @see §10.1. + * FIDO AppID Extension (appid) + */ + private final AppId appid; - @Override - public Set getExtensionIds() { - Set ids = new HashSet<>(); + @JsonCreator + private AssertionExtensionInputs(@JsonProperty("appid") AppId appid) { + this.appid = appid; + } - getAppid().ifPresent((id) -> ids.add("appid")); + @Override + public Set getExtensionIds() { + Set ids = new HashSet<>(); - return ids; - } + getAppid().ifPresent((id) -> ids.add("appid")); - public static class AssertionExtensionInputsBuilder { - private AppId appid = null; + return ids; + } - /** - * The input to the FIDO AppID Extension (appid). - * - *

- * This extension allows WebAuthn Relying Parties that have previously registered a credential using the legacy FIDO - * JavaScript APIs to request an assertion. The FIDO APIs use an alternative identifier for Relying Parties called - * an AppID, - * and any credentials created using those APIs will be scoped to that identifier. Without this extension, they - * would need to be re-registered in order to be scoped to an RP ID. - *

- *

- * This extension does not allow FIDO-compatible credentials to be created. Thus, credentials created with WebAuthn - * are not backwards compatible with the FIDO JavaScript APIs. - *

- * - *

- * {@link RelyingParty#startAssertion(StartAssertionOptions)} sets this extension input automatically if the {@link - * RelyingParty.RelyingPartyBuilder#appId(Optional)} parameter is given when constructing the {@link RelyingParty} - * instance. - *

- * - * @see §10.1. FIDO AppID Extension - * (appid) - */ - public AssertionExtensionInputsBuilder appid(@NonNull Optional appid) { - return this.appid(appid.orElse(null)); - } + public static class AssertionExtensionInputsBuilder { + private AppId appid = null; - /** - * The input to the FIDO AppID Extension (appid). - * - *

- * This extension allows WebAuthn Relying Parties that have previously registered a credential using the legacy FIDO - * JavaScript APIs to request an assertion. The FIDO APIs use an alternative identifier for Relying Parties called - * an AppID, - * and any credentials created using those APIs will be scoped to that identifier. Without this extension, they - * would need to be re-registered in order to be scoped to an RP ID. - *

- *

- * This extension does not allow FIDO-compatible credentials to be created. Thus, credentials created with WebAuthn - * are not backwards compatible with the FIDO JavaScript APIs. - *

- * - *

- * {@link RelyingParty#startAssertion(StartAssertionOptions)} sets this extension input automatically if the {@link - * RelyingParty.RelyingPartyBuilder#appId(Optional)} parameter is given when constructing the {@link RelyingParty} - * instance. - *

- * - * @see §10.1. FIDO AppID Extension - * (appid) - */ - public AssertionExtensionInputsBuilder appid(AppId appid) { - this.appid = appid; - return this; - } + /** + * The input to the FIDO AppID Extension (appid). + * + *

This extension allows WebAuthn Relying Parties that have previously registered a + * credential using the legacy FIDO JavaScript APIs to request an assertion. The FIDO APIs use + * an alternative identifier for Relying Parties called an AppID, + * and any credentials created using those APIs will be scoped to that identifier. Without this + * extension, they would need to be re-registered in order to be scoped to an RP ID. + * + *

This extension does not allow FIDO-compatible credentials to be created. Thus, credentials + * created with WebAuthn are not backwards compatible with the FIDO JavaScript APIs. + * + *

{@link RelyingParty#startAssertion(StartAssertionOptions)} sets this extension input + * automatically if the {@link RelyingParty.RelyingPartyBuilder#appId(Optional)} parameter is + * given when constructing the {@link RelyingParty} instance. + * + * @see §10.1. + * FIDO AppID Extension (appid) + */ + public AssertionExtensionInputsBuilder appid(@NonNull Optional appid) { + return this.appid(appid.orElse(null)); } /** * The input to the FIDO AppID Extension (appid). * - *

- * This extension allows WebAuthn Relying Parties that have previously registered a credential using the legacy FIDO - * JavaScript APIs to request an assertion. The FIDO APIs use an alternative identifier for Relying Parties called - * an AppID, - * and any credentials created using those APIs will be scoped to that identifier. Without this extension, they - * would need to be re-registered in order to be scoped to an RP ID. - *

- *

- * This extension does not allow FIDO-compatible credentials to be created. Thus, credentials created with WebAuthn - * are not backwards compatible with the FIDO JavaScript APIs. - *

+ *

This extension allows WebAuthn Relying Parties that have previously registered a + * credential using the legacy FIDO JavaScript APIs to request an assertion. The FIDO APIs use + * an alternative identifier for Relying Parties called an AppID, + * and any credentials created using those APIs will be scoped to that identifier. Without this + * extension, they would need to be re-registered in order to be scoped to an RP ID. + * + *

This extension does not allow FIDO-compatible credentials to be created. Thus, credentials + * created with WebAuthn are not backwards compatible with the FIDO JavaScript APIs. * - *

- * {@link RelyingParty#startAssertion(StartAssertionOptions)} sets this extension input automatically if the {@link - * RelyingParty.RelyingPartyBuilder#appId(Optional)} parameter is given when constructing the {@link RelyingParty} - * instance. - *

+ *

{@link RelyingParty#startAssertion(StartAssertionOptions)} sets this extension input + * automatically if the {@link RelyingParty.RelyingPartyBuilder#appId(Optional)} parameter is + * given when constructing the {@link RelyingParty} instance. * - * @see §10.1. FIDO AppID Extension - * (appid) + * @see §10.1. + * FIDO AppID Extension (appid) */ - public Optional getAppid() { - return Optional.ofNullable(appid); + public AssertionExtensionInputsBuilder appid(AppId appid) { + this.appid = appid; + return this; } + } + /** + * The input to the FIDO AppID Extension (appid). + * + *

This extension allows WebAuthn Relying Parties that have previously registered a credential + * using the legacy FIDO JavaScript APIs to request an assertion. The FIDO APIs use an alternative + * identifier for Relying Parties called an AppID, + * and any credentials created using those APIs will be scoped to that identifier. Without this + * extension, they would need to be re-registered in order to be scoped to an RP ID. + * + *

This extension does not allow FIDO-compatible credentials to be created. Thus, credentials + * created with WebAuthn are not backwards compatible with the FIDO JavaScript APIs. + * + *

{@link RelyingParty#startAssertion(StartAssertionOptions)} sets this extension input + * automatically if the {@link RelyingParty.RelyingPartyBuilder#appId(Optional)} parameter is + * given when constructing the {@link RelyingParty} instance. + * + * @see §10.1. + * FIDO AppID Extension (appid) + */ + public Optional getAppid() { + return Optional.ofNullable(appid); + } } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AttestationConveyancePreference.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AttestationConveyancePreference.java index bdfc38292..29bfab293 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AttestationConveyancePreference.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AttestationConveyancePreference.java @@ -34,69 +34,68 @@ import lombok.AllArgsConstructor; import lombok.NonNull; - /** - * Relying Parties may use this to specify their preference regarding attestation conveyance during credential - * generation. + * Relying Parties may use this to specify their preference regarding attestation conveyance during + * credential generation. * - * @see §5.4.6. Attestation Conveyance - * Preference - * Enumeration (enum AttestationConveyancePreference) - * + * @see §5.4.6. + * Attestation Conveyance Preference Enumeration (enum AttestationConveyancePreference) */ @JsonSerialize(using = JsonStringSerializer.class) @AllArgsConstructor(access = AccessLevel.PRIVATE) public enum AttestationConveyancePreference implements JsonStringSerializable { - /** - * Indicates that the Relying Party is not interested in authenticator attestation. - *

- * For example, in order to potentially avoid having to obtain user consent to relay identifying information to the - * Relying Party, or to save a roundtrip to an Attestation - * CA. - *

- *

- * This is the default value. - *

- */ - NONE("none"), - - /** - * Indicates that the Relying Party prefers an attestation conveyance yielding verifiable attestation statements, - * but allows the client to decide how to obtain such attestation statements. The client MAY replace the - * authenticator-generated attestation statements with attestation statements generated by an Anonymization CA, in order to - * protect the user’s privacy, or to assist Relying Parties with attestation verification in a heterogeneous - * ecosystem. - *

- * Note: There is no guarantee that the Relying Party will obtain a verifiable attestation statement in this case. - * For example, in the case that the authenticator employs self attestation. - *

- */ - INDIRECT("indirect"), + /** + * Indicates that the Relying Party is not interested in authenticator attestation. + * + *

For example, in order to potentially avoid having to obtain user consent to relay + * identifying information to the Relying Party, or to save a roundtrip to an Attestation CA. + * + *

This is the default value. + */ + NONE("none"), - /** - * Indicates that the Relying Party wants to receive the attestation statement as generated by the authenticator. - */ - DIRECT("direct"); + /** + * Indicates that the Relying Party prefers an attestation conveyance yielding verifiable + * attestation statements, but allows the client to decide how to obtain such attestation + * statements. The client MAY replace the authenticator-generated attestation statements with + * attestation statements generated by an Anonymization CA, + * in order to protect the user’s privacy, or to assist Relying Parties with attestation + * verification in a heterogeneous ecosystem. + * + *

Note: There is no guarantee that the Relying Party will obtain a verifiable attestation + * statement in this case. For example, in the case that the authenticator employs self + * attestation. + */ + INDIRECT("indirect"), - @NonNull - private final String id; + /** + * Indicates that the Relying Party wants to receive the attestation statement as generated by the + * authenticator. + */ + DIRECT("direct"); - private static Optional fromString(@NonNull String id) { - return Stream.of(values()).filter(v -> v.id.equals(id)).findAny(); - } + @NonNull private final String id; - @JsonCreator - private static AttestationConveyancePreference fromJsonString(@NonNull String id) { - return fromString(id).orElseThrow(() -> new IllegalArgumentException(String.format( - "Unknown %s value: %s", AttestationConveyancePreference.class.getSimpleName(), id - ))); - } + private static Optional fromString(@NonNull String id) { + return Stream.of(values()).filter(v -> v.id.equals(id)).findAny(); + } - @Override - public String toJsonString() { - return id; - } + @JsonCreator + private static AttestationConveyancePreference fromJsonString(@NonNull String id) { + return fromString(id) + .orElseThrow( + () -> + new IllegalArgumentException( + String.format( + "Unknown %s value: %s", + AttestationConveyancePreference.class.getSimpleName(), id))); + } + @Override + public String toJsonString() { + return id; + } } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AttestationObject.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AttestationObject.java index abd168633..498f06340 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AttestationObject.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AttestationObject.java @@ -36,142 +36,137 @@ import lombok.NonNull; import lombok.Value; - /** - * Authenticators MUST provide some form of attestation. The basic requirement is that the authenticator can produce, - * for each credential public key, an attestation statement verifiable by the WebAuthn Relying Party. Typically, this - * attestation statement contains a signature by an attestation private key over the attested credential public key and - * a challenge, as well as a certificate or similar data providing provenance information for the attestation public - * key, enabling the Relying Party to make a trust decision. However, if an attestation key pair is not available, then - * the authenticator MUST perform self - * attestation of the credential public key with the corresponding credential private key. All this information is - * returned by authenticators any time a new public key credential is generated, in the overall form of an attestation - * object. The relationship of the attestation object with authenticator data (containing attested credential data) and - * the attestation statement is illustrated in figure - * 5. + * Authenticators MUST provide some form of attestation. The basic requirement is that the + * authenticator can produce, for each credential public key, an attestation statement verifiable by + * the WebAuthn Relying Party. Typically, this attestation statement contains a signature by an + * attestation private key over the attested credential public key and a challenge, as well as a + * certificate or similar data providing provenance information for the attestation public key, + * enabling the Relying Party to make a trust decision. However, if an attestation key pair is not + * available, then the authenticator MUST perform self attestation of + * the credential public key with the corresponding credential private key. All this information is + * returned by authenticators any time a new public key credential is generated, in the overall form + * of an attestation object. The relationship of the attestation object with authenticator data + * (containing attested credential data) and the attestation statement is illustrated in figure 5. * - * @see §6.4. Attestation + * @see §6.4. + * Attestation */ @Value @JsonSerialize(using = AttestationObject.JsonSerializer.class) public class AttestationObject { - /** - * The original raw byte array that this object is decoded from. - * - * @see §6.4. Attestation - */ - @NonNull - private final ByteArray bytes; - - /** - * The authenticator data embedded inside this attestation object. This is one part of the signed data that the - * signature in the attestation statement (if any) is computed over. - */ - @NonNull - private final transient AuthenticatorData authenticatorData; - - /** - * The attestation statement format identifier of this attestation object. - * - * @see §8. Defined - * Attestation Statement Formats - * - *

- * Users of this library should not need to access this value directly. - *

- */ - @NonNull - private final transient String format; - - /** - * An important component of the attestation object is the attestation statement. This is a specific type of signed - * data object, containing statements about a public key credential itself and the authenticator that created it. It - * contains an attestation signature created using the key of the attesting authority (except for the case of self - * attestation, when it is created using the credential private key). - * - *

- * Users of this library should not need to access this value directly. - *

- */ - @NonNull - private final transient ObjectNode attestationStatement; - - /** - * Decode an {@link AttestationObject} object from a raw attestation object byte array. - * - * @throws IOException if bytes cannot be parsed as a CBOR map. - */ - @JsonCreator - public AttestationObject(@NonNull ByteArray bytes) throws IOException { - this.bytes = bytes; - - final JsonNode decoded = JacksonCodecs.cbor().readTree(bytes.getBytes()); - final ByteArray authDataBytes; - - ExceptionUtil.assure( - decoded != null, - "Failed to parse attestation object from bytes: %s", - bytes.getBase64Url() - ); - - if (!decoded.isObject()) { - throw new IllegalArgumentException("Attestation object must be a JSON object."); - } - - final JsonNode authData = decoded.get("authData"); - if (authData == null) { - throw new IllegalArgumentException("Required property \"authData\" missing from attestation object: " + bytes.getBase64Url()); - } else { - if (authData.isBinary()) { - authDataBytes = new ByteArray(authData.binaryValue()); - } else { - throw new IllegalArgumentException(String.format( - "Property \"authData\" of attestation object must be a CBOR byte array, was: %s. Attestation object: %s", - authData.getNodeType(), - bytes.getBase64Url() - )); - } - } - - final JsonNode format = decoded.get("fmt"); - if (format == null) { - throw new IllegalArgumentException("Required property \"fmt\" missing from attestation object: " + bytes.getBase64Url()); - } else { - if (format.isTextual()) { - this.format = decoded.get("fmt").textValue(); - } else { - throw new IllegalArgumentException(String.format( - "Property \"fmt\" of attestation object must be a CBOR text value, was: %s. Attestation object: %s", - format.getNodeType(), - bytes.getBase64Url() - )); - } - } - - final JsonNode attStmt = decoded.get("attStmt"); - if (attStmt == null) { - throw new IllegalArgumentException("Required property \"attStmt\" missing from attestation object: " + bytes.getBase64Url()); - } else { - if (attStmt.isObject()) { - this.attestationStatement = (ObjectNode) attStmt; - } else { - throw new IllegalArgumentException(String.format( - "Property \"attStmt\" of attestation object must be a CBOR map, was: %s. Attestation object: %s", - attStmt.getNodeType(), - bytes.getBase64Url() - )); - } - } - - authenticatorData = new AuthenticatorData(authDataBytes); + /** + * The original raw byte array that this object is decoded from. + * + * @see §6.4. + * Attestation + */ + @NonNull private final ByteArray bytes; + + /** + * The authenticator data embedded inside this attestation object. This is one part of the signed + * data that the signature in the attestation statement (if any) is computed over. + */ + @NonNull private final transient AuthenticatorData authenticatorData; + + /** + * The attestation statement format identifier of this attestation object. + * + * @see §8. + * Defined Attestation Statement Formats + *

Users of this library should not need to access this value directly. + */ + @NonNull private final transient String format; + + /** + * An important component of the attestation object is the attestation statement. This is a + * specific type of signed data object, containing statements about a public key credential itself + * and the authenticator that created it. It contains an attestation signature created using the + * key of the attesting authority (except for the case of self attestation, when it is created + * using the credential private key). + * + *

Users of this library should not need to access this value directly. + */ + @NonNull private final transient ObjectNode attestationStatement; + + /** + * Decode an {@link AttestationObject} object from a raw attestation object byte array. + * + * @throws IOException if bytes cannot be parsed as a CBOR map. + */ + @JsonCreator + public AttestationObject(@NonNull ByteArray bytes) throws IOException { + this.bytes = bytes; + + final JsonNode decoded = JacksonCodecs.cbor().readTree(bytes.getBytes()); + final ByteArray authDataBytes; + + ExceptionUtil.assure( + decoded != null, "Failed to parse attestation object from bytes: %s", bytes.getBase64Url()); + + if (!decoded.isObject()) { + throw new IllegalArgumentException("Attestation object must be a JSON object."); + } + + final JsonNode authData = decoded.get("authData"); + if (authData == null) { + throw new IllegalArgumentException( + "Required property \"authData\" missing from attestation object: " + + bytes.getBase64Url()); + } else { + if (authData.isBinary()) { + authDataBytes = new ByteArray(authData.binaryValue()); + } else { + throw new IllegalArgumentException( + String.format( + "Property \"authData\" of attestation object must be a CBOR byte array, was: %s. Attestation object: %s", + authData.getNodeType(), bytes.getBase64Url())); + } } - static class JsonSerializer extends com.fasterxml.jackson.databind.JsonSerializer { - @Override - public void serialize(AttestationObject value, JsonGenerator gen, SerializerProvider serializers) throws IOException { - gen.writeString(value.getBytes().getBase64Url()); - } + final JsonNode format = decoded.get("fmt"); + if (format == null) { + throw new IllegalArgumentException( + "Required property \"fmt\" missing from attestation object: " + bytes.getBase64Url()); + } else { + if (format.isTextual()) { + this.format = decoded.get("fmt").textValue(); + } else { + throw new IllegalArgumentException( + String.format( + "Property \"fmt\" of attestation object must be a CBOR text value, was: %s. Attestation object: %s", + format.getNodeType(), bytes.getBase64Url())); + } } + final JsonNode attStmt = decoded.get("attStmt"); + if (attStmt == null) { + throw new IllegalArgumentException( + "Required property \"attStmt\" missing from attestation object: " + bytes.getBase64Url()); + } else { + if (attStmt.isObject()) { + this.attestationStatement = (ObjectNode) attStmt; + } else { + throw new IllegalArgumentException( + String.format( + "Property \"attStmt\" of attestation object must be a CBOR map, was: %s. Attestation object: %s", + attStmt.getNodeType(), bytes.getBase64Url())); + } + } + + authenticatorData = new AuthenticatorData(authDataBytes); + } + + static class JsonSerializer + extends com.fasterxml.jackson.databind.JsonSerializer { + @Override + public void serialize( + AttestationObject value, JsonGenerator gen, SerializerProvider serializers) + throws IOException { + gen.writeString(value.getBytes().getBase64Url()); + } + } } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AttestationType.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AttestationType.java index a92bca0ae..06f0c11f4 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AttestationType.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AttestationType.java @@ -24,91 +24,120 @@ package com.yubico.webauthn.data; - /** - * Web Authentication supports several attestation types, defining the semantics of attestation statements and their - * underlying trust models. + * Web Authentication supports several attestation types, defining the semantics of attestation + * statements and their underlying trust models. * - * @see §6.4.3. Attestation Types + * @see §6.4.3. + * Attestation Types */ public enum AttestationType { - /** - * In the case of basic attestation, the authenticator’s attestation key pair is specific to an authenticator model. - * Thus, authenticators of the same model often share the same attestation key pair. See §14.4 Attestation Privacy for - * further information. - * - * @see Basic Attestation - */ - BASIC, + /** + * In the case of basic attestation, the authenticator’s attestation key pair is specific to an + * authenticator model. Thus, authenticators of the same model often share the same attestation + * key pair. See §14.4 + * Attestation Privacy for further information. + * + * @see Basic + * Attestation + */ + BASIC, + + /** + * In the case of self attestation, also known as surrogate basic attestation, the authenticator + * does not have any specific attestation key. Instead it uses the credential private key to + * create the attestation signature. Authenticators without meaningful protection measures for an + * attestation private key typically use this attestation type. + * + * @see Self + * Attestation + */ + SELF_ATTESTATION, - /** - * In the case of self attestation, also known as surrogate basic attestation, the authenticator does not have any - * specific attestation key. Instead it uses the credential private key to create the attestation signature. - * Authenticators without meaningful protection measures for an attestation private key typically use this - * attestation type. - * - * @see Self Attestation - */ - SELF_ATTESTATION, + /** + * In this case, an authenticator is based on a Trusted Platform Module (TPM) and holds an + * authenticator-specific "endorsement key" (EK). This key is used to securely communicate with a + * trusted third party, the Attestation CA (formerly known as a "Privacy CA"). The authenticator + * can generate multiple attestation identity key pairs (AIK) and requests an Attestation CA to + * issue an AIK certificate for each. Using this approach, such an authenticator can limit the + * exposure of the EK (which is a global correlation handle) to Attestation CA(s). AIKs can be + * requested for each authenticator-generated public key credential individually, and conveyed to + * Relying Parties as attestation certificates. + * + *

Note: This concept typically leads to multiple attestation certificates. The attestation + * certificate requested most recently is called "active". + * + *

Note: Attestation statements conveying attestations of this type use the same data structure + * as attestation statements conveying attestations of type #BASIC, so the two attestation types + * are, in general, distinguishable only with externally provided knowledge regarding the contents + * of the attestation certificates conveyed in the attestation statement. + * + * @see Attestation + * CA + */ + ATTESTATION_CA, - /** - * In this case, an authenticator is based on a Trusted Platform Module (TPM) and holds an authenticator-specific - * "endorsement key" (EK). This key is used to securely communicate with a trusted third party, the Attestation CA - * (formerly known as a "Privacy CA"). The authenticator can generate multiple attestation identity key pairs (AIK) - * and requests an Attestation CA to issue an AIK certificate for each. Using this approach, such an authenticator - * can limit the exposure of the EK (which is a global correlation handle) to Attestation CA(s). AIKs can be - * requested for each authenticator-generated public key credential individually, and conveyed to Relying Parties as - * attestation certificates. - *

- * Note: This concept typically leads to multiple attestation certificates. The attestation certificate requested - * most recently is called "active". - *

- *

- * Note: Attestation statements conveying attestations of this type use the same data structure as attestation - * statements conveying attestations of type #BASIC, so the two attestation types are, in general, distinguishable - * only with externally provided knowledge regarding the contents of the attestation certificates conveyed in the - * attestation statement. - *

- * - * @see Attestation CA - */ - ATTESTATION_CA, + /** + * In this case, the authenticator uses an Anonymization CA which dynamically generates + * per-credential attestation certificates such that the attestation statements presented to + * Relying Parties do not provide uniquely identifiable information, e.g., that might be used for + * tracking purposes. + * + *

Note: Attestation statements conveying attestations of type AttCA or AnonCA use the same + * data structure as those of type Basic, so the three attestation types are, in general, + * distinguishable only with externally provided knowledge regarding the contents of the + * attestation certificates conveyed in the attestation statement. + * + *

Note: Attestation statements conveying attestations of this type use the same data structure + * as attestation statements conveying attestations of type #BASIC, so the two attestation types + * are, in general, distinguishable only with externally provided knowledge regarding the contents + * of the attestation certificates conveyed in the attestation statement. + * + * @see Anonymization + * CA + */ + ANONYMIZATION_CA, - /** - * In this case, the Authenticator receives direct anonymous attestation (DAA) credentials from a single DAA-Issuer. - * These DAA credentials are used along with blinding to sign the attested credential data. The concept of blinding - * avoids the DAA credentials being misused as global correlation handle. WebAuthn supports DAA using elliptic curve - * cryptography and bilinear pairings, called ECDAA. See the FIDO - * ECDAA Algorithm for details. - * - * @see Elliptic Curve based Direct Anonymous - * Attestation (ECDAA) - * @see FIDO - * ECDAA Algorithm - */ - ECDAA, + /** + * In this case, the Authenticator receives direct anonymous attestation (DAA) credentials from a + * single DAA-Issuer. These DAA credentials are used along with blinding to sign the attested + * credential data. The concept of blinding avoids the DAA credentials being misused as global + * correlation handle. WebAuthn supports DAA using elliptic curve cryptography and bilinear + * pairings, called ECDAA. See the FIDO + * ECDAA Algorithm for details. + * + * @see Elliptic Curve based + * Direct Anonymous Attestation (ECDAA) + * @see FIDO + * ECDAA Algorithm + */ + ECDAA, - /** - * In this case, no attestation information is available. See also §8.7 - * None Attestation Statement Format. - * - * @see §8.7 None Attestation Statement - * Format - */ - NONE, + /** + * In this case, no attestation information is available. See also §8.7 None Attestation + * Statement Format. + * + * @see §8.7 None + * Attestation Statement Format + */ + NONE, - /** - * In this case, attestation information is present but was not understood by the library. - *

- * For example, the attestation statement might be using a new attestation statement format not yet supported by the - * library. - *

- * - * @see §6.4.3. Attestation - * Types - * @see §8. Defined - * Attestation Statement Formats - */ - UNKNOWN + /** + * In this case, attestation information is present but was not understood by the library. + * + *

For example, the attestation statement might be using a new attestation statement format not + * yet supported by the library. + * + * @see §6.4.3. + * Attestation Types + * @see §8. + * Defined Attestation Statement Formats + */ + UNKNOWN } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AttestedCredentialData.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AttestedCredentialData.java index b7be60173..bf0e10655 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AttestedCredentialData.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AttestedCredentialData.java @@ -31,51 +31,45 @@ import lombok.Value; /** - * Attested credential data is a variable-length byte array added to the authenticator data when generating an - * attestation object for a given credential. This class provides access to the three data segments of that byte array. + * Attested credential data is a variable-length byte array added to the authenticator data when + * generating an attestation object for a given credential. This class provides access to the three + * data segments of that byte array. * - * @see 6.4.1. Attested - * Credential Data + * @see 6.4.1. + * Attested Credential Data */ @Value @Builder(toBuilder = true) public class AttestedCredentialData { - /** - * The AAGUID of the authenticator. - */ - @NonNull - private final ByteArray aaguid; + /** The AAGUID of the authenticator. */ + @NonNull private final ByteArray aaguid; - /** - * The credential ID of the attested credential. - */ - @NonNull - private final ByteArray credentialId; + /** The credential ID of the attested credential. */ + @NonNull private final ByteArray credentialId; - /** - * The credential public key encoded in COSE_Key format, as defined in Section 7 of RFC 8152. - */ - @NonNull - // TODO: verify requirements https://www.w3.org/TR/webauthn/#sec-attestation-data - private final ByteArray credentialPublicKey; + /** + * The credential public key encoded in COSE_Key format, as defined in Section 7 of RFC 8152. + */ + @NonNull + // TODO: verify requirements https://www.w3.org/TR/webauthn/#sec-attestation-data + private final ByteArray credentialPublicKey; - @JsonCreator - private AttestedCredentialData( - @NonNull @JsonProperty("aaguid") ByteArray aaguid, - @NonNull @JsonProperty("credentialId") ByteArray credentialId, - @NonNull @JsonProperty("credentialPublicKey") ByteArray credentialPublicKey - ) { - this.aaguid = aaguid; - this.credentialId = credentialId; - this.credentialPublicKey = credentialPublicKey; - } + @JsonCreator + private AttestedCredentialData( + @NonNull @JsonProperty("aaguid") ByteArray aaguid, + @NonNull @JsonProperty("credentialId") ByteArray credentialId, + @NonNull @JsonProperty("credentialPublicKey") ByteArray credentialPublicKey) { + this.aaguid = aaguid; + this.credentialId = credentialId; + this.credentialPublicKey = credentialPublicKey; + } - static AttestedCredentialDataBuilder builder() { - return new AttestedCredentialDataBuilder(); - } - - static class AttestedCredentialDataBuilder {} + static AttestedCredentialDataBuilder builder() { + return new AttestedCredentialDataBuilder(); + } + static class AttestedCredentialDataBuilder {} } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorAssertionResponse.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorAssertionResponse.java index 17ef6033e..599bf7a97 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorAssertionResponse.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorAssertionResponse.java @@ -34,118 +34,123 @@ import lombok.NonNull; import lombok.Value; - /** - * Represents an authenticator's response to a client’s request for generation of a new authentication assertion given - * the WebAuthn Relying Party's {@linkplain PublicKeyCredentialRequestOptions#challenge challenge} and OPTIONAL - * {@linkplain PublicKeyCredentialRequestOptions#allowCredentials list of credentials} it is aware of. This response - * contains a cryptographic {@linkplain #signature} proving possession of the credential private key, and optionally - * evidence of user consent to a specific transaction. + * Represents an authenticator's response to a client’s request for generation of a new + * authentication assertion given the WebAuthn Relying Party's {@linkplain + * PublicKeyCredentialRequestOptions#challenge challenge} and OPTIONAL {@linkplain + * PublicKeyCredentialRequestOptions#allowCredentials list of credentials} it is aware of. This + * response contains a cryptographic {@linkplain #signature} proving possession of the credential + * private key, and optionally evidence of user consent to a specific transaction. * - * @see §5.2.2. Web - * Authentication Assertion (interface AuthenticatorAssertionResponse) - * + * @see §5.2.2. + * Web Authentication Assertion (interface AuthenticatorAssertionResponse) */ @Value public class AuthenticatorAssertionResponse implements AuthenticatorResponse { - @NonNull - @Getter(onMethod = @__({ @Override })) - private final ByteArray authenticatorData; - - @NonNull - @Getter(onMethod = @__({ @Override })) - private final ByteArray clientDataJSON; + @NonNull + @Getter(onMethod = @__({@Override})) + private final ByteArray authenticatorData; + + @NonNull + @Getter(onMethod = @__({@Override})) + private final ByteArray clientDataJSON; + + /** + * The raw signature returned from the authenticator. See §6.3.3 The + * authenticatorGetAssertion Operation. + */ + @NonNull private final ByteArray signature; + + /** + * The user handle returned from the authenticator, or empty if the authenticator did not return a + * user handle. See §6.3.3 The + * authenticatorGetAssertion Operation. + */ + private final ByteArray userHandle; + + @NonNull + @Getter(onMethod = @__({@Override})) + private final transient CollectedClientData clientData; + + @JsonCreator + @Builder(toBuilder = true) + private AuthenticatorAssertionResponse( + @NonNull @JsonProperty("authenticatorData") final ByteArray authenticatorData, + @NonNull @JsonProperty("clientDataJSON") final ByteArray clientDataJSON, + @NonNull @JsonProperty("signature") final ByteArray signature, + @JsonProperty("userHandle") final ByteArray userHandle) + throws IOException, Base64UrlException { + this.authenticatorData = authenticatorData; + this.clientDataJSON = clientDataJSON; + this.signature = signature; + this.userHandle = userHandle; + this.clientData = new CollectedClientData(this.clientDataJSON); + } + + /** + * The user handle returned from the authenticator, or empty if the authenticator did not return a + * user handle. See §6.3.3 The + * authenticatorGetAssertion Operation. + */ + public Optional getUserHandle() { + return Optional.ofNullable(userHandle); + } + + public static AuthenticatorAssertionResponseBuilder.MandatoryStages builder() { + return new AuthenticatorAssertionResponseBuilder.MandatoryStages(); + } + + public static class AuthenticatorAssertionResponseBuilder { + private ByteArray userHandle = null; + + public static class MandatoryStages { + private final AuthenticatorAssertionResponseBuilder builder = + new AuthenticatorAssertionResponseBuilder(); + + public Step2 authenticatorData(ByteArray authenticatorData) { + builder.authenticatorData(authenticatorData); + return new Step2(); + } + + public class Step2 { + public Step3 clientDataJSON(ByteArray clientDataJSON) { + builder.clientDataJSON(clientDataJSON); + return new Step3(); + } + } - /** - * The raw signature returned from the authenticator. See §6.3.3 - * The authenticatorGetAssertion Operation. - */ - @NonNull - private final ByteArray signature; + public class Step3 { + public AuthenticatorAssertionResponseBuilder signature(ByteArray signature) { + return builder.signature(signature); + } + } + } /** - * The user handle returned from the authenticator, or empty if the authenticator did not return a user handle. See - * §6.3.3 The authenticatorGetAssertion - * Operation. + * The user handle returned from the authenticator, or empty if the authenticator did not return + * a user handle. See §6.3.3 The + * authenticatorGetAssertion Operation. */ - private final ByteArray userHandle; - - @NonNull - @Getter(onMethod = @__({ @Override })) - private final transient CollectedClientData clientData; - - @JsonCreator - @Builder(toBuilder = true) - private AuthenticatorAssertionResponse( - @NonNull @JsonProperty("authenticatorData") final ByteArray authenticatorData, - @NonNull @JsonProperty("clientDataJSON") final ByteArray clientDataJSON, - @NonNull @JsonProperty("signature") final ByteArray signature, - @JsonProperty("userHandle") final ByteArray userHandle - ) throws IOException, Base64UrlException { - this.authenticatorData = authenticatorData; - this.clientDataJSON = clientDataJSON; - this.signature = signature; - this.userHandle = userHandle; - this.clientData = new CollectedClientData(this.clientDataJSON); + public AuthenticatorAssertionResponseBuilder userHandle( + @NonNull Optional userHandle) { + return this.userHandle(userHandle.orElse(null)); } /** - * The user handle returned from the authenticator, or empty if the authenticator did not return a user handle. See - * §6.3.3 The authenticatorGetAssertion - * Operation. + * The user handle returned from the authenticator, or empty if the authenticator did not return + * a user handle. See §6.3.3 The + * authenticatorGetAssertion Operation. */ - public Optional getUserHandle() { - return Optional.ofNullable(userHandle); - } - - public static AuthenticatorAssertionResponseBuilder.MandatoryStages builder() { - return new AuthenticatorAssertionResponseBuilder.MandatoryStages(); + public AuthenticatorAssertionResponseBuilder userHandle(ByteArray userHandle) { + this.userHandle = userHandle; + return this; } - - public static class AuthenticatorAssertionResponseBuilder { - private ByteArray userHandle = null; - - public static class MandatoryStages { - private final AuthenticatorAssertionResponseBuilder builder = new AuthenticatorAssertionResponseBuilder(); - - public Step2 authenticatorData(ByteArray authenticatorData) { - builder.authenticatorData(authenticatorData); - return new Step2(); - } - - public class Step2 { - public Step3 clientDataJSON(ByteArray clientDataJSON) { - builder.clientDataJSON(clientDataJSON); - return new Step3(); - } - } - - public class Step3 { - public AuthenticatorAssertionResponseBuilder signature(ByteArray signature) { - return builder.signature(signature); - } - } - } - - /** - * The user handle returned from the authenticator, or empty if the authenticator did not return a user handle. See - * §6.3.3 The authenticatorGetAssertion - * Operation. - */ - public AuthenticatorAssertionResponseBuilder userHandle(@NonNull Optional userHandle) { - return this.userHandle(userHandle.orElse(null)); - } - - /** - * The user handle returned from the authenticator, or empty if the authenticator did not return a user handle. See - * §6.3.3 The authenticatorGetAssertion - * Operation. - */ - public AuthenticatorAssertionResponseBuilder userHandle(ByteArray userHandle) { - this.userHandle = userHandle; - return this; - } - } - + } } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorAttachment.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorAttachment.java index 34f24db68..de8c35eeb 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorAttachment.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorAttachment.java @@ -24,7 +24,6 @@ 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; @@ -35,66 +34,64 @@ import lombok.NonNull; /** - * This enumeration’s values describe authenticators' attachment + * This enumeration’s values describe authenticators' attachment * modalities. Relying Parties use this for two purposes: * *

    - *
  • - * to express a preferred authenticator attachment modality when calling navigator.credentials.create() to - * create a credential, and - *
  • - * - *
  • - * to inform the client of the Relying Party's best belief about how to locate the managing authenticators of the - * credentials listed in {@link PublicKeyCredentialRequestOptions#allowCredentials} when calling - * navigator.credentials.get(). - *
  • + *
  • to express a preferred authenticator attachment modality when calling + * navigator.credentials.create() to create a credential, and + *
  • to inform the client of the Relying Party's best belief about how to locate the managing + * authenticators of the credentials listed in {@link + * PublicKeyCredentialRequestOptions#allowCredentials} when calling + * navigator.credentials.get(). *
* - * @see §5.4.5. Authenticator - * Attachment Enumeration (enum AuthenticatorAttachment) - * + * @see §5.4.5. + * Authenticator Attachment Enumeration (enum AuthenticatorAttachment) */ @JsonSerialize(using = JsonStringSerializer.class) @AllArgsConstructor public enum AuthenticatorAttachment implements JsonStringSerializable { - /** - * Indicates cross-platform - * attachment. - *

- * Authenticators of this class are removable from, and can "roam" among, client platforms. - *

- */ - CROSS_PLATFORM("cross-platform"), + /** + * Indicates cross-platform + * attachment. + * + *

Authenticators of this class are removable from, and can "roam" among, client platforms. + */ + CROSS_PLATFORM("cross-platform"), - /** - * Indicates platform - * attachment. - *

- * Usually, authenticators of this class are not removable from the platform. - *

- */ - PLATFORM("platform"); + /** + * Indicates platform + * attachment. + * + *

Usually, authenticators of this class are not removable from the platform. + */ + PLATFORM("platform"); - @NonNull - private final String id; + @NonNull private final String id; - private static Optional fromString(@NonNull String id) { - return Stream.of(values()).filter(v -> v.id.equals(id)).findAny(); - } + private static Optional fromString(@NonNull String id) { + return Stream.of(values()).filter(v -> v.id.equals(id)).findAny(); + } - @JsonCreator - private static AuthenticatorAttachment fromJsonString(@NonNull String id) { - return fromString(id).orElseThrow(() -> new IllegalArgumentException(String.format( - "Unknown %s value: %s", AuthenticatorAttachment.class.getSimpleName(), id - ))); - } - - @Override - public String toJsonString() { - return id; - } + @JsonCreator + private static AuthenticatorAttachment fromJsonString(@NonNull String id) { + return fromString(id) + .orElseThrow( + () -> + new IllegalArgumentException( + String.format( + "Unknown %s value: %s", + AuthenticatorAttachment.class.getSimpleName(), id))); + } + @Override + public String toJsonString() { + return id; + } } - diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorAttestationResponse.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorAttestationResponse.java index 2b7ee38ac..ea2763e01 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorAttestationResponse.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorAttestationResponse.java @@ -34,88 +34,86 @@ import lombok.NonNull; import lombok.Value; - /** - * Represents the authenticator's response to a client's request for the creation of a new public key credential. It - * contains information about the new credential that can be used to identify it for later use, and metadata that can be - * used by the WebAuthn Relying Party to assess the characteristics of the credential during registration. + * Represents the authenticator's response to a client's request for the creation of a new public + * key credential. It contains information about the new credential that can be used to identify it + * for later use, and metadata that can be used by the WebAuthn Relying Party to assess the + * characteristics of the credential during registration. * - * @see §5.2.1. Information - * About Public Key Credential (interface AuthenticatorAttestationResponse) - * + * @see §5.2.1. + * Information About Public Key Credential (interface AuthenticatorAttestationResponse) */ @Value public class AuthenticatorAttestationResponse implements AuthenticatorResponse { - /** - * Contains an attestation object, which is opaque to, and cryptographically protected against tampering by, the - * client. The attestation object contains both authenticator data and an attestation statement. The former contains - * the AAGUID, a unique credential ID, and the credential public key. The contents of the attestation statement are - * determined by the attestation statement format used by the authenticator. It also contains any additional - * information that the Relying Party's server requires to validate the attestation statement, as well as to decode - * and validate the authenticator data along with the JSON-serialized client data. For more details, see §6.4 Attestation, §6.4.4 Generating an - * Attestation Object, and Figure - * 5. - */ - @NonNull - private final ByteArray attestationObject; - - @NonNull - @Getter(onMethod = @__({ @Override })) - private final ByteArray clientDataJSON; - - /** - * The {@link #attestationObject} parsed as a domain object. - */ - @NonNull - @JsonIgnore - private final transient AttestationObject attestation; - - @NonNull - @JsonIgnore - @Getter(onMethod = @__({ @Override })) - private final transient CollectedClientData clientData; - - @Override - @JsonIgnore - public ByteArray getAuthenticatorData() { - return attestation.getAuthenticatorData().getBytes(); - } - - @Builder(toBuilder = true) - @JsonCreator - private AuthenticatorAttestationResponse( - @NonNull @JsonProperty("attestationObject") ByteArray attestationObject, - @NonNull @JsonProperty("clientDataJSON") ByteArray clientDataJSON - ) throws IOException, Base64UrlException { - this.attestationObject = attestationObject; - this.clientDataJSON = clientDataJSON; - - attestation = new AttestationObject(attestationObject); - this.clientData = new CollectedClientData(clientDataJSON); - } - - public static AuthenticatorAttestationResponseBuilder.MandatoryStages builder() { - return new AuthenticatorAttestationResponseBuilder.MandatoryStages(); - } - - public static class AuthenticatorAttestationResponseBuilder { - public static class MandatoryStages { - private final AuthenticatorAttestationResponseBuilder builder = new AuthenticatorAttestationResponseBuilder(); - - public Step2 attestationObject(ByteArray attestationObject) { - builder.attestationObject(attestationObject); - return new Step2(); - } - - public class Step2 { - public AuthenticatorAttestationResponseBuilder clientDataJSON(ByteArray clientDataJSON) { - return builder.clientDataJSON(clientDataJSON); - } - } + /** + * Contains an attestation object, which is opaque to, and cryptographically protected against + * tampering by, the client. The attestation object contains both authenticator data and an + * attestation statement. The former contains the AAGUID, a unique credential ID, and the + * credential public key. The contents of the attestation statement are determined by the + * attestation statement format used by the authenticator. It also contains any additional + * information that the Relying Party's server requires to validate the attestation statement, as + * well as to decode and validate the authenticator data along with the JSON-serialized client + * data. For more details, see §6.4 Attestation, + * §6.4.4 + * Generating an Attestation Object, and Figure 5. + */ + @NonNull private final ByteArray attestationObject; + + @NonNull + @Getter(onMethod = @__({@Override})) + private final ByteArray clientDataJSON; + + /** The {@link #attestationObject} parsed as a domain object. */ + @NonNull @JsonIgnore private final transient AttestationObject attestation; + + @NonNull + @JsonIgnore + @Getter(onMethod = @__({@Override})) + private final transient CollectedClientData clientData; + + @Override + @JsonIgnore + public ByteArray getAuthenticatorData() { + return attestation.getAuthenticatorData().getBytes(); + } + + @Builder(toBuilder = true) + @JsonCreator + private AuthenticatorAttestationResponse( + @NonNull @JsonProperty("attestationObject") ByteArray attestationObject, + @NonNull @JsonProperty("clientDataJSON") ByteArray clientDataJSON) + throws IOException, Base64UrlException { + this.attestationObject = attestationObject; + this.clientDataJSON = clientDataJSON; + + attestation = new AttestationObject(attestationObject); + this.clientData = new CollectedClientData(clientDataJSON); + } + + public static AuthenticatorAttestationResponseBuilder.MandatoryStages builder() { + return new AuthenticatorAttestationResponseBuilder.MandatoryStages(); + } + + public static class AuthenticatorAttestationResponseBuilder { + public static class MandatoryStages { + private final AuthenticatorAttestationResponseBuilder builder = + new AuthenticatorAttestationResponseBuilder(); + + public Step2 attestationObject(ByteArray attestationObject) { + builder.attestationObject(attestationObject); + return new Step2(); + } + + public class Step2 { + public AuthenticatorAttestationResponseBuilder clientDataJSON(ByteArray clientDataJSON) { + return builder.clientDataJSON(clientDataJSON); } + } } - + } } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorData.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorData.java index 6f61ab86a..64d96490a 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorData.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorData.java @@ -41,240 +41,225 @@ import lombok.NonNull; import lombok.Value; - /** - * The authenticator data structure is a byte array of 37 bytes or more. This class presents the authenticator data - * decoded as a high-level object. + * The authenticator data structure is a byte array of 37 bytes or more. This class presents the + * authenticator data decoded as a high-level object. * - *

- * The authenticator data structure encodes contextual bindings made by the authenticator. These bindings are controlled - * by the authenticator itself, and derive their trust from the WebAuthn Relying Party's assessment of the security - * properties of the authenticator. In one extreme case, the authenticator may be embedded in the client, and its - * bindings may be no more trustworthy than the client data. At the other extreme, the authenticator may be a discrete - * entity with high-security hardware and software, connected to the client over a secure channel. In both cases, the - * Relying Party receives the authenticator data in the same format, and uses its knowledge of the authenticator to make - * trust decisions. - *

+ *

The authenticator data structure encodes contextual bindings made by the authenticator. These + * bindings are controlled by the authenticator itself, and derive their trust from the WebAuthn + * Relying Party's assessment of the security properties of the authenticator. In one extreme case, + * the authenticator may be embedded in the client, and its bindings may be no more trustworthy than + * the client data. At the other extreme, the authenticator may be a discrete entity with + * high-security hardware and software, connected to the client over a secure channel. In both + * cases, the Relying Party receives the authenticator data in the same format, and uses its + * knowledge of the authenticator to make trust decisions. * - * @see §6.1. Authenticator Data + * @see §6.1. + * Authenticator Data */ @Value @JsonSerialize(using = AuthenticatorData.JsonSerializer.class) public class AuthenticatorData { - /** - * The original raw byte array that this object is decoded from. This is a byte array of 37 bytes or more. - * - * @see §6.1. Authenticator - * Data - */ - @NonNull - private final ByteArray bytes; - - /** - * The flags bit field. - */ - @NonNull - private final transient AuthenticatorDataFlags flags; - - /** - * Attested credential data, if present. - * - *

- * This member is present if and only if the {@link AuthenticatorDataFlags#AT} flag is set. - *

- * - * @see #flags - */ - private final transient AttestedCredentialData attestedCredentialData; - - private final transient CBORObject extensions; - - private static final int RP_ID_HASH_INDEX = 0; - private static final int RP_ID_HASH_END = RP_ID_HASH_INDEX + 32; - - private static final int FLAGS_INDEX = RP_ID_HASH_END; - private static final int FLAGS_END = FLAGS_INDEX + 1; - - private static final int COUNTER_INDEX = FLAGS_END; - private static final int COUNTER_END = COUNTER_INDEX + 4; - - private static final int FIXED_LENGTH_PART_END_INDEX = COUNTER_END; - - /** - * Decode an {@link AuthenticatorData} object from a raw authenticator data byte array. - */ - @JsonCreator - public AuthenticatorData(@NonNull ByteArray bytes) { - ExceptionUtil.assure( - bytes.size() >= FIXED_LENGTH_PART_END_INDEX, - "%s byte array must be at least %d bytes, was %d: %s", - AuthenticatorData.class.getSimpleName(), - FIXED_LENGTH_PART_END_INDEX, - bytes.size(), - bytes.getBase64Url() - ); - - this.bytes = bytes; - - final byte[] rawBytes = bytes.getBytes(); - - this.flags = new AuthenticatorDataFlags(rawBytes[FLAGS_INDEX]); - - if (flags.AT) { - VariableLengthParseResult parseResult = parseAttestedCredentialData( - flags, - Arrays.copyOfRange(rawBytes, FIXED_LENGTH_PART_END_INDEX, rawBytes.length) - ); - attestedCredentialData = parseResult.getAttestedCredentialData(); - extensions = parseResult.getExtensions(); - } else if (flags.ED) { - attestedCredentialData = null; - extensions = parseExtensions(Arrays.copyOfRange(rawBytes, FIXED_LENGTH_PART_END_INDEX, rawBytes.length)); - } else { - attestedCredentialData = null; - extensions = null; - } - } - - /** - * The SHA-256 hash of the RP ID the credential is scoped to. - */ - @JsonProperty("rpIdHash") - public ByteArray getRpIdHash() { - return new ByteArray(Arrays.copyOfRange(bytes.getBytes(), RP_ID_HASH_INDEX, RP_ID_HASH_END)); - } - - /** - * The 32-bit unsigned signature counter. - */ - public long getSignatureCounter() { - return BinaryUtil.getUint32(Arrays.copyOfRange(bytes.getBytes(), COUNTER_INDEX, COUNTER_END)); - } - - private static VariableLengthParseResult parseAttestedCredentialData(AuthenticatorDataFlags flags, byte[] bytes) { - final int AAGUID_INDEX = 0; - final int AAGUID_END = AAGUID_INDEX + 16; - - final int CREDENTIAL_ID_LENGTH_INDEX = AAGUID_END; - final int CREDENTIAL_ID_LENGTH_END = CREDENTIAL_ID_LENGTH_INDEX + 2; - - ExceptionUtil.assure( - bytes.length >= CREDENTIAL_ID_LENGTH_END, - "Attested credential data must contain at least %d bytes, was %d: %s", - CREDENTIAL_ID_LENGTH_END, - bytes.length, - new ByteArray(bytes).getHex() - ); - - byte[] credentialIdLengthBytes = Arrays.copyOfRange(bytes, CREDENTIAL_ID_LENGTH_INDEX, CREDENTIAL_ID_LENGTH_END); - - final int L; - try { - L = BinaryUtil.getUint16(credentialIdLengthBytes); - } catch (IllegalArgumentException e) { - throw new IllegalArgumentException("Invalid credential ID length bytes: " + Arrays.asList(credentialIdLengthBytes), e); - } - - final int CREDENTIAL_ID_INDEX = CREDENTIAL_ID_LENGTH_END; - final int CREDENTIAL_ID_END = CREDENTIAL_ID_INDEX + L; - - final int CREDENTIAL_PUBLIC_KEY_INDEX = CREDENTIAL_ID_END; - final int CREDENTIAL_PUBLIC_KEY_AND_EXTENSION_DATA_END = bytes.length; - - ExceptionUtil.assure( - bytes.length >= CREDENTIAL_ID_END, - "Expected credential ID of length %d, but attested credential data and extension data is only %d bytes: %s", - CREDENTIAL_ID_END, - bytes.length, - new ByteArray(bytes).getHex() - ); - - ByteArrayInputStream indefiniteLengthBytes = new ByteArrayInputStream( - Arrays.copyOfRange(bytes, CREDENTIAL_PUBLIC_KEY_INDEX, CREDENTIAL_PUBLIC_KEY_AND_EXTENSION_DATA_END) - ); - - final CBORObject credentialPublicKey = CBORObject.Read(indefiniteLengthBytes); - final CBORObject extensions; - - if (flags.ED && indefiniteLengthBytes.available() > 0) { - try { - extensions = CBORObject.Read(indefiniteLengthBytes); - } catch (CBORException e) { - throw new IllegalArgumentException("Failed to parse extension data", e); - } - } else if (indefiniteLengthBytes.available() > 0) { - throw new IllegalArgumentException(String.format( - "Flags indicate no extension data, but %d bytes remain after attested credential data.", - indefiniteLengthBytes.available() - )); - } else if (flags.ED) { - throw new IllegalArgumentException( - "Flags indicate there should be extension data, but no bytes remain after attested credential data." - ); - } else { - extensions = null; - } - - return new VariableLengthParseResult( - AttestedCredentialData.builder() - .aaguid(new ByteArray(Arrays.copyOfRange(bytes, AAGUID_INDEX, AAGUID_END))) - .credentialId(new ByteArray(Arrays.copyOfRange(bytes, CREDENTIAL_ID_INDEX, CREDENTIAL_ID_END))) - .credentialPublicKey(new ByteArray(credentialPublicKey.EncodeToBytes())) - .build(), - extensions - ); - } - - private static CBORObject parseExtensions(byte[] bytes) { - try { - return CBORObject.DecodeFromBytes(bytes); - } catch (CBORException e) { - throw new IllegalArgumentException("Failed to parse extension data", e); - } + /** + * The original raw byte array that this object is decoded from. This is a byte array of 37 bytes + * or more. + * + * @see §6.1. + * Authenticator Data + */ + @NonNull private final ByteArray bytes; + + /** The flags bit field. */ + @NonNull private final transient AuthenticatorDataFlags flags; + + /** + * Attested credential data, if present. + * + *

This member is present if and only if the {@link AuthenticatorDataFlags#AT} flag is set. + * + * @see #flags + */ + private final transient AttestedCredentialData attestedCredentialData; + + private final transient CBORObject extensions; + + private static final int RP_ID_HASH_INDEX = 0; + private static final int RP_ID_HASH_END = RP_ID_HASH_INDEX + 32; + + private static final int FLAGS_INDEX = RP_ID_HASH_END; + private static final int FLAGS_END = FLAGS_INDEX + 1; + + private static final int COUNTER_INDEX = FLAGS_END; + private static final int COUNTER_END = COUNTER_INDEX + 4; + + private static final int FIXED_LENGTH_PART_END_INDEX = COUNTER_END; + + /** Decode an {@link AuthenticatorData} object from a raw authenticator data byte array. */ + @JsonCreator + public AuthenticatorData(@NonNull ByteArray bytes) { + ExceptionUtil.assure( + bytes.size() >= FIXED_LENGTH_PART_END_INDEX, + "%s byte array must be at least %d bytes, was %d: %s", + AuthenticatorData.class.getSimpleName(), + FIXED_LENGTH_PART_END_INDEX, + bytes.size(), + bytes.getBase64Url()); + + this.bytes = bytes; + + final byte[] rawBytes = bytes.getBytes(); + + this.flags = new AuthenticatorDataFlags(rawBytes[FLAGS_INDEX]); + + if (flags.AT) { + VariableLengthParseResult parseResult = + parseAttestedCredentialData( + flags, Arrays.copyOfRange(rawBytes, FIXED_LENGTH_PART_END_INDEX, rawBytes.length)); + attestedCredentialData = parseResult.getAttestedCredentialData(); + extensions = parseResult.getExtensions(); + } else if (flags.ED) { + attestedCredentialData = null; + extensions = + parseExtensions( + Arrays.copyOfRange(rawBytes, FIXED_LENGTH_PART_END_INDEX, rawBytes.length)); + } else { + attestedCredentialData = null; + extensions = null; } - - @Value - private static class VariableLengthParseResult { - AttestedCredentialData attestedCredentialData; - CBORObject extensions; + } + + /** The SHA-256 hash of the RP ID the credential is scoped to. */ + @JsonProperty("rpIdHash") + public ByteArray getRpIdHash() { + return new ByteArray(Arrays.copyOfRange(bytes.getBytes(), RP_ID_HASH_INDEX, RP_ID_HASH_END)); + } + + /** The 32-bit unsigned signature counter. */ + public long getSignatureCounter() { + return BinaryUtil.getUint32(Arrays.copyOfRange(bytes.getBytes(), COUNTER_INDEX, COUNTER_END)); + } + + private static VariableLengthParseResult parseAttestedCredentialData( + AuthenticatorDataFlags flags, byte[] bytes) { + final int AAGUID_INDEX = 0; + final int AAGUID_END = AAGUID_INDEX + 16; + + final int CREDENTIAL_ID_LENGTH_INDEX = AAGUID_END; + final int CREDENTIAL_ID_LENGTH_END = CREDENTIAL_ID_LENGTH_INDEX + 2; + + ExceptionUtil.assure( + bytes.length >= CREDENTIAL_ID_LENGTH_END, + "Attested credential data must contain at least %d bytes, was %d: %s", + CREDENTIAL_ID_LENGTH_END, + bytes.length, + new ByteArray(bytes).getHex()); + + byte[] credentialIdLengthBytes = + Arrays.copyOfRange(bytes, CREDENTIAL_ID_LENGTH_INDEX, CREDENTIAL_ID_LENGTH_END); + + final int L; + try { + L = BinaryUtil.getUint16(credentialIdLengthBytes); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException( + "Invalid credential ID length bytes: " + Arrays.asList(credentialIdLengthBytes), e); } - /** - * Attested credential data, if present. - * - *

- * This member is present if and only if the {@link AuthenticatorDataFlags#AT} flag is set. - *

- * - * @see #flags - */ - public Optional getAttestedCredentialData() { - return Optional.ofNullable(attestedCredentialData); + final int CREDENTIAL_ID_INDEX = CREDENTIAL_ID_LENGTH_END; + final int CREDENTIAL_ID_END = CREDENTIAL_ID_INDEX + L; + + final int CREDENTIAL_PUBLIC_KEY_INDEX = CREDENTIAL_ID_END; + final int CREDENTIAL_PUBLIC_KEY_AND_EXTENSION_DATA_END = bytes.length; + + ExceptionUtil.assure( + bytes.length >= CREDENTIAL_ID_END, + "Expected credential ID of length %d, but attested credential data and extension data is only %d bytes: %s", + CREDENTIAL_ID_END, + bytes.length, + new ByteArray(bytes).getHex()); + + ByteArrayInputStream indefiniteLengthBytes = + new ByteArrayInputStream( + Arrays.copyOfRange( + bytes, CREDENTIAL_PUBLIC_KEY_INDEX, CREDENTIAL_PUBLIC_KEY_AND_EXTENSION_DATA_END)); + + final CBORObject credentialPublicKey = CBORObject.Read(indefiniteLengthBytes); + final CBORObject extensions; + + if (flags.ED && indefiniteLengthBytes.available() > 0) { + try { + extensions = CBORObject.Read(indefiniteLengthBytes); + } catch (CBORException e) { + throw new IllegalArgumentException("Failed to parse extension data", e); + } + } else if (indefiniteLengthBytes.available() > 0) { + throw new IllegalArgumentException( + String.format( + "Flags indicate no extension data, but %d bytes remain after attested credential data.", + indefiniteLengthBytes.available())); + } else if (flags.ED) { + throw new IllegalArgumentException( + "Flags indicate there should be extension data, but no bytes remain after attested credential data."); + } else { + extensions = null; } - /** - * Extension-defined authenticator data, if present. - * - *

- * This member is present if and only if the {@link AuthenticatorDataFlags#ED} flag is set. - *

- * - *

- * Changes to the returned value are not reflected in the {@link AuthenticatorData} object. - *

- * - * @see #flags - */ - public Optional getExtensions() { - return Optional.ofNullable(extensions).map(JacksonCodecs::deepCopy); + return new VariableLengthParseResult( + AttestedCredentialData.builder() + .aaguid(new ByteArray(Arrays.copyOfRange(bytes, AAGUID_INDEX, AAGUID_END))) + .credentialId( + new ByteArray(Arrays.copyOfRange(bytes, CREDENTIAL_ID_INDEX, CREDENTIAL_ID_END))) + .credentialPublicKey(new ByteArray(credentialPublicKey.EncodeToBytes())) + .build(), + extensions); + } + + private static CBORObject parseExtensions(byte[] bytes) { + try { + return CBORObject.DecodeFromBytes(bytes); + } catch (CBORException e) { + throw new IllegalArgumentException("Failed to parse extension data", e); } - - static class JsonSerializer extends com.fasterxml.jackson.databind.JsonSerializer { - @Override - public void serialize(AuthenticatorData value, JsonGenerator gen, SerializerProvider serializers) throws IOException { - gen.writeString(value.getBytes().getBase64Url()); - } + } + + @Value + private static class VariableLengthParseResult { + AttestedCredentialData attestedCredentialData; + CBORObject extensions; + } + + /** + * Attested credential data, if present. + * + *

This member is present if and only if the {@link AuthenticatorDataFlags#AT} flag is set. + * + * @see #flags + */ + public Optional getAttestedCredentialData() { + return Optional.ofNullable(attestedCredentialData); + } + + /** + * Extension-defined authenticator data, if present. + * + *

This member is present if and only if the {@link AuthenticatorDataFlags#ED} flag is set. + * + *

Changes to the returned value are not reflected in the {@link AuthenticatorData} object. + * + * @see #flags + */ + public Optional getExtensions() { + return Optional.ofNullable(extensions).map(JacksonCodecs::deepCopy); + } + + static class JsonSerializer + extends com.fasterxml.jackson.databind.JsonSerializer { + @Override + public void serialize( + AuthenticatorData value, JsonGenerator gen, SerializerProvider serializers) + throws IOException { + gen.writeString(value.getBytes().getBase64Url()); } - + } } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorDataFlags.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorDataFlags.java index 4063a077d..043206d09 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorDataFlags.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorDataFlags.java @@ -37,58 +37,51 @@ @ToString @EqualsAndHashCode public final class AuthenticatorDataFlags { - public final byte value; + public final byte value; - /** - * User present - */ - public final boolean UP; + /** User present */ + public final boolean UP; - /** User verified */ - public final boolean UV; + /** User verified */ + public final boolean UV; - /** - * Attested credential data present. - * - *

- * Users of this library should not need to inspect this value directly. - *

- * - * @see AuthenticatorData#getAttestedCredentialData() - */ - public final boolean AT; + /** + * Attested credential data present. + * + *

Users of this library should not need to inspect this value directly. + * + * @see AuthenticatorData#getAttestedCredentialData() + */ + public final boolean AT; - /** - * Extension data present. - * - * @see AuthenticatorData#getExtensions() - */ - public final boolean ED; + /** + * Extension data present. + * + * @see AuthenticatorData#getExtensions() + */ + public final boolean ED; - /** - * Decode an {@link AuthenticatorDataFlags} object from a raw bit field byte. - */ - @JsonCreator - public AuthenticatorDataFlags(@JsonProperty("value") byte value) { - this.value = value; + /** Decode an {@link AuthenticatorDataFlags} object from a raw bit field byte. */ + @JsonCreator + public AuthenticatorDataFlags(@JsonProperty("value") byte value) { + this.value = value; - UP = (value & Bitmasks.UP) != 0; - UV = (value & Bitmasks.UV) != 0; - AT = (value & Bitmasks.AT) != 0; - ED = (value & Bitmasks.ED) != 0; - } + UP = (value & Bitmasks.UP) != 0; + UV = (value & Bitmasks.UV) != 0; + AT = (value & Bitmasks.AT) != 0; + ED = (value & Bitmasks.ED) != 0; + } - private static final class Bitmasks { - static final byte UP = 0x01; - static final byte UV = 0x04; - static final byte AT = 0x40; - static final byte ED = -0x80; - - /* Reserved bits */ - // final boolean RFU1 = (value & 0x02) > 0; - // final boolean RFU2_1 = (value & 0x08) > 0; - // final boolean RFU2_2 = (value & 0x10) > 0; - // static final boolean RFU2_3 = (value & 0x20) > 0; - } + private static final class Bitmasks { + static final byte UP = 0x01; + static final byte UV = 0x04; + static final byte AT = 0x40; + static final byte ED = -0x80; + /* Reserved bits */ + // final boolean RFU1 = (value & 0x02) > 0; + // final boolean RFU2_1 = (value & 0x08) > 0; + // final boolean RFU2_2 = (value & 0x10) > 0; + // static final boolean RFU2_3 = (value & 0x20) > 0; + } } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorResponse.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorResponse.java index e3217e9f0..d2d20be12 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorResponse.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorResponse.java @@ -30,39 +30,35 @@ * Authenticators respond to Relying Party requests by returning an object derived from the {@link * AuthenticatorResponse} interface. * - * @see §5.2. Authenticator Responses - * (interface AuthenticatorResponse) - * + * @see §5.2. + * Authenticator Responses (interface AuthenticatorResponse) */ public interface AuthenticatorResponse { - /** - * The authenticator data returned by the authenticator. See §6.1 - * Authenticator Data. - */ - ByteArray getAuthenticatorData(); + /** + * The authenticator data returned by the authenticator. See §6.1 + * Authenticator Data. + */ + ByteArray getAuthenticatorData(); - /** - * {@link #getAuthenticatorData()} parsed as a domain object. - */ - @JsonIgnore - default AuthenticatorData getParsedAuthenticatorData() { - return new AuthenticatorData(getAuthenticatorData()); - } + /** {@link #getAuthenticatorData()} parsed as a domain object. */ + @JsonIgnore + default AuthenticatorData getParsedAuthenticatorData() { + return new AuthenticatorData(getAuthenticatorData()); + } - /** - * The JSON-serialized client data (see §5.10.1 - * Client Data Used in WebAuthn Signatures (dictionary {@link CollectedClientData})) passed to the authenticator - * by the client in the call to either navigator.credentials.create() or - * navigator.credentials.get(). The exact JSON serialization MUST be preserved, as the hash of the - * serialized client data has been computed over it. - */ - ByteArray getClientDataJSON(); - - /** - * {@link #getClientDataJSON()} parsed as a domain object. - */ - @JsonIgnore - CollectedClientData getClientData(); + /** + * The JSON-serialized client data (see §5.10.1 Client Data + * Used in WebAuthn Signatures (dictionary {@link CollectedClientData})) passed to the + * authenticator by the client in the call to either navigator.credentials.create() + * or navigator.credentials.get(). The exact JSON serialization MUST be preserved, as + * the hash of the serialized client data has been computed over it. + */ + ByteArray getClientDataJSON(); + /** {@link #getClientDataJSON()} parsed as a domain object. */ + @JsonIgnore + CollectedClientData getClientData(); } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorSelectionCriteria.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorSelectionCriteria.java index c46ac95ca..c36b17180 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorSelectionCriteria.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorSelectionCriteria.java @@ -31,83 +31,82 @@ import lombok.NonNull; import lombok.Value; - /** * This class may be used to specify requirements regarding authenticator attributes. * - * @see §5.4.4. - * Authenticator Selection Criteria (dictionary AuthenticatorSelectionCriteria) - * + * @see §5.4.4. + * Authenticator Selection Criteria (dictionary AuthenticatorSelectionCriteria) */ @Value @Builder(toBuilder = true) public class AuthenticatorSelectionCriteria { - /** - * If present, eligible authenticators are filtered to only authenticators attached with the specified §5.4.5 Authenticator Attachment Enumeration - * (enum AuthenticatorAttachment). - */ - private final AuthenticatorAttachment authenticatorAttachment; + /** + * If present, eligible authenticators are filtered to only authenticators attached with the + * specified §5.4.5 + * Authenticator Attachment Enumeration (enum AuthenticatorAttachment). + */ + private final AuthenticatorAttachment authenticatorAttachment; - /** - * Describes the Relying Party's requirements regarding resident credentials. If set to true, the - * authenticator MUST create a client-side-resident - * public key credential source when creating a public key credential. - */ - @Builder.Default - private final boolean requireResidentKey = false; + /** + * Describes the Relying Party's requirements regarding resident credentials. If set to true + * , the authenticator MUST create a client-side-resident + * public key credential source when creating a public key credential. + */ + @Builder.Default private final boolean requireResidentKey = false; - /** - * Describes the Relying Party's requirements regarding user - * verification for the - * navigator.credentials.create() operation. Eligible authenticators are filtered to only those - * capable of satisfying this requirement. - */ - @NonNull - @Builder.Default - private UserVerificationRequirement userVerification = UserVerificationRequirement.PREFERRED; + /** + * Describes the Relying Party's requirements regarding user verification + * for the navigator.credentials.create() operation. Eligible authenticators are + * filtered to only those capable of satisfying this requirement. + */ + @NonNull @Builder.Default + private UserVerificationRequirement userVerification = UserVerificationRequirement.PREFERRED; - /** - * If present, eligible authenticators are filtered to only authenticators attached with the specified §5.4.5 Authenticator Attachment Enumeration - * (enum AuthenticatorAttachment). - */ - public Optional getAuthenticatorAttachment() { - return Optional.ofNullable(authenticatorAttachment); - } + /** + * If present, eligible authenticators are filtered to only authenticators attached with the + * specified §5.4.5 + * Authenticator Attachment Enumeration (enum AuthenticatorAttachment). + */ + public Optional getAuthenticatorAttachment() { + return Optional.ofNullable(authenticatorAttachment); + } - @JsonCreator - private AuthenticatorSelectionCriteria( - @JsonProperty("authenticatorAttachment") AuthenticatorAttachment authenticatorAttachment, - @JsonProperty("requireResidentKey") boolean requireResidentKey, - @NonNull @JsonProperty("userVerification") UserVerificationRequirement userVerification - ) { - this.authenticatorAttachment = authenticatorAttachment; - this.requireResidentKey = requireResidentKey; - this.userVerification = userVerification; - } + @JsonCreator + private AuthenticatorSelectionCriteria( + @JsonProperty("authenticatorAttachment") AuthenticatorAttachment authenticatorAttachment, + @JsonProperty("requireResidentKey") boolean requireResidentKey, + @NonNull @JsonProperty("userVerification") UserVerificationRequirement userVerification) { + this.authenticatorAttachment = authenticatorAttachment; + this.requireResidentKey = requireResidentKey; + this.userVerification = userVerification; + } - public static class AuthenticatorSelectionCriteriaBuilder { - private AuthenticatorAttachment authenticatorAttachment = null; + public static class AuthenticatorSelectionCriteriaBuilder { + private AuthenticatorAttachment authenticatorAttachment = null; - /** - * If present, eligible authenticators are filtered to only authenticators attached with the specified §5.4.5 Authenticator Attachment Enumeration - * (enum AuthenticatorAttachment). - */ - public AuthenticatorSelectionCriteriaBuilder authenticatorAttachment(@NonNull Optional authenticatorAttachment) { - return this.authenticatorAttachment(authenticatorAttachment.orElse(null)); - } + /** + * If present, eligible authenticators are filtered to only authenticators attached with the + * specified §5.4.5 + * Authenticator Attachment Enumeration (enum AuthenticatorAttachment). + */ + public AuthenticatorSelectionCriteriaBuilder authenticatorAttachment( + @NonNull Optional authenticatorAttachment) { + return this.authenticatorAttachment(authenticatorAttachment.orElse(null)); + } - /** - * If present, eligible authenticators are filtered to only authenticators attached with the specified §5.4.5 Authenticator Attachment Enumeration - * (enum AuthenticatorAttachment). - */ - public AuthenticatorSelectionCriteriaBuilder authenticatorAttachment(AuthenticatorAttachment authenticatorAttachment) { - this.authenticatorAttachment = authenticatorAttachment; - return this; - } + /** + * If present, eligible authenticators are filtered to only authenticators attached with the + * specified §5.4.5 + * Authenticator Attachment Enumeration (enum AuthenticatorAttachment). + */ + public AuthenticatorSelectionCriteriaBuilder authenticatorAttachment( + AuthenticatorAttachment authenticatorAttachment) { + this.authenticatorAttachment = authenticatorAttachment; + return this; } + } } 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 cbd0621f6..ee10d23e3 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 @@ -24,7 +24,6 @@ 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; @@ -36,98 +35,104 @@ import lombok.Value; /** - * Authenticators may communicate with Clients using a variety of transports. This enumeration defines a hint as to how - * Clients might communicate with a particular Authenticator in order to obtain an assertion for a specific credential. - * Note that these hints represent the Relying Party's best belief as to how an Authenticator may be reached. A Relying - * Party may obtain a list of transports hints from some attestation statement formats or via some out-of-band - * mechanism; it is outside the scope of this specification to define that mechanism. - *

- * Authenticators may implement various transports for communicating with clients. This enumeration defines hints as to - * how clients might communicate with a particular authenticator in order to obtain an assertion for a specific - * credential. Note that these hints represent the WebAuthn Relying Party's best belief as to how an authenticator may - * be reached. A Relying Party may obtain a list of transports hints from some attestation statement formats or via some - * out-of-band mechanism; it is outside the scope of the Web Authentication specification to define that mechanism. - *

+ * Authenticators may communicate with Clients using a variety of transports. This enumeration + * defines a hint as to how Clients might communicate with a particular Authenticator in order to + * obtain an assertion for a specific credential. Note that these hints represent the Relying + * Party's best belief as to how an Authenticator may be reached. A Relying Party may obtain a list + * of transports hints from some attestation statement formats or via some out-of-band mechanism; it + * is outside the scope of this specification to define that mechanism. + * + *

Authenticators may implement various transports for communicating with clients. This + * enumeration defines hints as to how clients might communicate with a particular authenticator in + * order to obtain an assertion for a specific credential. Note that these hints represent the + * WebAuthn Relying Party's best belief as to how an authenticator may be reached. A Relying Party + * may obtain a list of transports hints from some attestation statement formats or via some + * out-of-band mechanism; it is outside the scope of the Web Authentication specification to define + * that mechanism. * - * @see §5.10.4. Authenticator - * Transport Enumeration (enum AuthenticatorTransport) + * @see §5.10.4. + * Authenticator Transport Enumeration (enum AuthenticatorTransport) */ @JsonSerialize(using = JsonStringSerializer.class) @Value @AllArgsConstructor(access = AccessLevel.PRIVATE) -public class AuthenticatorTransport implements Comparable, JsonStringSerializable { +public class AuthenticatorTransport + implements Comparable, JsonStringSerializable { - @NonNull - private final String id; + @NonNull private final String id; - /** - * Indicates the respective authenticator can be contacted over removable USB. - */ - public static final AuthenticatorTransport USB = new AuthenticatorTransport("usb"); + /** Indicates the respective authenticator can be contacted over removable USB. */ + public static final AuthenticatorTransport USB = new AuthenticatorTransport("usb"); - /** - * Indicates the respective authenticator can be contacted over Near Field Communication (NFC). - */ - public static final AuthenticatorTransport NFC = new AuthenticatorTransport("nfc"); + /** + * Indicates the respective authenticator can be contacted over Near Field Communication (NFC). + */ + public static final AuthenticatorTransport NFC = new AuthenticatorTransport("nfc"); - /** - * Indicates the respective authenticator can be contacted over Bluetooth Smart (Bluetooth Low Energy / BLE). - */ - public static final AuthenticatorTransport BLE = new AuthenticatorTransport("ble"); + /** + * Indicates the respective authenticator can be contacted over Bluetooth Smart (Bluetooth Low + * Energy / 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. - */ - public static final AuthenticatorTransport INTERNAL = new AuthenticatorTransport("internal"); + /** + * Indicates the respective authenticator is contacted using a client device-specific transport. + * These authenticators are not removable from the client device. + */ + public static final AuthenticatorTransport INTERNAL = new AuthenticatorTransport("internal"); - /** - * @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 }; - } + /** + * @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}; + } - /** - * @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) - */ - @JsonCreator - public static AuthenticatorTransport of(@NonNull String id) { - return Stream.of(values()) - .filter(v -> v.getId().equals(id)) - .findAny() - .orElseGet(() -> new AuthenticatorTransport(id)); - } + /** + * @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) + */ + @JsonCreator + public static AuthenticatorTransport of(@NonNull String id) { + return Stream.of(values()) + .filter(v -> v.getId().equals(id)) + .findAny() + .orElseGet(() -> new AuthenticatorTransport(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 constant com.yubico.webauthn.data.AuthenticatorTransport." + name); - } + /** + * @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 constant com.yubico.webauthn.data.AuthenticatorTransport." + name); } + } - @Override - public String toJsonString() { - return id; - } - - @Override - public int compareTo(AuthenticatorTransport other) { - return id.compareTo(other.id); - } + @Override + 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/ByteArray.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/ByteArray.java index 13b2922d7..83f8b8d43 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/ByteArray.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/ByteArray.java @@ -37,138 +37,120 @@ import lombok.NonNull; import lombok.ToString; -/** - * An immutable byte array with support for encoding/decoding to/from various encodings. - */ +/** An immutable byte array with support for encoding/decoding to/from various encodings. */ @JsonSerialize(using = JsonStringSerializer.class) @EqualsAndHashCode @ToString(includeFieldNames = false, onlyExplicitlyIncluded = true) public final class ByteArray implements Comparable, JsonStringSerializable { - private final static Base64.Encoder BASE64_ENCODER = Base64.getEncoder(); - private final static Base64.Decoder BASE64_DECODER = Base64.getDecoder(); + private static final Base64.Encoder BASE64_ENCODER = Base64.getEncoder(); + private static final Base64.Decoder BASE64_DECODER = Base64.getDecoder(); - private final static Base64.Encoder BASE64URL_ENCODER = Base64.getUrlEncoder().withoutPadding(); - private final static Base64.Decoder BASE64URL_DECODER = Base64.getUrlDecoder(); + private static final Base64.Encoder BASE64URL_ENCODER = Base64.getUrlEncoder().withoutPadding(); + private static final Base64.Decoder BASE64URL_DECODER = Base64.getUrlDecoder(); - @NonNull - private final byte[] bytes; + @NonNull private final byte[] bytes; - @NonNull - private final String base64; + @NonNull private final String base64; - /** - * Create a new instance by copying the contents of bytes. - */ - public ByteArray(@NonNull byte[] bytes) { - this.bytes = BinaryUtil.copy(bytes); - this.base64 = BASE64URL_ENCODER.encodeToString(this.bytes); - } - - private ByteArray(String base64) throws Base64UrlException { - try { - this.bytes = BASE64URL_DECODER.decode(base64); - } catch (IllegalArgumentException e) { - throw new Base64UrlException("Invalid Base64Url encoding: " + base64, e); - } - this.base64 = base64; - } - - /** - * Create a new instance by decoding base64 as classic Base64 data. - */ - public static ByteArray fromBase64(@NonNull final String base64) { - return new ByteArray(BASE64_DECODER.decode(base64)); - } - - /** - * Create a new instance by decoding base64 as Base64Url data. - * - * @throws Base64UrlException if base64 is not valid Base64Url data. - */ - @JsonCreator - public static ByteArray fromBase64Url(@NonNull final String base64) throws Base64UrlException { - return new ByteArray(base64); - } - - /** - * Create a new instance by decoding hex as hexadecimal data. - * - * @throws HexException if hex is not valid hexadecimal data. - */ - public static ByteArray fromHex(@NonNull final String hex) throws HexException { - try { - return new ByteArray(BinaryUtil.fromHex(hex)); - } catch (Exception e) { - throw new HexException("Invalid hexadecimal encoding: " + hex, e); - } - } + /** Create a new instance by copying the contents of bytes. */ + public ByteArray(@NonNull byte[] bytes) { + this.bytes = BinaryUtil.copy(bytes); + this.base64 = BASE64URL_ENCODER.encodeToString(this.bytes); + } - /** - * @return a new instance containing a copy of this instance followed by a copy of tail. - */ - public ByteArray concat(@NonNull ByteArray tail) { - return new ByteArray(Bytes.concat(this.bytes, tail.bytes)); + private ByteArray(String base64) throws Base64UrlException { + try { + this.bytes = BASE64URL_DECODER.decode(base64); + } catch (IllegalArgumentException e) { + throw new Base64UrlException("Invalid Base64Url encoding: " + base64, e); } - - public boolean isEmpty() { - return size() == 0; + this.base64 = base64; + } + + /** Create a new instance by decoding base64 as classic Base64 data. */ + public static ByteArray fromBase64(@NonNull final String base64) { + return new ByteArray(BASE64_DECODER.decode(base64)); + } + + /** + * Create a new instance by decoding base64 as Base64Url data. + * + * @throws Base64UrlException if base64 is not valid Base64Url data. + */ + @JsonCreator + public static ByteArray fromBase64Url(@NonNull final String base64) throws Base64UrlException { + return new ByteArray(base64); + } + + /** + * Create a new instance by decoding hex as hexadecimal data. + * + * @throws HexException if hex is not valid hexadecimal data. + */ + public static ByteArray fromHex(@NonNull final String hex) throws HexException { + try { + return new ByteArray(BinaryUtil.fromHex(hex)); + } catch (Exception e) { + throw new HexException("Invalid hexadecimal encoding: " + hex, e); } - - public int size() { - return this.bytes.length; + } + + /** + * @return a new instance containing a copy of this instance followed by a copy of tail + * . + */ + public ByteArray concat(@NonNull ByteArray tail) { + return new ByteArray(Bytes.concat(this.bytes, tail.bytes)); + } + + public boolean isEmpty() { + return size() == 0; + } + + public int size() { + return this.bytes.length; + } + + /** @return a copy of the raw byte contents. */ + public byte[] getBytes() { + return BinaryUtil.copy(bytes); + } + + /** @return the content bytes encoded as classic Base64 data. */ + public String getBase64() { + return BASE64_ENCODER.encodeToString(bytes); + } + + /** @return the content bytes encoded as Base64Url data. */ + public String getBase64Url() { + return base64; + } + + /** @return the content bytes encoded as hexadecimal data. */ + @ToString.Include + public String getHex() { + return BinaryUtil.toHex(bytes); + } + + /** Used by JSON serializer. */ + @Override + public String toJsonString() { + return base64; + } + + @Override + public int compareTo(ByteArray other) { + if (bytes.length != other.bytes.length) { + return bytes.length - other.bytes.length; } - /** - * @return a copy of the raw byte contents. - */ - public byte[] getBytes() { - return BinaryUtil.copy(bytes); - } - - /** - * @return the content bytes encoded as classic Base64 data. - */ - public String getBase64() { - return BASE64_ENCODER.encodeToString(bytes); - } - - /** - * @return the content bytes encoded as Base64Url data. - */ - public String getBase64Url() { - return base64; - } - - /** - * @return the content bytes encoded as hexadecimal data. - */ - @ToString.Include - public String getHex() { - return BinaryUtil.toHex(bytes); - } - - /** - * Used by JSON serializer. - */ - @Override - public String toJsonString() { - return base64; - } - - @Override - public int compareTo(ByteArray other) { - if (bytes.length != other.bytes.length) { - return bytes.length - other.bytes.length; - } - - for (int i = 0; i < bytes.length; ++i) { - if (bytes[i] != other.bytes[i]) { - return bytes[i] - other.bytes[i]; - } - } - - return 0; + for (int i = 0; i < bytes.length; ++i) { + if (bytes[i] != other.bytes[i]) { + return bytes[i] - other.bytes[i]; + } } + return 0; + } } 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 10ecc17ab..f977a9b22 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 @@ -33,38 +33,40 @@ import lombok.Getter; /** - * A number identifying a cryptographic algorithm. The algorithm identifiers SHOULD be values registered in the IANA - * COSE Algorithms registry, for instance, -7 for "ES256" and -257 for "RS256". + * A number identifying a cryptographic algorithm. The algorithm identifiers SHOULD be values + * registered in the IANA COSE Algorithms registry, for instance, -7 for "ES256" and -257 for + * "RS256". * - * @see §5.10.5. - * Cryptographic Algorithm Identifier (typedef COSEAlgorithmIdentifier) + * @see §5.10.5. + * Cryptographic Algorithm Identifier (typedef COSEAlgorithmIdentifier) */ @JsonSerialize(using = JsonLongSerializer.class) public enum COSEAlgorithmIdentifier implements JsonLongSerializable { - EdDSA(-8), - ES256(-7), - RS256(-257), - RS1(-65535); + EdDSA(-8), + ES256(-7), + RS256(-257), + RS1(-65535); - @Getter - private final long id; + @Getter private final long id; - COSEAlgorithmIdentifier(long id) { - this.id = id; - } + COSEAlgorithmIdentifier(long id) { + this.id = id; + } - public static Optional fromId(long id) { - return Stream.of(values()).filter(v -> v.id == id).findAny(); - } + public static Optional fromId(long id) { + return Stream.of(values()).filter(v -> v.id == id).findAny(); + } - @JsonCreator - private static COSEAlgorithmIdentifier fromJson(long id) { - return fromId(id).orElseThrow(() -> new IllegalArgumentException("Unknown COSE algorithm identifier: " + id)); - } - - @Override - public long toJsonNumber() { - return id; - } + @JsonCreator + private static COSEAlgorithmIdentifier fromJson(long id) { + return fromId(id) + .orElseThrow( + () -> new IllegalArgumentException("Unknown COSE algorithm identifier: " + id)); + } + @Override + public long toJsonNumber() { + return id; + } } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/ClientAssertionExtensionOutputs.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/ClientAssertionExtensionOutputs.java index a295297f7..b1b763fcd 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/ClientAssertionExtensionOutputs.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/ClientAssertionExtensionOutputs.java @@ -34,110 +34,96 @@ import lombok.Value; /** - * Contains client extension - * outputs from a - * navigator.credentials.get() operation. + * Contains client extension + * outputs from a navigator.credentials.get() operation. * - *

- * Note that there is no guarantee that any extension input present in {@link AssertionExtensionInputs} will have a - * corresponding output present here. - *

+ *

Note that there is no guarantee that any extension input present in {@link + * AssertionExtensionInputs} will have a corresponding output present here. * - *

- * The authenticator extension outputs are contained in the {@link AuthenticatorData} structure. - *

+ *

The authenticator extension outputs are contained in the {@link AuthenticatorData} structure. * - * @see §9. WebAuthn Extensions + * @see §9. WebAuthn + * Extensions */ @Value @Builder(toBuilder = true) public class ClientAssertionExtensionOutputs implements ClientExtensionOutputs { + /** + * The output from the FIDO AppID Extension (appid). + * + *

This value should be ignored because its behaviour is underspecified, see: https://github.com/w3c/webauthn/issues/1034. + * + * @see §10.1. + * FIDO AppID Extension (appid) + */ + private final Boolean appid; + + @JsonCreator + private ClientAssertionExtensionOutputs(@JsonProperty("appid") Boolean appid) { + this.appid = appid; + } + + @Override + public Set getExtensionIds() { + Set ids = new HashSet<>(); + + getAppid().ifPresent((id) -> ids.add("appid")); + + return ids; + } + + /** + * The output from the FIDO AppID Extension (appid). + * + *

This value should be ignored because its behaviour is underspecified, see: https://github.com/w3c/webauthn/issues/1034. + * + * @see §10.1. + * FIDO AppID Extension (appid) + */ + public Optional getAppid() { + return Optional.ofNullable(appid); + } + + public static class ClientAssertionExtensionOutputsBuilder { + private Boolean appid = null; + /** * The output from the FIDO AppID Extension (appid). * - *

- * This value should be ignored because its behaviour is underspecified, see: This value should be ignored because its behaviour is underspecified, see: https://github.com/w3c/webauthn/issues/1034. - *

* - * @see §10.1. FIDO AppID Extension - * (appid) + * @see §10.1. + * FIDO AppID Extension (appid) */ - private final Boolean appid; - - @JsonCreator - private ClientAssertionExtensionOutputs( - @JsonProperty("appid") Boolean appid - ) { - this.appid = appid; + public ClientAssertionExtensionOutputsBuilder appid(@NonNull Optional appid) { + this.appid = appid.orElse(null); + return this; } - @Override - public Set getExtensionIds() { - Set ids = new HashSet<>(); - - getAppid().ifPresent((id) -> ids.add("appid")); - - return ids; + /* + * Workaround, see: https://github.com/rzwitserloot/lombok/issues/2623#issuecomment-714816001 + * Consider reverting this workaround if Lombok fixes that issue. + */ + private ClientAssertionExtensionOutputsBuilder appid(Boolean appid) { + return this.appid(Optional.ofNullable(appid)); } /** * The output from the FIDO AppID Extension (appid). * - *

- * This value should be ignored because its behaviour is underspecified, see: This value should be ignored because its behaviour is underspecified, see: https://github.com/w3c/webauthn/issues/1034. - *

* - * @see §10.1. FIDO AppID Extension - * (appid) + * @see §10.1. + * FIDO AppID Extension (appid) */ - public Optional getAppid() { - return Optional.ofNullable(appid); - } - - public static class ClientAssertionExtensionOutputsBuilder { - private Boolean appid = null; - - /** - * The output from the FIDO AppID Extension (appid). - * - *

- * This value should be ignored because its behaviour is underspecified, see: https://github.com/w3c/webauthn/issues/1034. - *

- * - * @see §10.1. FIDO AppID Extension - * (appid) - */ - public ClientAssertionExtensionOutputsBuilder appid(@NonNull Optional appid) { - this.appid = appid.orElse(null); - return this; - } - - /* - * Workaround, see: https://github.com/rzwitserloot/lombok/issues/2623#issuecomment-714816001 - * Consider reverting this workaround if Lombok fixes that issue. - */ - private ClientAssertionExtensionOutputsBuilder appid(Boolean appid) { - return this.appid(Optional.ofNullable(appid)); - } - - /** - * The output from the FIDO AppID Extension (appid). - * - *

- * This value should be ignored because its behaviour is underspecified, see: https://github.com/w3c/webauthn/issues/1034. - *

- * - * @see §10.1. FIDO AppID Extension - * (appid) - */ - public ClientAssertionExtensionOutputsBuilder appid(boolean appid) { - return this.appid(Optional.of(appid)); - } + public ClientAssertionExtensionOutputsBuilder appid(boolean appid) { + return this.appid(Optional.of(appid)); } - + } } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/ClientExtensionOutputs.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/ClientExtensionOutputs.java index edf73003c..5c10c6913 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/ClientExtensionOutputs.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/ClientExtensionOutputs.java @@ -29,10 +29,7 @@ public interface ClientExtensionOutputs { - /** - * Returns a {@link Set} of the extension IDs for which an extension output is present. - */ - @JsonIgnore - Set getExtensionIds(); - + /** Returns a {@link Set} of the extension IDs for which an extension output is present. */ + @JsonIgnore + Set getExtensionIds(); } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/ClientRegistrationExtensionOutputs.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/ClientRegistrationExtensionOutputs.java index cde509582..0e0fa0c85 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/ClientRegistrationExtensionOutputs.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/ClientRegistrationExtensionOutputs.java @@ -31,31 +31,26 @@ import lombok.Builder; import lombok.Value; - /** - * Contains client extension - * outputs from a - * navigator.credentials.create() operation. + * Contains client extension + * outputs from a navigator.credentials.create() operation. * - *

- * Note that there is no guarantee that any extension input present in {@link AssertionExtensionInputs} will have a - * corresponding output present here. - *

+ *

Note that there is no guarantee that any extension input present in {@link + * AssertionExtensionInputs} will have a corresponding output present here. * - *

- * The authenticator extension outputs are contained in the {@link AuthenticatorData} structure. - *

+ *

The authenticator extension outputs are contained in the {@link AuthenticatorData} structure. * - * @see §9. WebAuthn Extensions + * @see §9. WebAuthn + * Extensions */ @Value @AllArgsConstructor(access = AccessLevel.PRIVATE) @Builder(toBuilder = true) public class ClientRegistrationExtensionOutputs implements ClientExtensionOutputs { - @Override - public Set getExtensionIds() { - return Collections.emptySet(); - } - + @Override + public Set getExtensionIds() { + return Collections.emptySet(); + } } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/CollectedClientData.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/CollectedClientData.java index 3759df20f..72ceb8250 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/CollectedClientData.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/CollectedClientData.java @@ -43,121 +43,121 @@ /** * The client data represents the contextual bindings of both the Relying Party and the client. * - * @see §5.10.1. Client Data Used - * in WebAuthn Signatures (dictionary CollectedClientData) - * + * @see §5.10.1. + * Client Data Used in WebAuthn Signatures (dictionary CollectedClientData) */ @Value @JsonSerialize(using = CollectedClientData.JsonSerializer.class) public class CollectedClientData { - /** - * The client data returned from the client. - */ - @NonNull - @Getter(AccessLevel.NONE) - private final ByteArray clientDataJson; - - @NonNull - @Getter(AccessLevel.NONE) - private final transient ObjectNode clientData; - - /** - * The base64url encoding of the challenge provided by the Relying Party. See the §13.1 Cryptographic - * Challenges security consideration. - */ - @NonNull - private final transient ByteArray challenge; - - /** - * The fully qualified origin of the requester, as provided to the authenticator by the client, in the syntax - * defined by RFC 6454. - */ - @NonNull - private final transient String origin; - - /** - * The type of the requested operation, set by the client. - */ - @NonNull - private final transient String type; - - @JsonCreator - public CollectedClientData(@NonNull ByteArray clientDataJSON) throws IOException, Base64UrlException { - JsonNode clientData = JacksonCodecs.json().readTree(clientDataJSON.getBytes()); - - ExceptionUtil.assure( - clientData != null && clientData.isObject(), - "Collected client data must be JSON object." - ); - - this.clientDataJson = clientDataJSON; - this.clientData = (ObjectNode) clientData; - - try { - challenge = ByteArray.fromBase64Url(clientData.get("challenge").textValue()); - } catch (NullPointerException e) { - throw new IllegalArgumentException("Missing field: \"challenge\""); - } catch (Base64UrlException e) { - throw new Base64UrlException("Invalid \"challenge\" value", e); - } - - try { - origin = clientData.get("origin").textValue(); - } catch (NullPointerException e) { - throw new IllegalArgumentException("Missing field: \"origin\""); - } - - try { - type = clientData.get("type").textValue(); - } catch (NullPointerException e) { - throw new IllegalArgumentException("Missing field: \"type\""); - } - - final JsonNode authenticatorExtensions = clientData.get("authenticatorExtensions"); - if (authenticatorExtensions != null && !authenticatorExtensions.isObject()) { - throw new IllegalArgumentException("Field \"authenticatorExtensions\" must be an object if present."); - } - - final JsonNode clientExtensions = clientData.get("clientExtensions"); - if (clientExtensions != null && !clientExtensions.isObject()) { - throw new IllegalArgumentException("Field \"clientExtensions\" must be an object if present."); - } + /** The client data returned from the client. */ + @NonNull + @Getter(AccessLevel.NONE) + private final ByteArray clientDataJson; + + @NonNull + @Getter(AccessLevel.NONE) + private final transient ObjectNode clientData; + + /** + * The base64url encoding of the challenge provided by the Relying Party. See the §13.1 + * Cryptographic Challenges security consideration. + */ + @NonNull private final transient ByteArray challenge; + + /** + * The fully qualified origin of the requester, as provided to the authenticator by the client, in + * the syntax defined by RFC 6454. + */ + @NonNull private final transient String origin; + + /** The type of the requested operation, set by the client. */ + @NonNull private final transient String type; + + @JsonCreator + public CollectedClientData(@NonNull ByteArray clientDataJSON) + throws IOException, Base64UrlException { + JsonNode clientData = JacksonCodecs.json().readTree(clientDataJSON.getBytes()); + + ExceptionUtil.assure( + clientData != null && clientData.isObject(), "Collected client data must be JSON object."); + + this.clientDataJson = clientDataJSON; + this.clientData = (ObjectNode) clientData; + + try { + challenge = ByteArray.fromBase64Url(clientData.get("challenge").textValue()); + } catch (NullPointerException e) { + throw new IllegalArgumentException("Missing field: \"challenge\""); + } catch (Base64UrlException e) { + throw new Base64UrlException("Invalid \"challenge\" value", e); } - /** - * Information about the state of the Token Binding protocol used - * when communicating with the Relying Party. Its absence indicates that the client doesn't support token binding. - */ - public final Optional getTokenBinding() { - return Optional.ofNullable(clientData.get("tokenBinding")) - .map(tb -> { - if (tb.isObject()) { - String status = tb.get("status").textValue(); - return new TokenBindingInfo( - TokenBindingStatus.fromJsonString(status), - Optional.ofNullable(tb.get("id")) - .map(JsonNode::textValue) - .map(id -> { - try { - return ByteArray.fromBase64Url(id); - } catch (Base64UrlException e) { - throw new IllegalArgumentException("Property \"id\" is not valid Base64Url data", e); - } - }) - ); - } else { - throw new IllegalArgumentException("Property \"tokenBinding\" missing from client data."); - } - }); + try { + origin = clientData.get("origin").textValue(); + } catch (NullPointerException e) { + throw new IllegalArgumentException("Missing field: \"origin\""); + } + + try { + type = clientData.get("type").textValue(); + } catch (NullPointerException e) { + throw new IllegalArgumentException("Missing field: \"type\""); } - static class JsonSerializer extends com.fasterxml.jackson.databind.JsonSerializer { - @Override - public void serialize(CollectedClientData value, JsonGenerator gen, SerializerProvider serializers) throws IOException { - gen.writeString(value.clientDataJson.getBase64Url()); - } + final JsonNode authenticatorExtensions = clientData.get("authenticatorExtensions"); + if (authenticatorExtensions != null && !authenticatorExtensions.isObject()) { + throw new IllegalArgumentException( + "Field \"authenticatorExtensions\" must be an object if present."); } + final JsonNode clientExtensions = clientData.get("clientExtensions"); + if (clientExtensions != null && !clientExtensions.isObject()) { + throw new IllegalArgumentException( + "Field \"clientExtensions\" must be an object if present."); + } + } + + /** + * Information about the state of the Token Binding + * protocol used when communicating with the Relying Party. Its absence indicates that the + * client doesn't support token binding. + */ + public final Optional getTokenBinding() { + return Optional.ofNullable(clientData.get("tokenBinding")) + .map( + tb -> { + if (tb.isObject()) { + String status = tb.get("status").textValue(); + return new TokenBindingInfo( + TokenBindingStatus.fromJsonString(status), + Optional.ofNullable(tb.get("id")) + .map(JsonNode::textValue) + .map( + id -> { + try { + return ByteArray.fromBase64Url(id); + } catch (Base64UrlException e) { + throw new IllegalArgumentException( + "Property \"id\" is not valid Base64Url data", e); + } + })); + } else { + throw new IllegalArgumentException( + "Property \"tokenBinding\" missing from client data."); + } + }); + } + + static class JsonSerializer + extends com.fasterxml.jackson.databind.JsonSerializer { + @Override + public void serialize( + CollectedClientData value, JsonGenerator gen, SerializerProvider serializers) + throws IOException { + gen.writeString(value.clientDataJson.getBase64Url()); + } + } } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/ExtensionInputs.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/ExtensionInputs.java index 41712fcf3..769f3966c 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/ExtensionInputs.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/ExtensionInputs.java @@ -29,10 +29,7 @@ public interface ExtensionInputs { - /** - * Returns a {@link Set} of the extension IDs for which an extension input is present. - */ - @JsonIgnore - Set getExtensionIds(); - + /** Returns a {@link Set} of the extension IDs for which an extension input is present. */ + @JsonIgnore + Set getExtensionIds(); } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredential.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredential.java index f6e8c8f91..5f2ba61e5 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredential.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredential.java @@ -34,192 +34,213 @@ import lombok.NonNull; import lombok.Value; - /** - * The PublicKeyCredential interface inherits from Credential [CREDENTIAL-MANAGEMENT-1], - * and contains the attributes that are returned to the caller when a new credential is created, or a new assertion is - * requested. + * The PublicKeyCredential interface inherits from Credential [CREDENTIAL-MANAGEMENT-1], and contains + * the attributes that are returned to the caller when a new credential is created, or a new + * assertion is requested. * - * @see §5.1. PublicKeyCredential - * Interface + * @see §5.1. + * PublicKeyCredential Interface */ @Value @Builder(toBuilder = true) -public class PublicKeyCredential { - - /** - * The raw Credential ID of this credential, corresponding to the rawId attribute in the WebAuthn API. - */ - @NonNull - private final ByteArray id; - - /** - * The authenticator's response to the client’s request to either create a public key credential, or generate an - * authentication assertion. - *

- * If the {@link PublicKeyCredential} was created in response to - * navigator.credentials.create(), this attribute’s value will - * be an {@link AuthenticatorAttestationResponse}, otherwise, the {@link PublicKeyCredential} was created in - * response to - * navigator.credentials.get(), and this attribute’s value will - * be an {@link AuthenticatorAssertionResponse}. - *

- */ - @NonNull - private final A response; - - /** - * A map containing extension identifier → client extension output entries produced by the extension’s client - * extension processing. - */ - @NonNull - private final B clientExtensionResults; - - /** - * The {@link PublicKeyCredential}'s type value is the string "public-key". - */ - @NonNull - @Builder.Default - private final PublicKeyCredentialType type = PublicKeyCredentialType.PUBLIC_KEY; - - @JsonCreator - private PublicKeyCredential( - @JsonProperty("id") ByteArray id, - @JsonProperty("rawId") ByteArray rawId, - @NonNull @JsonProperty("response") A response, - @NonNull @JsonProperty("clientExtensionResults") B clientExtensionResults, - @NonNull @JsonProperty("type") PublicKeyCredentialType type - ) { - if (id == null && rawId == null) { - throw new NullPointerException("At least one of \"id\" and \"rawId\" must be non-null."); - } - if (id != null && rawId != null && !id.equals(rawId)) { - throw new IllegalArgumentException(String.format("\"id\" and \"rawId\" are not equal: %s != %s", id, rawId)); - } - - this.id = id == null ? rawId : id; - this.response = response; - this.clientExtensionResults = clientExtensionResults; - this.type = type; +public class PublicKeyCredential< + A extends AuthenticatorResponse, B extends ClientExtensionOutputs> { + + /** + * The raw Credential ID of this credential, corresponding to the rawId attribute in + * the WebAuthn API. + */ + @NonNull private final ByteArray id; + + /** + * The authenticator's response to the client’s request to either create a public key credential, + * or generate an authentication assertion. + * + *

If the {@link PublicKeyCredential} was created in response to + * navigator.credentials.create(), this attribute’s value will be an {@link + * AuthenticatorAttestationResponse}, otherwise, the {@link PublicKeyCredential} was created in + * response to navigator.credentials.get(), and this attribute’s value will be an + * {@link AuthenticatorAssertionResponse}. + */ + @NonNull private final A response; + + /** + * A map containing extension identifier → client extension output entries produced by the + * extension’s client extension processing. + */ + @NonNull private final B clientExtensionResults; + + /** The {@link PublicKeyCredential}'s type value is the string "public-key". */ + @NonNull @Builder.Default + private final PublicKeyCredentialType type = PublicKeyCredentialType.PUBLIC_KEY; + + @JsonCreator + private PublicKeyCredential( + @JsonProperty("id") ByteArray id, + @JsonProperty("rawId") ByteArray rawId, + @NonNull @JsonProperty("response") A response, + @NonNull @JsonProperty("clientExtensionResults") B clientExtensionResults, + @NonNull @JsonProperty("type") PublicKeyCredentialType type) { + if (id == null && rawId == null) { + throw new NullPointerException("At least one of \"id\" and \"rawId\" must be non-null."); } - - private PublicKeyCredential( - ByteArray id, - @NonNull A response, - @NonNull B clientExtensionResults, - @NonNull PublicKeyCredentialType type - ) { - this(id, null, response, clientExtensionResults, type); + if (id != null && rawId != null && !id.equals(rawId)) { + throw new IllegalArgumentException( + String.format("\"id\" and \"rawId\" are not equal: %s != %s", id, rawId)); } - public static PublicKeyCredentialBuilder.MandatoryStages builder() { - return new PublicKeyCredentialBuilder().start(); + this.id = id == null ? rawId : id; + this.response = response; + this.clientExtensionResults = clientExtensionResults; + this.type = type; + } + + private PublicKeyCredential( + ByteArray id, + @NonNull A response, + @NonNull B clientExtensionResults, + @NonNull PublicKeyCredentialType type) { + this(id, null, response, clientExtensionResults, type); + } + + public static + PublicKeyCredentialBuilder.MandatoryStages builder() { + return new PublicKeyCredentialBuilder().start(); + } + + public static class PublicKeyCredentialBuilder< + A extends AuthenticatorResponse, B extends ClientExtensionOutputs> { + private MandatoryStages start() { + return new MandatoryStages(this); } - public static class PublicKeyCredentialBuilder { - private MandatoryStages start() { - return new MandatoryStages(this); - } + @AllArgsConstructor + public class MandatoryStages { + private final PublicKeyCredentialBuilder builder; + + public Step2 id(ByteArray id) { + builder.id(id); + return new Step2(); + } - @AllArgsConstructor - public class MandatoryStages { - private final PublicKeyCredentialBuilder builder; - - public Step2 id(ByteArray id) { - builder.id(id); - return new Step2(); - } - - public class Step2 { - public Step3 response(A response) { - builder.response(response); - return new Step3(); - } - } - - public class Step3 { - public PublicKeyCredentialBuilder clientExtensionResults(B clientExtensionResults) { - return builder.clientExtensionResults(clientExtensionResults); - } - } + public class Step2 { + public Step3 response(A response) { + builder.response(response); + return new Step3(); } + } + public class Step3 { + public PublicKeyCredentialBuilder clientExtensionResults(B clientExtensionResults) { + return builder.clientExtensionResults(clientExtensionResults); + } + } } - - /** - * Parse a {@link PublicKeyCredential} object from JSON. - * - *

The json should be of the following format:

- * - *
-     * {
-     *   "id": "(resp.id)",
-     *   "response": {
-     *     "attestationObject": "(Base64Url encoded resp.attestationObject)",
-     *     "clientDataJSON": "(Base64Url encoded resp.clientDataJSON)"
-     *   },
-     *   "clientExtensionResults": { (resp.getClientExtensionResults()) },
-     *   "type": "public-key"
-     * }
-     * 
- * - *
- *
resp:
The PublicKeyCredential object returned from a registration ceremony.
- *
id:
The string value of resp.id
- *
response.attestationObject:
The value of resp.attestationObject, Base64Url encoded as a string
- *
response.clientDataJSON:
The value of resp.clientDataJSON, Base64Url encoded as a string
- *
clientExtensionResults:
The return value of resp.getClientExtensionResults()
- *
type:
The literal string value "public-key"
- *
- * - * @param json a JSON string of the above format - * @throws IOException if the json is invalid or cannot be decoded as a {@link PublicKeyCredential} - */ - public static PublicKeyCredential parseRegistrationResponseJson(String json) throws IOException { - return JacksonCodecs.json().readValue( + } + + /** + * Parse a {@link PublicKeyCredential} object from JSON. + * + *

The json should be of the following format: + * + *

+   * {
+   *   "id": "(resp.id)",
+   *   "response": {
+   *     "attestationObject": "(Base64Url encoded resp.attestationObject)",
+   *     "clientDataJSON": "(Base64Url encoded resp.clientDataJSON)"
+   *   },
+   *   "clientExtensionResults": { (resp.getClientExtensionResults()) },
+   *   "type": "public-key"
+   * }
+   * 
+ * + *
+ *
resp: + *
The PublicKeyCredential + * object returned from a registration ceremony. + *
id: + *
The string value of resp.id + *
response.attestationObject: + *
The value of resp.attestationObject, Base64Url encoded as a string + *
response.clientDataJSON: + *
The value of resp.clientDataJSON, Base64Url encoded as a string + *
clientExtensionResults: + *
The return value of resp.getClientExtensionResults() + *
type: + *
The literal string value "public-key" + *
+ * + * @param json a JSON string of the above format + * @throws IOException if the json is invalid or cannot be decoded as a {@link + * PublicKeyCredential} + */ + public static PublicKeyCredential< + AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs> + parseRegistrationResponseJson(String json) throws IOException { + return JacksonCodecs.json() + .readValue( json, - new TypeReference>(){} - ); - } - - /** - * Parse a {@link PublicKeyCredential} object from JSON. - * - *

The json should be of the following format:

- * - *
-     * {
-     *   "id": "(resp.id)",
-     *   "response": {
-     *     "authenticatorData": "(Base64Url encoded resp.authenticatorData)",
-     *     "signature": "(Base64Url encoded resp.signature)",
-     *     "clientDataJSON": "(Base64Url encoded resp.clientDataJSON)",
-     *     "userHandle": "(null, undefined or Base64Url encoded resp.userHandle)"
-     *   },
-     *   "clientExtensionResults": { (resp.getClientExtensionResults()) },
-     *   "type": "public-key"
-     * }
-     * 
- * - *
- *
resp:
The PublicKeyCredential object returned from an authentication ceremony.
- *
id:
The string value of resp.id
- *
response.authenticatorData:
The value of resp.authenticatorData, Base64Url encoded as a string
- *
response.signature:
The value of resp.signature, Base64Url encoded as a string
- *
response.clientDataJSON:
The value of resp.clientDataJSON, Base64Url encoded as a string
- *
response.userHandle:
The value of resp.userHandle Base64Url encoded as a string if present, otherwise null or undefined
- *
clientExtensionResults:
The return value of resp.getClientExtensionResults()
- *
type:
The literal string value "public-key"
- *
- * - * @param json a JSON string of the above format - * @throws IOException if the json is invalid or cannot be decoded as a {@link PublicKeyCredential} - */ - public static PublicKeyCredential parseAssertionResponseJson(String json) throws IOException { - return JacksonCodecs.json().readValue( + new TypeReference< + PublicKeyCredential< + AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs>>() {}); + } + + /** + * Parse a {@link PublicKeyCredential} object from JSON. + * + *

The json should be of the following format: + * + *

+   * {
+   *   "id": "(resp.id)",
+   *   "response": {
+   *     "authenticatorData": "(Base64Url encoded resp.authenticatorData)",
+   *     "signature": "(Base64Url encoded resp.signature)",
+   *     "clientDataJSON": "(Base64Url encoded resp.clientDataJSON)",
+   *     "userHandle": "(null, undefined or Base64Url encoded resp.userHandle)"
+   *   },
+   *   "clientExtensionResults": { (resp.getClientExtensionResults()) },
+   *   "type": "public-key"
+   * }
+   * 
+ * + *
+ *
resp: + *
The PublicKeyCredential + * object returned from an authentication ceremony. + *
id: + *
The string value of resp.id + *
response.authenticatorData: + *
The value of resp.authenticatorData, Base64Url encoded as a string + *
response.signature: + *
The value of resp.signature, Base64Url encoded as a string + *
response.clientDataJSON: + *
The value of resp.clientDataJSON, Base64Url encoded as a string + *
response.userHandle: + *
The value of resp.userHandle Base64Url encoded as a string if present, + * otherwise null or undefined + *
clientExtensionResults: + *
The return value of resp.getClientExtensionResults() + *
type: + *
The literal string value "public-key" + *
+ * + * @param json a JSON string of the above format + * @throws IOException if the json is invalid or cannot be decoded as a {@link + * PublicKeyCredential} + */ + public static PublicKeyCredential + parseAssertionResponseJson(String json) throws IOException { + return JacksonCodecs.json() + .readValue( json, - new TypeReference>(){} - ); - } - + new TypeReference< + PublicKeyCredential< + AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs>>() {}); + } } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialCreationOptions.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialCreationOptions.java index fd8096859..36ad7d551 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialCreationOptions.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialCreationOptions.java @@ -35,237 +35,233 @@ import lombok.NonNull; import lombok.Value; - /** * Parameters for a call to navigator.credentials.create(). * - * @see §5.4. - * Options for Credential Creation (dictionary PublicKeyCredentialCreationOptions) + * @see §5.4. + * Options for Credential Creation (dictionary PublicKeyCredentialCreationOptions) */ @Value @Builder(toBuilder = true) public class PublicKeyCredentialCreationOptions { - /** - * Contains data about the Relying Party responsible for the request. - *

- * Its value's {@link RelyingPartyIdentity#getId() id} member specifies the RP - * ID the credential should be scoped to. If omitted, its value will be set by the client. See {@link - * RelyingPartyIdentity} for further details. - *

- */ - @NonNull - private final RelyingPartyIdentity rp; + /** + * Contains data about the Relying Party responsible for the request. + * + *

Its value's {@link RelyingPartyIdentity#getId() id} member specifies the RP ID the credential should + * be scoped to. If omitted, its value will be set by the client. See {@link RelyingPartyIdentity} + * for further details. + */ + @NonNull private final RelyingPartyIdentity rp; + + /** Contains data about the user account for which the Relying Party is requesting attestation. */ + @NonNull private final UserIdentity user; + + /** + * A challenge intended to be used for generating the newly created credential’s attestation + * object. See the §13.1 + * Cryptographic Challenges security consideration. + */ + @NonNull private final ByteArray challenge; + + /** + * Information about the desired properties of the credential to be created. + * + *

The sequence is ordered from most preferred to least preferred. The client makes a + * best-effort to create the most preferred credential that it can. + */ + @NonNull private final List pubKeyCredParams; + + /** + * A time, in milliseconds, that the caller is willing to wait for the call to complete. This is + * treated as a hint, and MAY be overridden by the client. + */ + private final Long timeout; + + /** + * Intended for use by Relying Parties that wish to limit the creation of multiple credentials for + * the same account on a single authenticator. The client is requested to return an error if the + * new credential would be created on an authenticator that also contains one of the credentials + * enumerated in this parameter. + */ + private final Set excludeCredentials; + + /** + * Intended for use by Relying Parties that wish to select the appropriate authenticators to + * participate in the create() operation. + */ + private final AuthenticatorSelectionCriteria authenticatorSelection; + + /** + * Intended for use by Relying Parties that wish to express their preference for attestation + * conveyance. The default is {@link AttestationConveyancePreference#NONE}. + */ + @NonNull private final AttestationConveyancePreference attestation; + + /** + * Additional parameters requesting additional processing by the client and authenticator. + * + *

For example, the caller may request that only authenticators with certain capabilities be + * used to create the credential, or that particular information be returned in the attestation + * object. Some extensions are defined in §9 WebAuthn Extensions; + * consult the IANA "WebAuthn Extension Identifier" registry established by [WebAuthn-Registries] + * for an up-to-date list of registered WebAuthn Extensions. + */ + @NonNull private final RegistrationExtensionInputs extensions; + + @JsonCreator + private PublicKeyCredentialCreationOptions( + @NonNull @JsonProperty("rp") RelyingPartyIdentity rp, + @NonNull @JsonProperty("user") UserIdentity user, + @NonNull @JsonProperty("challenge") ByteArray challenge, + @NonNull @JsonProperty("pubKeyCredParams") + List pubKeyCredParams, + @JsonProperty("timeout") Long timeout, + @JsonProperty("excludeCredentials") Set excludeCredentials, + @JsonProperty("authenticatorSelection") AuthenticatorSelectionCriteria authenticatorSelection, + @JsonProperty("attestation") AttestationConveyancePreference attestation, + @JsonProperty("extensions") RegistrationExtensionInputs extensions) { + this.rp = rp; + this.user = user; + this.challenge = challenge; + this.pubKeyCredParams = CollectionUtil.immutableList(pubKeyCredParams); + this.timeout = timeout; + this.excludeCredentials = + excludeCredentials == null + ? null + : CollectionUtil.immutableSortedSet(new TreeSet<>(excludeCredentials)); + this.authenticatorSelection = authenticatorSelection; + this.attestation = attestation == null ? AttestationConveyancePreference.NONE : attestation; + this.extensions = + extensions == null ? RegistrationExtensionInputs.builder().build() : extensions; + } + + public Optional getTimeout() { + return Optional.ofNullable(timeout); + } + + public Optional> getExcludeCredentials() { + return Optional.ofNullable(excludeCredentials); + } + + public Optional getAuthenticatorSelection() { + return Optional.ofNullable(authenticatorSelection); + } + + public static PublicKeyCredentialCreationOptionsBuilder.MandatoryStages builder() { + return new PublicKeyCredentialCreationOptionsBuilder.MandatoryStages(); + } + + public static class PublicKeyCredentialCreationOptionsBuilder { + private Long timeout = null; + private Set excludeCredentials = null; + private AuthenticatorSelectionCriteria authenticatorSelection = null; + + public static class MandatoryStages { + private PublicKeyCredentialCreationOptionsBuilder builder = + new PublicKeyCredentialCreationOptionsBuilder(); + + /** @see PublicKeyCredentialCreationOptions#getRp() */ + public Step2 rp(RelyingPartyIdentity rp) { + builder.rp(rp); + return new Step2(); + } + + /** @see PublicKeyCredentialCreationOptions#getUser() */ + public class Step2 { + public Step3 user(UserIdentity user) { + builder.user(user); + return new Step3(); + } + } - /** - * Contains data about the user account for which the Relying Party is requesting attestation. - */ - @NonNull - private final UserIdentity user; + /** @see PublicKeyCredentialCreationOptions#getChallenge() */ + public class Step3 { + public Step4 challenge(ByteArray challenge) { + builder.challenge(challenge); + return new Step4(); + } + } - /** - * A challenge intended to be used for generating the newly created credential’s attestation object. See the §13.1 Cryptographic - * Challenges security consideration. - */ - @NonNull - private final ByteArray challenge; + /** @see PublicKeyCredentialCreationOptions#getPubKeyCredParams() */ + public class Step4 { + public PublicKeyCredentialCreationOptionsBuilder pubKeyCredParams( + List pubKeyCredParams) { + return builder.pubKeyCredParams(pubKeyCredParams); + } + } + } /** - * Information about the desired properties of the credential to be created. - *

- * The sequence is ordered from most preferred to least preferred. The client makes a best-effort to create the most - * preferred credential that it can. - *

+ * A time, in milliseconds, that the caller is willing to wait for the call to complete. This is + * treated as a hint, and MAY be overridden by the client. */ - @NonNull - private final List pubKeyCredParams; + public PublicKeyCredentialCreationOptionsBuilder timeout(@NonNull Optional timeout) { + this.timeout = timeout.orElse(null); + return this; + } - /** - * A time, in milliseconds, that the caller is willing to wait for the call to complete. This is treated as a hint, - * and MAY be overridden by the client. + /* + * Workaround, see: https://github.com/rzwitserloot/lombok/issues/2623#issuecomment-714816001 + * Consider reverting this workaround if Lombok fixes that issue. */ - private final Long timeout; + private PublicKeyCredentialCreationOptionsBuilder timeout(Long timeout) { + return this.timeout(Optional.ofNullable(timeout)); + } /** - * Intended for use by Relying Parties that wish to limit the creation of multiple credentials for the same account - * on a single authenticator. The client is requested to return an error if the new credential would be created on - * an authenticator that also contains one of the credentials enumerated in this parameter. + * A time, in milliseconds, that the caller is willing to wait for the call to complete. This is + * treated as a hint, and MAY be overridden by the client. */ - private final Set excludeCredentials; + public PublicKeyCredentialCreationOptionsBuilder timeout(long timeout) { + return this.timeout(Optional.of(timeout)); + } /** - * Intended for use by Relying Parties that wish to select the appropriate authenticators to participate in the - * create() operation. + * Intended for use by Relying Parties that wish to limit the creation of multiple credentials + * for the same account on a single authenticator. The client is requested to return an error if + * the new credential would be created on an authenticator that also contains one of the + * credentials enumerated in this parameter. */ - private final AuthenticatorSelectionCriteria authenticatorSelection; + public PublicKeyCredentialCreationOptionsBuilder excludeCredentials( + Optional> excludeCredentials) { + return this.excludeCredentials(excludeCredentials.orElse(null)); + } /** - * Intended for use by Relying Parties that wish to express their preference for attestation conveyance. The default - * is {@link AttestationConveyancePreference#NONE}. + * Intended for use by Relying Parties that wish to limit the creation of multiple credentials + * for the same account on a single authenticator. The client is requested to return an error if + * the new credential would be created on an authenticator that also contains one of the + * credentials enumerated in this parameter. */ - @NonNull - private final AttestationConveyancePreference attestation; + public PublicKeyCredentialCreationOptionsBuilder excludeCredentials( + Set excludeCredentials) { + this.excludeCredentials = excludeCredentials; + return this; + } /** - * Additional parameters requesting additional processing by the client and authenticator. - *

- * For example, the caller may request that only authenticators with certain capabilities be used to create the - * credential, or that particular information be returned in the attestation object. Some extensions are defined in - * §9 WebAuthn Extensions; consult the - * IANA "WebAuthn Extension Identifier" registry established by - * [WebAuthn-Registries] for an - * up-to-date list of registered WebAuthn Extensions. - *

+ * Intended for use by Relying Parties that wish to select the appropriate authenticators to + * participate in the create() operation. */ - @NonNull - private final RegistrationExtensionInputs extensions; - - @JsonCreator - private PublicKeyCredentialCreationOptions( - @NonNull @JsonProperty("rp") RelyingPartyIdentity rp, - @NonNull @JsonProperty("user") UserIdentity user, - @NonNull @JsonProperty("challenge") ByteArray challenge, - @NonNull @JsonProperty("pubKeyCredParams") List pubKeyCredParams, - @JsonProperty("timeout") Long timeout, - @JsonProperty("excludeCredentials") Set excludeCredentials, - @JsonProperty("authenticatorSelection") AuthenticatorSelectionCriteria authenticatorSelection, - @JsonProperty("attestation") AttestationConveyancePreference attestation, - @JsonProperty("extensions") RegistrationExtensionInputs extensions - ) { - this.rp = rp; - this.user = user; - this.challenge = challenge; - this.pubKeyCredParams = CollectionUtil.immutableList(pubKeyCredParams); - this.timeout = timeout; - this.excludeCredentials = excludeCredentials == null ? null : CollectionUtil.immutableSortedSet(new TreeSet<>(excludeCredentials)); - this.authenticatorSelection = authenticatorSelection; - this.attestation = attestation == null ? AttestationConveyancePreference.NONE : attestation; - this.extensions = extensions == null ? RegistrationExtensionInputs.builder().build() : extensions; - } - - public Optional getTimeout() { - return Optional.ofNullable(timeout); - } - - public Optional> getExcludeCredentials() { - return Optional.ofNullable(excludeCredentials); + public PublicKeyCredentialCreationOptionsBuilder authenticatorSelection( + @NonNull Optional authenticatorSelection) { + return this.authenticatorSelection(authenticatorSelection.orElse(null)); } - public Optional getAuthenticatorSelection() { - return Optional.ofNullable(authenticatorSelection); - } - - public static PublicKeyCredentialCreationOptionsBuilder.MandatoryStages builder() { - return new PublicKeyCredentialCreationOptionsBuilder.MandatoryStages(); - } - - public static class PublicKeyCredentialCreationOptionsBuilder { - private Long timeout = null; - private Set excludeCredentials = null; - private AuthenticatorSelectionCriteria authenticatorSelection = null; - - public static class MandatoryStages { - private PublicKeyCredentialCreationOptionsBuilder builder = new PublicKeyCredentialCreationOptionsBuilder(); - - /** - * @see PublicKeyCredentialCreationOptions#getRp() - */ - public Step2 rp(RelyingPartyIdentity rp) { - builder.rp(rp); - return new Step2(); - } - - /** - * @see PublicKeyCredentialCreationOptions#getUser() - */ - public class Step2 { - public Step3 user(UserIdentity user) { - builder.user(user); - return new Step3(); - } - } - - /** - * @see PublicKeyCredentialCreationOptions#getChallenge() - */ - public class Step3 { - public Step4 challenge(ByteArray challenge) { - builder.challenge(challenge); - return new Step4(); - } - } - - /** - * @see PublicKeyCredentialCreationOptions#getPubKeyCredParams() - */ - public class Step4 { - public PublicKeyCredentialCreationOptionsBuilder pubKeyCredParams(List pubKeyCredParams) { - return builder.pubKeyCredParams(pubKeyCredParams); - } - } - } - - /** - * A time, in milliseconds, that the caller is willing to wait for the call to complete. This is treated as a hint, - * and MAY be overridden by the client. - */ - public PublicKeyCredentialCreationOptionsBuilder timeout(@NonNull Optional timeout) { - this.timeout = timeout.orElse(null); - return this; - } - - /* - * Workaround, see: https://github.com/rzwitserloot/lombok/issues/2623#issuecomment-714816001 - * Consider reverting this workaround if Lombok fixes that issue. - */ - private PublicKeyCredentialCreationOptionsBuilder timeout(Long timeout) { - return this.timeout(Optional.ofNullable(timeout)); - } - - /** - * A time, in milliseconds, that the caller is willing to wait for the call to complete. This is treated as a hint, - * and MAY be overridden by the client. - */ - public PublicKeyCredentialCreationOptionsBuilder timeout(long timeout) { - return this.timeout(Optional.of(timeout)); - } - - /** - * Intended for use by Relying Parties that wish to limit the creation of multiple credentials for the same account - * on a single authenticator. The client is requested to return an error if the new credential would be created on - * an authenticator that also contains one of the credentials enumerated in this parameter. - */ - public PublicKeyCredentialCreationOptionsBuilder excludeCredentials(Optional> excludeCredentials) { - return this.excludeCredentials(excludeCredentials.orElse(null)); - } - - /** - * Intended for use by Relying Parties that wish to limit the creation of multiple credentials for the same account - * on a single authenticator. The client is requested to return an error if the new credential would be created on - * an authenticator that also contains one of the credentials enumerated in this parameter. - */ - public PublicKeyCredentialCreationOptionsBuilder excludeCredentials(Set excludeCredentials) { - this.excludeCredentials = excludeCredentials; - return this; - } - - /** - * Intended for use by Relying Parties that wish to select the appropriate authenticators to participate in the - * create() operation. - */ - public PublicKeyCredentialCreationOptionsBuilder authenticatorSelection(@NonNull Optional authenticatorSelection) { - return this.authenticatorSelection(authenticatorSelection.orElse(null)); - } - - /** - * Intended for use by Relying Parties that wish to select the appropriate authenticators to participate in the - * create() operation. - */ - public PublicKeyCredentialCreationOptionsBuilder authenticatorSelection(AuthenticatorSelectionCriteria authenticatorSelection) { - this.authenticatorSelection = authenticatorSelection; - return this; - } + /** + * Intended for use by Relying Parties that wish to select the appropriate authenticators to + * participate in the create() operation. + */ + public PublicKeyCredentialCreationOptionsBuilder authenticatorSelection( + AuthenticatorSelectionCriteria authenticatorSelection) { + this.authenticatorSelection = authenticatorSelection; + return this; } - + } } 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 22acd050a..9c7a8443a 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 @@ -36,109 +36,106 @@ import lombok.NonNull; import lombok.Value; - /** - * The attributes that are specified by a caller when referring to a public key credential as an input parameter to the - * navigator.credentials.create() or navigator.credentials.get() methods. It mirrors the - * fields of the {@link PublicKeyCredential} object returned by the latter methods. + * The attributes that are specified by a caller when referring to a public key credential as an + * input parameter to the navigator.credentials.create() or + * navigator.credentials.get() methods. It mirrors the fields of the {@link + * PublicKeyCredential} object returned by the latter methods. * - * @see §5.10.3. - * Credential Descriptor (dictionary PublicKeyCredentialDescriptor) + * @see §5.10.3. + * Credential Descriptor (dictionary PublicKeyCredentialDescriptor) */ @Value @Builder(toBuilder = true) public class PublicKeyCredentialDescriptor implements Comparable { - /** - * The type of the credential the caller is referring to. - */ - @NonNull - @Builder.Default - private final PublicKeyCredentialType type = PublicKeyCredentialType.PUBLIC_KEY; - - /** - * The credential ID of the public key credential the caller is referring to. - */ - @NonNull - private final ByteArray id; + /** The type of the credential the caller is referring to. */ + @NonNull @Builder.Default + private final PublicKeyCredentialType type = PublicKeyCredentialType.PUBLIC_KEY; + + /** The credential ID of the public key credential the caller is referring to. */ + @NonNull private final ByteArray id; + + /** + * An OPTIONAL hint as to how the client might communicate with the managing authenticator of the + * public key credential the caller is referring to. + */ + private final SortedSet transports; + + @JsonCreator + private PublicKeyCredentialDescriptor( + @NonNull @JsonProperty("type") PublicKeyCredentialType type, + @NonNull @JsonProperty("id") ByteArray id, + @JsonProperty("transports") Set transports) { + this.type = type; + this.id = id; + this.transports = + transports == null ? null : CollectionUtil.immutableSortedSet(new TreeSet<>(transports)); + } + + @Override + public int compareTo(PublicKeyCredentialDescriptor other) { + int idComparison = id.compareTo(other.id); + if (idComparison != 0) { + return idComparison; + } - /** - * An OPTIONAL hint as to how the client might communicate with the managing authenticator of the public key - * credential the caller is referring to. - */ - private final SortedSet transports; - - @JsonCreator - private PublicKeyCredentialDescriptor( - @NonNull @JsonProperty("type") PublicKeyCredentialType type, - @NonNull @JsonProperty("id") ByteArray id, - @JsonProperty("transports") Set transports - ) { - this.type = type; - this.id = id; - this.transports = transports == null ? null : CollectionUtil.immutableSortedSet(new TreeSet<>(transports)); + if (type.compareTo(other.type) != 0) { + return type.compareTo(other.type); } - @Override - public int compareTo(PublicKeyCredentialDescriptor other) { - int idComparison = id.compareTo(other.id); - if (idComparison != 0) { - return idComparison; - } - - if (type.compareTo(other.type) != 0) { - return type.compareTo(other.type); - } - - if (!getTransports().isPresent() && other.getTransports().isPresent()) { - return -1; - } else if (getTransports().isPresent() && !other.getTransports().isPresent()) { - return 1; - } else if (getTransports().isPresent() && other.getTransports().isPresent()) { - int transportsComparison = ComparableUtil.compareComparableSets(getTransports().get(), other.getTransports().get()); - if (transportsComparison != 0) { - return transportsComparison; - } - } - - return 0; + if (!getTransports().isPresent() && other.getTransports().isPresent()) { + return -1; + } else if (getTransports().isPresent() && !other.getTransports().isPresent()) { + return 1; + } else if (getTransports().isPresent() && other.getTransports().isPresent()) { + int transportsComparison = + ComparableUtil.compareComparableSets(getTransports().get(), other.getTransports().get()); + if (transportsComparison != 0) { + return transportsComparison; + } } - public static PublicKeyCredentialDescriptorBuilder.MandatoryStages builder() { - return new PublicKeyCredentialDescriptorBuilder.MandatoryStages(); + return 0; + } + + public static PublicKeyCredentialDescriptorBuilder.MandatoryStages builder() { + return new PublicKeyCredentialDescriptorBuilder.MandatoryStages(); + } + + public static class PublicKeyCredentialDescriptorBuilder { + private Set transports = null; + + public static class MandatoryStages { + private PublicKeyCredentialDescriptorBuilder builder = + new PublicKeyCredentialDescriptorBuilder(); + + public PublicKeyCredentialDescriptorBuilder id(ByteArray id) { + return builder.id(id); + } } - public static class PublicKeyCredentialDescriptorBuilder { - private Set transports = null; - - public static class MandatoryStages { - private PublicKeyCredentialDescriptorBuilder builder = new PublicKeyCredentialDescriptorBuilder(); - - public PublicKeyCredentialDescriptorBuilder id(ByteArray id) { - return builder.id(id); - } - } - - /** - * An OPTIONAL hint as to how the client might communicate with the managing authenticator of the public key - * credential the caller is referring to. - */ - public PublicKeyCredentialDescriptorBuilder transports(@NonNull Optional> transports) { - return this.transports(transports.orElse(null)); - } - - /** - * An OPTIONAL hint as to how the client might communicate with the managing authenticator of the public key - * credential the caller is referring to. - */ - public PublicKeyCredentialDescriptorBuilder transports(Set transports) { - this.transports = transports; - return this; - } + /** + * An OPTIONAL hint as to how the client might communicate with the managing authenticator of + * the public key credential the caller is referring to. + */ + public PublicKeyCredentialDescriptorBuilder transports( + @NonNull Optional> transports) { + return this.transports(transports.orElse(null)); } - public Optional> getTransports() { - return Optional.ofNullable(transports); + /** + * An OPTIONAL hint as to how the client might communicate with the managing authenticator of + * the public key credential the caller is referring to. + */ + public PublicKeyCredentialDescriptorBuilder transports(Set transports) { + this.transports = transports; + return this; } + } + public Optional> getTransports() { + return Optional.ofNullable(transports); + } } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialEntity.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialEntity.java index e27a00d4e..2a818b9ee 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialEntity.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialEntity.java @@ -27,68 +27,70 @@ import java.net.URL; import java.util.Optional; - /** - * Describes a user account, or a WebAuthn Relying Party, which a public key credential is associated with or scoped to, - * respectively. + * Describes a user account, or a WebAuthn Relying Party, which a public key credential is + * associated with or scoped to, respectively. * - * @see §5.4.1. Public Key - * Entity Description (dictionary PublicKeyCredentialEntity) - * + * @see §5.4.1. + * Public Key Entity Description (dictionary PublicKeyCredentialEntity) */ public interface PublicKeyCredentialEntity { - /** - * A human-palatable name for the entity. Its function depends on what the PublicKeyCredentialEntity represents: - * - *
    - *
  • When inherited by PublicKeyCredentialRpEntity it is a human-palatable identifier for the Relying Party, - * intended only for display. For example, "ACME Corporation", "Wonderful Widgets, Inc." or "ОАО Примертех". - *
      - *
    • Relying Parties SHOULD perform enforcement, as prescribed in Section 2.3 of [RFC8266] for the Nickname - * Profile of the PRECIS FreeformClass [RFC8264], when setting name's value, or displaying the value to the - * user.
    • - *
    • Clients SHOULD perform enforcement, as prescribed in Section 2.3 of [RFC8266] for the Nickname Profile of - * the PRECIS FreeformClass [RFC8264], on name's value prior to displaying the value to the user or including the - * value as a parameter of the authenticatorMakeCredential operation.
    • - *
    - *
  • - *
  • When inherited by PublicKeyCredentialUserEntity, it is a human-palatable identifier for a user account. It - * is intended only for display, i.e., aiding the user in determining the difference between user accounts with - * similar displayNames. For example, "alexm", "alex.p.mueller@example.com" or "+14255551234". - *
      - *
    • The Relying Party MAY let the user choose this value. The Relying Party SHOULD perform enforcement, as - * prescribed in Section 3.4.3 of [RFC8265] for the UsernameCasePreserved Profile of the PRECIS IdentifierClass - * [RFC8264], when setting name's value, or displaying the value to the user.
    • - *
    • Clients SHOULD perform enforcement, as prescribed in Section 3.4.3 of [RFC8265] for the - * UsernameCasePreserved Profile of the PRECIS IdentifierClass [RFC8264], on name's value prior to displaying the - * value to the user or including the value as a parameter of the authenticatorMakeCredential operation.
    • - *
    - *
- *

- * When clients, client platforms, or authenticators display a name's value, they should always use UI elements to - * provide a clear boundary around the displayed value, and not allow overflow into other elements. - *

- *

- * Authenticators MUST accept and store a 64-byte minimum length for a name member’s value. Authenticators MAY - * truncate a name member’s value to a length equal to or greater than 64 bytes. - *

- * - * @see RFC 8264 - * @see RFC 8265 - */ - String getName(); - - /** - * A serialized URL which resolves to an image associated with the entity. - * - *

- * For example, this could be a user's avatar or a Relying Party's logo. This URL MUST be an a priori authenticated - * URL. Authenticators MUST accept and store a 128-byte minimum length for an icon member's value. Authenticators - * MAY ignore an icon member's value if its length is greater than 128 bytes. The URL's scheme MAY be "data" to - * avoid fetches of the URL, at the cost of needing more storage. - *

- */ - Optional getIcon(); + /** + * A human-palatable name for the entity. Its function depends on what the + * PublicKeyCredentialEntity represents: + * + *
    + *
  • When inherited by PublicKeyCredentialRpEntity it is a human-palatable identifier for the + * Relying Party, intended only for display. For example, "ACME Corporation", "Wonderful + * Widgets, Inc." or "ОАО Примертех". + *
      + *
    • Relying Parties SHOULD perform enforcement, as prescribed in Section 2.3 of + * [RFC8266] for the Nickname Profile of the PRECIS FreeformClass [RFC8264], when + * setting name's value, or displaying the value to the user. + *
    • Clients SHOULD perform enforcement, as prescribed in Section 2.3 of [RFC8266] for + * the Nickname Profile of the PRECIS FreeformClass [RFC8264], on name's value prior + * to displaying the value to the user or including the value as a parameter of the + * authenticatorMakeCredential operation. + *
    + *
  • When inherited by PublicKeyCredentialUserEntity, it is a human-palatable identifier for a + * user account. It is intended only for display, i.e., aiding the user in determining the + * difference between user accounts with similar displayNames. For example, "alexm", + * "alex.p.mueller@example.com" or "+14255551234". + *
      + *
    • The Relying Party MAY let the user choose this value. The Relying Party SHOULD + * perform enforcement, as prescribed in Section 3.4.3 of [RFC8265] for the + * UsernameCasePreserved Profile of the PRECIS IdentifierClass [RFC8264], when setting + * name's value, or displaying the value to the user. + *
    • Clients SHOULD perform enforcement, as prescribed in Section 3.4.3 of [RFC8265] for + * the UsernameCasePreserved Profile of the PRECIS IdentifierClass [RFC8264], on + * name's value prior to displaying the value to the user or including the value as a + * parameter of the authenticatorMakeCredential operation. + *
    + *
+ * + *

When clients, client platforms, or authenticators display a name's value, they should always + * use UI elements to provide a clear boundary around the displayed value, and not allow overflow + * into other elements. + * + *

Authenticators MUST accept and store a 64-byte minimum length for a name member’s value. + * Authenticators MAY truncate a name member’s value to a length equal to or greater than 64 + * bytes. + * + * @see RFC 8264 + * @see RFC 8265 + */ + String getName(); + /** + * A serialized URL which resolves to an image associated with the entity. + * + *

For example, this could be a user's avatar or a Relying Party's logo. This URL MUST be an a + * priori authenticated URL. Authenticators MUST accept and store a 128-byte minimum length for an + * icon member's value. Authenticators MAY ignore an icon member's value if its length is greater + * than 128 bytes. The URL's scheme MAY be "data" to avoid fetches of the URL, at the cost of + * needing more storage. + */ + Optional getIcon(); } 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 b6b56e62a..3e157d4e2 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 @@ -29,71 +29,75 @@ import lombok.NonNull; import lombok.Value; - /** * Used to supply additional parameters when creating a new credential. * - * @see §5.3. - * Parameters for Credential Generation (dictionary PublicKeyCredentialParameters) - * + * @see §5.3. + * Parameters for Credential Generation (dictionary PublicKeyCredentialParameters) */ @Value @Builder(toBuilder = true) public class PublicKeyCredentialParameters { - /** - * Specifies the cryptographic signature algorithm with which the newly generated credential will be used, and thus - * also the type of asymmetric key pair to be generated, e.g., RSA or Elliptic Curve. - */ - @NonNull - private final COSEAlgorithmIdentifier alg; + /** + * Specifies the cryptographic signature algorithm with which the newly generated credential will + * be used, and thus also the type of asymmetric key pair to be generated, e.g., RSA or Elliptic + * Curve. + */ + @NonNull private final COSEAlgorithmIdentifier alg; - /** - * Specifies the type of credential to be created. - */ - @NonNull - @Builder.Default - private final PublicKeyCredentialType type = PublicKeyCredentialType.PUBLIC_KEY; + /** Specifies the type of credential to be created. */ + @NonNull @Builder.Default + private final PublicKeyCredentialType type = PublicKeyCredentialType.PUBLIC_KEY; - private PublicKeyCredentialParameters( - @NonNull @JsonProperty("alg") COSEAlgorithmIdentifier alg, - @NonNull @JsonProperty("type") PublicKeyCredentialType type - ) { - this.alg = alg; - this.type = type; - } + private PublicKeyCredentialParameters( + @NonNull @JsonProperty("alg") COSEAlgorithmIdentifier alg, + @NonNull @JsonProperty("type") PublicKeyCredentialType type) { + this.alg = alg; + this.type = type; + } - /** - * Algorithm {@link COSEAlgorithmIdentifier#EdDSA} and type {@link PublicKeyCredentialType#PUBLIC_KEY}. - */ - public static final PublicKeyCredentialParameters EdDSA = builder().alg(COSEAlgorithmIdentifier.EdDSA).build(); + /** + * Algorithm {@link COSEAlgorithmIdentifier#EdDSA} and type {@link + * PublicKeyCredentialType#PUBLIC_KEY}. + */ + public static final PublicKeyCredentialParameters EdDSA = + builder().alg(COSEAlgorithmIdentifier.EdDSA).build(); - /** - * Algorithm {@link COSEAlgorithmIdentifier#ES256} and type {@link PublicKeyCredentialType#PUBLIC_KEY}. - */ - public static final PublicKeyCredentialParameters ES256 = builder().alg(COSEAlgorithmIdentifier.ES256).build(); + /** + * Algorithm {@link COSEAlgorithmIdentifier#ES256} and type {@link + * PublicKeyCredentialType#PUBLIC_KEY}. + */ + 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#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}. - */ - public static final PublicKeyCredentialParameters RS256 = builder().alg(COSEAlgorithmIdentifier.RS256).build(); + /** + * Algorithm {@link COSEAlgorithmIdentifier#RS256} and type {@link + * PublicKeyCredentialType#PUBLIC_KEY}. + */ + public static final PublicKeyCredentialParameters RS256 = + builder().alg(COSEAlgorithmIdentifier.RS256).build(); - public static PublicKeyCredentialParametersBuilder.MandatoryStages builder() { - return new PublicKeyCredentialParametersBuilder.MandatoryStages(); - } + public static PublicKeyCredentialParametersBuilder.MandatoryStages builder() { + return new PublicKeyCredentialParametersBuilder.MandatoryStages(); + } - public static class PublicKeyCredentialParametersBuilder { - public static class MandatoryStages { - private PublicKeyCredentialParametersBuilder builder = new PublicKeyCredentialParametersBuilder(); + public static class PublicKeyCredentialParametersBuilder { + public static class MandatoryStages { + private PublicKeyCredentialParametersBuilder builder = + new PublicKeyCredentialParametersBuilder(); - public PublicKeyCredentialParametersBuilder alg(COSEAlgorithmIdentifier alg) { - return builder.alg(alg); - } - } + public PublicKeyCredentialParametersBuilder alg(COSEAlgorithmIdentifier alg) { + return builder.alg(alg); + } } + } } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialRequestOptions.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialRequestOptions.java index 544740bde..0fdcf1123 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialRequestOptions.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialRequestOptions.java @@ -33,184 +33,179 @@ import lombok.NonNull; import lombok.Value; - /** - * The PublicKeyCredentialRequestOptions dictionary supplies get() with the data it needs to generate an assertion. - *

- * Its `challenge` member must be present, while its other members are optional. - *

+ * The PublicKeyCredentialRequestOptions dictionary supplies get() with the data it needs to + * generate an assertion. + * + *

Its `challenge` member must be present, while its other members are optional. * - * @see §5.5. - * Options for Assertion Generation (dictionary PublicKeyCredentialRequestOptions) - * + * @see §5.5. + * Options for Assertion Generation (dictionary PublicKeyCredentialRequestOptions) */ @Value @Builder(toBuilder = true) public class PublicKeyCredentialRequestOptions { - /** - * A challenge that the selected authenticator signs, along with other data, when producing an authentication - * assertion. See the §13.1 - * Cryptographic Challenges security consideration. - */ - @NonNull - private final ByteArray challenge; + /** + * A challenge that the selected authenticator signs, along with other data, when producing an + * authentication assertion. See the §13.1 + * Cryptographic Challenges security consideration. + */ + @NonNull private final ByteArray challenge; + + /** + * Specifies a time, in milliseconds, that the caller is willing to wait for the call to complete. + * + *

This is treated as a hint, and MAY be overridden by the client. + */ + private final Long timeout; + + /** + * Specifies the relying party identifier claimed by the caller. + * + *

If omitted, its value will be set by the client. + */ + private final String rpId; + + /** + * A list of {@link PublicKeyCredentialDescriptor} objects representing public key credentials + * acceptable to the caller, in descending order of the caller’s preference (the first item in the + * list is the most preferred credential, and so on down the list). + */ + private final List allowCredentials; + + /** + * Describes the Relying Party's requirements regarding user verification + * for the navigator.credentials.get() operation. + * + *

Eligible authenticators are filtered to only those capable of satisfying this requirement. + */ + @NonNull @Builder.Default + private final UserVerificationRequirement userVerification = + UserVerificationRequirement.PREFERRED; + + /** + * Additional parameters requesting additional processing by the client and authenticator. + * + *

For example, if transaction confirmation is sought from the user, then the prompt string + * might be included as an extension. + */ + @NonNull @Builder.Default + private final AssertionExtensionInputs extensions = AssertionExtensionInputs.builder().build(); + + @JsonCreator + private PublicKeyCredentialRequestOptions( + @NonNull @JsonProperty("challenge") ByteArray challenge, + @JsonProperty("timeout") Long timeout, + @JsonProperty("rpId") String rpId, + @JsonProperty("allowCredentials") List allowCredentials, + @NonNull @JsonProperty("userVerification") UserVerificationRequirement userVerification, + @NonNull @JsonProperty("extensions") AssertionExtensionInputs extensions) { + this.challenge = challenge; + this.timeout = timeout; + this.rpId = rpId; + this.allowCredentials = + allowCredentials == null ? null : CollectionUtil.immutableList(allowCredentials); + this.userVerification = userVerification; + this.extensions = extensions; + } + + public Optional getTimeout() { + return Optional.ofNullable(timeout); + } + + public Optional> getAllowCredentials() { + return Optional.ofNullable(allowCredentials); + } + + public static PublicKeyCredentialRequestOptionsBuilder.MandatoryStages builder() { + return new PublicKeyCredentialRequestOptionsBuilder.MandatoryStages(); + } + + public static class PublicKeyCredentialRequestOptionsBuilder { + private Long timeout = null; + private String rpId = null; + private List allowCredentials = null; + + public static class MandatoryStages { + private PublicKeyCredentialRequestOptionsBuilder builder = + new PublicKeyCredentialRequestOptionsBuilder(); + + public PublicKeyCredentialRequestOptionsBuilder challenge(ByteArray challenge) { + return builder.challenge(challenge); + } + } /** - * Specifies a time, in milliseconds, that the caller is willing to wait for the call to complete. - *

- * This is treated as a hint, and MAY be overridden by the client. - *

+ * Specifies a time, in milliseconds, that the caller is willing to wait for the call to + * complete. + * + *

This is treated as a hint, and MAY be overridden by the client. */ - private final Long timeout; + public PublicKeyCredentialRequestOptionsBuilder timeout(@NonNull Optional timeout) { + this.timeout = timeout.orElse(null); + return this; + } - /** - * Specifies the relying party identifier claimed by the caller. - *

- * If omitted, its value will be set by the client. - *

+ /* + * Workaround, see: https://github.com/rzwitserloot/lombok/issues/2623#issuecomment-714816001 + * Consider reverting this workaround if Lombok fixes that issue. */ - private final String rpId; + private PublicKeyCredentialRequestOptionsBuilder timeout(Long timeout) { + return this.timeout(Optional.ofNullable(timeout)); + } /** - * A list of {@link PublicKeyCredentialDescriptor} objects representing public key credentials acceptable to the - * caller, in descending order of the caller’s preference (the first item in the list is the most preferred - * credential, and so on down the list). + * Specifies a time, in milliseconds, that the caller is willing to wait for the call to + * complete. + * + *

This is treated as a hint, and MAY be overridden by the client. */ - private final List allowCredentials; + public PublicKeyCredentialRequestOptionsBuilder timeout(long timeout) { + return this.timeout(Optional.of(timeout)); + } /** - * Describes the Relying Party's requirements regarding user - * verification for the navigator.credentials.get() operation. - *

- * Eligible authenticators are filtered to only those capable of satisfying this requirement. - *

+ * Specifies the relying party identifier claimed by the caller. + * + *

If omitted, its value will be set by the client. */ - @NonNull - @Builder.Default - private final UserVerificationRequirement userVerification = UserVerificationRequirement.PREFERRED; + public PublicKeyCredentialRequestOptionsBuilder rpId(@NonNull Optional rpId) { + return this.rpId(rpId.orElse(null)); + } /** - * Additional parameters requesting additional processing by the client and authenticator. - *

- * For example, if transaction confirmation is sought from the user, then the prompt string might be included as an - * extension. - *

+ * Specifies the relying party identifier claimed by the caller. + * + *

If omitted, its value will be set by the client. */ - @NonNull - @Builder.Default - private final AssertionExtensionInputs extensions = AssertionExtensionInputs.builder().build(); - - @JsonCreator - private PublicKeyCredentialRequestOptions( - @NonNull @JsonProperty("challenge") ByteArray challenge, - @JsonProperty("timeout") Long timeout, - @JsonProperty("rpId") String rpId, - @JsonProperty("allowCredentials") List allowCredentials, - @NonNull @JsonProperty("userVerification") UserVerificationRequirement userVerification, - @NonNull @JsonProperty("extensions") AssertionExtensionInputs extensions - ) { - this.challenge = challenge; - this.timeout = timeout; - this.rpId = rpId; - this.allowCredentials = allowCredentials == null ? null : CollectionUtil.immutableList(allowCredentials); - this.userVerification = userVerification; - this.extensions = extensions; - } - - public Optional getTimeout() { - return Optional.ofNullable(timeout); - } - - public Optional> getAllowCredentials() { - return Optional.ofNullable(allowCredentials); + public PublicKeyCredentialRequestOptionsBuilder rpId(String rpId) { + this.rpId = rpId; + return this; } - public static PublicKeyCredentialRequestOptionsBuilder.MandatoryStages builder() { - return new PublicKeyCredentialRequestOptionsBuilder.MandatoryStages(); + /** + * A list of {@link PublicKeyCredentialDescriptor} objects representing public key credentials + * acceptable to the caller, in descending order of the caller’s preference (the first item in + * the list is the most preferred credential, and so on down the list). + */ + public PublicKeyCredentialRequestOptionsBuilder allowCredentials( + @NonNull Optional> allowCredentials) { + return this.allowCredentials(allowCredentials.orElse(null)); } - public static class PublicKeyCredentialRequestOptionsBuilder { - private Long timeout = null; - private String rpId = null; - private List allowCredentials = null; - - public static class MandatoryStages { - private PublicKeyCredentialRequestOptionsBuilder builder = new PublicKeyCredentialRequestOptionsBuilder(); - - public PublicKeyCredentialRequestOptionsBuilder challenge(ByteArray challenge) { - return builder.challenge(challenge); - } - } - - /** - * Specifies a time, in milliseconds, that the caller is willing to wait for the call to complete. - *

- * This is treated as a hint, and MAY be overridden by the client. - *

- */ - public PublicKeyCredentialRequestOptionsBuilder timeout(@NonNull Optional timeout) { - this.timeout = timeout.orElse(null); - return this; - } - - /* - * Workaround, see: https://github.com/rzwitserloot/lombok/issues/2623#issuecomment-714816001 - * Consider reverting this workaround if Lombok fixes that issue. - */ - private PublicKeyCredentialRequestOptionsBuilder timeout(Long timeout) { - return this.timeout(Optional.ofNullable(timeout)); - } - - /** - * Specifies a time, in milliseconds, that the caller is willing to wait for the call to complete. - *

- * This is treated as a hint, and MAY be overridden by the client. - *

- */ - public PublicKeyCredentialRequestOptionsBuilder timeout(long timeout) { - return this.timeout(Optional.of(timeout)); - } - - /** - * Specifies the relying party identifier claimed by the caller. - *

- * If omitted, its value will be set by the client. - *

- */ - public PublicKeyCredentialRequestOptionsBuilder rpId(@NonNull Optional rpId) { - return this.rpId(rpId.orElse(null)); - } - - /** - * Specifies the relying party identifier claimed by the caller. - *

- * If omitted, its value will be set by the client. - *

- */ - public PublicKeyCredentialRequestOptionsBuilder rpId(String rpId) { - this.rpId = rpId; - return this; - } - - /** - * A list of {@link PublicKeyCredentialDescriptor} objects representing public key credentials acceptable to the - * caller, in descending order of the caller’s preference (the first item in the list is the most preferred - * credential, and so on down the list). - */ - public PublicKeyCredentialRequestOptionsBuilder allowCredentials(@NonNull Optional> allowCredentials) { - return this.allowCredentials(allowCredentials.orElse(null)); - } - - /** - * A list of {@link PublicKeyCredentialDescriptor} objects representing public key credentials acceptable to the - * caller, in descending order of the caller’s preference (the first item in the list is the most preferred - * credential, and so on down the list). - */ - public PublicKeyCredentialRequestOptionsBuilder allowCredentials(List allowCredentials) { - this.allowCredentials = allowCredentials; - return this; - } + /** + * A list of {@link PublicKeyCredentialDescriptor} objects representing public key credentials + * acceptable to the caller, in descending order of the caller’s preference (the first item in + * the list is the most preferred credential, and so on down the list). + */ + public PublicKeyCredentialRequestOptionsBuilder allowCredentials( + List allowCredentials) { + this.allowCredentials = allowCredentials; + return this; } - + } } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialType.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialType.java index ffc7a7f3f..40a34bed9 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialType.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialType.java @@ -35,41 +35,41 @@ /** * Defines the valid credential types. - *

- * It is an extensions point; values may be added to it in the future, as more credential types are defined. The values - * of this enumeration are used for versioning the Authentication Assertion and attestation structures according to the - * type of the authenticator. - *

- *

- * Currently one credential type is defined, namely {@link #PUBLIC_KEY}. - *

* - * @see §5.10.2. Credential Type Enumeration - * (enum PublicKeyCredentialType) + *

It is an extensions point; values may be added to it in the future, as more credential types + * are defined. The values of this enumeration are used for versioning the Authentication Assertion + * and attestation structures according to the type of the authenticator. + * + *

Currently one credential type is defined, namely {@link #PUBLIC_KEY}. + * + * @see §5.10.2. + * Credential Type Enumeration (enum PublicKeyCredentialType) */ @JsonSerialize(using = JsonStringSerializer.class) @AllArgsConstructor public enum PublicKeyCredentialType implements JsonStringSerializable { - PUBLIC_KEY("public-key"); + PUBLIC_KEY("public-key"); - @NonNull - private final String id; + @NonNull private final String id; - private static Optional fromString(@NonNull String id) { - return Stream.of(values()).filter(v -> v.id.equals(id)).findAny(); - } + private static Optional fromString(@NonNull String id) { + return Stream.of(values()).filter(v -> v.id.equals(id)).findAny(); + } - @JsonCreator - private static PublicKeyCredentialType fromJsonString(@NonNull String id) { - return fromString(id).orElseThrow(() -> new IllegalArgumentException(String.format( - "Unknown %s value: %s", PublicKeyCredentialType.class.getSimpleName(), id - ))); - } - - @Override - public String toJsonString() { - return id; - } + @JsonCreator + private static PublicKeyCredentialType fromJsonString(@NonNull String id) { + return fromString(id) + .orElseThrow( + () -> + new IllegalArgumentException( + String.format( + "Unknown %s value: %s", + PublicKeyCredentialType.class.getSimpleName(), id))); + } + @Override + public String toJsonString() { + return id; + } } - diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/RegistrationExtensionInputs.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/RegistrationExtensionInputs.java index 20e93f4fe..11d45cd48 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/RegistrationExtensionInputs.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/RegistrationExtensionInputs.java @@ -32,24 +32,22 @@ import lombok.Value; /** - * Contains client extension - * inputs to a - * navigator.credentials.create() operation. All members are optional. + * Contains client + * extension inputs to a navigator.credentials.create() operation. All members are + * optional. * - *

- * The authenticator extension inputs are derived from these client extension inputs. - *

+ *

The authenticator extension inputs are derived from these client extension inputs. * - * @see §9. WebAuthn Extensions + * @see §9. WebAuthn + * Extensions */ @Value @AllArgsConstructor(access = AccessLevel.PRIVATE) @Builder(toBuilder = true) public class RegistrationExtensionInputs implements ExtensionInputs { - @Override - public Set getExtensionIds() { - return Collections.emptySet(); - } - + @Override + public Set getExtensionIds() { + return Collections.emptySet(); + } } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/RelyingPartyIdentity.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/RelyingPartyIdentity.java index daf708367..b5b75261d 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/RelyingPartyIdentity.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/RelyingPartyIdentity.java @@ -33,116 +33,108 @@ import lombok.NonNull; import lombok.Value; - /** * Used to supply additional Relying Party attributes when creating a new credential. * - * @see §5.4.2. Relying - * Party Parameters for Credential Generation (dictionary PublicKeyCredentialRpEntity) - * + * @see §5.4.2. + * Relying Party Parameters for Credential Generation (dictionary PublicKeyCredentialRpEntity) + * */ @Value @Builder(toBuilder = true) public class RelyingPartyIdentity implements PublicKeyCredentialEntity { - /** - * The human-palatable name of the Relaying Party. - * - *

- * For example: "ACME Corporation", "Wonderful Widgets, Inc." or "ОАО Примертех". - *

- */ - @NonNull - @Getter(onMethod = @__({ @Override })) - private final String name; + /** + * The human-palatable name of the Relaying Party. + * + *

For example: "ACME Corporation", "Wonderful Widgets, Inc." or "ОАО Примертех". + */ + @NonNull + @Getter(onMethod = @__({@Override})) + private final String name; + + /** + * A unique identifier for the Relying Party, which sets the RP ID. + * + * @see RP ID + */ + @NonNull private final String id; + + /** + * A URL which resolves to an image associated with the entity. For example, this could be the + * Relying Party's logo. + * + *

This URL MUST be an a priori authenticated URL. Authenticators MUST accept and store a + * 128-byte minimum length for an icon member’s value. Authenticators MAY ignore an icon member’s + * value if its length is greater than 128 bytes. The URL’s scheme MAY be "data" to avoid fetches + * of the URL, at the cost of needing more storage. + */ + private final URL icon; + + @JsonCreator + private RelyingPartyIdentity( + @NonNull @JsonProperty("name") String name, + @NonNull @JsonProperty("id") String id, + @JsonProperty("icon") URL icon) { + this.name = name; + this.id = id; + this.icon = icon; + } + + public static RelyingPartyIdentityBuilder.MandatoryStages builder() { + return new RelyingPartyIdentityBuilder.MandatoryStages(); + } + + public static class RelyingPartyIdentityBuilder { + private URL icon = null; + + public static class MandatoryStages { + private RelyingPartyIdentityBuilder builder = new RelyingPartyIdentityBuilder(); + + public Step2 id(String id) { + builder.id(id); + return new Step2(); + } + + public class Step2 { + public RelyingPartyIdentityBuilder name(String name) { + return builder.name(name); + } + } + } /** - * A unique identifier for the Relying Party, which sets the RP - * ID. + * A URL which resolves to an image associated with the entity. For example, this could be the + * Relying Party's logo. * - * @see RP ID + *

This URL MUST be an a priori authenticated URL. Authenticators MUST accept and store a + * 128-byte minimum length for an icon member’s value. Authenticators MAY ignore an icon + * member’s value if its length is greater than 128 bytes. The URL’s scheme MAY be "data" to + * avoid fetches of the URL, at the cost of needing more storage. */ - @NonNull - private final String id; + public RelyingPartyIdentityBuilder icon(@NonNull Optional icon) { + return this.icon(icon.orElse(null)); + } /** - * A URL which resolves to an image associated with the entity. For example, this could be the Relying Party's - * logo. + * A URL which resolves to an image associated with the entity. For example, this could be the + * Relying Party's logo. * *

This URL MUST be an a priori authenticated URL. Authenticators MUST accept and store a - * 128-byte minimum length for an icon member’s value. Authenticators MAY ignore an icon member’s value if its - * length is greater than 128 bytes. The URL’s scheme MAY be "data" to avoid fetches of the URL, at the cost of - * needing more storage. - *

+ * 128-byte minimum length for an icon member’s value. Authenticators MAY ignore an icon + * member’s value if its length is greater than 128 bytes. The URL’s scheme MAY be "data" to + * avoid fetches of the URL, at the cost of needing more storage. */ - private final URL icon; - - @JsonCreator - private RelyingPartyIdentity( - @NonNull @JsonProperty("name") String name, - @NonNull @JsonProperty("id") String id, - @JsonProperty("icon") URL icon - ) { - this.name = name; - this.id = id; - this.icon = icon; - } - - public static RelyingPartyIdentityBuilder.MandatoryStages builder() { - return new RelyingPartyIdentityBuilder.MandatoryStages(); - } - - public static class RelyingPartyIdentityBuilder { - private URL icon = null; - - public static class MandatoryStages { - private RelyingPartyIdentityBuilder builder = new RelyingPartyIdentityBuilder(); - - public Step2 id(String id) { - builder.id(id); - return new Step2(); - } - - public class Step2 { - public RelyingPartyIdentityBuilder name(String name) { - return builder.name(name); - } - } - } - - /** - * A URL which resolves to an image associated with the entity. For example, this could be the Relying Party's - * logo. - * - *

This URL MUST be an a priori authenticated URL. Authenticators MUST accept and store a - * 128-byte minimum length for an icon member’s value. Authenticators MAY ignore an icon member’s value if its - * length is greater than 128 bytes. The URL’s scheme MAY be "data" to avoid fetches of the URL, at the cost of - * needing more storage. - *

- */ - public RelyingPartyIdentityBuilder icon(@NonNull Optional icon) { - return this.icon(icon.orElse(null)); - } - - /** - * A URL which resolves to an image associated with the entity. For example, this could be the Relying Party's - * logo. - * - *

This URL MUST be an a priori authenticated URL. Authenticators MUST accept and store a - * 128-byte minimum length for an icon member’s value. Authenticators MAY ignore an icon member’s value if its - * length is greater than 128 bytes. The URL’s scheme MAY be "data" to avoid fetches of the URL, at the cost of - * needing more storage. - *

- */ - public RelyingPartyIdentityBuilder icon(URL icon) { - this.icon = icon; - return this; - } - } - - @Override - public Optional getIcon() { - return Optional.ofNullable(icon); + public RelyingPartyIdentityBuilder icon(URL icon) { + this.icon = icon; + return this; } + } + @Override + public Optional getIcon() { + return Optional.ofNullable(icon); + } } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/TokenBindingInfo.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/TokenBindingInfo.java index 3b27d957c..9c6dc1d86 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/TokenBindingInfo.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/TokenBindingInfo.java @@ -24,65 +24,61 @@ package com.yubico.webauthn.data; +import static com.yubico.internal.util.ExceptionUtil.assure; + import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import java.util.Optional; import lombok.NonNull; import lombok.Value; -import static com.yubico.internal.util.ExceptionUtil.assure; - /** - * Information about the state of the Token Binding protocol used when - * communicating with the Relying Party. + * Information about the state of the Token Binding + * protocol used when communicating with the Relying Party. * - * @see dictionary TokenBinding + * @see dictionary + * TokenBinding */ @Value public class TokenBindingInfo { - @NonNull - private final TokenBindingStatus status; + @NonNull private final TokenBindingStatus status; - /** - * This member MUST be present if {@link #status} is present, and MUST be a base64url encoding of the Token Binding - * ID that was used when communicating with the Relying Party. - */ - private final ByteArray id; + /** + * This member MUST be present if {@link #status} is present, and MUST be a base64url encoding of + * the Token Binding ID that was used when communicating with the Relying Party. + */ + private final ByteArray id; - @JsonCreator - TokenBindingInfo( - @NonNull @JsonProperty("status") TokenBindingStatus status, - @NonNull @JsonProperty("id") Optional id - ) { - if (status == TokenBindingStatus.PRESENT) { - assure( - id.isPresent(), - "Token binding ID must be present if status is \"%s\".", - TokenBindingStatus.PRESENT - ); - } else { - assure( - !id.isPresent(), - "Token binding ID must not be present if status is not \"%s\".", - TokenBindingStatus.PRESENT - ); - } - - this.status = status; - this.id = id.orElse(null); + @JsonCreator + TokenBindingInfo( + @NonNull @JsonProperty("status") TokenBindingStatus status, + @NonNull @JsonProperty("id") Optional id) { + if (status == TokenBindingStatus.PRESENT) { + assure( + id.isPresent(), + "Token binding ID must be present if status is \"%s\".", + TokenBindingStatus.PRESENT); + } else { + assure( + !id.isPresent(), + "Token binding ID must not be present if status is not \"%s\".", + TokenBindingStatus.PRESENT); } - public static TokenBindingInfo present(@NonNull ByteArray id) { - return new TokenBindingInfo(TokenBindingStatus.PRESENT, Optional.of(id)); - } + this.status = status; + this.id = id.orElse(null); + } - public static TokenBindingInfo supported() { - return new TokenBindingInfo(TokenBindingStatus.SUPPORTED, Optional.empty()); - } + public static TokenBindingInfo present(@NonNull ByteArray id) { + return new TokenBindingInfo(TokenBindingStatus.PRESENT, Optional.of(id)); + } - public Optional getId() { - return Optional.ofNullable(id); - } + public static TokenBindingInfo supported() { + return new TokenBindingInfo(TokenBindingStatus.SUPPORTED, Optional.empty()); + } + public Optional getId() { + return Optional.ofNullable(id); + } } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/TokenBindingStatus.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/TokenBindingStatus.java index 431ce4258..35263c376 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/TokenBindingStatus.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/TokenBindingStatus.java @@ -34,47 +34,47 @@ import lombok.NonNull; /** - * Indicators of whether a {@link TokenBindingInfo}'s {@link TokenBindingInfo#getId() id} member is present and, if not, - * whether the client supports token binding. + * Indicators of whether a {@link TokenBindingInfo}'s {@link TokenBindingInfo#getId() id} member is + * present and, if not, whether the client supports token binding. * - * @see enum TokenBindingStatus + * @see enum + * TokenBindingStatus * @see TokenBindingInfo */ @AllArgsConstructor @JsonSerialize(using = JsonStringSerializer.class) public enum TokenBindingStatus implements JsonStringSerializable { - /** - * Indicates token binding was used when communicating with the Relying Party. In this case, the {@link - * TokenBindingStatus#id} member MUST be present. - */ - PRESENT("present"), + /** + * Indicates token binding was used when communicating with the Relying Party. In this case, the + * {@link TokenBindingStatus#id} member MUST be present. + */ + PRESENT("present"), - /** - * Indicates the client supports token binding, but it was not negotiated when communicating with the Relying - * Party. - */ - SUPPORTED("supported"); + /** + * Indicates the client supports token binding, but it was not negotiated when communicating with + * the Relying Party. + */ + SUPPORTED("supported"); - @NonNull - private final String id; + @NonNull private final String id; - private static Optional fromString(@NonNull String value) { - return Arrays.stream(values()) - .filter(v -> v.id.equals(value)) - .findAny(); - } + private static Optional fromString(@NonNull String value) { + return Arrays.stream(values()).filter(v -> v.id.equals(value)).findAny(); + } - @JsonCreator - public static TokenBindingStatus fromJsonString(@NonNull String id) { - return fromString(id).orElseThrow(() -> new IllegalArgumentException(String.format( - "Unknown %s value: %s", TokenBindingStatus.class.getSimpleName(), id - ))); - } - - @Override - public String toJsonString() { - return id; - } + @JsonCreator + public static TokenBindingStatus fromJsonString(@NonNull String id) { + return fromString(id) + .orElseThrow( + () -> + new IllegalArgumentException( + String.format( + "Unknown %s value: %s", TokenBindingStatus.class.getSimpleName(), id))); + } + @Override + public String toJsonString() { + return id; + } } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/UserIdentity.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/UserIdentity.java index 6384fe665..fe33cdf8d 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/UserIdentity.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/UserIdentity.java @@ -33,168 +33,158 @@ import lombok.NonNull; import lombok.Value; - /** * Describes a user account, with which public key credentials can be associated. * - * @see §5.4.3. User - * Account Parameters for Credential Generation (dictionary PublicKeyCredentialUserEntity) - * + * @see §5.4.3. + * User Account Parameters for Credential Generation (dictionary PublicKeyCredentialUserEntity) + * */ @Value @Builder(toBuilder = true) public class UserIdentity implements PublicKeyCredentialEntity { - /** - * A human-palatable identifier for a user account. It is intended only for display, i.e., aiding the user in - * determining the difference between user accounts with similar {@link #displayName}s. - *

- * For example: "alexm", "alex.p.mueller@example.com" or "+14255551234". - *

- */ - @NonNull - @Getter(onMethod = @__({ @Override })) - private final String name; + /** + * A human-palatable identifier for a user account. It is intended only for display, i.e., aiding + * the user in determining the difference between user accounts with similar {@link + * #displayName}s. + * + *

For example: "alexm", "alex.p.mueller@example.com" or "+14255551234". + */ + @NonNull + @Getter(onMethod = @__({@Override})) + private final String name; + + /** + * A human-palatable name for the user account, intended only for display. For example, "Alex P. + * Müller" or "田中 倫". The Relying Party SHOULD let the user choose this, and SHOULD NOT restrict + * the choice more than necessary. + * + *

    + *
  • Relying Parties SHOULD perform enforcement, as prescribed in Section 2.3 of [RFC8266] for + * the Nickname Profile of the PRECIS FreeformClass [RFC8264], when setting {@link + * #displayName}'s value, or displaying the value to the user. + *
  • Clients SHOULD perform enforcement, as prescribed in Section 2.3 of [RFC8266] for the + * Nickname Profile of the PRECIS FreeformClass [RFC8264], on {@link #displayName}'s value + * prior to displaying the value to the user or including the value as a parameter of the + * authenticatorMakeCredential operation. + *
+ * + *

When clients, client platforms, or authenticators display a {@link #displayName}'s value, + * they should always use UI elements to provide a clear boundary around the displayed value, and + * not allow overflow into other elements. + * + *

Authenticators MUST accept and store a 64-byte minimum length for a {@link #displayName} + * member's value. Authenticators MAY truncate a {@link #displayName} member's value to a length + * equal to or greater than 64 bytes. + * + * @see RFC 8264 + * @see RFC 8266 + */ + @NonNull private final String displayName; + + /** + * The user handle for + * the account, specified by the Relying Party. + * + *

A user handle is an opaque byte sequence with a maximum size of 64 bytes. User handles are + * not meant to be displayed to users. The user handle SHOULD NOT contain personally identifying + * information about the user, such as a username or e-mail address; see §14.9 User + * Handle Contents for details. + * + *

To ensure secure operation, authentication and authorization decisions MUST be made on the + * basis of this {@link #id} member, not the {@link #displayName} nor {@link #name} members. See + * Section 6.1 of RFC 8266. + * + *

An authenticator will never contain more than one credential for a given Relying Party under + * the same user handle. + */ + @NonNull private final ByteArray id; + + /** + * A URL which resolves to an image associated with the entity. For example, this could be the + * user’s avatar. + * + *

This URL MUST be an a priori authenticated URL. Authenticators MUST accept and store a + * 128-byte minimum length for an icon member’s value. Authenticators MAY ignore an icon member’s + * value if its length is greater than 128 bytes. The URL’s scheme MAY be "data" to avoid fetches + * of the URL, at the cost of needing more storage. + */ + private final URL icon; + + @JsonCreator + private UserIdentity( + @NonNull @JsonProperty("name") String name, + @NonNull @JsonProperty("displayName") String displayName, + @NonNull @JsonProperty("id") ByteArray id, + @JsonProperty("icon") URL icon) { + this.name = name; + this.displayName = displayName; + this.id = id; + this.icon = icon; + } + + public static UserIdentityBuilder.MandatoryStages builder() { + return new UserIdentityBuilder.MandatoryStages(); + } + + public static class UserIdentityBuilder { + private URL icon = null; + + public static class MandatoryStages { + private UserIdentityBuilder builder = new UserIdentityBuilder(); + + public Step2 name(String name) { + builder.name(name); + return new Step2(); + } + + public class Step2 { + public Step3 displayName(String displayName) { + builder.displayName(displayName); + return new Step3(); + } + } - /** - * A human-palatable name for the user account, intended only for display. For example, "Alex P. Müller" or "田中 倫". - * The Relying Party SHOULD let the user choose this, and SHOULD NOT restrict the choice more than necessary. - * - *

    - *
  • Relying Parties SHOULD perform enforcement, as prescribed in Section 2.3 of [RFC8266] for the Nickname - * Profile of the PRECIS FreeformClass [RFC8264], when setting {@link #displayName}'s value, or displaying the value - * to the user.
  • - *
  • Clients SHOULD perform enforcement, as prescribed in Section 2.3 of [RFC8266] for the Nickname Profile of - * the PRECIS FreeformClass [RFC8264], on {@link #displayName}'s value prior to displaying the value to the user or - * including the value as a parameter of the authenticatorMakeCredential operation.
  • - *
- * - *

- * When clients, client platforms, or authenticators display a {@link #displayName}'s value, they should always use - * UI elements to provide a clear boundary around the displayed value, and not allow overflow into other elements. - *

- * - *

- * Authenticators MUST accept and store a 64-byte minimum length for a {@link #displayName} member's value. - * Authenticators MAY truncate a {@link #displayName} member's value to a length equal to or greater than 64 bytes. - *

- * - * @see RFC 8264 - * @see RFC 8266 - */ - @NonNull - private final String displayName; + public class Step3 { + public UserIdentityBuilder id(ByteArray id) { + return builder.id(id); + } + } + } /** - * The user handle for the account, - * specified by the Relying Party. + * A URL which resolves to an image associated with the entity. For example, this could be the + * user’s avatar. * - *

- * A user handle is an opaque byte sequence with a maximum size of 64 bytes. User handles are not meant to be - * displayed to users. The user handle SHOULD NOT contain personally identifying information about the user, such as - * a username or e-mail address; see §14.9 - * User Handle Contents for details. - *

- * - *

- * To ensure secure operation, authentication and authorization decisions MUST be made on the basis of this {@link - * #id} member, not the {@link #displayName} nor {@link #name} members. See Section - * 6.1 of RFC 8266. - *

- * - *

- * An authenticator will never contain more than one credential for a given Relying Party under the same user - * handle. - *

+ *

This URL MUST be an a priori authenticated URL. Authenticators MUST accept and store a + * 128-byte minimum length for an icon member’s value. Authenticators MAY ignore an icon + * member’s value if its length is greater than 128 bytes. The URL’s scheme MAY be "data" to + * avoid fetches of the URL, at the cost of needing more storage. */ - @NonNull - private final ByteArray id; + public UserIdentityBuilder icon(@NonNull Optional icon) { + return this.icon(icon.orElse(null)); + } /** - * A URL which resolves to an image associated with the entity. For example, this could be the user’s avatar. + * A URL which resolves to an image associated with the entity. For example, this could be the + * user’s avatar. * *

This URL MUST be an a priori authenticated URL. Authenticators MUST accept and store a - * 128-byte minimum length for an icon member’s value. Authenticators MAY ignore an icon member’s value if its - * length is greater than 128 bytes. The URL’s scheme MAY be "data" to avoid fetches of the URL, at the cost of - * needing more storage. - *

+ * 128-byte minimum length for an icon member’s value. Authenticators MAY ignore an icon + * member’s value if its length is greater than 128 bytes. The URL’s scheme MAY be "data" to + * avoid fetches of the URL, at the cost of needing more storage. */ - private final URL icon; - - @JsonCreator - private UserIdentity( - @NonNull @JsonProperty("name") String name, - @NonNull @JsonProperty("displayName") String displayName, - @NonNull @JsonProperty("id") ByteArray id, - @JsonProperty("icon") URL icon - ) { - this.name = name; - this.displayName = displayName; - this.id = id; - this.icon = icon; - } - - public static UserIdentityBuilder.MandatoryStages builder() { - return new UserIdentityBuilder.MandatoryStages(); - } - - public static class UserIdentityBuilder { - private URL icon = null; - - public static class MandatoryStages { - private UserIdentityBuilder builder = new UserIdentityBuilder(); - - public Step2 name(String name) { - builder.name(name); - return new Step2(); - } - - public class Step2 { - public Step3 displayName(String displayName) { - builder.displayName(displayName); - return new Step3(); - } - } - - public class Step3 { - public UserIdentityBuilder id(ByteArray id) { - return builder.id(id); - } - - } - } - - /** - * A URL which resolves to an image associated with the entity. For example, this could be the user’s avatar. - * - *

This URL MUST be an a priori authenticated URL. Authenticators MUST accept and store a - * 128-byte minimum length for an icon member’s value. Authenticators MAY ignore an icon member’s value if its - * length is greater than 128 bytes. The URL’s scheme MAY be "data" to avoid fetches of the URL, at the cost of - * needing more storage. - *

- */ - public UserIdentityBuilder icon(@NonNull Optional icon) { - return this.icon(icon.orElse(null)); - } - - /** - * A URL which resolves to an image associated with the entity. For example, this could be the user’s avatar. - * - *

This URL MUST be an a priori authenticated URL. Authenticators MUST accept and store a - * 128-byte minimum length for an icon member’s value. Authenticators MAY ignore an icon member’s value if its - * length is greater than 128 bytes. The URL’s scheme MAY be "data" to avoid fetches of the URL, at the cost of - * needing more storage. - *

- */ - public UserIdentityBuilder icon(URL icon) { - this.icon = icon; - return this; - } - } - - @Override - public Optional getIcon() { - return Optional.ofNullable(icon); + public UserIdentityBuilder icon(URL icon) { + this.icon = icon; + return this; } + } + @Override + public Optional getIcon() { + return Optional.ofNullable(icon); + } } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/UserVerificationRequirement.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/UserVerificationRequirement.java index abcfea21f..11294c71f 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/UserVerificationRequirement.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/UserVerificationRequirement.java @@ -33,54 +33,57 @@ import lombok.AllArgsConstructor; import lombok.NonNull; - /** - * A WebAuthn Relying Party may require user - * verification for some of its operations but not for others, and may use this type to express its needs. + * A WebAuthn Relying Party may require user verification + * for some of its operations but not for others, and may use this type to express its needs. * - * @see §5.10.6. User - * Verification Requirement Enumeration (enum UserVerificationRequirement) + * @see §5.10.6. + * User Verification Requirement Enumeration (enum UserVerificationRequirement) */ @JsonSerialize(using = JsonStringSerializer.class) @AllArgsConstructor public enum UserVerificationRequirement implements JsonStringSerializable { - /** - * This value indicates that the Relying Party does not want user verification employed during the operation (e.g., - * in the interest of minimizing disruption to the user interaction flow). - */ - DISCOURAGED("discouraged"), - - /** - * This value indicates that the Relying Party prefers user verification for the operation if possible, but will not - * fail the operation if the response does not have the {@link AuthenticatorDataFlags#UV} flag set. - */ - PREFERRED("preferred"), + /** + * This value indicates that the Relying Party does not want user verification employed during the + * operation (e.g., in the interest of minimizing disruption to the user interaction flow). + */ + DISCOURAGED("discouraged"), - /** - * Indicates that the Relying Party requires user verification for the operation and will fail the operation if the - * response does not have the {@link AuthenticatorDataFlags#UV} flag set. - */ - REQUIRED("required"); + /** + * This value indicates that the Relying Party prefers user verification for the operation if + * possible, but will not fail the operation if the response does not have the {@link + * AuthenticatorDataFlags#UV} flag set. + */ + PREFERRED("preferred"), - @NonNull - private final String id; + /** + * Indicates that the Relying Party requires user verification for the operation and will fail the + * operation if the response does not have the {@link AuthenticatorDataFlags#UV} flag set. + */ + REQUIRED("required"); - private static Optional fromString(@NonNull String id) { - return Stream.of(values()).filter(v -> v.id.equals(id)).findAny(); - } + @NonNull private final String id; - @JsonCreator - private static UserVerificationRequirement fromJsonString(@NonNull String id) { - return fromString(id).orElseThrow(() -> new IllegalArgumentException(String.format( - "Unknown %s value: %s", UserVerificationRequirement.class.getSimpleName(), id - ))); - } + private static Optional fromString(@NonNull String id) { + return Stream.of(values()).filter(v -> v.id.equals(id)).findAny(); + } - @Override - public String toJsonString() { - return id; - } + @JsonCreator + private static UserVerificationRequirement fromJsonString(@NonNull String id) { + return fromString(id) + .orElseThrow( + () -> + new IllegalArgumentException( + String.format( + "Unknown %s value: %s", + UserVerificationRequirement.class.getSimpleName(), id))); + } + @Override + public String toJsonString() { + return id; + } } - diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/exception/Base64UrlException.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/exception/Base64UrlException.java index 25c03ab82..c7eb0e87a 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/exception/Base64UrlException.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/exception/Base64UrlException.java @@ -26,8 +26,7 @@ public final class Base64UrlException extends Exception { - public Base64UrlException(String s, Throwable throwable) { - super(s, throwable); - } - + public Base64UrlException(String s, Throwable throwable) { + super(s, throwable); + } } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/exception/HexException.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/exception/HexException.java index 7440eed9c..af33b1673 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/exception/HexException.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/exception/HexException.java @@ -26,8 +26,7 @@ public final class HexException extends Exception { - public HexException(String s, Throwable cause) { - super(s, cause); - } - + public HexException(String s, Throwable cause) { + super(s, cause); + } } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/exception/AssertionFailedException.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/exception/AssertionFailedException.java index 180d70f68..262fb9cfc 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/exception/AssertionFailedException.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/exception/AssertionFailedException.java @@ -26,12 +26,11 @@ public class AssertionFailedException extends Exception { - public AssertionFailedException(IllegalArgumentException e) { - super(e); - } - - public AssertionFailedException(String message) { - super(message); - } + public AssertionFailedException(IllegalArgumentException e) { + super(e); + } + public AssertionFailedException(String message) { + super(message); + } } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/exception/InvalidSignatureCountException.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/exception/InvalidSignatureCountException.java index 97d4cdc1c..1e3fa5961 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/exception/InvalidSignatureCountException.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/exception/InvalidSignatureCountException.java @@ -8,19 +8,18 @@ @EqualsAndHashCode(callSuper = true) public class InvalidSignatureCountException extends AssertionFailedException { - private final ByteArray credentialId; - private final long expectedMinimum; - private final long received; + private final ByteArray credentialId; + private final long expectedMinimum; + private final long received; - public InvalidSignatureCountException(ByteArray credentialId, long expectedMinimum, long received) { - super(String.format( + public InvalidSignatureCountException( + ByteArray credentialId, long expectedMinimum, long received) { + super( + String.format( "Signature counter must increase. Expected minimum: %s, received value: %s", - expectedMinimum, - received - )); - this.credentialId = credentialId; - this.expectedMinimum = expectedMinimum; - this.received = received; - } - + expectedMinimum, received)); + this.credentialId = credentialId; + this.expectedMinimum = expectedMinimum; + this.received = received; + } } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/exception/RegistrationFailedException.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/exception/RegistrationFailedException.java index b40199e94..66ae31729 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/exception/RegistrationFailedException.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/exception/RegistrationFailedException.java @@ -26,8 +26,7 @@ public final class RegistrationFailedException extends Exception { - public RegistrationFailedException(IllegalArgumentException e) { - super(e); - } - + public RegistrationFailedException(IllegalArgumentException e) { + super(e); + } } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/extension/appid/AppId.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/extension/appid/AppId.java index a6d53c4d6..a1c7ae50a 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/extension/appid/AppId.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/extension/appid/AppId.java @@ -35,78 +35,84 @@ import lombok.NonNull; import lombok.Value; - /** * A FIDO AppID verified to be syntactically valid. * - * @see FIDO AppID and Facet Specification + * @see FIDO + * AppID and Facet Specification */ @Value @JsonSerialize(using = AppId.JsonSerializer.class) public class AppId { - /** - * The underlying string representation of this AppID. - */ - private final String id; + /** The underlying string representation of this AppID. */ + private final String id; - /** - * Verify that the appId is a valid FIDO AppID, and wrap it as an {@link AppId}. - * - * @throws InvalidAppIdException if appId is not a valid FIDO AppID. - * @see FIDO AppID and Facet Specification - */ - @JsonCreator - public AppId(@NonNull String appId) throws InvalidAppIdException { - checkIsValid(appId); - this.id = appId; - } + /** + * Verify that the appId is a valid FIDO AppID, and wrap it as an {@link AppId}. + * + * @throws InvalidAppIdException if appId is not a valid FIDO AppID. + * @see FIDO + * AppID and Facet Specification + */ + @JsonCreator + public AppId(@NonNull String appId) throws InvalidAppIdException { + checkIsValid(appId); + this.id = appId; + } - /** - * Throws {@link InvalidAppIdException} if the given App ID is found to be incompatible with the U2F specification or any major - * U2F Client implementation. - * - * @param appId the App ID to be validated - */ - private static void checkIsValid(String appId) throws InvalidAppIdException { - if(!appId.contains(":")) { - throw new InvalidAppIdException("App ID does not look like a valid facet or URL. Web facets must start with 'https://'."); - } - if(appId.startsWith("http:")) { - throw new InvalidAppIdException("HTTP is not supported for App IDs (by Chrome). Use HTTPS instead."); - } - if(appId.startsWith("https://")) { - URI url = checkValidUrl(appId); - checkPathIsNotSlash(url); - checkNotIpAddress(url); - } + /** + * Throws {@link InvalidAppIdException} if the given App ID is found to be incompatible with the + * U2F specification or any major U2F Client implementation. + * + * @param appId the App ID to be validated + */ + private static void checkIsValid(String appId) throws InvalidAppIdException { + if (!appId.contains(":")) { + throw new InvalidAppIdException( + "App ID does not look like a valid facet or URL. Web facets must start with 'https://'."); } - - private static void checkPathIsNotSlash(URI url) throws InvalidAppIdException { - if("/".equals(url.getPath())) { - throw new InvalidAppIdException("The path of the URL set as App ID is '/'. This is probably not what you want -- remove the trailing slash of the App ID URL."); - } + if (appId.startsWith("http:")) { + throw new InvalidAppIdException( + "HTTP is not supported for App IDs (by Chrome). Use HTTPS instead."); } + if (appId.startsWith("https://")) { + URI url = checkValidUrl(appId); + checkPathIsNotSlash(url); + checkNotIpAddress(url); + } + } - private static URI checkValidUrl(String appId) throws InvalidAppIdException { - try { - return new URI(appId); - } catch (URISyntaxException e) { - throw new InvalidAppIdException("App ID looks like a HTTPS URL, but has syntax errors.", e); - } + private static void checkPathIsNotSlash(URI url) throws InvalidAppIdException { + if ("/".equals(url.getPath())) { + throw new InvalidAppIdException( + "The path of the URL set as App ID is '/'. This is probably not what you want -- remove the trailing slash of the App ID URL."); } + } - private static void checkNotIpAddress(URI url) throws InvalidAppIdException { - if (InetAddresses.isInetAddress(url.getAuthority()) || (url.getHost() != null && InetAddresses.isInetAddress(url.getHost()))) { - throw new InvalidAppIdException("App ID must not be an IP-address, since it is not supported (by Chrome). Use a host name instead."); - } + private static URI checkValidUrl(String appId) throws InvalidAppIdException { + try { + return new URI(appId); + } catch (URISyntaxException e) { + throw new InvalidAppIdException("App ID looks like a HTTPS URL, but has syntax errors.", e); } + } - static class JsonSerializer extends com.fasterxml.jackson.databind.JsonSerializer { - @Override - public void serialize(AppId value, JsonGenerator gen, SerializerProvider serializers) throws IOException { - gen.writeString(value.getId()); - } + private static void checkNotIpAddress(URI url) throws InvalidAppIdException { + if (InetAddresses.isInetAddress(url.getAuthority()) + || (url.getHost() != null && InetAddresses.isInetAddress(url.getHost()))) { + throw new InvalidAppIdException( + "App ID must not be an IP-address, since it is not supported (by Chrome). Use a host name instead."); } + } + static class JsonSerializer extends com.fasterxml.jackson.databind.JsonSerializer { + @Override + public void serialize(AppId value, JsonGenerator gen, SerializerProvider serializers) + throws IOException { + gen.writeString(value.getId()); + } + } } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/extension/appid/InvalidAppIdException.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/extension/appid/InvalidAppIdException.java index 4630979d4..e2aefd611 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/extension/appid/InvalidAppIdException.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/extension/appid/InvalidAppIdException.java @@ -25,11 +25,11 @@ package com.yubico.webauthn.extension.appid; public final class InvalidAppIdException extends Exception { - public InvalidAppIdException(String message) { - super(message); - } + public InvalidAppIdException(String message) { + super(message); + } - public InvalidAppIdException(String message, Throwable cause) { - super(message, cause); - } + public InvalidAppIdException(String message, Throwable cause) { + super(message, cause); + } } 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 0ba028704..a8063f341 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 @@ -32,49 +32,34 @@ import lombok.AllArgsConstructor; import lombok.NonNull; -/** - * A representation of Web Authentication specification document statuses. - */ +/** A representation of Web Authentication specification document statuses. */ @JsonSerialize(using = JsonStringSerializer.class) @AllArgsConstructor public enum DocumentStatus implements JsonStringSerializable { - /** - * An editor's draft is a changing work in progress. - */ - EDITORS_DRAFT("editors-draft"), + /** An editor's draft is a changing work in progress. */ + EDITORS_DRAFT("editors-draft"), - /** - * A working draft is a named snapshot of a particular state of an editor's draft. - */ - WORKING_DRAFT("working-draft"), + /** A working draft is a named snapshot of a particular state of an editor's draft. */ + WORKING_DRAFT("working-draft"), - /** - * A candidate recommendation is a specification release candidate. - */ - CANDIDATE_RECOMMENDATION("candidate-recommendation"), + /** A candidate recommendation is a specification release candidate. */ + CANDIDATE_RECOMMENDATION("candidate-recommendation"), - /** - * A proposed recommendation is a finished draft intended for release. - */ - PROPOSED_RECOMMENDATION("proposed-recommendation"), + /** A proposed recommendation is a finished draft intended for release. */ + PROPOSED_RECOMMENDATION("proposed-recommendation"), - /** - * A recommendation is a finished and released specification. - */ - RECOMMENDATION("recommendation"); + /** A recommendation is a finished and released specification. */ + RECOMMENDATION("recommendation"); - private final String id; + 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. - */ - @Override - public String toJsonString() { - return id; - } + static Optional fromString(@NonNull String id) { + return Stream.of(values()).filter(v -> v.id.equals(id)).findAny(); + } + /** Used by JSON serializer. */ + @Override + public String toJsonString() { + return id; + } } 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 c24bd2dc0..cd47822a9 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 @@ -30,30 +30,17 @@ import lombok.NonNull; import lombok.Value; - -/** - * Description of this version of this library - */ +/** Description of this version of this library */ @Value @AllArgsConstructor(access = AccessLevel.PACKAGE) public class Implementation { - /** - * The version number of this release of the library. - */ - @NonNull - private final String version; - - /** - * Address to where the source code for this library can be found. - */ - @NonNull - private final URL sourceCodeUrl; + /** The version number of this release of the library. */ + @NonNull private final String version; - /** - * The commit ID of the source code the library was built from, if known. - */ - @NonNull - private final String gitCommit; + /** Address to where the source code for this library can be found. */ + @NonNull private final URL sourceCodeUrl; + /** 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/Specification.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/meta/Specification.java index ed769c9dc..6d77e107c 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/meta/Specification.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/meta/Specification.java @@ -33,40 +33,28 @@ import lombok.Builder; import lombok.Value; - -/** - * Reference to a particular version of a specification document. - */ +/** Reference to a particular version of a specification document. */ @Value @AllArgsConstructor(access = AccessLevel.PRIVATE) @Builder public class Specification { - /** - * Address to this version of the specification. - */ - private final URL url; + /** Address to this version of the specification. */ + private final URL url; - /** - * Address to the latest version of this specification. - */ - private final URL latestVersionUrl; + /** Address to the latest version of this specification. */ + private final URL latestVersionUrl; - /** - * An object indicating the status of the specification document. - */ - private final DocumentStatus status; + /** An object indicating the status of the specification document. */ + private final DocumentStatus status; - /** - * The release date of the specification document. - */ - @JsonSerialize(using = LocalDateJsonSerializer.class) - private final LocalDate releaseDate; + /** The release date of the specification document. */ + @JsonSerialize(using = LocalDateJsonSerializer.class) + private final LocalDate releaseDate; - static SpecificationBuilder builder() { - return new SpecificationBuilder(); - } + static SpecificationBuilder builder() { + return new SpecificationBuilder(); + } - static class SpecificationBuilder { - } + static class SpecificationBuilder {} } 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 f9c0dbffb..203d33a68 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 @@ -34,7 +34,6 @@ import lombok.Value; import lombok.extern.slf4j.Slf4j; - /** * Contains version information for the com.yubico.webauthn package. * @@ -44,53 +43,50 @@ @Value public class VersionInfo { - private static VersionInfo instance; - - public static VersionInfo getInstance() { - if (instance == null) { - try { - instance = new VersionInfo(); - } catch (IOException e) { - throw ExceptionUtil.wrapAndLog(log, "Failed to create VersionInfo", e); - } - } + private static VersionInfo instance; - return instance; + public static VersionInfo getInstance() { + if (instance == null) { + try { + instance = new VersionInfo(); + } catch (IOException e) { + throw ExceptionUtil.wrapAndLog(log, "Failed to create VersionInfo", e); + } } - /** - * Represents the specification this implementation is based on - */ - private final Specification specification = Specification.builder() - .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(); + return instance; + } - /** - * Description of this version of this library - */ - private final Implementation implementation = new Implementation( - findValueInManifest("Implementation-Version"), - new URL(findValueInManifest("Implementation-Source-Url")), - findValueInManifest("Git-Commit") - ); + /** Represents the specification this implementation is based on */ + private final Specification specification = + Specification.builder() + .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(); - private VersionInfo() throws IOException { - } + /** Description of this version of this library */ + private final Implementation implementation = + new Implementation( + findValueInManifest("Implementation-Version"), + new URL(findValueInManifest("Implementation-Source-Url")), + findValueInManifest("Git-Commit")); - private String findValueInManifest(String key) throws IOException { - final Enumeration resources = getClass().getClassLoader().getResources("META-INF/MANIFEST.MF"); + private VersionInfo() throws IOException {} - 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."); - } + 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 manifest.getMainAttributes().getValue(key); + } + } + throw new NoSuchElementException("Could not find \"" + key + "\" in manifest."); + } } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/package-info.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/package-info.java index c092276df..dce04e9c8 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/package-info.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/package-info.java @@ -1,203 +1,177 @@ /** * This package makes up the public API of the webauthn-server-core library. * - *

- * The main entry point is the {@link com.yubico.webauthn.RelyingParty} class. It provides methods for generating inputs - * to the navigator.credentials.create() and navigator.credentials.get() methods and for - * processing the return values from those same methods. In order to do this, the {@link - * com.yubico.webauthn.RelyingParty} needs an instance of the {@link com.yubico.webauthn.CredentialRepository} interface - * to use for looking up the credential IDs and public keys registered to each user, among other things. - *

- * + *

The main entry point is the {@link com.yubico.webauthn.RelyingParty} class. It provides + * methods for generating inputs to the navigator.credentials.create() and + * navigator.credentials.get() methods and for processing the return values from those same + * methods. In order to do this, the {@link com.yubico.webauthn.RelyingParty} needs an instance of + * the {@link com.yubico.webauthn.CredentialRepository} interface to use for looking up the + * credential IDs and public keys registered to each user, among other things. * *

What this library does not do

* - *

- * This library has no concept of accounts, sessions, permissions or identity federation - it only deals with executing - * the Web Authentication authentication mechanism. Sessions, account management and other higher level concepts - * can make use of this authentication mechanism, but the authentication mechanism alone does not make a security - * system. - *

- * + *

This library has no concept of accounts, sessions, permissions or identity federation - it + * only deals with executing the Web Authentication authentication mechanism. Sessions, + * account management and other higher level concepts can make use of this authentication mechanism, + * but the authentication mechanism alone does not make a security system. * *

Usage overview

- *

- * At its core, the library provides four operations: - *

+ * + *

At its core, the library provides four operations: * *

    - *
  • - * Initiate a registration operation given a user and some settings for the credential to be created - *
  • - *
  • - * Finish a registration operation given the initiation request and the authenticator response - *
  • - *
  • - * Initiate an authentication operation given a username - *
  • - *
  • - * Finish an authentication operation given the initiation request and the authenticator response - *
  • + *
  • Initiate a registration operation given a user and some settings for the credential to be + * created + *
  • Finish a registration operation given the initiation request and the authenticator response + *
  • Initiate an authentication operation given a username + *
  • Finish an authentication operation given the initiation request and the authenticator + * response *
* - *

- * The "start" methods return request objects containing the parameters to be used in the call to - * navigator.credentials.create() or navigator.credentials.get(), and the "finish" methods - * expect a pair of such a request object and the response object returned from the browser. The library itself is - * stateless; once constructed, a {@link com.yubico.webauthn.RelyingParty} instance never modifies its fields, and the - * "finish" methods return plain object representations of the results. These methods perform all the verification logic - * specified by Web Authentication, but it is your responsibility as the library user to store pending requests and act - * upon the returned results - including enforcing policies and updating databases. - *

- * + *

The "start" methods return request objects containing the parameters to be used in the call to + * navigator.credentials.create() or navigator.credentials.get(), and the + * "finish" methods expect a pair of such a request object and the response object returned from the + * browser. The library itself is stateless; once constructed, a {@link + * com.yubico.webauthn.RelyingParty} instance never modifies its fields, and the "finish" methods + * return plain object representations of the results. These methods perform all the verification + * logic specified by Web Authentication, but it is your responsibility as the library user to store + * pending requests and act upon the returned results - including enforcing policies and updating + * databases. * *

Data classes and builders

* - *

- * Logic classes as well as data classes in this library are all immutable, and provide builders for their construction. Most builders have required - * parameters, which is encoded in the type system - the - * build() method will be made available only once all required parameters have been set. The data classes - * also each have a toBuilder() method which can be used to create a modified copy of the instance. - *

- * + *

Logic classes as well as data classes in this library are all immutable, and provide builders for their construction. Most + * builders have required parameters, which is encoded in the type system - the build() + * method will be made available only once all required parameters have been set. The data classes + * also each have a toBuilder() method which can be used to create a modified copy of + * the instance. * *

Instantiating the library

- *

- * The main entry point to the library is the {@link com.yubico.webauthn.RelyingParty} class, which can be instantiated - * via its {@link com.yubico.webauthn.RelyingParty#builder() builder}. Refer to the {@link - * com.yubico.webauthn.RelyingParty.RelyingPartyBuilder} documentation for descriptions of the parameters. Of particular - * note is the {@link com.yubico.webauthn.RelyingParty.RelyingPartyBuilder#credentialRepository(com.yubico.webauthn.CredentialRepository) - * credentialRepository} parameter, which takes an application-specific database adapter to use for looking up users' - * credentials. You'll need to implement the {@link com.yubico.webauthn.CredentialRepository} interface with your own - * database access logic. - *

- *

- * Like all other classes in the library, {@link com.yubico.webauthn.RelyingParty} is stateless and therefore thread - * safe. - *

* + *

The main entry point to the library is the {@link com.yubico.webauthn.RelyingParty} class, + * which can be instantiated via its {@link com.yubico.webauthn.RelyingParty#builder() builder}. + * Refer to the {@link com.yubico.webauthn.RelyingParty.RelyingPartyBuilder} documentation for + * descriptions of the parameters. Of particular note is the {@link + * com.yubico.webauthn.RelyingParty.RelyingPartyBuilder#credentialRepository(com.yubico.webauthn.CredentialRepository) + * credentialRepository} parameter, which takes an application-specific database adapter to use for + * looking up users' credentials. You'll need to implement the {@link + * com.yubico.webauthn.CredentialRepository} interface with your own database access logic. + * + *

Like all other classes in the library, {@link com.yubico.webauthn.RelyingParty} is stateless + * and therefore thread safe. * *

Registration

* - *

- * To initiate a registration operation, construct a {@link com.yubico.webauthn.StartRegistrationOptions} instance using - * its {@link com.yubico.webauthn.StartRegistrationOptions#builder() builder} and pass that into {@link - * com.yubico.webauthn.RelyingParty#startRegistration(StartRegistrationOptions)}. The only required parameter is a - * {@link com.yubico.webauthn.data.UserIdentity} describing the user for which to create a credential. One noteworthy - * part of {@link com.yubico.webauthn.data.UserIdentity} is the {@link com.yubico.webauthn.data.UserIdentity#getId() id} - * field, containing the user handle for the - * user. This should be a stable, unique identifier for the user - equivalent to a username, in most cases. However, due - * to privacy considerations it - * is recommended to set the user handle to a random byte array rather than, say, the username encoded in UTF-8. - *

- * - *

- * The {@link com.yubico.webauthn.RelyingParty#startRegistration(StartRegistrationOptions) startRegistration} method - * returns a {@link com.yubico.webauthn.data.PublicKeyCredentialCreationOptions} which can be serialized to JSON and - * passed as the publicKey argument to navigator.credentials.create(). You can use the {@link - * com.yubico.webauthn.data.PublicKeyCredentialCreationOptions#toBuilder() toBuilder()} method to make any modifications - * you need. You should store this in temporary storage so that it can later be passed as an argument to {@link + *

To initiate a registration operation, construct a {@link + * com.yubico.webauthn.StartRegistrationOptions} instance using its {@link + * com.yubico.webauthn.StartRegistrationOptions#builder() builder} and pass that into {@link + * com.yubico.webauthn.RelyingParty#startRegistration(StartRegistrationOptions)}. The only required + * parameter is a {@link com.yubico.webauthn.data.UserIdentity} describing the user for which to + * create a credential. One noteworthy part of {@link com.yubico.webauthn.data.UserIdentity} is the + * {@link com.yubico.webauthn.data.UserIdentity#getId() id} field, containing the user handle for the user. + * This should be a stable, unique identifier for the user - equivalent to a username, in most + * cases. However, due to privacy + * considerations it is recommended to set the user handle to a random byte array rather than, + * say, the username encoded in UTF-8. + * + *

The {@link com.yubico.webauthn.RelyingParty#startRegistration(StartRegistrationOptions) + * startRegistration} method returns a {@link + * com.yubico.webauthn.data.PublicKeyCredentialCreationOptions} which can be serialized to JSON and + * passed as the publicKey argument to navigator.credentials.create(). You + * can use the {@link com.yubico.webauthn.data.PublicKeyCredentialCreationOptions#toBuilder() + * toBuilder()} method to make any modifications you need. You should store this in temporary + * storage so that it can later be passed as an argument to {@link * com.yubico.webauthn.RelyingParty#finishRegistration(FinishRegistrationOptions)}. - *

- * - *

- * After receiving the response from the client, construct a {@link com.yubico.webauthn.data.PublicKeyCredential}<{@link - * com.yubico.webauthn.data.AuthenticatorAttestationResponse}, {@link com.yubico.webauthn.data.ClientRegistrationExtensionOutputs}> - * from the response and wrap that in a {@link com.yubico.webauthn.FinishRegistrationOptions} along with the {@link - * com.yubico.webauthn.data.PublicKeyCredentialCreationOptions} used to initiate the request. Pass that as the argument - * to {@link com.yubico.webauthn.RelyingParty#finishRegistration(FinishRegistrationOptions)}, which will return a {@link - * com.yubico.webauthn.RegistrationResult} if successful and throw an exception if not. Regardless of whether it - * succeeds, you should remove the {@link com.yubico.webauthn.data.PublicKeyCredentialCreationOptions} from the pending - * requests storage to prevent retries. - *

- * - *

- * Finally, use the {@link com.yubico.webauthn.RegistrationResult} to update any database(s) and take other actions - * depending on your application's needs. In particular: - *

+ * + *

After receiving the response from the client, construct a {@link + * com.yubico.webauthn.data.PublicKeyCredential}<{@link + * com.yubico.webauthn.data.AuthenticatorAttestationResponse}, {@link + * com.yubico.webauthn.data.ClientRegistrationExtensionOutputs}> from the response and wrap that + * in a {@link com.yubico.webauthn.FinishRegistrationOptions} along with the {@link + * com.yubico.webauthn.data.PublicKeyCredentialCreationOptions} used to initiate the request. Pass + * that as the argument to {@link + * com.yubico.webauthn.RelyingParty#finishRegistration(FinishRegistrationOptions)}, which will + * return a {@link com.yubico.webauthn.RegistrationResult} if successful and throw an exception if + * not. Regardless of whether it succeeds, you should remove the {@link + * com.yubico.webauthn.data.PublicKeyCredentialCreationOptions} from the pending requests storage to + * prevent retries. + * + *

Finally, use the {@link com.yubico.webauthn.RegistrationResult} to update any database(s) and + * take other actions depending on your application's needs. In particular: * *

    - *
  • - * Store the {@link com.yubico.webauthn.RegistrationResult#getKeyId() keyId} and {@link - * com.yubico.webauthn.RegistrationResult#getPublicKeyCose() publicKeyCose} as a new credential for the user. The {@link - * com.yubico.webauthn.CredentialRepository} will need to look these up for authentication. - *
  • - *
  • - * Inspect the {@link com.yubico.webauthn.RegistrationResult#getWarnings() warnings} - ideally there should of course be - * none. - *
  • - *
  • - * If you care about authenticator attestation, use the {@link com.yubico.webauthn.RegistrationResult#isAttestationTrusted() - * attestationTrusted}, {@link com.yubico.webauthn.RegistrationResult#getAttestationType() attestationType} and {@link - * com.yubico.webauthn.RegistrationResult#getAttestationMetadata() attestationMetadata} fields to enforce your - * attestation policy. - *
  • - *
  • - * If you care about authenticator attestation, it is recommended to also store the raw {@link - * com.yubico.webauthn.data.AuthenticatorAttestationResponse#getAttestationObject() attestation object} as part of the - * credential. This enables you to retroactively inspect credential attestations in response to policy changes and/or - * compromised authenticators. - *
  • + *
  • Store the {@link com.yubico.webauthn.RegistrationResult#getKeyId() keyId} and {@link + * com.yubico.webauthn.RegistrationResult#getPublicKeyCose() publicKeyCose} as a new + * credential for the user. The {@link com.yubico.webauthn.CredentialRepository} will need to + * look these up for authentication. + *
  • Inspect the {@link com.yubico.webauthn.RegistrationResult#getWarnings() warnings} - ideally + * there should of course be none. + *
  • If you care about authenticator attestation, use the {@link + * com.yubico.webauthn.RegistrationResult#isAttestationTrusted() attestationTrusted}, {@link + * com.yubico.webauthn.RegistrationResult#getAttestationType() attestationType} and {@link + * com.yubico.webauthn.RegistrationResult#getAttestationMetadata() attestationMetadata} fields + * to enforce your attestation policy. + *
  • If you care about authenticator attestation, it is recommended to also store the raw {@link + * com.yubico.webauthn.data.AuthenticatorAttestationResponse#getAttestationObject() + * attestation object} as part of the credential. This enables you to retroactively inspect + * credential attestations in response to policy changes and/or compromised authenticators. *
* - * *

Authentication

* - *

- * Authentication works much like registration, except less complex because of the fewer parameters and the absence of - * authenticator attestation complications. - *

- * - *

- * To initiate an authentication operation, call {@link com.yubico.webauthn.RelyingParty#startAssertion(StartAssertionOptions)}. - * The main parameter you need to set here is the {@link com.yubico.webauthn.StartAssertionOptions.StartAssertionOptionsBuilder#username(java.util.Optional) - * username} of the user to authenticate, but even this parameter is optional. If the username is not set, then the - * {@link com.yubico.webauthn.data.PublicKeyCredentialRequestOptions#getAllowCredentials() allowCredentials} parameter - * will not be set. This which means the user must use a client-side-resident - * credential to authenticate; also known as "first-factor authentication". This use case has both advantages and - * disadvantages; see the Web Authentication specification for an extended discussion of this. - *

- * - *

- * The {@link com.yubico.webauthn.RelyingParty#startAssertion(StartAssertionOptions) startAssertion} method returns an - * {@link com.yubico.webauthn.AssertionRequest} containing the username, if any, and a {@link - * com.yubico.webauthn.data.PublicKeyCredentialRequestOptions} instance which can be serialized to JSON and passed as - * the publicKey argument to navigator.credentials.get(). Again, store the {@link - * com.yubico.webauthn.AssertionRequest} in temporary storage so it can be passed as an argument to {@link + *

Authentication works much like registration, except less complex because of the fewer + * parameters and the absence of authenticator attestation complications. + * + *

To initiate an authentication operation, call {@link + * com.yubico.webauthn.RelyingParty#startAssertion(StartAssertionOptions)}. The main parameter you + * need to set here is the {@link + * com.yubico.webauthn.StartAssertionOptions.StartAssertionOptionsBuilder#username(java.util.Optional) + * username} of the user to authenticate, but even this parameter is optional. If the username is + * not set, then the {@link + * com.yubico.webauthn.data.PublicKeyCredentialRequestOptions#getAllowCredentials() + * allowCredentials} parameter will not be set. This which means the user must use a client-side-resident + * credential to authenticate; also known as "first-factor authentication". This use case has + * both advantages and disadvantages; see the Web Authentication specification for an extended + * discussion of this. + * + *

The {@link com.yubico.webauthn.RelyingParty#startAssertion(StartAssertionOptions) + * startAssertion} method returns an {@link com.yubico.webauthn.AssertionRequest} containing the + * username, if any, and a {@link com.yubico.webauthn.data.PublicKeyCredentialRequestOptions} + * instance which can be serialized to JSON and passed as the publicKey argument to + * navigator.credentials.get(). Again, store the {@link + * com.yubico.webauthn.AssertionRequest} in temporary storage so it can be passed as an argument to + * {@link * com.yubico.webauthn.RelyingParty#finishAssertion(com.yubico.webauthn.FinishAssertionOptions)}. - *

- * - *

- * After receiving the response from the client, construct a {@link com.yubico.webauthn.data.PublicKeyCredential}<{@link - * com.yubico.webauthn.data.AuthenticatorAssertionResponse}, {@link com.yubico.webauthn.data.ClientAssertionExtensionOutputs}> - * from the response and wrap that in a {@link com.yubico.webauthn.FinishAssertionOptions} along with the {@link - * com.yubico.webauthn.AssertionRequest} used to initiate the request. Pass that as the argument to {@link - * com.yubico.webauthn.RelyingParty#finishAssertion(com.yubico.webauthn.FinishAssertionOptions)}, which will return an - * {@link com.yubico.webauthn.AssertionResult} if successful and throw an exception if not. Regardless of whether it - * succeeds, you should remove the {@link com.yubico.webauthn.AssertionRequest} from the pending requests storage to - * prevent retries. - *

* - *

- * Finally, use the {@link com.yubico.webauthn.AssertionResult} to update any database(s) and take other actions - * depending on your application's needs. In particular: - *

+ *

After receiving the response from the client, construct a {@link + * com.yubico.webauthn.data.PublicKeyCredential}<{@link + * com.yubico.webauthn.data.AuthenticatorAssertionResponse}, {@link + * com.yubico.webauthn.data.ClientAssertionExtensionOutputs}> from the response and wrap that in + * a {@link com.yubico.webauthn.FinishAssertionOptions} along with the {@link + * com.yubico.webauthn.AssertionRequest} used to initiate the request. Pass that as the argument to + * {@link + * com.yubico.webauthn.RelyingParty#finishAssertion(com.yubico.webauthn.FinishAssertionOptions)}, + * which will return an {@link com.yubico.webauthn.AssertionResult} if successful and throw an + * exception if not. Regardless of whether it succeeds, you should remove the {@link + * com.yubico.webauthn.AssertionRequest} from the pending requests storage to prevent retries. + * + *

Finally, use the {@link com.yubico.webauthn.AssertionResult} to update any database(s) and + * take other actions depending on your application's needs. In particular: * *

    - *
  • - * Use the {@link com.yubico.webauthn.AssertionResult#getUsername() username} and/or {@link - * com.yubico.webauthn.AssertionResult#getUserHandle() userHandle} results to initiate a user session. - *
  • - *
  • - * Update the stored signature count for the credential (identified by the {@link com.yubico.webauthn.AssertionResult#getCredentialId() - * credentialId} result) to equal the value returned in the {@link com.yubico.webauthn.AssertionResult#getSignatureCount() - * signatureCount} result. - *
  • - *
  • - * Inspect the {@link com.yubico.webauthn.RegistrationResult#getWarnings() warnings} - ideally there should of course be - * none. - *
  • + *
  • Use the {@link com.yubico.webauthn.AssertionResult#getUsername() username} and/or {@link + * com.yubico.webauthn.AssertionResult#getUserHandle() userHandle} results to initiate a user + * session. + *
  • Update the stored signature count for the credential (identified by the {@link + * com.yubico.webauthn.AssertionResult#getCredentialId() credentialId} result) to equal the + * value returned in the {@link com.yubico.webauthn.AssertionResult#getSignatureCount() + * signatureCount} result. + *
  • Inspect the {@link com.yubico.webauthn.RegistrationResult#getWarnings() warnings} - ideally + * there should of course be none. *
*/ package com.yubico.webauthn; diff --git a/webauthn-server-core/src/test/java/com/yubico/webauthn/FinishAssertionOptionsTest.java b/webauthn-server-core/src/test/java/com/yubico/webauthn/FinishAssertionOptionsTest.java index 85a3fe6ba..326180f73 100644 --- a/webauthn-server-core/src/test/java/com/yubico/webauthn/FinishAssertionOptionsTest.java +++ b/webauthn-server-core/src/test/java/com/yubico/webauthn/FinishAssertionOptionsTest.java @@ -7,20 +7,19 @@ public class FinishAssertionOptionsTest { - @Test(expected = NullPointerException.class) - public void itHasANonOptionalCallerTokenBindingIdMethod() throws HexException { - FinishAssertionOptions.builder() - .request(null) - .response(null) - .callerTokenBindingId(ByteArray.fromHex("aa")); - } - - @Test(expected = NullPointerException.class) - public void itHasAnOptionalCallerTokenBindingIdMethod() throws HexException { - FinishAssertionOptions.builder() - .request(null) - .response(null) - .callerTokenBindingId(Optional.of(ByteArray.fromHex("aa"))); - } + @Test(expected = NullPointerException.class) + public void itHasANonOptionalCallerTokenBindingIdMethod() throws HexException { + FinishAssertionOptions.builder() + .request(null) + .response(null) + .callerTokenBindingId(ByteArray.fromHex("aa")); + } + @Test(expected = NullPointerException.class) + public void itHasAnOptionalCallerTokenBindingIdMethod() throws HexException { + FinishAssertionOptions.builder() + .request(null) + .response(null) + .callerTokenBindingId(Optional.of(ByteArray.fromHex("aa"))); + } } diff --git a/webauthn-server-core/src/test/java/com/yubico/webauthn/FinishRegistrationOptionsTest.java b/webauthn-server-core/src/test/java/com/yubico/webauthn/FinishRegistrationOptionsTest.java index c6fe88494..df315b6c2 100644 --- a/webauthn-server-core/src/test/java/com/yubico/webauthn/FinishRegistrationOptionsTest.java +++ b/webauthn-server-core/src/test/java/com/yubico/webauthn/FinishRegistrationOptionsTest.java @@ -7,20 +7,19 @@ public class FinishRegistrationOptionsTest { - @Test(expected = NullPointerException.class) - public void itHasANonOptionalCallerTokenBindingIdMethod() throws HexException { - FinishRegistrationOptions.builder() - .request(null) - .response(null) - .callerTokenBindingId(ByteArray.fromHex("aa")); - } - - @Test(expected = NullPointerException.class) - public void itHasAnOptionalCallerTokenBindingIdMethod() throws HexException { - FinishAssertionOptions.builder() - .request(null) - .response(null) - .callerTokenBindingId(Optional.of(ByteArray.fromHex("aa"))); - } + @Test(expected = NullPointerException.class) + public void itHasANonOptionalCallerTokenBindingIdMethod() throws HexException { + FinishRegistrationOptions.builder() + .request(null) + .response(null) + .callerTokenBindingId(ByteArray.fromHex("aa")); + } + @Test(expected = NullPointerException.class) + public void itHasAnOptionalCallerTokenBindingIdMethod() throws HexException { + FinishAssertionOptions.builder() + .request(null) + .response(null) + .callerTokenBindingId(Optional.of(ByteArray.fromHex("aa"))); + } } 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 276227ffe..11473d659 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 @@ -1,5 +1,8 @@ package com.yubico.webauthn; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + import com.yubico.webauthn.attestation.Attestation; import com.yubico.webauthn.attestation.MetadataService; import com.yubico.webauthn.data.AttestationConveyancePreference; @@ -17,63 +20,85 @@ 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) - public void itHasTheseBuilderMethods() throws InvalidAppIdException { + @Test(expected = NullPointerException.class) + public void itHasTheseBuilderMethods() throws InvalidAppIdException { - final MetadataService metadataService = new MetadataService() { - @Override public Attestation getAttestation(List attestationCertificateChain) throws CertificateEncodingException { return null; } + final MetadataService metadataService = + new MetadataService() { + @Override + public Attestation getAttestation(List attestationCertificateChain) + throws CertificateEncodingException { + return null; + } }; - RelyingParty.builder() - .identity(null) - .credentialRepository(null) - .origins(Collections.emptySet()) - .appId(new AppId("https://example.com")) - .appId(Optional.of(new AppId("https://example.com"))) - .attestationConveyancePreference(AttestationConveyancePreference.DIRECT) - .attestationConveyancePreference(Optional.of(AttestationConveyancePreference.DIRECT)) - .metadataService(metadataService) - .metadataService(Optional.of(metadataService)) - .preferredPubkeyParams(Collections.emptyList()) - .allowUnrequestedExtensions(true) - .allowUntrustedAttestation(true) - .validateSignatureCounter(true) - ; - } + RelyingParty.builder() + .identity(null) + .credentialRepository(null) + .origins(Collections.emptySet()) + .appId(new AppId("https://example.com")) + .appId(Optional.of(new AppId("https://example.com"))) + .attestationConveyancePreference(AttestationConveyancePreference.DIRECT) + .attestationConveyancePreference(Optional.of(AttestationConveyancePreference.DIRECT)) + .metadataService(metadataService) + .metadataService(Optional.of(metadataService)) + .preferredPubkeyParams(Collections.emptyList()) + .allowUnrequestedExtensions(true) + .allowUntrustedAttestation(true) + .validateSignatureCounter(true); + } - @Test - public void originsIsImmutable() { - Set origins = new HashSet<>(); + @Test + public void originsIsImmutable() { + Set origins = new HashSet<>(); - RelyingParty rp = RelyingParty.builder() + 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; } - }) + .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() - ; + .build(); - assertEquals(0, rp.getOrigins().size()); + assertEquals(0, rp.getOrigins().size()); - origins.add("test"); - 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()); - } + 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/java/com/yubico/webauthn/StartAssertionOptionsTest.java b/webauthn-server-core/src/test/java/com/yubico/webauthn/StartAssertionOptionsTest.java index e4bf50e48..116247da6 100644 --- a/webauthn-server-core/src/test/java/com/yubico/webauthn/StartAssertionOptionsTest.java +++ b/webauthn-server-core/src/test/java/com/yubico/webauthn/StartAssertionOptionsTest.java @@ -7,17 +7,16 @@ public class StartAssertionOptionsTest { - @Test - public void itHasTheseBuilderMethods() { - StartAssertionOptions.builder() - .username("") - .username(Optional.of("")) - .extensions(AssertionExtensionInputs.builder().build()) - .userVerification(UserVerificationRequirement.REQUIRED) - .userVerification(Optional.of(UserVerificationRequirement.REQUIRED)) - .timeout(1) - .timeout(Optional.of(1l)) - .build(); - } - + @Test + public void itHasTheseBuilderMethods() { + StartAssertionOptions.builder() + .username("") + .username(Optional.of("")) + .extensions(AssertionExtensionInputs.builder().build()) + .userVerification(UserVerificationRequirement.REQUIRED) + .userVerification(Optional.of(UserVerificationRequirement.REQUIRED)) + .timeout(1) + .timeout(Optional.of(1l)) + .build(); + } } diff --git a/webauthn-server-core/src/test/java/com/yubico/webauthn/StartRegistrationOptionsTest.java b/webauthn-server-core/src/test/java/com/yubico/webauthn/StartRegistrationOptionsTest.java index fa324a702..1cfe45338 100644 --- a/webauthn-server-core/src/test/java/com/yubico/webauthn/StartRegistrationOptionsTest.java +++ b/webauthn-server-core/src/test/java/com/yubico/webauthn/StartRegistrationOptionsTest.java @@ -7,14 +7,13 @@ public class StartRegistrationOptionsTest { - @Test(expected = NullPointerException.class) - public void itHasTheseBuilderMethods() { - StartRegistrationOptions.builder() - .user(null) - .authenticatorSelection(AuthenticatorSelectionCriteria.builder().build()) - .authenticatorSelection(Optional.of(AuthenticatorSelectionCriteria.builder().build())) - .extensions(RegistrationExtensionInputs.builder().build()) - .build(); - } - + @Test(expected = NullPointerException.class) + public void itHasTheseBuilderMethods() { + StartRegistrationOptions.builder() + .user(null) + .authenticatorSelection(AuthenticatorSelectionCriteria.builder().build()) + .authenticatorSelection(Optional.of(AuthenticatorSelectionCriteria.builder().build())) + .extensions(RegistrationExtensionInputs.builder().build()) + .build(); + } } diff --git a/webauthn-server-core/src/test/java/com/yubico/webauthn/attestation/TransportTest.java b/webauthn-server-core/src/test/java/com/yubico/webauthn/attestation/TransportTest.java index 0ca10e1ba..a66615557 100644 --- a/webauthn-server-core/src/test/java/com/yubico/webauthn/attestation/TransportTest.java +++ b/webauthn-server-core/src/test/java/com/yubico/webauthn/attestation/TransportTest.java @@ -24,44 +24,45 @@ package com.yubico.webauthn.attestation; +import static org.junit.Assert.assertEquals; + import java.util.EnumSet; import org.junit.Test; -import static org.junit.Assert.assertEquals; - public class TransportTest { - @Test - public void testParsingSingleValuesFromInt() { - assertEquals(EnumSet.of(Transport.BT_CLASSIC), Transport.fromInt(1)); - assertEquals(EnumSet.of(Transport.BLE), Transport.fromInt(2)); - assertEquals(EnumSet.of(Transport.USB), Transport.fromInt(4)); - assertEquals(EnumSet.of(Transport.NFC), Transport.fromInt(8)); - } - - @Test - public void testParsingSetsFromInt() { - assertEquals(EnumSet.noneOf(Transport.class), Transport.fromInt(0)); - assertEquals(EnumSet.of(Transport.BLE, Transport.NFC), Transport.fromInt(10)); - assertEquals(EnumSet.of(Transport.USB, Transport.BT_CLASSIC), Transport.fromInt(5)); - assertEquals(EnumSet.of(Transport.BT_CLASSIC, Transport.BLE, Transport.USB, Transport.NFC), - Transport.fromInt(15)); - } + @Test + public void testParsingSingleValuesFromInt() { + assertEquals(EnumSet.of(Transport.BT_CLASSIC), Transport.fromInt(1)); + assertEquals(EnumSet.of(Transport.BLE), Transport.fromInt(2)); + assertEquals(EnumSet.of(Transport.USB), Transport.fromInt(4)); + assertEquals(EnumSet.of(Transport.NFC), Transport.fromInt(8)); + } - @Test - public void testEncodingSingleValuesToInt() { - assertEquals(1, Transport.toInt(Transport.BT_CLASSIC)); - assertEquals(2, Transport.toInt(Transport.BLE)); - assertEquals(4, Transport.toInt(Transport.USB)); - assertEquals(8, Transport.toInt(Transport.NFC)); - } + @Test + public void testParsingSetsFromInt() { + assertEquals(EnumSet.noneOf(Transport.class), Transport.fromInt(0)); + assertEquals(EnumSet.of(Transport.BLE, Transport.NFC), Transport.fromInt(10)); + assertEquals(EnumSet.of(Transport.USB, Transport.BT_CLASSIC), Transport.fromInt(5)); + assertEquals( + EnumSet.of(Transport.BT_CLASSIC, Transport.BLE, Transport.USB, Transport.NFC), + Transport.fromInt(15)); + } - @Test - public void testEncodingSetsToInt() { - assertEquals(0, Transport.toInt()); - assertEquals(10, Transport.toInt(Transport.BLE, Transport.NFC)); - assertEquals(5, Transport.toInt(Transport.USB, Transport.BT_CLASSIC)); - assertEquals(15, Transport.toInt(Transport.BT_CLASSIC, Transport.BLE, Transport.USB, Transport.NFC)); - } + @Test + public void testEncodingSingleValuesToInt() { + assertEquals(1, Transport.toInt(Transport.BT_CLASSIC)); + assertEquals(2, Transport.toInt(Transport.BLE)); + assertEquals(4, Transport.toInt(Transport.USB)); + assertEquals(8, Transport.toInt(Transport.NFC)); + } + @Test + public void testEncodingSetsToInt() { + assertEquals(0, Transport.toInt()); + assertEquals(10, Transport.toInt(Transport.BLE, Transport.NFC)); + assertEquals(5, Transport.toInt(Transport.USB, Transport.BT_CLASSIC)); + assertEquals( + 15, Transport.toInt(Transport.BT_CLASSIC, Transport.BLE, Transport.USB, Transport.NFC)); + } } diff --git a/webauthn-server-core/src/test/java/com/yubico/webauthn/data/AssertionExtensionInputsTest.java b/webauthn-server-core/src/test/java/com/yubico/webauthn/data/AssertionExtensionInputsTest.java index cfa211ce7..c0e4a23dc 100644 --- a/webauthn-server-core/src/test/java/com/yubico/webauthn/data/AssertionExtensionInputsTest.java +++ b/webauthn-server-core/src/test/java/com/yubico/webauthn/data/AssertionExtensionInputsTest.java @@ -7,12 +7,11 @@ public class AssertionExtensionInputsTest { - @Test - public void itHasTheseBuilderMethods() throws InvalidAppIdException { - AssertionExtensionInputs.builder() - .appid(new AppId("https://example.com")) - .appid(Optional.of(new AppId("https://example.com"))) - .build(); - } - + @Test + public void itHasTheseBuilderMethods() throws InvalidAppIdException { + AssertionExtensionInputs.builder() + .appid(new AppId("https://example.com")) + .appid(Optional.of(new AppId("https://example.com"))) + .build(); + } } diff --git a/webauthn-server-core/src/test/java/com/yubico/webauthn/data/AuthenticatorSelectionCriteriaTest.java b/webauthn-server-core/src/test/java/com/yubico/webauthn/data/AuthenticatorSelectionCriteriaTest.java index 006dbcd2e..7fbae3b41 100644 --- a/webauthn-server-core/src/test/java/com/yubico/webauthn/data/AuthenticatorSelectionCriteriaTest.java +++ b/webauthn-server-core/src/test/java/com/yubico/webauthn/data/AuthenticatorSelectionCriteriaTest.java @@ -5,14 +5,13 @@ public class AuthenticatorSelectionCriteriaTest { - @Test - public void itHasTheseBuilderMethods() { - AuthenticatorSelectionCriteria.builder() - .authenticatorAttachment(AuthenticatorAttachment.CROSS_PLATFORM) - .authenticatorAttachment(Optional.of(AuthenticatorAttachment.CROSS_PLATFORM)) - .requireResidentKey(false) - .userVerification(UserVerificationRequirement.PREFERRED) - .build(); - } - + @Test + public void itHasTheseBuilderMethods() { + AuthenticatorSelectionCriteria.builder() + .authenticatorAttachment(AuthenticatorAttachment.CROSS_PLATFORM) + .authenticatorAttachment(Optional.of(AuthenticatorAttachment.CROSS_PLATFORM)) + .requireResidentKey(false) + .userVerification(UserVerificationRequirement.PREFERRED) + .build(); + } } diff --git a/webauthn-server-core/src/test/java/com/yubico/webauthn/data/ByteArrayTest.java b/webauthn-server-core/src/test/java/com/yubico/webauthn/data/ByteArrayTest.java index 0243798e5..5e065f55c 100644 --- a/webauthn-server-core/src/test/java/com/yubico/webauthn/data/ByteArrayTest.java +++ b/webauthn-server-core/src/test/java/com/yubico/webauthn/data/ByteArrayTest.java @@ -24,58 +24,58 @@ package com.yubico.webauthn.data; +import static org.junit.Assert.assertEquals; + import com.yubico.webauthn.data.exception.Base64UrlException; import org.junit.Test; -import static org.junit.Assert.assertEquals; - public class ByteArrayTest { - @Test - public void testEncodeBase64Url() { - byte[] input = "Test".getBytes(); - String base64Data = new ByteArray(input).getBase64Url(); + @Test + public void testEncodeBase64Url() { + byte[] input = "Test".getBytes(); + String base64Data = new ByteArray(input).getBase64Url(); - // No padding. - assertEquals("VGVzdA", base64Data); - } + // No padding. + assertEquals("VGVzdA", base64Data); + } - @Test - public void decodeTest() throws Base64UrlException { - String base64Data = "VGVzdA"; - String base64DataWithPadding = "VGVzdA=="; - String base64DataEmpty = ""; + @Test + public void decodeTest() throws Base64UrlException { + String base64Data = "VGVzdA"; + String base64DataWithPadding = "VGVzdA=="; + String base64DataEmpty = ""; - // Verify that Base64 data with and without padding ('=') are decoded correctly. - String out1 = new String(ByteArray.fromBase64Url(base64Data).getBytes()); - String out2 = new String(ByteArray.fromBase64Url(base64DataWithPadding).getBytes()); - String out3 = new String(ByteArray.fromBase64Url(base64DataEmpty).getBytes()); + // Verify that Base64 data with and without padding ('=') are decoded correctly. + String out1 = new String(ByteArray.fromBase64Url(base64Data).getBytes()); + String out2 = new String(ByteArray.fromBase64Url(base64DataWithPadding).getBytes()); + String out3 = new String(ByteArray.fromBase64Url(base64DataEmpty).getBytes()); - assertEquals(out1, out2); - assertEquals(out1, "Test"); - assertEquals(out3, ""); - } + assertEquals(out1, out2); + assertEquals(out1, "Test"); + assertEquals(out3, ""); + } - @Test - public void codecMimeTest() { - String base64 = "ab+/+/=="; - String base64WithoutPadding = "ab+/+/"; - String expectedRecoded = "ab-_-w"; - String expectedRecodedMime = "ab+/+w=="; + @Test + public void codecMimeTest() { + String base64 = "ab+/+/=="; + String base64WithoutPadding = "ab+/+/"; + String expectedRecoded = "ab-_-w"; + String expectedRecodedMime = "ab+/+w=="; - assertEquals(expectedRecoded, ByteArray.fromBase64(base64).getBase64Url()); - assertEquals(expectedRecoded, ByteArray.fromBase64(base64WithoutPadding).getBase64Url()); - assertEquals(expectedRecodedMime, ByteArray.fromBase64(base64).getBase64()); - assertEquals(expectedRecodedMime, ByteArray.fromBase64(base64WithoutPadding).getBase64()); - } + assertEquals(expectedRecoded, ByteArray.fromBase64(base64).getBase64Url()); + assertEquals(expectedRecoded, ByteArray.fromBase64(base64WithoutPadding).getBase64Url()); + assertEquals(expectedRecodedMime, ByteArray.fromBase64(base64).getBase64()); + assertEquals(expectedRecodedMime, ByteArray.fromBase64(base64WithoutPadding).getBase64()); + } - @Test(expected = Base64UrlException.class) - public void decodeBadAlphabetTest() throws Base64UrlException { - ByteArray.fromBase64Url("****"); - } + @Test(expected = Base64UrlException.class) + public void decodeBadAlphabetTest() throws Base64UrlException { + ByteArray.fromBase64Url("****"); + } - @Test(expected = Base64UrlException.class) - public void decodeBadPaddingTest() throws Base64UrlException { - ByteArray.fromBase64Url("A==="); - } + @Test(expected = Base64UrlException.class) + public void decodeBadPaddingTest() throws Base64UrlException { + ByteArray.fromBase64Url("A==="); + } } diff --git a/webauthn-server-core/src/test/java/com/yubico/webauthn/data/ClientAssertionExtensionOutputsTest.java b/webauthn-server-core/src/test/java/com/yubico/webauthn/data/ClientAssertionExtensionOutputsTest.java index 84f3b9d1e..08b0e0528 100644 --- a/webauthn-server-core/src/test/java/com/yubico/webauthn/data/ClientAssertionExtensionOutputsTest.java +++ b/webauthn-server-core/src/test/java/com/yubico/webauthn/data/ClientAssertionExtensionOutputsTest.java @@ -5,12 +5,8 @@ public class ClientAssertionExtensionOutputsTest { - @Test - public void itHasTheseBuilderMethods() { - ClientAssertionExtensionOutputs.builder() - .appid(false) - .appid(Optional.of(false)) - .build(); - } - + @Test + public void itHasTheseBuilderMethods() { + ClientAssertionExtensionOutputs.builder().appid(false).appid(Optional.of(false)).build(); + } } diff --git a/webauthn-server-core/src/test/java/com/yubico/webauthn/data/PublicKeyCredentialCreationOptionsTest.java b/webauthn-server-core/src/test/java/com/yubico/webauthn/data/PublicKeyCredentialCreationOptionsTest.java index 848c1c5f1..7d42f8602 100644 --- a/webauthn-server-core/src/test/java/com/yubico/webauthn/data/PublicKeyCredentialCreationOptionsTest.java +++ b/webauthn-server-core/src/test/java/com/yubico/webauthn/data/PublicKeyCredentialCreationOptionsTest.java @@ -6,22 +6,20 @@ public class PublicKeyCredentialCreationOptionsTest { - @Test(expected = NullPointerException.class) - public void itHasTheseBuilderMethods() { - PublicKeyCredentialCreationOptions.builder() - .rp(null) - .user(null) - .challenge(null) - .pubKeyCredParams(null) - .attestation(null) - .authenticatorSelection(AuthenticatorSelectionCriteria.builder().build()) - .authenticatorSelection(Optional.of(AuthenticatorSelectionCriteria.builder().build())) - .excludeCredentials(Collections.emptySet()) - .excludeCredentials(Optional.of(Collections.emptySet())) - .extensions(RegistrationExtensionInputs.builder().build()) - .timeout(0) - .timeout(Optional.of(0L)) - ; - } - + @Test(expected = NullPointerException.class) + public void itHasTheseBuilderMethods() { + PublicKeyCredentialCreationOptions.builder() + .rp(null) + .user(null) + .challenge(null) + .pubKeyCredParams(null) + .attestation(null) + .authenticatorSelection(AuthenticatorSelectionCriteria.builder().build()) + .authenticatorSelection(Optional.of(AuthenticatorSelectionCriteria.builder().build())) + .excludeCredentials(Collections.emptySet()) + .excludeCredentials(Optional.of(Collections.emptySet())) + .extensions(RegistrationExtensionInputs.builder().build()) + .timeout(0) + .timeout(Optional.of(0L)); + } } diff --git a/webauthn-server-core/src/test/java/com/yubico/webauthn/data/PublicKeyCredentialDescriptorTest.java b/webauthn-server-core/src/test/java/com/yubico/webauthn/data/PublicKeyCredentialDescriptorTest.java index f9fce0c3e..603aec4dc 100644 --- a/webauthn-server-core/src/test/java/com/yubico/webauthn/data/PublicKeyCredentialDescriptorTest.java +++ b/webauthn-server-core/src/test/java/com/yubico/webauthn/data/PublicKeyCredentialDescriptorTest.java @@ -6,13 +6,11 @@ public class PublicKeyCredentialDescriptorTest { - @Test(expected = NullPointerException.class) - public void itHasTheseBuilderMethods() { - PublicKeyCredentialDescriptor.builder() - .id(null) - .transports(Collections.emptySet()) - .transports(Optional.of(Collections.emptySet())) - ; - } - + @Test(expected = NullPointerException.class) + public void itHasTheseBuilderMethods() { + PublicKeyCredentialDescriptor.builder() + .id(null) + .transports(Collections.emptySet()) + .transports(Optional.of(Collections.emptySet())); + } } diff --git a/webauthn-server-core/src/test/java/com/yubico/webauthn/data/PublicKeyCredentialRequestOptionsTest.java b/webauthn-server-core/src/test/java/com/yubico/webauthn/data/PublicKeyCredentialRequestOptionsTest.java index 8fc2c8d20..49fa52ab4 100644 --- a/webauthn-server-core/src/test/java/com/yubico/webauthn/data/PublicKeyCredentialRequestOptionsTest.java +++ b/webauthn-server-core/src/test/java/com/yubico/webauthn/data/PublicKeyCredentialRequestOptionsTest.java @@ -6,18 +6,17 @@ public class PublicKeyCredentialRequestOptionsTest { - @Test(expected = NullPointerException.class) - public void itHasTheseBuilderMethods() { - PublicKeyCredentialRequestOptions.builder() - .challenge(null) - .timeout(0) - .timeout(Optional.of(0L)) - .rpId("") - .rpId(Optional.of("")) - .allowCredentials(Collections.emptyList()) - .allowCredentials(Optional.of(Collections.emptyList())) - .userVerification(UserVerificationRequirement.PREFERRED) - .extensions(AssertionExtensionInputs.builder().build()) - ; - } + @Test(expected = NullPointerException.class) + public void itHasTheseBuilderMethods() { + PublicKeyCredentialRequestOptions.builder() + .challenge(null) + .timeout(0) + .timeout(Optional.of(0L)) + .rpId("") + .rpId(Optional.of("")) + .allowCredentials(Collections.emptyList()) + .allowCredentials(Optional.of(Collections.emptyList())) + .userVerification(UserVerificationRequirement.PREFERRED) + .extensions(AssertionExtensionInputs.builder().build()); + } } diff --git a/webauthn-server-core/src/test/java/com/yubico/webauthn/data/RelyingPartyIdentityTest.java b/webauthn-server-core/src/test/java/com/yubico/webauthn/data/RelyingPartyIdentityTest.java index d950c5e8c..1c9121090 100644 --- a/webauthn-server-core/src/test/java/com/yubico/webauthn/data/RelyingPartyIdentityTest.java +++ b/webauthn-server-core/src/test/java/com/yubico/webauthn/data/RelyingPartyIdentityTest.java @@ -7,14 +7,12 @@ public class RelyingPartyIdentityTest { - @Test - public void itHasTheseBuilderMethods() throws MalformedURLException { - RelyingPartyIdentity.builder() - .id("") - .name("") - .icon(new URL("https://example.com")) - .icon(Optional.of(new URL("https://example.com"))) - ; - } - + @Test + public void itHasTheseBuilderMethods() throws MalformedURLException { + RelyingPartyIdentity.builder() + .id("") + .name("") + .icon(new URL("https://example.com")) + .icon(Optional.of(new URL("https://example.com"))); + } } diff --git a/webauthn-server-core/src/test/java/com/yubico/webauthn/data/UserIdentityTest.java b/webauthn-server-core/src/test/java/com/yubico/webauthn/data/UserIdentityTest.java index 2bb16fc83..f88148ace 100644 --- a/webauthn-server-core/src/test/java/com/yubico/webauthn/data/UserIdentityTest.java +++ b/webauthn-server-core/src/test/java/com/yubico/webauthn/data/UserIdentityTest.java @@ -7,15 +7,13 @@ public class UserIdentityTest { - @Test - public void itHasTheseBuilderMethods() throws MalformedURLException { - UserIdentity.builder() - .name("") - .displayName("") - .id(new ByteArray(new byte[]{})) - .icon(new URL("https://example.com")) - .icon(Optional.of(new URL("https://example.com"))) - ; - } - + @Test + public void itHasTheseBuilderMethods() throws MalformedURLException { + UserIdentity.builder() + .name("") + .displayName("") + .id(new ByteArray(new byte[] {})) + .icon(new URL("https://example.com")) + .icon(Optional.of(new URL("https://example.com"))); + } } diff --git a/webauthn-server-core/src/test/java/com/yubico/webauthn/extension/appid/AppIdTest.java b/webauthn-server-core/src/test/java/com/yubico/webauthn/extension/appid/AppIdTest.java index f3c79c58c..c3d0572b0 100644 --- a/webauthn-server-core/src/test/java/com/yubico/webauthn/extension/appid/AppIdTest.java +++ b/webauthn-server-core/src/test/java/com/yubico/webauthn/extension/appid/AppIdTest.java @@ -24,77 +24,74 @@ package com.yubico.webauthn.extension.appid; -import com.yubico.internal.util.JacksonCodecs; -import java.io.IOException; -import org.junit.Test; - import static junit.framework.TestCase.assertFalse; import static junit.framework.TestCase.assertTrue; import static org.junit.Assert.assertEquals; +import com.yubico.internal.util.JacksonCodecs; +import java.io.IOException; +import org.junit.Test; + public class AppIdTest { - @Test - public void validUrls() { - assertTrue(isValid("https://www.example.com")); - assertTrue(isValid("https://internal-server")); - assertTrue(isValid("https://åäö.se:8443")); - assertTrue(isValid("https://localhost:8443/myAppId.json")); - } + @Test + public void validUrls() { + assertTrue(isValid("https://www.example.com")); + assertTrue(isValid("https://internal-server")); + assertTrue(isValid("https://åäö.se:8443")); + assertTrue(isValid("https://localhost:8443/myAppId.json")); + } - @Test - public void validUris() { - assertTrue(isValid("android:apk-key-hash:585215fd5153209a7e246f53286035838a0be227")); - assertTrue(isValid("ios:bundle-id:com.example.Example")); - } + @Test + public void validUris() { + assertTrue(isValid("android:apk-key-hash:585215fd5153209a7e246f53286035838a0be227")); + assertTrue(isValid("ios:bundle-id:com.example.Example")); + } - @Test - public void disallowHttp() { - assertFalse(isValid("http://www.example.com")); - } + @Test + public void disallowHttp() { + assertFalse(isValid("http://www.example.com")); + } - @Test - public void disallowSlashAsPath() { - assertFalse(isValid("https://www.example.com/")); - } + @Test + public void disallowSlashAsPath() { + assertFalse(isValid("https://www.example.com/")); + } - @Test - public void disallowIP() { - assertFalse(isValid("https://127.0.0.1:8443")); - assertFalse(isValid("https://127.0.0.1")); - assertFalse(isValid("https://127.0.0.1/foo")); - assertFalse(isValid("https://2001:0db8:0000:0000:0000:ff00:0042:8329")); - assertFalse(isValid("https://2001:0db8:0000:0000:0000:ff00:0042:8329/åäö")); - } + @Test + public void disallowIP() { + assertFalse(isValid("https://127.0.0.1:8443")); + assertFalse(isValid("https://127.0.0.1")); + assertFalse(isValid("https://127.0.0.1/foo")); + assertFalse(isValid("https://2001:0db8:0000:0000:0000:ff00:0042:8329")); + assertFalse(isValid("https://2001:0db8:0000:0000:0000:ff00:0042:8329/åäö")); + } - @Test - public void badSyntax() { - assertFalse(isValid("https://bad[syntax]")); - } + @Test + public void badSyntax() { + assertFalse(isValid("https://bad[syntax]")); + } - @Test - public void jsonDecode() throws InvalidAppIdException, IOException { - assertEquals( - new AppId("https://example.org"), - JacksonCodecs.json().readValue("\"https://example.org\"", AppId.class) - ); - } + @Test + public void jsonDecode() throws InvalidAppIdException, IOException { + assertEquals( + new AppId("https://example.org"), + JacksonCodecs.json().readValue("\"https://example.org\"", AppId.class)); + } - @Test - public void jsonEncode() throws InvalidAppIdException, IOException { - assertEquals( - "\"https://example.org\"", - JacksonCodecs.json().writeValueAsString(new AppId("https://example.org")) - ); - } + @Test + public void jsonEncode() throws InvalidAppIdException, IOException { + assertEquals( + "\"https://example.org\"", + JacksonCodecs.json().writeValueAsString(new AppId("https://example.org"))); + } - private static boolean isValid(String appId) { - try { - new AppId(appId); - return true; - } catch (InvalidAppIdException e) { - return false; - } + private static boolean isValid(String appId) { + try { + new AppId(appId); + return true; + } catch (InvalidAppIdException e) { + return false; } - + } } diff --git a/webauthn-server-core/src/test/scala/com/yubico/scalacheck/gen/JacksonGenerators.scala b/webauthn-server-core/src/test/scala/com/yubico/scalacheck/gen/JacksonGenerators.scala index 9a66bd5f6..40d68e690 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/scalacheck/gen/JacksonGenerators.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/scalacheck/gen/JacksonGenerators.scala @@ -29,24 +29,35 @@ import com.fasterxml.jackson.databind.node.JsonNodeFactory import com.fasterxml.jackson.databind.node.ObjectNode import com.upokecenter.cbor.CBORObject import org.scalacheck.Arbitrary -import org.scalacheck.Gen import org.scalacheck.Arbitrary.arbitrary +import org.scalacheck.Gen import scala.jdk.CollectionConverters._ - object JacksonGenerators { private def jsonFactory: JsonNodeFactory = JsonNodeFactory.instance - implicit val arbitraryJsonNode: Arbitrary[JsonNode] = Arbitrary(arbitrary[String] map (value => jsonFactory.textNode(value))) - implicit val arbitraryObjectNode: Arbitrary[ObjectNode] = Arbitrary(arbitrary[Map[String, _ <: JsonNode]] map (exts => { val o = jsonFactory.objectNode(); o.setAll(exts.asJava); o })) + implicit val arbitraryJsonNode: Arbitrary[JsonNode] = Arbitrary( + arbitrary[String] map (value => jsonFactory.textNode(value)) + ) + implicit val arbitraryObjectNode: Arbitrary[ObjectNode] = Arbitrary( + arbitrary[Map[String, _ <: JsonNode]] map (exts => { + val o = jsonFactory.objectNode(); o.setAll(exts.asJava); o + }) + ) - def objectNode(names: Gen[String] = arbitrary[String], suggestedValues: Gen[JsonNode] = arbitrary[JsonNode]): Gen[ObjectNode] = + def objectNode( + names: Gen[String] = arbitrary[String], + suggestedValues: Gen[JsonNode] = arbitrary[JsonNode], + ): Gen[ObjectNode] = Gen.sized { size => for { numValues <- Gen.choose(0, size) names: List[String] <- Gen.listOfN(numValues, names) - values: List[JsonNode] <- Gen.listOfN(numValues, Gen.oneOf(suggestedValues, arbitrary[JsonNode])) + values: List[JsonNode] <- Gen.listOfN( + numValues, + Gen.oneOf(suggestedValues, arbitrary[JsonNode]), + ) } yield { val o = jsonFactory.objectNode() for { (name, value) <- names.zip(values) } { diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/AppleAttestationStatementVerifierSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/AppleAttestationStatementVerifierSpec.scala new file mode 100644 index 000000000..287f700d3 --- /dev/null +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/AppleAttestationStatementVerifierSpec.scala @@ -0,0 +1,173 @@ +// Copyright (c) 2021, Yubico AB +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package com.yubico.webauthn + +import com.yubico.internal.util.CertificateParser +import com.yubico.webauthn.TestAuthenticator.AttestationMaker +import com.yubico.webauthn.data.ByteArray +import com.yubico.webauthn.data.Generators.arbitraryByteArray +import com.yubico.webauthn.test.RealExamples +import org.junit.runner.RunWith +import org.scalatest.FunSpec +import org.scalatest.Matchers +import org.scalatestplus.junit.JUnitRunner +import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks + +@RunWith(classOf[JUnitRunner]) +class AppleAttestationStatementVerifierSpec + extends FunSpec + with Matchers + with TestWithEachProvider + with ScalaCheckDrivenPropertyChecks { + + val verifier = new AppleAttestationStatementVerifier + + testWithEachProvider { it => + describe("AppleAttestationStatementVerifier") { + + describe("accepts") { + describe("a real apple attestation statement example") { + it("from iOS.") { + val example = RealExamples.AppleAttestationIos + val result = verifier.verifyAttestationSignature( + example.attestation.attestationObject, + example.attestation.clientDataJSONHash, + ) + result should be(true) + } + + it("from MacOS.") { + val example = RealExamples.AppleAttestationMacos + val result = verifier.verifyAttestationSignature( + example.attestation.attestationObject, + example.attestation.clientDataJSONHash, + ) + result should be(true) + } + } + + it("a test-generated apple attestation statement.") { + val (attestationMaker, _, _) = AttestationMaker.apple() + val (pkc, _) = TestAuthenticator.createBasicAttestedCredential( + attestationMaker = attestationMaker + ) + val result = verifier.verifyAttestationSignature( + pkc.getResponse.getAttestation, + Crypto.sha256(pkc.getResponse.getClientDataJSON), + ) + result should be(true) + } + } + + describe("rejects") { + + it("a real apple attestation statement example that doesn't match the clientDataJSONHash.") { + val example = RealExamples.AppleAttestationIos + verifier.verifyAttestationSignature( + example.attestation.attestationObject, + example.attestation.clientDataJSONHash, + ) should be(true) + + forAll( + com.yubico.webauthn.data.Generators + .flipOneBit(example.attestation.clientDataJSONHash) + ) { modifiedHash => + an[IllegalArgumentException] shouldBe thrownBy { + verifier.verifyAttestationSignature( + example.attestation.attestationObject, + modifiedHash, + ) + } + } + } + + it("an attestation statement without the attestation cert extension 1.2.840.113635.100.8.2 .") { + val (attestationMaker, _, _) = + AttestationMaker.apple(addNonceExtension = false) + val (pkc, _) = TestAuthenticator.createBasicAttestedCredential( + attestationMaker = attestationMaker + ) + an[IllegalArgumentException] shouldBe thrownBy { + verifier.verifyAttestationSignature( + pkc.getResponse.getAttestation, + Crypto.sha256(pkc.getResponse.getClientDataJSON), + ) + } + } + + it("an attestation statement where the 1.2.840.113635.100.8.2 extension value does not equal the nonceToHash.") { + forAll { incorrectNonce: ByteArray => + val (attestationMaker, _, _) = + AttestationMaker.apple(nonceValue = Some(incorrectNonce)) + val (pkc, _) = TestAuthenticator.createBasicAttestedCredential( + attestationMaker = attestationMaker + ) + + an[IllegalArgumentException] shouldBe thrownBy { + verifier.verifyAttestationSignature( + pkc.getResponse.getAttestation, + Crypto.sha256(pkc.getResponse.getClientDataJSON), + ) + } + } + } + + it("an attestation statement where the certificate subject public key does not equal the credential public key.") { + val certSubjectKeypair = TestAuthenticator.generateEcKeypair() + val (appleAttestationMaker, caCert, _) = + AttestationMaker.apple(certSubjectPublicKey = + Some(certSubjectKeypair.getPublic) + ) + val (pkc, _) = TestAuthenticator.createBasicAttestedCredential( + attestationMaker = appleAttestationMaker + ) + + // In this test, the signature chain on its own is valid... + val certNodes = + pkc.getResponse.getAttestation.getAttestationStatement.get("x5c") + var cert = CertificateParser.parseDer(certNodes.get(0).binaryValue) + for { certIndex <- 1 until certNodes.size } { + val nextCert = + CertificateParser.parseDer(certNodes.get(certIndex).binaryValue) + cert.verify(nextCert.getPublicKey) + cert = nextCert + } + if (cert != caCert) { + cert.verify(caCert.getPublicKey) + } + + // ...but the leaf subject has the wrong public key. + an[IllegalArgumentException] shouldBe thrownBy { + verifier.verifyAttestationSignature( + pkc.getResponse.getAttestation, + Crypto.sha256(pkc.getResponse.getClientDataJSON), + ) + } + } + } + } + + } +} diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/Generators.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/Generators.scala index 08d793a66..508b7fddb 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/Generators.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/Generators.scala @@ -1,64 +1,74 @@ package com.yubico.webauthn -import java.util.Optional - import com.yubico.scalacheck.gen.JavaGenerators._ import com.yubico.webauthn.attestation.Attestation import com.yubico.webauthn.attestation.Generators._ -import com.yubico.webauthn.data.ByteArray -import com.yubico.webauthn.data.PublicKeyCredentialDescriptor import com.yubico.webauthn.data.AttestationType +import com.yubico.webauthn.data.ByteArray import com.yubico.webauthn.data.Generators._ +import com.yubico.webauthn.data.PublicKeyCredentialDescriptor import org.scalacheck.Arbitrary import org.scalacheck.Arbitrary.arbitrary +import java.util.Optional object Generators { - implicit val arbitraryAssertionResult: Arbitrary[AssertionResult] = Arbitrary(for { - credentialId <- arbitrary[ByteArray] - signatureCount <- arbitrary[Long] - signatureCounterValid <- arbitrary[Boolean] - success <- arbitrary[Boolean] - userHandle <- arbitrary[ByteArray] - username <- arbitrary[String] - warnings <- arbitrary[java.util.List[String]] - } yield AssertionResult.builder() - .success(success) - .credentialId(credentialId) - .userHandle(userHandle) - .username(username) - .signatureCount(signatureCount) - .signatureCounterValid(signatureCounterValid) - .warnings(warnings) - .build()) + implicit val arbitraryAssertionResult: Arbitrary[AssertionResult] = Arbitrary( + for { + credentialId <- arbitrary[ByteArray] + signatureCount <- arbitrary[Long] + signatureCounterValid <- arbitrary[Boolean] + success <- arbitrary[Boolean] + userHandle <- arbitrary[ByteArray] + username <- arbitrary[String] + warnings <- arbitrary[java.util.List[String]] + } yield AssertionResult + .builder() + .success(success) + .credentialId(credentialId) + .userHandle(userHandle) + .username(username) + .signatureCount(signatureCount) + .signatureCounterValid(signatureCounterValid) + .warnings(warnings) + .build() + ) - implicit val arbitraryRegistrationResult: Arbitrary[RegistrationResult] = Arbitrary(for { - attestationMetadata <- arbitrary[Optional[Attestation]] - attestationTrusted <- arbitrary[Boolean] - attestationType <- arbitrary[AttestationType] - keyId <- arbitrary[PublicKeyCredentialDescriptor] - publicKeyCose <- arbitrary[ByteArray] - warnings <- arbitrary[java.util.List[String]] - } yield RegistrationResult.builder() - .keyId(keyId) - .attestationTrusted(attestationTrusted) - .attestationType(attestationType) - .publicKeyCose(publicKeyCose) - .attestationMetadata(attestationMetadata) - .warnings(warnings) - .build()) + implicit val arbitraryRegistrationResult: Arbitrary[RegistrationResult] = + Arbitrary( + for { + attestationMetadata <- arbitrary[Optional[Attestation]] + attestationTrusted <- arbitrary[Boolean] + attestationType <- arbitrary[AttestationType] + keyId <- arbitrary[PublicKeyCredentialDescriptor] + publicKeyCose <- arbitrary[ByteArray] + warnings <- arbitrary[java.util.List[String]] + } yield RegistrationResult + .builder() + .keyId(keyId) + .attestationTrusted(attestationTrusted) + .attestationType(attestationType) + .publicKeyCose(publicKeyCose) + .attestationMetadata(attestationMetadata) + .warnings(warnings) + .build() + ) - implicit val arbitraryRegisteredCredential: Arbitrary[RegisteredCredential] = Arbitrary(for { - credentialId <- arbitrary[ByteArray] - userHandle <- arbitrary[ByteArray] - publicKeyCose <- arbitrary[ByteArray] - signatureCount <- arbitrary[Int] - } yield RegisteredCredential.builder() - .credentialId(credentialId) - .userHandle(userHandle) - .publicKeyCose(publicKeyCose) - .signatureCount(signatureCount) - .build()) + implicit val arbitraryRegisteredCredential: Arbitrary[RegisteredCredential] = + Arbitrary( + for { + credentialId <- arbitrary[ByteArray] + userHandle <- arbitrary[ByteArray] + publicKeyCose <- arbitrary[ByteArray] + signatureCount <- arbitrary[Int] + } yield RegisteredCredential + .builder() + .credentialId(credentialId) + .userHandle(userHandle) + .publicKeyCose(publicKeyCose) + .signatureCount(signatureCount) + .build() + ) } 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 index 2e5dccd47..975430f6f 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/OriginMatcherSpec.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/OriginMatcherSpec.scala @@ -24,8 +24,6 @@ package com.yubico.webauthn -import java.net.URL - import org.junit.runner.RunWith import org.scalacheck.Arbitrary import org.scalacheck.Arbitrary.arbitrary @@ -35,19 +33,29 @@ import org.scalatest.Matchers import org.scalatestplus.junit.JUnitRunner import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks +import java.net.URL import scala.jdk.CollectionConverters._ @RunWith(classOf[JUnitRunner]) -class OriginMatcherSpec extends FunSpec with Matchers with ScalaCheckDrivenPropertyChecks { - - private def urlWithMaybePort(protocol: String, host: String, port: Option[Int], file: String): URL = +class OriginMatcherSpec + extends FunSpec + with Matchers + with ScalaCheckDrivenPropertyChecks { + + 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 def replacePort(url: URL, port: Int): URL = + new URL(url.getProtocol, url.getHost, port, url.getFile) - private implicit val arbitraryUrl: Arbitrary[URL] = Arbitrary(for { + implicit private val arbitraryUrl: Arbitrary[URL] = Arbitrary(for { scheme <- Gen.oneOf("http", "https") host <- Gen.alphaNumStr suchThat { _.nonEmpty } port <- Gen.option(Gen.posNum[Int]) @@ -56,7 +64,7 @@ class OriginMatcherSpec extends FunSpec with Matchers with ScalaCheckDrivenPrope private val urlOrArbitraryString: Gen[String] = Gen.oneOf( arbitrary[URL].map(_.toExternalForm), - arbitrary[String] + arbitrary[String], ) private val urlWithoutPort: Gen[URL] = for { @@ -70,7 +78,9 @@ class OriginMatcherSpec extends FunSpec with Matchers with ScalaCheckDrivenPrope private val superAndSubdomain: Gen[(URL, URL)] = for { superdomain <- urlWithoutPort - subdomainPrefixParts <- Gen.nonEmptyListOf(Gen.alphaNumStr suchThat { _.nonEmpty }) + subdomainPrefixParts <- Gen.nonEmptyListOf(Gen.alphaNumStr suchThat { + _.nonEmpty + }) subdomainPrefix = subdomainPrefixParts.reduceLeft(_ + "." + _) host = subdomainPrefix + "." + superdomain.getHost subdomain = new URL(superdomain.getProtocol, host, superdomain.getFile) @@ -89,38 +99,41 @@ class OriginMatcherSpec extends FunSpec with Matchers with ScalaCheckDrivenPrope 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) + 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) + 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) + forAll(superAndSubdomain) { + case (origin: URL, allowedOrigin: URL) => + println(allowedOrigin, origin) + OriginMatcher.isAllowed( + origin.toExternalForm, + Set(allowedOrigin.toExternalForm).asJava, + true, + true, + ) shouldBe (false) } } @@ -134,36 +147,38 @@ class OriginMatcherSpec extends FunSpec with Matchers with ScalaCheckDrivenPrope origin.toExternalForm, Set(allowedOrigin.toExternalForm).asJava, allowPort, - false + 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) + 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) + 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) + 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) + OriginMatcher.isAllowed( + invalidOrigin, + Set(allowedOrigin.toExternalForm).asJava, + true, + true, + ) shouldBe (false) } } @@ -176,7 +191,7 @@ class OriginMatcherSpec extends FunSpec with Matchers with ScalaCheckDrivenPrope origin.toExternalForm, Set(allowedOrigin.toExternalForm).asJava, allowPort, - true + true, ) shouldBe (true) } } @@ -184,45 +199,56 @@ class OriginMatcherSpec extends FunSpec with Matchers with ScalaCheckDrivenPrope 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) - } + 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) + forAll(urlWithPort, arbitrary[Boolean]) { + (origin: URL, allowSubdomain: Boolean) => + println(origin) - OriginMatcher.isAllowed( - origin.toExternalForm, - Set(origin.toExternalForm).asJava, - false, - allowSubdomain - ) shouldBe (true) + 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) => + 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) + val origin = urlWithMaybePort( + allowedOrigin.getProtocol, + allowedOrigin.getHost, + port, + allowedOrigin.getFile, + ) println(allowedOrigin, origin) OriginMatcher.isAllowed( origin.toExternalForm, Set(allowedOrigin.toExternalForm).asJava, true, - allowSubdomain + allowSubdomain, ) shouldBe (true) } } @@ -230,15 +256,16 @@ class OriginMatcherSpec extends FunSpec with Matchers with ScalaCheckDrivenPrope } 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) + 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 4eae7dd90..3b2adf847 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 @@ -25,11 +25,11 @@ package com.yubico.webauthn import com.yubico.webauthn.Crypto.isP256 -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 com.yubico.webauthn.data.ByteArray +import com.yubico.webauthn.data.COSEAlgorithmIdentifier +import com.yubico.webauthn.test.Util import org.junit.runner.RunWith import org.scalatest.FunSpec import org.scalatest.Matchers @@ -39,9 +39,11 @@ import java.security.interfaces.ECPrivateKey import scala.util.Success import scala.util.Try - @RunWith(classOf[JUnitRunner]) -class PackedAttestationStatementVerifierSpec extends FunSpec with Matchers with TestWithEachProvider { +class PackedAttestationStatementVerifierSpec + extends FunSpec + with Matchers + with TestWithEachProvider { val verifier = new PackedAttestationStatementVerifier @@ -52,46 +54,57 @@ class PackedAttestationStatementVerifierSpec extends FunSpec with Matchers with it("which pass Klas's attestation certificate.") { - val cert = Util.importCertFromPem(getClass.getResourceAsStream("klas-cert.pem")) + val cert = Util.importCertFromPem( + getClass.getResourceAsStream("klas-cert.pem") + ) - val result = Try(verifier.verifyX5cRequirements(cert, ByteArray.fromHex("F8A011F38C0A4D15800617111F9EDC7D"))) + val result = Try( + verifier.verifyX5cRequirements( + cert, + ByteArray.fromHex("F8A011F38C0A4D15800617111F9EDC7D"), + ) + ) - result shouldBe a [Success[_]] - result.get should be (true) + result shouldBe a[Success[_]] + result.get should be(true) } } describe("supports attestation certificates with the algorithm") { - it ("ECDSA.") { + it("ECDSA.") { val (cert, key) = TestAuthenticator.generateAttestationCertificate() val (credential, _) = TestAuthenticator.createBasicAttestedCredential( - attestationMaker = AttestationMaker.packed(new AttestationCert(COSEAlgorithmIdentifier.ES256, (cert, key))), + attestationMaker = AttestationMaker.packed( + new AttestationCert(COSEAlgorithmIdentifier.ES256, (cert, key)) + ) ) val result = verifier.verifyAttestationSignature( credential.getResponse.getAttestation, - Crypto.hash(credential.getResponse.getClientDataJSON) + Crypto.sha256(credential.getResponse.getClientDataJSON), ) - key.getAlgorithm should be ("EC") - isP256(key.asInstanceOf[ECPrivateKey].getParams) should be (true) - result should be (true) + key.getAlgorithm should be("EC") + isP256(key.asInstanceOf[ECPrivateKey].getParams) should be(true) + result should be(true) } - it ("RSA.") { + it("RSA.") { val (cert, key) = TestAuthenticator.generateRsaCertificate() val (credential, _) = TestAuthenticator.createBasicAttestedCredential( - attestationMaker = AttestationMaker.packed(new AttestationCert(COSEAlgorithmIdentifier.RS256, (cert, key))), + attestationMaker = AttestationMaker.packed( + new AttestationCert(COSEAlgorithmIdentifier.RS256, (cert, key)) + ) ) val result = verifier.verifyAttestationSignature( credential.getResponse.getAttestation, - Crypto.hash(credential.getResponse.getClientDataJSON) + Crypto.sha256(credential.getResponse.getClientDataJSON), ) - key.getAlgorithm should be ("RSA") - result should be (true) + key.getAlgorithm should be("RSA") + result should be(true) } } 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 5cb8ae0a8..bf6adc768 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 @@ -24,58 +24,64 @@ package com.yubico.webauthn -import java.nio.charset.StandardCharsets -import java.security.cert.X509Certificate -import java.security.KeyPair - import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.node.JsonNodeFactory import com.fasterxml.jackson.databind.node.ObjectNode import com.yubico.internal.util.BinaryUtil import com.yubico.internal.util.CertificateParser -import com.yubico.internal.util.scala.JavaConverters._ import com.yubico.internal.util.JacksonCodecs +import com.yubico.internal.util.scala.JavaConverters._ +import com.yubico.webauthn.TestAuthenticator.AttestationCert +import com.yubico.webauthn.TestAuthenticator.AttestationMaker +import com.yubico.webauthn.TestAuthenticator.AttestationSigner import com.yubico.webauthn.data.AttestationObject +import com.yubico.webauthn.data.AuthenticatorAssertionResponse import com.yubico.webauthn.data.AuthenticatorAttestationResponse import com.yubico.webauthn.data.AuthenticatorSelectionCriteria import com.yubico.webauthn.data.ByteArray +import com.yubico.webauthn.data.COSEAlgorithmIdentifier +import com.yubico.webauthn.data.ClientAssertionExtensionOutputs import com.yubico.webauthn.data.ClientRegistrationExtensionOutputs import com.yubico.webauthn.data.CollectedClientData -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.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 java.nio.charset.StandardCharsets +import java.security.KeyPair +import java.security.cert.X509Certificate import scala.jdk.CollectionConverters._ import scala.util.Failure import scala.util.Success import scala.util.Try - object RegistrationTestDataGenerator extends App { regenerateTestData() def printTestDataCode( - testData: RegistrationTestData, + testData: RegistrationTestData ): Unit = { - println(s"""attestationObject = ByteArray.fromHex("${testData.attestationObject.getHex}"), + println( + s"""attestationObject = ByteArray.fromHex("${testData.attestationObject.getHex}"), |clientDataJson = \"\"\"${testData.clientDataJson}\"\"\", |privateKey = Some(ByteArray.fromHex("${testData.privateKey.get.getHex}")), - """.stripMargin) + """.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)}\"\"\") + | request = JacksonCodecs.json().readValue(\"\"\"${JacksonCodecs + .json() + .writeValueAsString( + assertion.request + )}\"\"\", classOf[AssertionRequest]), + | response = PublicKeyCredential.parseAssertionResponseJson(\"\"\"${JacksonCodecs + .json() + .writeValueAsString(assertion.response)}\"\"\") |)), """.stripMargin) } @@ -83,22 +89,24 @@ object RegistrationTestDataGenerator extends App { def regenerateTestData(): Unit = { val td = RegistrationTestData - for { (testData, i) <- List( - td.AndroidSafetynet.BasicAttestation, - td.AndroidSafetynet.WrongHostname, - td.AndroidSafetynet.FalseCtsProfileMatch, - td.FidoU2f.BasicAttestation, - td.FidoU2f.SelfAttestation, - td.NoneAttestation.Default, - td.Packed.BasicAttestation, - td.Packed.BasicAttestationEdDsa, - td.Packed.BasicAttestationRsa, - td.Packed.BasicAttestationRs1, - td.Packed.BasicAttestationWithoutAaguidExtension, - td.Packed.BasicAttestationWithWrongAaguidExtension, - td.Packed.SelfAttestation, - td.Packed.SelfAttestationRs1, - ).zipWithIndex } { + for { + (testData, i) <- List( + td.AndroidSafetynet.BasicAttestation, + td.AndroidSafetynet.WrongHostname, + td.AndroidSafetynet.FalseCtsProfileMatch, + td.FidoU2f.BasicAttestation, + td.FidoU2f.SelfAttestation, + td.NoneAttestation.Default, + td.Packed.BasicAttestation, + td.Packed.BasicAttestationEdDsa, + td.Packed.BasicAttestationRsa, + td.Packed.BasicAttestationRs1, + td.Packed.BasicAttestationWithoutAaguidExtension, + td.Packed.BasicAttestationWithWrongAaguidExtension, + td.Packed.SelfAttestation, + td.Packed.SelfAttestationRs1, + ).zipWithIndex + } { testData.regenerateFull() match { case Success(newTestData) => println(i) @@ -114,276 +122,546 @@ object RegistrationTestDataGenerator extends App { object RegistrationTestData { private def jsonFactory: JsonNodeFactory = JsonNodeFactory.instance - def defaultSettingsValidExamples = List( - AndroidSafetynet.RealExample, - AndroidSafetynet.BasicAttestation, - FidoU2f.BasicAttestation, - FidoU2f.SelfAttestation, - NoneAttestation.Default, - Packed.BasicAttestation, - Packed.BasicAttestationEdDsa, - Packed.BasicAttestationRsa, - Packed.BasicAttestationRsaReal, - Packed.SelfAttestation, - ) + def defaultSettingsValidExamples = + List( + AndroidSafetynet.RealExample, + AndroidSafetynet.BasicAttestation, + FidoU2f.BasicAttestation, + FidoU2f.SelfAttestation, + NoneAttestation.Default, + Packed.BasicAttestation, + Packed.BasicAttestationEdDsa, + Packed.BasicAttestationRsa, + Packed.BasicAttestationRsaReal, + Packed.SelfAttestation, + ) object AndroidKey { - val BasicAttestation: RegistrationTestData = Packed.SelfAttestation.setAttestationStatementFormat("android-key") + val BasicAttestation: RegistrationTestData = + Packed.SelfAttestation.setAttestationStatementFormat("android-key") } object AndroidSafetynet { val RealExample: RegistrationTestData = new RegistrationTestData( alg = COSEAlgorithmIdentifier.ES256, - attestationObject = ByteArray.fromBase64Url("o2NmbXRxYW5kcm9pZC1zYWZldHluZXRnYXR0U3RtdKJjdmVyaDE0Nzk5MDIxaHJlc3BvbnNlWRS9ZXlKaGJHY2lPaUpTVXpJMU5pSXNJbmcxWXlJNld5Sk5TVWxHYTJwRFEwSkljV2RCZDBsQ1FXZEpVVkpZY205T01GcFBaRkpyUWtGQlFVRkJRVkIxYm5wQlRrSm5hM0ZvYTJsSE9YY3dRa0ZSYzBaQlJFSkRUVkZ6ZDBOUldVUldVVkZIUlhkS1ZsVjZSV1ZOUW5kSFFURlZSVU5vVFZaU01qbDJXako0YkVsR1VubGtXRTR3U1VaT2JHTnVXbkJaTWxaNlRWSk5kMFZSV1VSV1VWRkVSWGR3U0ZaR1RXZFJNRVZuVFZVNGVFMUNORmhFVkVVMFRWUkJlRTFFUVROTlZHc3dUbFp2V0VSVVJUVk5WRUYzVDFSQk0wMVVhekJPVm05M1lrUkZURTFCYTBkQk1WVkZRbWhOUTFaV1RYaEZla0ZTUW1kT1ZrSkJaMVJEYTA1b1lrZHNiV0l6U25WaFYwVjRSbXBCVlVKblRsWkNRV05VUkZVeGRtUlhOVEJaVjJ4MVNVWmFjRnBZWTNoRmVrRlNRbWRPVmtKQmIxUkRhMlIyWWpKa2MxcFRRazFVUlUxNFIzcEJXa0puVGxaQ1FVMVVSVzFHTUdSSFZucGtRelZvWW0xU2VXSXliR3RNYlU1MllsUkRRMEZUU1hkRVVWbEtTMjlhU1doMlkwNUJVVVZDUWxGQlJHZG5SVkJCUkVORFFWRnZRMmRuUlVKQlRtcFlhM293WlVzeFUwVTBiU3N2UnpWM1QyOHJXRWRUUlVOeWNXUnVPRGh6UTNCU04yWnpNVFJtU3pCU2FETmFRMWxhVEVaSWNVSnJOa0Z0V2xaM01rczVSa2N3VHpseVVsQmxVVVJKVmxKNVJUTXdVWFZ1VXpsMVowaEROR1ZuT1c5MmRrOXRLMUZrV2pKd09UTllhSHAxYmxGRmFGVlhXRU40UVVSSlJVZEtTek5UTW1GQlpucGxPVGxRVEZNeU9XaE1ZMUYxV1ZoSVJHRkROMDlhY1U1dWIzTnBUMGRwWm5NNGRqRnFhVFpJTDNob2JIUkRXbVV5YkVvck4wZDFkSHBsZUV0d2VIWndSUzkwV2xObVlsazVNRFZ4VTJ4Q2FEbG1jR293TVRWamFtNVJSbXRWYzBGVmQyMUxWa0ZWZFdWVmVqUjBTMk5HU3pSd1pYWk9UR0Y0UlVGc0swOXJhV3hOZEVsWlJHRmpSRFZ1Wld3MGVFcHBlWE0wTVROb1lXZHhWekJYYUdnMVJsQXpPV2hIYXpsRkwwSjNVVlJxWVhwVGVFZGtkbGd3YlRaNFJsbG9hQzh5VmsxNVdtcFVORXQ2VUVwRlEwRjNSVUZCWVU5RFFXeG5kMmRuU2xWTlFUUkhRVEZWWkVSM1JVSXZkMUZGUVhkSlJtOUVRVlJDWjA1V1NGTlZSVVJFUVV0Q1oyZHlRbWRGUmtKUlkwUkJWRUZOUW1kT1ZraFNUVUpCWmpoRlFXcEJRVTFDTUVkQk1WVmtSR2RSVjBKQ1VYRkNVWGRIVjI5S1FtRXhiMVJMY1hWd2J6UlhObmhVTm1veVJFRm1RbWRPVmtoVFRVVkhSRUZYWjBKVFdUQm1hSFZGVDNaUWJTdDRaMjU0YVZGSE5rUnlabEZ1T1V0NlFtdENaMmR5UW1kRlJrSlJZMEpCVVZKWlRVWlpkMHAzV1VsTGQxbENRbEZWU0UxQlIwZEhNbWd3WkVoQk5reDVPWFpaTTA1M1RHNUNjbUZUTlc1aU1qbHVUREprTUdONlJuWk5WRUZ5UW1kbmNrSm5SVVpDVVdOM1FXOVpabUZJVWpCalJHOTJURE5DY21GVE5XNWlNamx1VERKa2VtTnFTWFpTTVZKVVRWVTRlRXh0VG5sa1JFRmtRbWRPVmtoU1JVVkdha0ZWWjJoS2FHUklVbXhqTTFGMVdWYzFhMk50T1hCYVF6VnFZakl3ZDBsUldVUldVakJuUWtKdmQwZEVRVWxDWjFwdVoxRjNRa0ZuU1hkRVFWbExTM2RaUWtKQlNGZGxVVWxHUVhwQmRrSm5UbFpJVWpoRlMwUkJiVTFEVTJkSmNVRm5hR2cxYjJSSVVuZFBhVGgyV1ROS2MweHVRbkpoVXpWdVlqSTVia3d3WkZWVmVrWlFUVk0xYW1OdGQzZG5aMFZGUW1kdmNrSm5SVVZCWkZvMVFXZFJRMEpKU0RGQ1NVaDVRVkJCUVdSM1EydDFVVzFSZEVKb1dVWkpaVGRGTmt4TldqTkJTMUJFVjFsQ1VHdGlNemRxYW1RNE1FOTVRVE5qUlVGQlFVRlhXbVJFTTFCTVFVRkJSVUYzUWtsTlJWbERTVkZEVTFwRFYyVk1Tblp6YVZaWE5rTm5LMmRxTHpsM1dWUktVbnAxTkVocGNXVTBaVmswWXk5dGVYcHFaMGxvUVV4VFlta3ZWR2g2WTNweGRHbHFNMlJyTTNaaVRHTkpWek5NYkRKQ01HODNOVWRSWkdoTmFXZGlRbWRCU0ZWQlZtaFJSMjFwTDFoM2RYcFVPV1ZIT1ZKTVNTdDRNRm95ZFdKNVdrVldla0UzTlZOWlZtUmhTakJPTUVGQlFVWnRXRkU1ZWpWQlFVRkNRVTFCVW1wQ1JVRnBRbU5EZDBFNWFqZE9WRWRZVURJM09IbzBhSEl2ZFVOSWFVRkdUSGx2UTNFeVN6QXJlVXhTZDBwVlltZEpaMlk0WjBocWRuQjNNbTFDTVVWVGFuRXlUMll6UVRCQlJVRjNRMnR1UTJGRlMwWlZlVm8zWmk5UmRFbDNSRkZaU2t0dldrbG9kbU5PUVZGRlRFSlJRVVJuWjBWQ1FVazVibFJtVWt0SlYyZDBiRmRzTTNkQ1REVTFSVlJXTm10aGVuTndhRmN4ZVVGak5VUjFiVFpZVHpReGExcDZkMG8yTVhkS2JXUlNVbFF2VlhORFNYa3hTMFYwTW1Nd1JXcG5iRzVLUTBZeVpXRjNZMFZYYkV4UldUSllVRXg1Um1wclYxRk9ZbE5vUWpGcE5GY3lUbEpIZWxCb2RETnRNV0kwT1doaWMzUjFXRTAyZEZnMVEzbEZTRzVVYURoQ2IyMDBMMWRzUm1sb2VtaG5iamd4Ukd4a2IyZDZMMHN5VlhkTk5sTTJRMEl2VTBWNGEybFdabllyZW1KS01ISnFkbWM1TkVGc1pHcFZabFYzYTBrNVZrNU5ha1ZRTldVNGVXUkNNMjlNYkRabmJIQkRaVVkxWkdkbVUxZzBWVGw0TXpWdmFpOUpTV1F6VlVVdlpGQndZaTl4WjBkMmMydG1aR1Y2ZEcxVmRHVXZTMU50Y21sM1kyZFZWMWRsV0daVVlra3plbk5wYTNkYVltdHdiVkpaUzIxcVVHMW9kalJ5YkdsNlIwTkhkRGhRYmpod2NUaE5Na3RFWmk5UU0ydFdiM1F6WlRFNFVUMGlMQ0pOU1VsRlUycERRMEY2UzJkQmQwbENRV2RKVGtGbFR6QnRjVWRPYVhGdFFrcFhiRkYxUkVGT1FtZHJjV2hyYVVjNWR6QkNRVkZ6UmtGRVFrMU5VMEYzU0dkWlJGWlJVVXhGZUdSSVlrYzVhVmxYZUZSaFYyUjFTVVpLZG1JelVXZFJNRVZuVEZOQ1UwMXFSVlJOUWtWSFFURlZSVU5vVFV0U01uaDJXVzFHYzFVeWJHNWlha1ZVVFVKRlIwRXhWVVZCZUUxTFVqSjRkbGx0Um5OVk1teHVZbXBCWlVaM01IaE9la0V5VFZSVmQwMUVRWGRPUkVwaFJuY3dlVTFVUlhsTlZGVjNUVVJCZDA1RVNtRk5SVWw0UTNwQlNrSm5UbFpDUVZsVVFXeFdWRTFTTkhkSVFWbEVWbEZSUzBWNFZraGlNamx1WWtkVloxWklTakZqTTFGblZUSldlV1J0YkdwYVdFMTRSWHBCVWtKblRsWkNRVTFVUTJ0a1ZWVjVRa1JSVTBGNFZIcEZkMmRuUldsTlFUQkhRMU54UjFOSllqTkVVVVZDUVZGVlFVRTBTVUpFZDBGM1oyZEZTMEZ2U1VKQlVVUlJSMDA1UmpGSmRrNHdOWHByVVU4NUszUk9NWEJKVW5aS2VucDVUMVJJVnpWRWVrVmFhRVF5WlZCRGJuWlZRVEJSYXpJNFJtZEpRMlpMY1VNNVJXdHpRelJVTW1aWFFsbHJMMnBEWmtNelVqTldXazFrVXk5a1RqUmFTME5GVUZwU2NrRjZSSE5wUzFWRWVsSnliVUpDU2pWM2RXUm5lbTVrU1UxWlkweGxMMUpIUjBac05YbFBSRWxMWjJwRmRpOVRTa2d2VlV3clpFVmhiSFJPTVRGQ2JYTkxLMlZSYlUxR0t5dEJZM2hIVG1oeU5UbHhUUzg1YVd3M01Va3laRTQ0UmtkbVkyUmtkM1ZoWldvMFlsaG9jREJNWTFGQ1ltcDRUV05KTjBwUU1HRk5NMVEwU1N0RWMyRjRiVXRHYzJKcWVtRlVUa001ZFhwd1JteG5UMGxuTjNKU01qVjRiM2x1VlhoMk9IWk9iV3R4TjNwa1VFZElXR3Q0VjFrM2IwYzVhaXRLYTFKNVFrRkNhemRZY2twbWIzVmpRbHBGY1VaS1NsTlFhemRZUVRCTVMxY3dXVE42Tlc5Nk1rUXdZekYwU2t0M1NFRm5UVUpCUVVkcVoyZEZlazFKU1VKTWVrRlBRbWRPVmtoUk9FSkJaamhGUWtGTlEwRlpXWGRJVVZsRVZsSXdiRUpDV1hkR1FWbEpTM2RaUWtKUlZVaEJkMFZIUTBOelIwRlJWVVpDZDAxRFRVSkpSMEV4VldSRmQwVkNMM2RSU1UxQldVSkJaamhEUVZGQmQwaFJXVVJXVWpCUFFrSlpSVVpLYWxJclJ6UlJOamdyWWpkSFEyWkhTa0ZpYjA5ME9VTm1NSEpOUWpoSFFURlZaRWwzVVZsTlFtRkJSa3AyYVVJeFpHNUlRamRCWVdkaVpWZGlVMkZNWkM5alIxbFpkVTFFVlVkRFEzTkhRVkZWUmtKM1JVSkNRMnQzU25wQmJFSm5aM0pDWjBWR1FsRmpkMEZaV1ZwaFNGSXdZMFJ2ZGt3eU9XcGpNMEYxWTBkMGNFeHRaSFppTW1OMldqTk9lVTFxUVhsQ1owNVdTRkk0UlV0NlFYQk5RMlZuU21GQmFtaHBSbTlrU0ZKM1QyazRkbGt6U25OTWJrSnlZVk0xYm1JeU9XNU1NbVI2WTJwSmRsb3pUbmxOYVRWcVkyMTNkMUIzV1VSV1VqQm5Ra1JuZDA1cVFUQkNaMXB1WjFGM1FrRm5TWGRMYWtGdlFtZG5ja0puUlVaQ1VXTkRRVkpaWTJGSVVqQmpTRTAyVEhrNWQyRXlhM1ZhTWpsMlduazVlVnBZUW5aak1td3dZak5LTlV4NlFVNUNaMnR4YUd0cFJ6bDNNRUpCVVhOR1FVRlBRMEZSUlVGSGIwRXJUbTV1TnpoNU5uQlNhbVE1V0d4UlYwNWhOMGhVWjJsYUwzSXpVazVIYTIxVmJWbElVRkZ4TmxOamRHazVVRVZoYW5aM1VsUXlhVmRVU0ZGeU1ESm1aWE54VDNGQ1dUSkZWRlYzWjFwUksyeHNkRzlPUm5ab2MwODVkSFpDUTA5SllYcHdjM2RYUXpsaFNqbDRhblUwZEZkRVVVZzRUbFpWTmxsYVdpOVlkR1ZFVTBkVk9WbDZTbkZRYWxrNGNUTk5SSGh5ZW0xeFpYQkNRMlkxYnpodGR5OTNTalJoTWtjMmVIcFZjalpHWWpaVU9FMWpSRTh5TWxCTVVrdzJkVE5OTkZSNmN6TkJNazB4YWpaaWVXdEtXV2s0ZDFkSlVtUkJka3RNVjFwMUwyRjRRbFppZWxsdGNXMTNhMjAxZWt4VFJGYzFia2xCU21KRlRFTlJRMXAzVFVnMU5uUXlSSFp4YjJaNGN6WkNRbU5EUmtsYVZWTndlSFUyZURaMFpEQldOMU4yU2tORGIzTnBjbE50U1dGMGFpODVaRk5UVmtSUmFXSmxkRGh4THpkVlN6UjJORnBWVGpnd1lYUnVXbm94ZVdjOVBTSmRmUS5leUp1YjI1alpTSTZJa3RDT1hwd01tTlJXRlpLVUdkbWIzWkpNREp6UVRkbk1rMVFMMWh2UzFaQlpHZzRNMjA1U2podGFWRTlJaXdpZEdsdFpYTjBZVzF3VFhNaU9qRTFORGMxTmpFNE1qSXhNakVzSW1Gd2ExQmhZMnRoWjJWT1lXMWxJam9pWTI5dExtZHZiMmRzWlM1aGJtUnliMmxrTG1kdGN5SXNJbUZ3YTBScFoyVnpkRk5vWVRJMU5pSTZJa3B4UzBNNVoweGlWMHRNZFM4eU1ubElLMDl0VUdSc1RuQXljemhOVlZGa1VUWXhXVUp5ZGpoQ2QyODlJaXdpWTNSelVISnZabWxzWlUxaGRHTm9JanAwY25WbExDSmhjR3REWlhKMGFXWnBZMkYwWlVScFoyVnpkRk5vWVRJMU5pSTZXeUk0VURGelZ6QkZVRXBqYzJ4M04xVjZVbk5wV0V3Mk5IY3JUelV3UldRclVrSkpRM1JoZVRGbk1qUk5QU0pkTENKaVlYTnBZMGx1ZEdWbmNtbDBlU0k2ZEhKMVpYMC5TVEp1N0xnR29VWHQzZmtveXZMZEIyd0VHZ3VHOUc0QnpEUWxFYkQ3a0dSVWdmZDd2ZHUxUGwtbWNlUUd6cnJWUEY5eHRERXFHWnR6YkpILWN1amVVN0RCbTF4eDRzNlBIenZra0xibjg0b1ROUFJNcmhFalpER0tFUHdVZzVXb0M0UUlPSERMN2xpbUN4alhxYVl5eTh0Tk4ybW1yWlBPV3oxRVJGOG1XSG1tU0VrSWNJeVpyTDhuVS1jMkhVM2pyYkF3SDFoWFoxZU1yMG9kUTllbWM2TU8tWUFhSjZ6X2g1a29MSnhGVkxnZTh3dmp6UkE3R0hZdXlzQ3FPTXdCb3FnbVBlVzUxODBLV0VtTXdJUVljd1VXanZDbTRILUlWWUl6RElGcHdMSUFaaFQxd1NHbUoyeDBaQ3lpMlF4SmhRR0RjR1JuZjZyTlllc3FnSXpqV0FoYXV0aERhdGFYxcRs74KtG1Rkd1kdAIsIdZ7D5tLstPOUdL_qaWmSXQO3RQAAAAC5P9lh8uZGL7EiggAiR954AEEBEAglXhhzprEVlCnJSjQ0f59qby7VJKFLROYDyglb0hOGk9VmPeojeh8mwnsf3exgIBoVovCmIaGggiF3YPIV8KUBAgMmIAEhWCDbwAl__SPL0bDsj9WldwIqhh0thFFVRWt0HHm8MT5AVyJYIL1z6R8jvPutwAinX77M3ahwoNxFWPvR15vuhv1af8c6"), + 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() + rpId = + RelyingPartyIdentity.builder().id("demo.yubico.com").name("").build(), ) val BasicAttestation: RegistrationTestData = new RegistrationTestData( 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(attestationMaker = AttestationMaker.androidSafetynet(AttestationSigner.ca(alg = COSEAlgorithmIdentifier.RS256, certSubject = new X500Name("CN=attest.android.com, O=Yubico, OU=Authenticator Attestation, C=SE")))) } + attestationObject = new ByteArray( + BinaryUtil.fromHex("bf68617574684461746158a449960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97634100000539000102030405060708090a0b0c0d0e0f0020d787c0d88c8b258e0e147af3f4442103fc7d41718ea92dd2b1be925c6a06113aa52258200b159bf750abbfa9bf8d6a4a806eff06533e9ac9f3576113f57ce6919306a51b03260102215820b9e1f17d71fc5cfbc2e19528b2fa6c9dd9e9a8bd35e692f5e12d71233e91950f200163666d7471616e64726f69642d7361666574796e65746761747453746d74bf6376657268313437393930323168726573706f6e73655907e665794a68624763694f694a53557a49314e694973496e67315979493657794a4e53556c4463565244513046724b32644264306c435157644a51304a55613364445a316c4a53323961535870714d45564264306c335957704662553144555564424d565646515864335a466459566d6c685630353253555a6b62466c72526a466b5232683153556857645746595557646b52315a365a45684e5a31457752586845656b464f516d644f566b4a42623031436247777857573173616d4a3652576c4e513046485154465652554e33643170525746597759556457645752486247705a57464a3259326c43516d5249556d786a4d314a6f5a456473646d4a715255784e515774485154465652554a6f54554e564d4656335347686a546b31555a336450564545795456526a4d4531715158645861474e4f5456526e6430395551544a4e56474d775457704264316471516d5a4e556e4e335231465a52465a5255555245516b706f5a45685362474d7a5558565a567a567259323035634670444e5770694d6a423452487042546b4a6e546c5a435157394e516d78734d566c7462477069656b567054554e42523045785655564464336461555668574d474648566e566b5232787157566853646d4e70516b4a6b53464a73597a4e536147524862485a69616b564d54554672523045785655564361453144565442566432646e52576c4e5154424851314e7852314e4a596a4e4555555643515646565155453053554a45643046335a3264465330467653554a425555524f6332307761544248624535514d575a4455474e4757444e3361316444533370315a466f3051324a5051304a544d6d394b4c7a4a335a6e5a7152555a324f456c4f5630703364325a73554539794b3063784e4552744f466734517a4d7256574e5251565a5763305249625535435a32314b513346475257307654473936633278685a5574335333465052326c3461484e464f53744e53477830616c7079634564614f467069596b527a4f554e705132357a646e6b7754564a4b645464495a4456784e585a7357553947596a4d315545563055585977516b5972557a426b4c3146685344647857476b30646a645a616b6f7a4d485672515752544d306f795747593262544e786153395563537455656c52716245637a53565277545539426556597653304a6b64574934613170774f456f775556497a556a6868614374615a554a7064564630513146335155746b636d314e63456c43555870496455356f613231614c324e736554597956554a31533264714c33646e4d55773156554533527a5a4f645734785a4642326157354d56557868526b6f3251334633513231576248426a53314635636b4a59596d70476445527962475a5256335a4f536c526c5633647a564756706345466e54554a4251556471536c5242616b31445255644465584e4851564652516d643156574e4255555646516b4a4a5255564251554a425a303146516c465a53454e42613074446433644f5247633464304e6e57556c4c6231704a656d6f77525546335355525451554633556c464a5a314e74555456504b7a6c3152576c784d544e70546d6b78566e5a4a645538335430357564546c46516b597a534549794f45687562445661554774445356464563474e68626d3958526b4a784d3074684d7a6c71536e68554e32784457473153616b4e6c54307868567a4a715a6d39715547316a63575a435a7a3039496c31392e65794a68634778445a584a3061575a70593246305a5552705a32567a64464e6f595449314e69493657794a4d5132457759544a714c3368764c7a56744d46553453465243516b3543546b4e4d57454a725a7a63725a79745a63475670523070744e54593050534a644c434a756232356a5a534936496c427165585a6b524539304e554a365a56523561474643646b307a554552705a303158636b6732515652736256464c636c6730566d68314e47733949697769595842725547466a6132466e5a553568625755694f694a6a623230756558566961574e764c6e646c596d4631644768754c6e526c633351694c434a6959584e7059306c756447566e636d6c306553493664484a315a53776964476c745a584e305957317754584d694f6a45314e546b314e6a6b794d7a67314f446773496d4677613052705a32567a64464e6f595449314e694936496b7844595442684d6d6f76654738764e5730775654684956454a43546b4a4f51307859516d746e4e79746e4b316c775a576c48536d30314e6a5139496977695933527a55484a765a6d6c735a55316864474e6f496a7030636e566c66512e76546e4f4f6151514b6e7959356747394d58636c63455237513145347275705a76415f6d77494a4e47745f2d5039436e6d64736366597a7a512d67714b3668537467714c326f6453485f6b6d473441464a4d367a304843712d324d41756262515f38435346507855615f6a5674397334552d7446415a564b64424b796e495f4b3863456b66683364684a664138774c70363268485f6f6b5a63744c6b5f437549787446646f4b587854793675706f4a4e496a4342614a4a314855304b4c424c78704e4543494346363876375368413855466e6e6863726a47456575794d6d634e5f6179535570306a3858536d5651496d7a754c714c4763476c545f647143486b764a673773303850616c53534131726e30634f505a526b41656c37706e65627746623854497373444852475a696639493255787530474774644b31364164797059555242633278642d4769302d30566c616631587577ffff") + ), + clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","type":"webauthn.create","tokenBinding":{"status":"supported"},"clientExtensions":{}}""", + ) { + 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( 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(attestationMaker = AttestationMaker.androidSafetynet(AttestationSigner.selfsigned(COSEAlgorithmIdentifier.RS256))) } + attestationObject = new ByteArray( + BinaryUtil.fromHex("bf68617574684461746158a449960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97634100000539000102030405060708090a0b0c0d0e0f0020bdbf5179112add51874c0824f0d860083d1ee7cb7f44c25ffa26c39271c3ed0aa5225820c35b4c850d7c13334432f7f17a5f1f9a5b1c85bff9b407dc0c962f0eb95eb9d10326010221582041d565e8ca57023f4510f150481d551f84f2dd09894fbc20d1c4707896943638200163666d7471616e64726f69642d7361666574796e65746761747453746d74bf6376657268313437393930323168726573706f6e73655907f165794a68624763694f694a53557a49314e694973496e67315979493657794a4e53556c4463315244513046735a57644264306c435157644a51304a55613364445a316c4a53323961535870714d45564264306c335957704662553144555564424d565646515864335a466459566d6c685630353253555a6b62466c72526a466b5232683153556857645746595557646b52315a365a45684e5a31457752586845656b464f516d644f566b4a42623031436247777857573173616d4a3652576c4e513046485154465652554e33643170525746597759556457645752486247705a57464a3259326c43516d5249556d786a4d314a6f5a456473646d4a715255784e515774485154465652554a6f54554e564d4656335347686a546b31555a336450564545795456526a4d4531715158645861474e4f5456526e6430395551544a4e56474d775457704264316471516d354e553031335356465a52465a5255555245516e42615a46644b63466b794f4764574d6c5a70555668574d4746484e47646b567a56775a454e434d467059546a426a656b56515455457752304578565556445a3364485631685761574658546e5a4e55306c335355465a52465a5255557845516d78435a466853623170584e5442685630356f5a45633565556c46526a426b52315a365a4564474d4746584f58564e55584e335131465a52465a525555644664307055556c5244513046545358644555566c4b53323961535768325930354255555643516c46425247646e5256424252454e44515646765132646e52554a4254573432535539336255786d5356427253546c5256315236546e6c5a4e304d3464444e6956444a5254484179654374324b3342325a6c70424d47686b5a31457852586875656d78574e57314f4e334a744d4735354b3046695a334646546d397265545533646a45764f57644359334d314c7a464e4c315a36555464735743746c4e325275516a4233516c42755355524b5a4374304d3064345a6d597659544645596b4977555535364d567033516a4d325545744f4e5856594e3274364b314d35516b5668616b644f4e47786a5a466c425232644654454a33513168745a55465a62316c47567974475a4763345a473034626d6f76575778314e3168426248464d4c7a423661577053576b6c535530704c4e46705157454e6f626a42314e3246464d553578567a52696332566a536b6c70656e5a34534868546547567257544134656b4673656c4579636a5235626e70754e3235334f476f30566a6c3357575254526d347a5630396d616c5654565578326344453061546c324f4549324e6d677264565a6f4e4578504d3231365a586c5652445a444e587035516e5a5a5757706a6256565a516d383162585268556a527164334250517a6c4963585a4b536a5a4c4e46453364336c4651304633525546425955317354554e4e64306c525755784c64316c43516b4648517a565364304a4255564646525764525555464252554e4264314647516d646a53554e5262307845515442505248704253304a6e5a33466f61327050554646525245466e546b6c4252454a4751576c46515445354e4442744d6e426865485a615645355251584e4953544678516d68756255737253335a564e7939704d6b52714b306c50623239775956564453555a6c4d453945515739584f4735484f544a74654531694e6d314a4e324e354c304d355a6b4535565445335755687161334673656b6b7a623273695858302e65794a68634778445a584a3061575a70593246305a5552705a32567a64464e6f595449314e69493657794a4d5132457759544a714c3368764c7a56744d46553453465243516b3543546b4e4d57454a725a7a63725a79745a63475670523070744e54593050534a644c434a756232356a5a534936496b4e4c544856715958413157444a475346566e61537444636b353153473072556c4a7456484a4b566c5642656b46504f5667305569746c557a513949697769595842725547466a6132466e5a553568625755694f694a6a623230756558566961574e764c6e646c596d4631644768754c6e526c633351694c434a6959584e7059306c756447566e636d6c306553493664484a315a53776964476c745a584e305957317754584d694f6a45314e546b314e6a6b794d7a67334f545173496d4677613052705a32567a64464e6f595449314e694936496b7844595442684d6d6f76654738764e5730775654684956454a43546b4a4f51307859516d746e4e79746e4b316c775a576c48536d30314e6a5139496977695933527a55484a765a6d6c735a55316864474e6f496a7030636e566c66512e54672d627854616c77687a78794c52415837576c66432d595a4e3670484a56434b4b6a5952556d6331467166496e45396a335847636f32596b697974306c4456635266494934314d4c7134464733585f4c476773553649344d323572706e69796b5f64706278684e486655564c4c326d4b4f314546704d4b6d51787549644d51584f33635f31746a42386f6279334c524179545652366a337144463449495f676b466a69755a5a4b42715741746b66694f636f78506b3559474d452d6f525968694e6879457063446650376a5963365443414861682d714c5337696a7a5a48736c6a504a326f6c534e7a593673587550316b544650475744496d4e5647616f795f4a63576a626e476c705f585932506e46716f6247424a49714d43446354674f344330335934746879387343557849365f484f43754c694b6d5a3268536554673076544873635052744c444733755969464d65355051ffff") + ), + clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","type":"webauthn.create","tokenBinding":{"status":"supported"},"clientExtensions":{}}""", + ) { + override def regenerate() = + TestAuthenticator.createBasicAttestedCredential(attestationMaker = + AttestationMaker.androidSafetynet( + AttestationSigner.selfsigned(COSEAlgorithmIdentifier.RS256) + ) + ) + } val FalseCtsProfileMatch: RegistrationTestData = new RegistrationTestData( 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(attestationMaker = AttestationMaker.androidSafetynet(AttestationSigner.ca(alg = COSEAlgorithmIdentifier.RS256, certSubject = new X500Name("CN=attest.android.com, O=Yubico, OU=Authenticator Attestation, C=SE")), ctsProfileMatch = false)) } + attestationObject = new ByteArray( + BinaryUtil.fromHex("bf68617574684461746158a449960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97634100000539000102030405060708090a0b0c0d0e0f0020a3c7abed63a16885b1cbf572b1bb29c6614812a886c222a00e07bc7c600764e4a52258209ab2b20ef874f994a3f99ad8f1e61ba590cf67357e5c0997dafcb71edf42ff9b0326010221582027834a6fcb02f7d3182bad5f2b3d16856c93fb574703503a847a36e457996275200163666d7471616e64726f69642d7361666574796e65746761747453746d74bf6376657268313437393930323168726573706f6e73655907e765794a68624763694f694a53557a49314e694973496e67315979493657794a4e53556c4463565244513046724b32644264306c435157644a51304a55613364445a316c4a53323961535870714d45564264306c335957704662553144555564424d565646515864335a466459566d6c685630353253555a6b62466c72526a466b5232683153556857645746595557646b52315a365a45684e5a31457752586845656b464f516d644f566b4a42623031436247777857573173616d4a3652576c4e513046485154465652554e33643170525746597759556457645752486247705a57464a3259326c43516d5249556d786a4d314a6f5a456473646d4a715255784e515774485154465652554a6f54554e564d4656335347686a546b31555a336450564545795456526a4d4531715158645861474e4f5456526e6430395551544a4e56474d775457704264316471516d5a4e556e4e335231465a52465a5255555245516b706f5a45685362474d7a5558565a567a567259323035634670444e5770694d6a423452487042546b4a6e546c5a435157394e516d78734d566c7462477069656b567054554e42523045785655564464336461555668574d474648566e566b5232787157566853646d4e70516b4a6b53464a73597a4e536147524862485a69616b564d54554672523045785655564361453144565442566432646e52576c4e5154424851314e7852314e4a596a4e4555555643515646565155453053554a45643046335a3264465330467653554a425555524d5a47317863474a616244687162454d3254304e364d484e7052475a7a4d48525157455644536a4a6e5230465253304e504d474d7261303832597a4a6d536b354b53473479617a427661553534644468595a4859794e6b704a5479394c555538764f4649794f55394b595567305233677a6247463656336c7065574d765a6e5651614845775a6a645a4b31686c536d354f575374494d3039365a4770534d474674575667345430565763565645526b74316358467862444251536e6c7a4e6a4e31516c70505548563256585a5556793931626b395a5430706d563352725655393563446c6d593235725a6d354c5230646e51326477574756485547786861486b3165455a465333564b4e6a4673536e565063455a5361484a725545737a596d78424f5455785745466f576b4e744e31685252444e4856323578636d687361546834567a4a79546a424b4f585a7a4e6a524b4f453169526d46724d465673546d353561305261623342724d46706a5746464252556f30545664595231527a4f44463554465a574f544e425931597261464e48534642545a3370334f4731564c32784d4f456452553235455530686a527a4251526c7042595456524d32704661307852536d59345745466e54554a4251556471536c5242616b31445255644465584e4851564652516d643156574e4255555646516b4a4a5255564251554a425a303146516c465a53454e42613074446433644f5247633464304e6e57556c4c6231704a656d6f77525546335355525451554633556c464a5a324d7a56316c5856533936596e6c3151334e6d6345704f4f544a46575563766457644b4d6c70564e31564b4d57526b4b324d76576b645252576444535646456232733461336c565a6a4e32535374735456687a5a565a57645452754e5667315332314d636b3575566b68485757566d4d326c565632457251543039496c31392e65794a68634778445a584a3061575a70593246305a5552705a32567a64464e6f595449314e69493657794a4d5132457759544a714c3368764c7a56744d46553453465243516b3543546b4e4d57454a725a7a63725a79745a63475670523070744e54593050534a644c434a756232356a5a534936496b59354d5559354c30526c616b49345a6a4a31516c646b596e425764557076655764346257744865486c77576c52516256646d625746695a57733949697769595842725547466a6132466e5a553568625755694f694a6a623230756558566961574e764c6e646c596d4631644768754c6e526c633351694c434a6959584e7059306c756447566e636d6c306553493664484a315a53776964476c745a584e305957317754584d694f6a45314e546b314e6a6b794d7a6b794d545973496d4677613052705a32567a64464e6f595449314e694936496b7844595442684d6d6f76654738764e5730775654684956454a43546b4a4f51307859516d746e4e79746e4b316c775a576c48536d30314e6a5139496977695933527a55484a765a6d6c735a55316864474e6f496a706d5957787a5a58302e415a6938386e4264644c315a4d336c4c6a654943724332384675386a657063573937686e79446a4d327a366b7870322d49514c484d3779706468674d424c386b307a6637436572647272634c4e63345671494f7a694a694677334752704a656a4a61625f65734d7137324f71742d3855727a48506849476d494251744347547647646577654551715453656131784f6b356576786a4f667630564571563272497263562d46445f5568527437586e38654479536658744e4254784a4765374a727858436b537061374f65465932577a4d6d76536e316c745f482d507a35784b6a5f6665564e34317362425a4e70624649496c724879324e4361374e676c5a50347a6948373769766979316e564f4977517a5866545a496b6b6f7434426a775630484a6557776a447166577375726c467578346b32715047327a6a41574b755a575877617137584751565f53687068384f38652d4d4977ffff") + ), + clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","type":"webauthn.create","tokenBinding":{"status":"supported"},"clientExtensions":{}}""", + ) { + 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( 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(attestationMaker = AttestationMaker.fidoU2f(AttestationSigner.selfsigned(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(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(AttestationMaker.fidoU2f(_)) } + attestationObject = new ByteArray( + BinaryUtil.fromHex("bf68617574684461746158a449960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97634100000539000102030405060708090a0b0c0d0e0f00205558386f4ed61a6c98a3fed94060fff66808947953754a0dff2aea9ae2164635a52258208d05cb87cec921d5e6fbc22c32a07fb35ed89c19a3f0a2866fcf4a248194e650032601022158202bb1c0846fca809059b41272f0c2953d733b31b50c14453b7a9855b7bfc98229200163666d74686669646f2d7532666761747453746d74bf63783563815901e7308201e330820189a00302010202020539300a06082a8648ce3d04030230673123302106035504030c1a59756269636f20576562417574686e20756e6974207465737473310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b3009060355040613025345301e170d3138303930363137343230305a170d3138303930363137343230305a30673123302106035504030c1a59756269636f20576562417574686e20756e6974207465737473310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b30090603550406130253453059301306072a8648ce3d020106082a8648ce3d030107034200042bb1c0846fca809059b41272f0c2953d733b31b50c14453b7a9855b7bfc982298d05cb87cec921d5e6fbc22c32a07fb35ed89c19a3f0a2866fcf4a248194e650a32530233021060b2b0601040182e51c01010404120410000102030405060708090a0b0c0d0e0f300a06082a8648ce3d0403020348003045022100a91c5499a6518bc59648bde7e7467488736e1ae82b5eb85c14957a0f82d23dfc02205a4b9963f88dbabaa0fa298eae6f0876b9f5e65650c4bd29f1f3f7eeb1312c24637369675847304502205af7085152ec65cc5ee097c5890316e6cac286379c32925a969ab414b013aa59022100b9b9d56cf4314e10c13caa57fb1fb0a01e87ffdec623c62637fddf56a8c4c62cffff") + ), + clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","type":"webauthn.create","tokenBinding":{"status":"supported"},"clientExtensions":{}}""", + ) { + 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() } + attestationObject = new ByteArray( + BinaryUtil.fromHex("bf68617574684461746158a449960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97634100000539000102030405060708090a0b0c0d0e0f002082e7622c8c35a5786e66815f44a82b954628df497361169e77af23bb9bea1b69a5225820ae947a15818d883351ac00b957ad794c4b0206e2df34ec7b52969016a215800e03260102215820763f33278817151fad81d172493b8826c3a736cb1acf884e38c26fbe65c2438a200163666d74646e6f6e656761747453746d74bfffff") + ), + clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","type":"webauthn.create","tokenBinding":{"status":"supported"},"clientExtensions":{}}""", + ) { + override def regenerate() = TestAuthenticator.createUnattestedCredential() + } } object Packed { val BasicAttestation: RegistrationTestData = new RegistrationTestData( alg = COSEAlgorithmIdentifier.ES256, - attestationObject = ByteArray.fromHex("bf68617574684461746158a449960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97634100000539000102030405060708090a0b0c0d0e0f00206b1549b3cf2524c30089b001f8a0f100de9a97681910d2c8181337c516cb2eb6a503260102200121582079c229789b5a262e7b3b2057ef8636b7a20930f262fac3636682e70bdcd4d906225820ca5084617d404d831791a8281eba451aa165726267f9d480dfc315313c95408d63666d74667061636b65646761747453746d74bf63616c67266373696758483046022100f1b2138ab5e8dbce9d0e88862295f574c1b636aa740b57d6705646c799084dd5022100d87f9df13302b854a1c6a726481afbd96ddd2caeb51f4cba89bd248676e9af1063783563825901ed308201e93082018fa00302010202020539300a06082a8648ce3d040302306a3126302406035504030c1d59756269636f20576562417574686e20756e6974207465737473204341310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b3009060355040613025345301e170d3138303930363137343230305a170d3138303930363137343230305a306a3126302406035504030c1d59756269636f20576562417574686e20756e6974207465737473204341310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b30090603550406130253453059301306072a8648ce3d020106082a8648ce3d030107034200043c010b106a69efe039327ab79f57f8e43285f59ad56a50cfd0264b8ba88f79bf2291d561768bb686431aadce9dddf56858aac55b1638d5c03d2a2c426b64b64aa32530233021060b2b0601040182e51c01010404120410000102030405060708090a0b0c0d0e0f300a06082a8648ce3d0403020348003045022100aa1943235627b47852deace94c46e2499a4b2bcab17ffe5502d0c5d17f0f883d022076402b6fe8f66040e4f157e74f732e4a4d31268115e2880faa999f248a0485e05901db308201d73082017da00302010202020539300a06082a8648ce3d040302306a3126302406035504030c1d59756269636f20576562417574686e20756e6974207465737473204341310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b3009060355040613025345301e170d3138303930363137343230305a170d3138303930363137343230305a306a3126302406035504030c1d59756269636f20576562417574686e20756e6974207465737473204341310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b30090603550406130253453059301306072a8648ce3d020106082a8648ce3d0301070342000448f77f8679a4c7bfff4a3ec8291f18995444d21b8624aeefdf2821e69444ac66ced7c7c10ea30d9167836ee84042a9b944d2c239f2a493d5fb2896a2ca0b83d0a3133011300f0603551d130101ff040530030101ff300a06082a8648ce3d0403020348003045022100acc2e79b65faaa5206b27714102f8cdb95ee656c567b7ae7511467b6c324e8e802202a5ac41e505ac43f9efcf3985db215a7506244ba67eb19bdf17aabef8773e1c1ffff"), + 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))) } + ) { + override def regenerate() = + TestAuthenticator.createBasicAttestedCredential(attestationMaker = + AttestationMaker.packed( + AttestationSigner.ca(COSEAlgorithmIdentifier.ES256) + ) + ) + } val BasicAttestationEdDsa: RegistrationTestData = new RegistrationTestData( alg = COSEAlgorithmIdentifier.EdDSA, - attestationObject = ByteArray.fromHex("bf686175746844617461588149960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97634100000539000102030405060708090a0b0c0d0e0f002089b13dc1075db05f34ea0f2e2fd843ce0c0b262a4a852f5eb03d3b2668f437dfa403270101200621582051be73800d9386b8bcfa03f80143ed1279486f95acb714515616b849b588298963666d74667061636b65646761747453746d74bf63616c6726637369675846304402207ef99a22fb1d6fac37ce859f768a3b3d85477ef3825ea53fb7824bb292b12139022073ba899784179bcd06fb3e75657a99cd710ed84a98edbdc8370ac9df885eb8bb63783563815901e7308201e330820189a00302010202020539300a06082a8648ce3d04030230673123302106035504030c1a59756269636f20576562417574686e20756e6974207465737473310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b3009060355040613025345301e170d3138303930363137343230305a170d3138303930363137343230305a30673123302106035504030c1a59756269636f20576562417574686e20756e6974207465737473310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b30090603550406130253453059301306072a8648ce3d020106082a8648ce3d03010703420004ba072ce8a10f63a776c3ce83972e20259089b0d2072501678daedaea755175ee34c785c7cc47e06561fac2b48b1f22e795173c4b89cdfd651a661bb7b9b180f1a32530233021060b2b0601040182e51c01010404120410000102030405060708090a0b0c0d0e0f300a06082a8648ce3d0403020348003045022100b75626efe7b98fb81dcf8dbb301a2a2a0dea354c5b43592368bb0b7345e1e6ea022003deb0739996db0c3a3b40c116f070d10d03e7261459426378fa2896a92e5024ffff"), + attestationObject = + ByteArray.fromHex("bf686175746844617461588149960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97634100000539000102030405060708090a0b0c0d0e0f002089b13dc1075db05f34ea0f2e2fd843ce0c0b262a4a852f5eb03d3b2668f437dfa403270101200621582051be73800d9386b8bcfa03f80143ed1279486f95acb714515616b849b588298963666d74667061636b65646761747453746d74bf63616c6726637369675846304402207ef99a22fb1d6fac37ce859f768a3b3d85477ef3825ea53fb7824bb292b12139022073ba899784179bcd06fb3e75657a99cd710ed84a98edbdc8370ac9df885eb8bb63783563815901e7308201e330820189a00302010202020539300a06082a8648ce3d04030230673123302106035504030c1a59756269636f20576562417574686e20756e6974207465737473310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b3009060355040613025345301e170d3138303930363137343230305a170d3138303930363137343230305a30673123302106035504030c1a59756269636f20576562417574686e20756e6974207465737473310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b30090603550406130253453059301306072a8648ce3d020106082a8648ce3d03010703420004ba072ce8a10f63a776c3ce83972e20259089b0d2072501678daedaea755175ee34c785c7cc47e06561fac2b48b1f22e795173c4b89cdfd651a661bb7b9b180f1a32530233021060b2b0601040182e51c01010404120410000102030405060708090a0b0c0d0e0f300a06082a8648ce3d0403020348003045022100b75626efe7b98fb81dcf8dbb301a2a2a0dea354c5b43592368bb0b7345e1e6ea022003deb0739996db0c3a3b40c116f070d10d03e7261459426378fa2896a92e5024ffff"), clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","type":"webauthn.create","tokenBinding":{"status":"supported"},"clientExtensions":{}}""", - 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))) } + 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( alg = COSEAlgorithmIdentifier.RS256, - attestationObject = ByteArray.fromHex("bf68617574684461746159016849960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97634100000539000102030405060708090a0b0c0d0e0f00202d027aec938e6fcf40460eb328f8596e43af1cbd99fddf61c1ffae6e7b0e404ba40339010001032059010100ab8f0b1c4ceef20c093cd67fe6cc264e3ba8208467f410e53b22415ef201ad1ad525ac1be334926ce4f565cfc777135924c1a9bfc3fad24e3d504d618602937b200fd1972ea0097ff9e7d33f68633263a8ce347550213de95228c9c093ca700042f782eb6c16da1b75ed2f481815b04c222cae865340592deeba809fee80e6c1199a3e36b50b400ef87570234754566b276a8fb0cbca7a6ffa1d24369878c8c831e415747b3142cce244ae8d4e0df921a4c9400ed615c1e9c98479af90be09fb2880512bf9d52f825ea031ff10daac369862df3da0d1a2782888415430d8040a0671a749269dcdc4ac22a66b42cf0ac3a3365a64c6ce82ff2548bfac493f6bad214301000163666d74667061636b65646761747453746d74bf63616c673901006373696759010054de4d2aae25f9bd1b9d0e20a9d4168a5feded7178fe1f47ee0fb9a8f19439c8cc1aeab7a7269e4d4edb29c7c9864fbd8202d8cc69584da0e73b4c1d731bff3ec29599964ebef12068a9791d0e52a0c9579d881c565e1ae8a0fc7f2de9ec8882d13919a164b362ab2a89faec3be869635f187b3ef30cd20986ec6f2ff667cb1a279871f77dd9d037f49a7da784cdf846e2d7220683aa928e3b422616be8b0609385a16e0509365a609e162a5239bdc1c4e7aa60c9a1860de753b99705173a72c9fc0390f42886ff9ff839f045cf6457ecb7cf26da34e95511fde6343e4812f40ceb8ff2e7dd24dafdd9c513225bf3418df4a7c1c0f5bc6a0155a31d9c2ddfe8c63783563815903733082036f30820257a00302010202020539300d06092a864886f70d01010b050030673123302106035504030c1a59756269636f20576562417574686e20756e6974207465737473310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b3009060355040613025345301e170d3138303930363137343230305a170d3138303930363137343230305a30673123302106035504030c1a59756269636f20576562417574686e20756e6974207465737473310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b300906035504061302534530820122300d06092a864886f70d01010105000382010f003082010a028201010097c332c418daf7cb31e27bf321f7f72f48c614650f6215db969d17a6ba24fc08d3140fa4d45ff2b2b0ce95fbd87b629e23ba84533dbf2ed90c4e2a770db459690ddfa433288a06fa0c2b1c012887926f1d366d2beac622788560d0a4197b4d90ba7bfd6f4b3250cc37f54e5f350160ad61136bca94b560ec783334cf0376cca042ff40b288049881f7fb3c265f6bbfd625c18efe5802c7dbd384b0b6f328ae9a1bbeb4a184b8eddf16ff419a76adef00d20b57e0927e997c2dfec964c24fb2f023848916c41b0de26636be72356b555d4d1090f2cbcf9003eff39d4b6f77498481d6fe8b2f2bfe2e895382494ca4495c8ac9a47c9fbc8832dc66f727852f814d0203010001a32530233021060b2b0601040182e51c01010404120410000102030405060708090a0b0c0d0e0f300d06092a864886f70d01010b050003820101002a801c27a9a78f74db8de08bc367a8877f53007c7edc01354716be772d7e1450ab99c7b9d4e1c30c05080e51cc69c98068f0130aeccea535e1eb4e7834413bba888633a0c3aad9b7286096084425500b8b442a30ffd52cb77520ee28e8341e2640c39b81be07d9fce48d49ee3bad11b6015c78505e2c1aeaeb829c167bd86bbb714310f6559f481bb9b970dbe8184c7b24d8a4ef2030331d6c8d41b966d5fb4bf08f8f736adedc918fe039100330a5c6a79c54c92351c907608abda0fc98f019ac182ed2858f3c65aeeb282562d1036a06573edac5bed696553b5d347620cf9412faddaab3319080263378085812b315357a3cbe3618ff81d2760c7276ded4afffff"), + 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)))} + ) { + 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"), + 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( + 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 - 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(), + 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(), origin = Some("https://demo3.yubico.test:8443"), - userId = UserIdentity.builder().name("foo").displayName("Foo Bar").id(ByteArray.fromBase64Url("NiBJtVMh4AmSpZYuJ--jnEWgFzZHHVbS6zx7HFgAjAc")).build() + userId = UserIdentity + .builder() + .name("foo") + .displayName("Foo Bar") + .id( + ByteArray.fromBase64Url("NiBJtVMh4AmSpZYuJ--jnEWgFzZHHVbS6zx7HFgAjAc") + ) + .build(), ) - val BasicAttestationWithoutAaguidExtension: RegistrationTestData = new RegistrationTestData( - 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( - alg = COSEAlgorithmIdentifier.ES256, + val BasicAttestationWithoutAaguidExtension: RegistrationTestData = + new RegistrationTestData( + 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, + ), + ) + ) + ) + } - 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 BasicAttestationWithWrongAaguidExtension: RegistrationTestData = + new RegistrationTestData( + 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(AttestationMaker.packed(_)) } + attestationObject = new ByteArray( + BinaryUtil.fromHex("bf68617574684461746158a449960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97634100000539000102030405060708090a0b0c0d0e0f0020fa616cbe1c046d224524e773b386f9f3fd0d0fb6d4c20700023288034e48f093a52258208b02052aeec1d7cfaf1244d9b72296a6bfaf9542c132273c4be8fc01388ee8f30326010221582081906607ef7095eaa3dea2517cfc5a7c0c9768685e30ddb5865f2ada0f5cc63c200163666d74667061636b65646761747453746d74bf6373696758473045022010511b27bd566c7bcdf6e4f08ef2fe4ea20a56826b76761253bbcc31b0be1fa2022100b2659e3efc858fd4389dc48cd0651487f2e7bc4f5eba59db154bdcd0ae60c9d163616c6726ffff") + ), + clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","type":"webauthn.create","tokenBinding":{"status":"supported"},"clientExtensions":{}}""", + ) { + override def regenerate() = + TestAuthenticator.createSelfAttestedCredential( + AttestationMaker.packed(_) + ) + } val SelfAttestationRs1: RegistrationTestData = new RegistrationTestData( alg = COSEAlgorithmIdentifier.RS1, - attestationObject = ByteArray.fromHex("bf68617574684461746159016849960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97634100000539000102030405060708090a0b0c0d0e0f0020a8330648c97e09686004be4c429ad3e46886f6033117632cb1aabf261c5d11cda40339fffe01032059010100cdb2b9448221d1432be58b681f27ad204e82fe4c6176c64aed49792fe57c5ca9ddf7ce6fe22f81b67205df310d96668c1c1107ea6e250f4107692842c555c13d6e3df41ca701ce153705269658a186d9a1abe013b127dd51483323f3c82e281962eccfd4f59c05d778ecbbfbfb5eb5902dc91e1e187aacd97d42373a3c3e05218d291989133cf32641d322e6e472c3e4812e613d9ddbb67e74580570d5ef173561c146d81c56bf7bc6353fde611b54cd1fe632a314ac7d3e74ac18c0b7886a70ddcce226dd836791444a88ac9323877adbc5978a51d2abca189651ad5b71169df782f065908edd8ab9edccdc997212c32071b577fd58d55b22557d303d070b77214301000163666d74667061636b65646761747453746d74bf63616c6739fffe63736967590100bbf85e350e87886d80591e44b1a8e8f7fe7a4b3c4748b112ac6bbd88096a7b83c5e2f268154eecec230784729d6418809ce1ea370c374fd3e6151790d0a7f5a7a9e57dcbfd2e0cad26b11002232087eaaa0baf7fdef65c30518237d4ae7d36b7c49cc96b499afb6c0eab2c6a728fa847595071b56515c049d909707fbea2ee22ce0a325939af3b9021e1371bfea19cd14fc9caa1d1a41d5408cba381197c5fddc4e33245411d720c3acb4e53b415b120581d8093e25d710e5acef7e77889a71e5dee935f02992a559eab33725c832f3f24bf3934de2f5ac2eb32a9cc23a652bf08fc7e94c342ef62b555524b733447a19b3307fb41257794e041e91d1e1fbb37ffff"), + 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) } + 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.setAttestationStatementFormat("tpm") + val PrivacyCa: RegistrationTestData = + Packed.BasicAttestation.setAttestationStatementFormat("tpm") } } 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(), + 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(), ) { 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)) - ) - }) + 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(): Unit = { - val alg = WebAuthnCodecs.getCoseKeyAlg(response.getResponse.getParsedAuthenticatorData.getAttestedCredentialData.get.getCredentialPublicKey).get + 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 clientDataJsonBytes: ByteArray = + new ByteArray(clientDataJson.getBytes("UTF-8")) def clientData = new CollectedClientData(clientDataJsonBytes) - def clientDataJsonHash: ByteArray = Crypto.hash(clientDataJsonBytes) - def aaguid: ByteArray = new AttestationObject(attestationObject).getAuthenticatorData.getAttestedCredentialData.get.getAaguid + def clientDataJsonHash: ByteArray = Crypto.sha256(clientDataJsonBytes) + def aaguid: ByteArray = + new AttestationObject( + attestationObject + ).getAuthenticatorData.getAttestedCredentialData.get.getAaguid def packedAttestationCert: X509Certificate = CertificateParser.parseDer( - new AttestationObject(attestationObject) - .getAttestationStatement + new AttestationObject(attestationObject).getAttestationStatement .get("x5c") .get(0) .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 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(updater: ObjectNode => JsonNode): RegistrationTestData = copy( - clientDataJson = JacksonCodecs.json.writeValueAsString( - updater(JacksonCodecs.json.readTree(clientDataJson).asInstanceOf[ObjectNode]) + def editClientData(updater: ObjectNode => JsonNode): RegistrationTestData = + copy( + clientDataJson = JacksonCodecs.json.writeValueAsString( + updater( + JacksonCodecs.json.readTree(clientDataJson).asInstanceOf[ObjectNode] + ) + ) ) - ) - def editClientData(name: String, value: JsonNode): RegistrationTestData = editClientData { clientData: ObjectNode => - clientData.set[ObjectNode](name, value) - } - def editClientData(name: String, value: String): RegistrationTestData = editClientData(name, RegistrationTestData.jsonFactory.textNode(value)) + def editClientData(name: String, value: JsonNode): RegistrationTestData = + editClientData { clientData: ObjectNode => + clientData.set[ObjectNode](name, value) + } + def editClientData(name: String, value: String): RegistrationTestData = + editClientData(name, RegistrationTestData.jsonFactory.textNode(value)) def responseChallenge: ByteArray = clientData.getChallenge def editClientData(name: String, value: ByteArray): RegistrationTestData = editClientData( name, - RegistrationTestData.jsonFactory.textNode(value.getBase64Url) + RegistrationTestData.jsonFactory.textNode(value.getBase64Url), ) - def editAttestationObject(name: String, value: JsonNode): RegistrationTestData = copy( - attestationObject = new ByteArray(JacksonCodecs.cbor.writeValueAsBytes( - JacksonCodecs.cbor.readTree(attestationObject.getBytes).asInstanceOf[ObjectNode] - .set(name, value) - )) - ) - def updateAttestationObject(name: String, updater: JsonNode => JsonNode): RegistrationTestData = { + def editAttestationObject( + name: String, + value: JsonNode, + ): RegistrationTestData = + copy( + attestationObject = new ByteArray( + JacksonCodecs.cbor.writeValueAsBytes( + JacksonCodecs.cbor + .readTree(attestationObject.getBytes) + .asInstanceOf[ObjectNode] + .set(name, value) + ) + ) + ) + def updateAttestationObject( + name: String, + updater: JsonNode => JsonNode, + ): RegistrationTestData = { val attObj = JacksonCodecs.cbor.readTree(attestationObject.getBytes) copy( - attestationObject = new ByteArray(JacksonCodecs.cbor.writeValueAsBytes( - attObj.asInstanceOf[ObjectNode] - .set(name, updater(attObj.get(name)) - )) + attestationObject = new ByteArray( + JacksonCodecs.cbor.writeValueAsBytes( + attObj + .asInstanceOf[ObjectNode] + .set(name, updater(attObj.get(name))) + ) ) ) } def setAttestationStatementFormat(value: String): RegistrationTestData = - editAttestationObject("fmt", RegistrationTestData.jsonFactory.textNode(value)) + editAttestationObject( + "fmt", + RegistrationTestData.jsonFactory.textNode(value), + ) - def editAuthenticatorData(updater: ByteArray => ByteArray): RegistrationTestData = { - val attObj: ObjectNode = JacksonCodecs.cbor.readTree(attestationObject.getBytes).asInstanceOf[ObjectNode] + def editAuthenticatorData( + updater: ByteArray => ByteArray + ): RegistrationTestData = { + val attObj: ObjectNode = JacksonCodecs.cbor + .readTree(attestationObject.getBytes) + .asInstanceOf[ObjectNode] val authData: ByteArray = new ByteArray(attObj.get("authData").binaryValue) - editAttestationObject("authData", RegistrationTestData.jsonFactory.binaryNode(updater(authData).getBytes)) + editAttestationObject( + "authData", + RegistrationTestData.jsonFactory.binaryNode(updater(authData).getBytes), + ) } - def request: PublicKeyCredentialCreationOptions = overrideRequest getOrElse PublicKeyCredentialCreationOptions.builder() + def request: PublicKeyCredentialCreationOptions = + overrideRequest getOrElse PublicKeyCredentialCreationOptions + .builder() .rp(rpId) .user(userId) .challenge(clientData.getChallenge) - .pubKeyCredParams(List(PublicKeyCredentialParameters.ES256, PublicKeyCredentialParameters.EdDSA, PublicKeyCredentialParameters.RS256).asJava) + .pubKeyCredParams( + List( + PublicKeyCredentialParameters.ES256, + PublicKeyCredentialParameters.EdDSA, + PublicKeyCredentialParameters.RS256, + ).asJava + ) .extensions(requestedExtensions) .authenticatorSelection(authenticatorSelection.asJava) .build() - def response: PublicKeyCredential[AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs] = PublicKeyCredential.builder() - .id(new AttestationObject(attestationObject).getAuthenticatorData.getAttestedCredentialData.get.getCredentialId) - .response( - AuthenticatorAttestationResponse.builder() - .attestationObject(attestationObject) - .clientDataJSON(clientDataJsonBytes) - .build() - ) - .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) - } + def response: PublicKeyCredential[ + AuthenticatorAttestationResponse, + ClientRegistrationExtensionOutputs, + ] = + PublicKeyCredential + .builder() + .id( + new AttestationObject( + attestationObject + ).getAuthenticatorData.getAttestedCredentialData.get.getCredentialId + ) + .response( + AuthenticatorAttestationResponse + .builder() + .attestationObject(attestationObject) + .clientDataJSON(clientDataJsonBytes) + .build() + ) + .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], + request: AssertionRequest, + response: PublicKeyCredential[ + AuthenticatorAssertionResponse, + ClientAssertionExtensionOutputs, + ], ) { def regenerate(testData: RegistrationTestData): AssertionTestData = { copy( 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 21d14ee19..521375b87 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 @@ -24,17 +24,6 @@ package com.yubico.webauthn -import java.io.IOException -import java.nio.charset.Charset -import java.security.KeyPair -import java.security.MessageDigest -import java.security.interfaces.ECPublicKey -import java.util.Optional -import scala.jdk.CollectionConverters._ -import scala.util.Failure -import scala.util.Success -import scala.util.Try - import com.fasterxml.jackson.core.`type`.TypeReference import com.fasterxml.jackson.databind.node.JsonNodeFactory import com.fasterxml.jackson.databind.node.ObjectNode @@ -64,85 +53,142 @@ import org.scalatest.Matchers import org.scalatestplus.junit.JUnitRunner import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks +import java.io.IOException +import java.nio.charset.Charset +import java.security.KeyPair +import java.security.MessageDigest +import java.security.interfaces.ECPublicKey +import java.util.Optional +import scala.jdk.CollectionConverters._ +import scala.util.Failure +import scala.util.Success +import scala.util.Try + @RunWith(classOf[JUnitRunner]) -class RelyingPartyAssertionSpec extends FunSpec with Matchers with ScalaCheckDrivenPropertyChecks with TestWithEachProvider { +class RelyingPartyAssertionSpec + extends FunSpec + with Matchers + with ScalaCheckDrivenPropertyChecks + with TestWithEachProvider { private def jsonFactory: JsonNodeFactory = JsonNodeFactory.instance - private def sha256(bytes: ByteArray): ByteArray = Crypto.hash(bytes) - private def sha256(data: String): ByteArray = sha256(new ByteArray(data.getBytes(Charset.forName("UTF-8")))) + private def sha256(bytes: ByteArray): ByteArray = Crypto.sha256(bytes) + private def sha256(data: String): ByteArray = + sha256(new ByteArray(data.getBytes(Charset.forName("UTF-8")))) private object Defaults { - val rpId = RelyingPartyIdentity.builder().id("localhost").name("Test party").build() + val rpId = + RelyingPartyIdentity.builder().id("localhost").name("Test party").build() // These values were generated using TestAuthenticator.makeAssertionExample() - val authenticatorData: ByteArray = ByteArray.fromHex("49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97630100000539") - val clientDataJson: String = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","type":"webauthn.get","tokenBinding":{"status":"supported"},"clientExtensions":{}}""" - val credentialId: ByteArray = ByteArray.fromBase64Url("AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8") + val authenticatorData: ByteArray = + ByteArray.fromHex("49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97630100000539") + val clientDataJson: String = + """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","type":"webauthn.get","tokenBinding":{"status":"supported"},"clientExtensions":{}}""" + val credentialId: ByteArray = + ByteArray.fromBase64Url("AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8") val credentialKey: KeyPair = TestAuthenticator.importEcKeypair( - privateBytes = ByteArray.fromHex("308193020100301306072a8648ce3d020106082a8648ce3d030107047930770201010420449d91b8a2a508b2927cd5cf4dde32db8e58f237fc155e395d3aad127e115f5aa00a06082a8648ce3d030107a1440342000446c68a2eb75057b1f19b6d06dd3733381063d021391b3637889b0b432c54aaa2b184b35e44d433c70e63a9dd82568dd1ec02c5daba3e66b90a3a881c0c1f4c1a"), - publicBytes = ByteArray.fromHex("3059301306072a8648ce3d020106082a8648ce3d0301070342000446c68a2eb75057b1f19b6d06dd3733381063d021391b3637889b0b432c54aaa2b184b35e44d433c70e63a9dd82568dd1ec02c5daba3e66b90a3a881c0c1f4c1a") + privateBytes = + ByteArray.fromHex("308193020100301306072a8648ce3d020106082a8648ce3d030107047930770201010420449d91b8a2a508b2927cd5cf4dde32db8e58f237fc155e395d3aad127e115f5aa00a06082a8648ce3d030107a1440342000446c68a2eb75057b1f19b6d06dd3733381063d021391b3637889b0b432c54aaa2b184b35e44d433c70e63a9dd82568dd1ec02c5daba3e66b90a3a881c0c1f4c1a"), + publicBytes = + ByteArray.fromHex("3059301306072a8648ce3d020106082a8648ce3d0301070342000446c68a2eb75057b1f19b6d06dd3733381063d021391b3637889b0b432c54aaa2b184b35e44d433c70e63a9dd82568dd1ec02c5daba3e66b90a3a881c0c1f4c1a"), ) - val signature: ByteArray = ByteArray.fromHex("304502201dfef99d44222410686605e23227853f19e9bf89cbab181fdb52b7f40d79f0d5022100c167309d699a03416887af363de0628d7d77f678a01d135da996f0ecbed7e8a5") + val signature: ByteArray = + ByteArray.fromHex("304502201dfef99d44222410686605e23227853f19e9bf89cbab181fdb52b7f40d79f0d5022100c167309d699a03416887af363de0628d7d77f678a01d135da996f0ecbed7e8a5") // These values are not signed over val username: String = "foo-user" - val userHandle: ByteArray = ByteArray.fromHex("6d8972d9603ce4f3fa5d520ce6d024bf") - val user: UserIdentity = UserIdentity.builder().name(username).displayName("Test user").id(userHandle).build() + val userHandle: ByteArray = + ByteArray.fromHex("6d8972d9603ce4f3fa5d520ce6d024bf") + val user: UserIdentity = UserIdentity + .builder() + .name(username) + .displayName("Test user") + .id(userHandle) + .build() // These values are defined by the attestationObject and clientDataJson above - val clientDataJsonBytes: ByteArray = new ByteArray(clientDataJson.getBytes("UTF-8")) + val clientDataJsonBytes: ByteArray = new ByteArray( + clientDataJson.getBytes("UTF-8") + ) val clientData = new CollectedClientData(clientDataJsonBytes) val challenge: ByteArray = clientData.getChallenge val requestedExtensions = AssertionExtensionInputs.builder().build() - val clientExtensionResults: ClientAssertionExtensionOutputs = ClientAssertionExtensionOutputs.builder().build() + val clientExtensionResults: ClientAssertionExtensionOutputs = + ClientAssertionExtensionOutputs.builder().build() } - private def getUserHandleIfDefault(username: String, userHandle: ByteArray = Defaults.userHandle): Optional[ByteArray] = + private def getUserHandleIfDefault( + username: String, + userHandle: ByteArray = Defaults.userHandle, + ): Optional[ByteArray] = if (username == Defaults.username) Some(userHandle).asJava else ??? - private def getUsernameIfDefault(userHandle: ByteArray, username: String = Defaults.username): Optional[String] = + private def getUsernameIfDefault( + userHandle: ByteArray, + username: String = Defaults.username, + ): Optional[String] = if (userHandle == Defaults.userHandle) Some(username).asJava else ??? - private def getPublicKeyBytes(credentialKey: KeyPair): ByteArray = WebAuthnTestCodecs.ecPublicKeyToCose(credentialKey.getPublic.asInstanceOf[ECPublicKey]) + private def getPublicKeyBytes(credentialKey: KeyPair): ByteArray = + WebAuthnTestCodecs.ecPublicKeyToCose( + credentialKey.getPublic.asInstanceOf[ECPublicKey] + ) 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, - clientDataJson: String = Defaults.clientDataJson, - clientExtensionResults: ClientAssertionExtensionOutputs = Defaults.clientExtensionResults, - credentialId: ByteArray = Defaults.credentialId, - credentialKey: KeyPair = Defaults.credentialKey, - credentialRepository: Option[CredentialRepository] = None, - origins: Option[Set[String]] = None, - requestedExtensions: AssertionExtensionInputs = Defaults.requestedExtensions, - rpId: RelyingPartyIdentity = Defaults.rpId, - signature: ByteArray = Defaults.signature, - userHandleForResponse: ByteArray = Defaults.userHandle, - userHandleForUser: ByteArray = Defaults.userHandle, - usernameForRequest: Option[String] = Some(Defaults.username), - usernameForUser: String = Defaults.username, - userVerificationRequirement: UserVerificationRequirement = UserVerificationRequirement.PREFERRED, - validateSignatureCounter: Boolean = true + allowCredentials: Option[java.util.List[PublicKeyCredentialDescriptor]] = + Some( + List( + PublicKeyCredentialDescriptor + .builder() + .id(Defaults.credentialId) + .build() + ).asJava + ), + allowOriginPort: Boolean = false, + allowOriginSubdomain: Boolean = false, + allowUnrequestedExtensions: Boolean = false, + authenticatorData: ByteArray = Defaults.authenticatorData, + callerTokenBindingId: Option[ByteArray] = None, + challenge: ByteArray = Defaults.challenge, + clientDataJson: String = Defaults.clientDataJson, + clientExtensionResults: ClientAssertionExtensionOutputs = + Defaults.clientExtensionResults, + credentialId: ByteArray = Defaults.credentialId, + credentialKey: KeyPair = Defaults.credentialKey, + credentialRepository: Option[CredentialRepository] = None, + origins: Option[Set[String]] = None, + requestedExtensions: AssertionExtensionInputs = + Defaults.requestedExtensions, + rpId: RelyingPartyIdentity = Defaults.rpId, + signature: ByteArray = Defaults.signature, + userHandleForResponse: ByteArray = Defaults.userHandle, + userHandleForUser: ByteArray = Defaults.userHandle, + usernameForRequest: Option[String] = Some(Defaults.username), + usernameForUser: String = Defaults.username, + userVerificationRequirement: UserVerificationRequirement = + UserVerificationRequirement.PREFERRED, + validateSignatureCounter: Boolean = true, ): FinishAssertionSteps = { - val clientDataJsonBytes: ByteArray = if (clientDataJson == null) null else new ByteArray(clientDataJson.getBytes("UTF-8")) + val clientDataJsonBytes: ByteArray = + if (clientDataJson == null) null + else new ByteArray(clientDataJson.getBytes("UTF-8")) val credentialPublicKeyBytes = getPublicKeyBytes(credentialKey) - val request = AssertionRequest.builder() + val request = AssertionRequest + .builder() .publicKeyCredentialRequestOptions( - PublicKeyCredentialRequestOptions.builder() + PublicKeyCredentialRequestOptions + .builder() .challenge(challenge) .rpId(rpId.getId) .allowCredentials(allowCredentials.asJava) @@ -153,12 +199,18 @@ class RelyingPartyAssertionSpec extends FunSpec with Matchers with ScalaCheckDri .username(usernameForRequest.asJava) .build() - val response = PublicKeyCredential.builder() + val response = PublicKeyCredential + .builder() .id(credentialId) .response( - AuthenticatorAssertionResponse.builder() - .authenticatorData(if (authenticatorData == null) null else authenticatorData) - .clientDataJSON(if (clientDataJsonBytes == null) null else clientDataJsonBytes) + AuthenticatorAssertionResponse + .builder() + .authenticatorData( + if (authenticatorData == null) null else authenticatorData + ) + .clientDataJSON( + if (clientDataJsonBytes == null) null else clientDataJsonBytes + ) .signature(if (signature == null) null else signature) .userHandle(userHandleForResponse) .build() @@ -166,32 +218,41 @@ class RelyingPartyAssertionSpec extends FunSpec with Matchers with ScalaCheckDri .clientExtensionResults(clientExtensionResults) .build() - val builder = RelyingParty.builder() + val builder = RelyingParty + .builder() .identity(rpId) .credentialRepository( credentialRepository getOrElse new CredentialRepository { override def lookup(credId: ByteArray, lookupUserHandle: ByteArray) = ( if (credId == credentialId) - Some(RegisteredCredential.builder() - .credentialId(credId) - .userHandle(userHandleForUser) - .publicKeyCose(credentialPublicKeyBytes) - .signatureCount(0) - .build() + Some( + RegisteredCredential + .builder() + .credentialId(credId) + .userHandle(userHandleForUser) + .publicKeyCose(credentialPublicKeyBytes) + .signatureCount(0) + .build() ) else None ).asJava - override def lookupAll(credId: ByteArray) = lookup(credId, null).asScala.toSet.asJava + override def lookupAll(credId: ByteArray) = + lookup(credId, null).asScala.toSet.asJava override def getCredentialIdsForUsername(username: String) = ??? - override def getUserHandleForUsername(username: String): Optional[ByteArray] = getUserHandleIfDefault(username, userHandle = userHandleForUser) - override def getUsernameForUserHandle(userHandle: ByteArray): Optional[String] = getUsernameIfDefault(userHandle, username = usernameForUser) + override def getUserHandleForUsername(username: String) + : Optional[ByteArray] = + getUserHandleIfDefault(username, userHandle = userHandleForUser) + override def getUsernameForUserHandle(userHandle: ByteArray) + : Optional[String] = + getUsernameIfDefault(userHandle, username = usernameForUser) } ) .preferredPubkeyParams(Nil.asJava) .allowOriginPort(allowOriginPort) .allowOriginSubdomain(allowOriginSubdomain) .allowUntrustedAttestation(false) + .allowUnrequestedExtensions(allowUnrequestedExtensions) .validateSignatureCounter(validateSignatureCounter) origins.map(_.asJava).foreach(builder.origins _) @@ -204,32 +265,50 @@ class RelyingPartyAssertionSpec extends FunSpec with Matchers with ScalaCheckDri testWithEachProvider { it => describe("RelyingParty.startAssertion") { - describe("respects the userVerification parameter in StartAssertionOptions.") { + describe( + "respects the userVerification parameter in StartAssertionOptions." + ) { val default = UserVerificationRequirement.PREFERRED it(s"If the parameter is not set, or set to empty, the default of ${default} is used.") { - val rp = RelyingParty.builder() + val rp = RelyingParty + .builder() .identity(Defaults.rpId) .credentialRepository(Helpers.CredentialRepository.empty) .build() - val request1 = rp.startAssertion(StartAssertionOptions.builder().build()) - val request2 = rp.startAssertion(StartAssertionOptions.builder().userVerification(Optional.empty[UserVerificationRequirement]).build()) + val request1 = + rp.startAssertion(StartAssertionOptions.builder().build()) + val request2 = rp.startAssertion( + StartAssertionOptions + .builder() + .userVerification(Optional.empty[UserVerificationRequirement]) + .build() + ) - request1.getPublicKeyCredentialRequestOptions.getUserVerification should equal (default) - request2.getPublicKeyCredentialRequestOptions.getUserVerification should equal (default) + request1.getPublicKeyCredentialRequestOptions.getUserVerification should equal( + default + ) + request2.getPublicKeyCredentialRequestOptions.getUserVerification should equal( + default + ) } it(s"If the parameter is set, that value is used.") { - val rp = RelyingParty.builder() + val rp = RelyingParty + .builder() .identity(Defaults.rpId) .credentialRepository(Helpers.CredentialRepository.empty) .build() forAll { uv: UserVerificationRequirement => - val request = rp.startAssertion(StartAssertionOptions.builder().userVerification(uv).build()) + val request = rp.startAssertion( + StartAssertionOptions.builder().userVerification(uv).build() + ) - request.getPublicKeyCredentialRequestOptions.getUserVerification should equal (uv) + request.getPublicKeyCredentialRequestOptions.getUserVerification should equal( + uv + ) } } } @@ -243,39 +322,54 @@ class RelyingPartyAssertionSpec extends FunSpec with Matchers with ScalaCheckDri describe("1. If the allowCredentials option was given when this authentication ceremony was initiated, verify that credential.id identifies one of the public key credentials that were listed in allowCredentials.") { it("Fails if returned credential ID is a requested one.") { val steps = finishAssertion( - allowCredentials = Some(List(PublicKeyCredentialDescriptor.builder().id(new ByteArray(Array(3, 2, 1, 0))).build()).asJava), - credentialId = new ByteArray(Array(0, 1, 2, 3)) + allowCredentials = Some( + List( + PublicKeyCredentialDescriptor + .builder() + .id(new ByteArray(Array(3, 2, 1, 0))) + .build() + ).asJava + ), + credentialId = new ByteArray(Array(0, 1, 2, 3)), ) val step: FinishAssertionSteps#Step1 = steps.begin.next - toStepWithUtilities(step).validations shouldBe a [Failure[_]] - step.validations.failed.get shouldBe an [IllegalArgumentException] - step.tryNext shouldBe a [Failure[_]] + toStepWithUtilities(step).validations shouldBe a[Failure[_]] + step.validations.failed.get shouldBe an[IllegalArgumentException] + step.tryNext shouldBe a[Failure[_]] } it("Succeeds if returned credential ID is a requested one.") { val steps = finishAssertion( - allowCredentials = Some(List( - PublicKeyCredentialDescriptor.builder().id(new ByteArray(Array(0, 1, 2, 3))).build(), - PublicKeyCredentialDescriptor.builder().id(new ByteArray(Array(4, 5, 6, 7))).build() - ).asJava), - credentialId = new ByteArray(Array(4, 5, 6, 7)) + allowCredentials = Some( + List( + PublicKeyCredentialDescriptor + .builder() + .id(new ByteArray(Array(0, 1, 2, 3))) + .build(), + PublicKeyCredentialDescriptor + .builder() + .id(new ByteArray(Array(4, 5, 6, 7))) + .build(), + ).asJava + ), + credentialId = new ByteArray(Array(4, 5, 6, 7)), ) val step: FinishAssertionSteps#Step1 = steps.begin.next - step.validations shouldBe a [Success[_]] - step.tryNext shouldBe a [Success[_]] + step.validations shouldBe a[Success[_]] + step.tryNext shouldBe a[Success[_]] } it("Succeeds if returned no credential IDs were requested.") { val steps = finishAssertion( allowCredentials = None, - credentialId = new ByteArray(Array(0, 1, 2, 3)) + credentialId = new ByteArray(Array(0, 1, 2, 3)), ) val step: FinishAssertionSteps#Step1 = steps.begin.next - step.validations shouldBe a [Success[_]] - step.tryNext shouldBe a [Success[_]] + step.validations shouldBe a[Success[_]] + step.tryNext shouldBe a[Success[_]] } } @@ -290,33 +384,49 @@ class RelyingPartyAssertionSpec extends FunSpec with Matchers with ScalaCheckDri } val credentialRepository = Some(new CredentialRepository { - override def lookup(id: ByteArray, uh: ByteArray) = Some( - RegisteredCredential.builder() - .credentialId(new ByteArray(Array(0, 1, 2, 3))) - .userHandle(owner.userHandle) - .publicKeyCose(getPublicKeyBytes(Defaults.credentialKey)) - .signatureCount(0) - .build() - ).asJava + override def lookup(id: ByteArray, uh: ByteArray) = + Some( + RegisteredCredential + .builder() + .credentialId(new ByteArray(Array(0, 1, 2, 3))) + .userHandle(owner.userHandle) + .publicKeyCose(getPublicKeyBytes(Defaults.credentialKey)) + .signatureCount(0) + .build() + ).asJava override def lookupAll(id: ByteArray) = ??? override def getCredentialIdsForUsername(username: String) = ??? - override def getUserHandleForUsername(username: String): Optional[ByteArray] = Some(if (username == owner.username) owner.userHandle else nonOwner.userHandle).asJava - override def getUsernameForUserHandle(userHandle: ByteArray): Optional[String] = Some(if (userHandle == owner.userHandle) owner.username else nonOwner.username).asJava + override def getUserHandleForUsername( + username: String + ): Optional[ByteArray] = + Some( + if (username == owner.username) owner.userHandle + else nonOwner.userHandle + ).asJava + override def getUsernameForUserHandle( + userHandle: ByteArray + ): Optional[String] = + Some( + if (userHandle == owner.userHandle) owner.username + else nonOwner.username + ).asJava }) describe("If the user was identified before the authentication ceremony was initiated, verify that the identified user is the owner of credentialSource. If credential.response.userHandle is present, verify that this value identifies the same user as was previously identified.") { - it("Fails if credential ID is not owned by the given user handle.") { + it( + "Fails if credential ID is not owned by the given user handle." + ) { val steps = finishAssertion( credentialRepository = credentialRepository, usernameForRequest = Some(owner.username), userHandleForUser = owner.userHandle, - userHandleForResponse = nonOwner.userHandle + userHandleForResponse = nonOwner.userHandle, ) val step: FinishAssertionSteps#Step2 = steps.begin.next.next - step.validations shouldBe a [Failure[_]] - step.validations.failed.get shouldBe an [IllegalArgumentException] - step.tryNext shouldBe a [Failure[_]] + step.validations shouldBe a[Failure[_]] + step.validations.failed.get shouldBe an[IllegalArgumentException] + step.tryNext shouldBe a[Failure[_]] } it("Succeeds if credential ID is owned by the given user handle.") { @@ -324,28 +434,30 @@ class RelyingPartyAssertionSpec extends FunSpec with Matchers with ScalaCheckDri credentialRepository = credentialRepository, usernameForRequest = Some(owner.username), userHandleForUser = owner.userHandle, - userHandleForResponse = owner.userHandle + userHandleForResponse = owner.userHandle, ) val step: FinishAssertionSteps#Step2 = steps.begin.next.next - step.validations shouldBe a [Success[_]] - step.tryNext shouldBe a [Success[_]] + step.validations shouldBe a[Success[_]] + step.tryNext shouldBe a[Success[_]] } } describe("If the user was not identified before the authentication ceremony was initiated, verify that credential.response.userHandle is present, and that the user identified by this value is the owner of credentialSource.") { - it("Fails if credential ID is not owned by the given user handle.") { + it( + "Fails if credential ID is not owned by the given user handle." + ) { val steps = finishAssertion( credentialRepository = credentialRepository, usernameForRequest = None, userHandleForUser = owner.userHandle, - userHandleForResponse = nonOwner.userHandle + userHandleForResponse = nonOwner.userHandle, ) val step: FinishAssertionSteps#Step2 = steps.begin.next.next - step.validations shouldBe a [Failure[_]] - step.validations.failed.get shouldBe an [IllegalArgumentException] - step.tryNext shouldBe a [Failure[_]] + step.validations shouldBe a[Failure[_]] + step.validations.failed.get shouldBe an[IllegalArgumentException] + step.tryNext shouldBe a[Failure[_]] } it("Succeeds if credential ID is owned by the given user handle.") { @@ -353,12 +465,12 @@ class RelyingPartyAssertionSpec extends FunSpec with Matchers with ScalaCheckDri credentialRepository = credentialRepository, usernameForRequest = None, userHandleForUser = owner.userHandle, - userHandleForResponse = owner.userHandle + userHandleForResponse = owner.userHandle, ) val step: FinishAssertionSteps#Step2 = steps.begin.next.next - step.validations shouldBe a [Success[_]] - step.tryNext shouldBe a [Success[_]] + step.validations shouldBe a[Success[_]] + step.tryNext shouldBe a[Success[_]] } } } @@ -368,67 +480,86 @@ class RelyingPartyAssertionSpec extends FunSpec with Matchers with ScalaCheckDri val steps = finishAssertion( credentialRepository = Some(Helpers.CredentialRepository.empty) ) - val step: steps.Step3 = new steps.Step3(Defaults.username, Defaults.userHandle, Nil.asJava) + val step: steps.Step3 = new steps.Step3( + Defaults.username, + Defaults.userHandle, + Nil.asJava, + ) - step.validations shouldBe a [Failure[_]] - step.validations.failed.get shouldBe an [IllegalArgumentException] - step.tryNext shouldBe a [Failure[_]] + step.validations shouldBe a[Failure[_]] + step.validations.failed.get shouldBe an[IllegalArgumentException] + step.tryNext shouldBe a[Failure[_]] } it("Succeeds if the credential ID is known.") { val steps = finishAssertion( - credentialRepository = Some(Helpers.CredentialRepository.withUser( - Defaults.user, - RegisteredCredential.builder() - .credentialId(Defaults.credentialId) - .userHandle(Defaults.userHandle) - .publicKeyCose(getPublicKeyBytes(Defaults.credentialKey)) - .signatureCount(0) - .build() - )) + credentialRepository = Some( + Helpers.CredentialRepository.withUser( + Defaults.user, + RegisteredCredential + .builder() + .credentialId(Defaults.credentialId) + .userHandle(Defaults.userHandle) + .publicKeyCose(getPublicKeyBytes(Defaults.credentialKey)) + .signatureCount(0) + .build(), + ) + ) ) val step: FinishAssertionSteps#Step3 = steps.begin.next.next.next - step.validations shouldBe a [Success[_]] - step.credential.getPublicKeyCose should equal (getPublicKeyBytes(Defaults.credentialKey)) - step.tryNext shouldBe a [Success[_]] + step.validations shouldBe a[Success[_]] + step.credential.getPublicKeyCose should equal( + getPublicKeyBytes(Defaults.credentialKey) + ) + step.tryNext shouldBe a[Success[_]] } } describe("4. Let cData, authData and sig denote the value of credential’s response's clientDataJSON, authenticatorData, and signature respectively.") { it("Succeeds if all three are present.") { val steps = finishAssertion() - val step: FinishAssertionSteps#Step4 = steps.begin.next.next.next.next + val step: FinishAssertionSteps#Step4 = + steps.begin.next.next.next.next - step.validations shouldBe a [Success[_]] + step.validations shouldBe a[Success[_]] step.clientData should not be null step.authenticatorData should not be null step.signature should not be null - step.tryNext shouldBe a [Success[_]] + step.tryNext shouldBe a[Success[_]] } it("Fails if clientDataJSON is missing.") { - a [NullPointerException] should be thrownBy finishAssertion(clientDataJson = null) + a[NullPointerException] should be thrownBy finishAssertion( + clientDataJson = null + ) } it("Fails if authenticatorData is missing.") { - a [NullPointerException] should be thrownBy finishAssertion(authenticatorData = null) + a[NullPointerException] should be thrownBy finishAssertion( + authenticatorData = null + ) } it("Fails if signature is missing.") { - a [NullPointerException] should be thrownBy finishAssertion(signature = null) + a[NullPointerException] should be thrownBy finishAssertion( + signature = null + ) } } describe("5. Let JSONtext be the result of running UTF-8 decode on the value of cData.") { - it("Nothing to test.") { - } + it("Nothing to test.") {} } describe("6. Let C, the client data claimed as used for the signature, be the result of running an implementation-specific JSON parser on JSONtext.") { it("Fails if cData is not valid JSON.") { - an [IOException] should be thrownBy new CollectedClientData(new ByteArray("{".getBytes(Charset.forName("UTF-8")))) - an [IOException] should be thrownBy finishAssertion(clientDataJson = "{") + an[IOException] should be thrownBy new CollectedClientData( + new ByteArray("{".getBytes(Charset.forName("UTF-8"))) + ) + an[IOException] should be thrownBy finishAssertion(clientDataJson = + "{" + ) } it("Succeeds if cData is valid JSON.") { @@ -439,43 +570,50 @@ class RelyingPartyAssertionSpec extends FunSpec with Matchers with ScalaCheckDri "type": "" }""" ) - val step: FinishAssertionSteps#Step6 = steps.begin.next.next.next.next.next.next + val step: FinishAssertionSteps#Step6 = + steps.begin.next.next.next.next.next.next - step.validations shouldBe a [Success[_]] + step.validations shouldBe a[Success[_]] step.clientData should not be null - step.tryNext shouldBe a [Success[_]] + step.tryNext shouldBe a[Success[_]] } } - describe("7. Verify that the value of C.type is the string webauthn.get.") { + describe( + "7. Verify that the value of C.type is the string webauthn.get." + ) { it("The default test case succeeds.") { val steps = finishAssertion() - val step: FinishAssertionSteps#Step7 = steps.begin.next.next.next.next.next.next.next + val step: FinishAssertionSteps#Step7 = + steps.begin.next.next.next.next.next.next.next - step.validations shouldBe a [Success[_]] + step.validations shouldBe a[Success[_]] } def assertFails(typeString: String): Unit = { val steps = finishAssertion( clientDataJson = JacksonCodecs.json.writeValueAsString( - JacksonCodecs.json.readTree(Defaults.clientDataJson).asInstanceOf[ObjectNode] + JacksonCodecs.json + .readTree(Defaults.clientDataJson) + .asInstanceOf[ObjectNode] .set("type", jsonFactory.textNode(typeString)) ) ) - val step: FinishAssertionSteps#Step7 = steps.begin.next.next.next.next.next.next.next + val step: FinishAssertionSteps#Step7 = + steps.begin.next.next.next.next.next.next.next - step.validations shouldBe a [Failure[_]] - step.validations.failed.get shouldBe an [IllegalArgumentException] + step.validations shouldBe a[Failure[_]] + step.validations.failed.get shouldBe an[IllegalArgumentException] } it("""Any value other than "webauthn.get" fails.""") { forAll { (typeString: String) => - whenever (typeString != "webauthn.get") { + whenever(typeString != "webauthn.get") { assertFails(typeString) } } forAll(Gen.alphaNumStr) { (typeString: String) => - whenever (typeString != "webauthn.get") { + whenever(typeString != "webauthn.get") { assertFails(typeString) } } @@ -483,52 +621,62 @@ class RelyingPartyAssertionSpec extends FunSpec with Matchers with ScalaCheckDri } it("8. Verify that the value of C.challenge matches the challenge that was sent to the authenticator in the PublicKeyCredentialRequestOptions passed to the get() call.") { - val steps = finishAssertion(challenge = new ByteArray(Array.fill(16)(0))) - val step: FinishAssertionSteps#Step8 = steps.begin.next.next.next.next.next.next.next.next - - step.validations shouldBe a [Failure[_]] - step.validations.failed.get shouldBe an [IllegalArgumentException] - step.tryNext shouldBe a [Failure[_]] + val steps = + finishAssertion(challenge = new ByteArray(Array.fill(16)(0))) + val step: FinishAssertionSteps#Step8 = + steps.begin.next.next.next.next.next.next.next.next + + step.validations shouldBe a[Failure[_]] + step.validations.failed.get shouldBe an[IllegalArgumentException] + step.tryNext shouldBe a[Failure[_]] } describe("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 + origin: String, + origins: Option[Set[String]] = None, + allowOriginPort: Boolean = false, + allowOriginSubdomain: Boolean = false, ): Unit = { - val clientDataJson: String = Defaults.clientDataJson.replace("\"https://localhost\"", "\"" + origin + "\"") + val clientDataJson: String = Defaults.clientDataJson.replace( + "\"https://localhost\"", + "\"" + origin + "\"", + ) val steps = finishAssertion( clientDataJson = clientDataJson, origins = origins, allowOriginPort = allowOriginPort, - allowOriginSubdomain = allowOriginSubdomain + allowOriginSubdomain = allowOriginSubdomain, ) - val step: FinishAssertionSteps#Step9 = steps.begin.next.next.next.next.next.next.next.next.next + val step: FinishAssertionSteps#Step9 = + steps.begin.next.next.next.next.next.next.next.next.next - step.validations shouldBe a [Success[_]] - step.tryNext shouldBe a [Success[_]] + 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 + origin: String, + origins: Option[Set[String]] = None, + allowOriginPort: Boolean = false, + allowOriginSubdomain: Boolean = false, ): Unit = { - val clientDataJson: String = Defaults.clientDataJson.replace("\"https://localhost\"", "\"" + origin + "\"") + val clientDataJson: String = Defaults.clientDataJson.replace( + "\"https://localhost\"", + "\"" + origin + "\"", + ) val steps = finishAssertion( clientDataJson = clientDataJson, origins = origins, allowOriginPort = allowOriginPort, - allowOriginSubdomain = allowOriginSubdomain + allowOriginSubdomain = allowOriginSubdomain, ) - val step: FinishAssertionSteps#Step9 = steps.begin.next.next.next.next.next.next.next.next.next + 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[Failure[_]] + step.validations.failed.get shouldBe an[IllegalArgumentException] + step.tryNext shouldBe a[Failure[_]] } it("Fails if origin is different.") { @@ -575,17 +723,21 @@ class RelyingPartyAssertionSpec extends FunSpec with Matchers with ScalaCheckDri } it("allowed if RP opts in to both.") { - checkAccepted(origin = origin, allowOriginPort = true, allowOriginSubdomain = true) + 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 + origins: Set[String], + acceptOrigins: Iterable[String], + rejectOrigins: Iterable[String], + allowOriginPort: Boolean = false, + allowOriginSubdomain: Boolean = false, ): Unit = { for { origin <- acceptOrigins } { it(s"${origin} is accepted.") { @@ -593,7 +745,7 @@ class RelyingPartyAssertionSpec extends FunSpec with Matchers with ScalaCheckDri origin = origin, origins = Some(origins), allowOriginPort = allowOriginPort, - allowOriginSubdomain = allowOriginSubdomain + allowOriginSubdomain = allowOriginSubdomain, ) } } @@ -604,14 +756,18 @@ class RelyingPartyAssertionSpec extends FunSpec with Matchers with ScalaCheckDri origin = origin, origins = Some(origins), allowOriginPort = allowOriginPort, - allowOriginSubdomain = allowOriginSubdomain + allowOriginSubdomain = allowOriginSubdomain, ) } } } describe("For allowOriginPort:") { - val origins = Set("https://example.org", "https://accounts.example.org", "https://acme.com:8443") + val origins = Set( + "https://example.org", + "https://accounts.example.org", + "https://acme.com:8443", + ) describe("false,") { check( @@ -619,15 +775,15 @@ class RelyingPartyAssertionSpec extends FunSpec with Matchers with ScalaCheckDri acceptOrigins = List( "https://example.org", "https://accounts.example.org", - "https://acme.com:8443" + "https://acme.com:8443", ), rejectOrigins = List( "https://example.org:8443", "https://shop.example.org", "https://acme.com", - "https://acme.com:9000" + "https://acme.com:9000", ), - allowOriginPort = false + allowOriginPort = false, ) } @@ -640,12 +796,12 @@ class RelyingPartyAssertionSpec extends FunSpec with Matchers with ScalaCheckDri "https://accounts.example.org", "https://acme.com", "https://acme.com:8443", - "https://acme.com:9000" + "https://acme.com:9000", ), rejectOrigins = List( "https://shop.example.org" ), - allowOriginPort = true + allowOriginPort = true, ) } } @@ -658,15 +814,15 @@ class RelyingPartyAssertionSpec extends FunSpec with Matchers with ScalaCheckDri origins = origins, acceptOrigins = List( "https://example.org", - "https://acme.com:8443" + "https://acme.com:8443", ), rejectOrigins = List( "https://example.org:8443", "https://accounts.example.org", "https://acme.com", - "https://shop.acme.com:8443" + "https://shop.acme.com:8443", ), - allowOriginSubdomain = false + allowOriginSubdomain = false, ) } @@ -677,13 +833,13 @@ class RelyingPartyAssertionSpec extends FunSpec with Matchers with ScalaCheckDri "https://example.org", "https://accounts.example.org", "https://acme.com:8443", - "https://shop.acme.com:8443" + "https://shop.acme.com:8443", ), rejectOrigins = List( "https://example.org:8443", - "https://acme.com" + "https://acme.com", ), - allowOriginSubdomain = true + allowOriginSubdomain = true, ) } } @@ -693,130 +849,157 @@ class RelyingPartyAssertionSpec extends FunSpec with Matchers with ScalaCheckDri 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.") { it("Verification succeeds if neither side uses token binding ID.") { val steps = finishAssertion() - val step: FinishAssertionSteps#Step10 = steps.begin.next.next.next.next.next.next.next.next.next.next + val step: FinishAssertionSteps#Step10 = + steps.begin.next.next.next.next.next.next.next.next.next.next - step.validations shouldBe a [Success[_]] - step.tryNext shouldBe a [Success[_]] + step.validations shouldBe a[Success[_]] + step.tryNext shouldBe a[Success[_]] } it("Verification succeeds if client data specifies token binding is unsupported, and RP does not use it.") { - val clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","hashAlgorithm":"SHA-256","type":"webauthn.get"}""" + val 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 + val step: FinishAssertionSteps#Step10 = + steps.begin.next.next.next.next.next.next.next.next.next.next - step.validations shouldBe a [Success[_]] - step.tryNext shouldBe a [Success[_]] + step.validations shouldBe a[Success[_]] + step.tryNext shouldBe a[Success[_]] } it("Verification succeeds if client data specifies token binding is supported, and RP does not use it.") { - val clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","hashAlgorithm":"SHA-256","tokenBinding":{"status":"supported"},"type":"webauthn.get"}""" + val 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 + val step: FinishAssertionSteps#Step10 = + steps.begin.next.next.next.next.next.next.next.next.next.next - step.validations shouldBe a [Success[_]] - step.tryNext shouldBe a [Success[_]] + step.validations shouldBe a[Success[_]] + step.tryNext shouldBe a[Success[_]] } it("Verification fails if client data does not specify token binding status and RP specifies token binding ID.") { - val clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","hashAlgorithm":"SHA-256","type":"webauthn.get"}""" + val clientDataJson = + """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","hashAlgorithm":"SHA-256","type":"webauthn.get"}""" val steps = finishAssertion( - callerTokenBindingId = Some(ByteArray.fromBase64Url("YELLOWSUBMARINE")), - clientDataJson = clientDataJson + callerTokenBindingId = + Some(ByteArray.fromBase64Url("YELLOWSUBMARINE")), + clientDataJson = clientDataJson, ) - val step: FinishAssertionSteps#Step10 = steps.begin.next.next.next.next.next.next.next.next.next.next + val step: FinishAssertionSteps#Step10 = + steps.begin.next.next.next.next.next.next.next.next.next.next - step.validations shouldBe a [Failure[_]] - step.validations.failed.get shouldBe an [IllegalArgumentException] - step.tryNext shouldBe a [Failure[_]] + step.validations shouldBe a[Failure[_]] + step.validations.failed.get shouldBe an[IllegalArgumentException] + step.tryNext shouldBe a[Failure[_]] } it("Verification succeeds if client data does not specify token binding status and RP does not specify token binding ID.") { - val clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","hashAlgorithm":"SHA-256","type":"webauthn.get"}""" + val clientDataJson = + """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","hashAlgorithm":"SHA-256","type":"webauthn.get"}""" val steps = finishAssertion( callerTokenBindingId = None, - clientDataJson = clientDataJson + clientDataJson = clientDataJson, ) - val step: FinishAssertionSteps#Step10 = steps.begin.next.next.next.next.next.next.next.next.next.next + val step: FinishAssertionSteps#Step10 = + steps.begin.next.next.next.next.next.next.next.next.next.next - step.validations shouldBe a [Success[_]] - step.tryNext shouldBe a [Success[_]] + step.validations shouldBe a[Success[_]] + step.tryNext shouldBe a[Success[_]] } it("Verification fails if client data specifies token binding ID but RP does not.") { - val clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","hashAlgorithm":"SHA-256","tokenBinding":{"status":"present","id":"YELLOWSUBMARINE"},"type":"webauthn.get"}""" + val clientDataJson = + """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","hashAlgorithm":"SHA-256","tokenBinding":{"status":"present","id":"YELLOWSUBMARINE"},"type":"webauthn.get"}""" val steps = finishAssertion( callerTokenBindingId = None, - clientDataJson = clientDataJson + clientDataJson = clientDataJson, ) - val step: FinishAssertionSteps#Step10 = steps.begin.next.next.next.next.next.next.next.next.next.next + val step: FinishAssertionSteps#Step10 = + steps.begin.next.next.next.next.next.next.next.next.next.next - step.validations shouldBe a [Failure[_]] - step.validations.failed.get shouldBe an [IllegalArgumentException] - step.tryNext shouldBe a [Failure[_]] + step.validations shouldBe a[Failure[_]] + step.validations.failed.get shouldBe an[IllegalArgumentException] + step.tryNext shouldBe a[Failure[_]] } describe("If Token Binding was used on that TLS connection, also verify that C.tokenBinding.id matches the base64url encoding of the Token Binding ID for the connection.") { it("Verification succeeds if both sides specify the same token binding ID.") { - val clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://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 + callerTokenBindingId = + Some(ByteArray.fromBase64Url("YELLOWSUBMARINE")), + clientDataJson = clientDataJson, ) - val step: FinishAssertionSteps#Step10 = steps.begin.next.next.next.next.next.next.next.next.next.next + val step: FinishAssertionSteps#Step10 = + steps.begin.next.next.next.next.next.next.next.next.next.next - step.validations shouldBe a [Success[_]] - step.tryNext shouldBe a [Success[_]] + step.validations shouldBe a[Success[_]] + step.tryNext shouldBe a[Success[_]] } it("Verification fails if ID is missing from tokenBinding in client data.") { - val clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","hashAlgorithm":"SHA-256","tokenBinding":{"status":"present"},"type":"webauthn.get"}""" + val 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 + callerTokenBindingId = + Some(ByteArray.fromBase64Url("YELLOWSUBMARINE")), + clientDataJson = clientDataJson, ) - val step: FinishAssertionSteps#Step10 = steps.begin.next.next.next.next.next.next.next.next.next.next + val step: FinishAssertionSteps#Step10 = + steps.begin.next.next.next.next.next.next.next.next.next.next - step.validations shouldBe a [Failure[_]] - step.validations.failed.get shouldBe an [IllegalArgumentException] - step.tryNext shouldBe a [Failure[_]] + step.validations shouldBe a[Failure[_]] + step.validations.failed.get shouldBe an[IllegalArgumentException] + step.tryNext shouldBe a[Failure[_]] } it("Verification fails if RP specifies token binding ID but client does not support it.") { - val clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","hashAlgorithm":"SHA-256","type":"webauthn.get"}""" + val clientDataJson = + """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","hashAlgorithm":"SHA-256","type":"webauthn.get"}""" val steps = finishAssertion( - callerTokenBindingId = Some(ByteArray.fromBase64Url("YELLOWSUBMARINE")), - clientDataJson = clientDataJson + callerTokenBindingId = + Some(ByteArray.fromBase64Url("YELLOWSUBMARINE")), + clientDataJson = clientDataJson, ) - val step: FinishAssertionSteps#Step10 = steps.begin.next.next.next.next.next.next.next.next.next.next + val step: FinishAssertionSteps#Step10 = + steps.begin.next.next.next.next.next.next.next.next.next.next - step.validations shouldBe a [Failure[_]] - step.validations.failed.get shouldBe an [IllegalArgumentException] - step.tryNext shouldBe a [Failure[_]] + step.validations shouldBe a[Failure[_]] + step.validations.failed.get shouldBe an[IllegalArgumentException] + step.tryNext shouldBe a[Failure[_]] } it("Verification fails if RP specifies token binding ID but client does not use it.") { - val clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","hashAlgorithm":"SHA-256","tokenBinding":{"status":"supported"},"type":"webauthn.get"}""" + val 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 + callerTokenBindingId = + Some(ByteArray.fromBase64Url("YELLOWSUBMARINE")), + clientDataJson = clientDataJson, ) - val step: FinishAssertionSteps#Step10 = steps.begin.next.next.next.next.next.next.next.next.next.next + val step: FinishAssertionSteps#Step10 = + steps.begin.next.next.next.next.next.next.next.next.next.next - step.validations shouldBe a [Failure[_]] - step.validations.failed.get shouldBe an [IllegalArgumentException] - step.tryNext shouldBe a [Failure[_]] + step.validations shouldBe a[Failure[_]] + step.validations.failed.get shouldBe an[IllegalArgumentException] + step.tryNext shouldBe a[Failure[_]] } it("Verification fails if client data and RP specify different token binding IDs.") { - val clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","hashAlgorithm":"SHA-256","tokenBinding":{"status":"present","id":"YELLOWSUBMARINE"},"type":"webauthn.get"}""" + val 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 + callerTokenBindingId = + Some(ByteArray.fromBase64Url("ORANGESUBMARINE")), + clientDataJson = clientDataJson, ) - val step: FinishAssertionSteps#Step10 = steps.begin.next.next.next.next.next.next.next.next.next.next + val step: FinishAssertionSteps#Step10 = + steps.begin.next.next.next.next.next.next.next.next.next.next - step.validations shouldBe a [Failure[_]] - step.validations.failed.get shouldBe an [IllegalArgumentException] - step.tryNext shouldBe a [Failure[_]] + step.validations shouldBe a[Failure[_]] + step.validations.failed.get shouldBe an[IllegalArgumentException] + step.tryNext shouldBe a[Failure[_]] } } } @@ -825,92 +1008,134 @@ class RelyingPartyAssertionSpec extends FunSpec with Matchers with ScalaCheckDri it("Fails if RP ID is different.") { val steps = finishAssertion( rpId = Defaults.rpId.toBuilder.id("root.evil").build(), - origins = Some(Set("https://localhost")) + origins = Some(Set("https://localhost")), ) - val step: FinishAssertionSteps#Step11 = steps.begin.next.next.next.next.next.next.next.next.next.next.next + val step: FinishAssertionSteps#Step11 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next - step.validations shouldBe a [Failure[_]] - step.validations.failed.get shouldBe an [IllegalArgumentException] - step.tryNext shouldBe a [Failure[_]] + step.validations shouldBe a[Failure[_]] + step.validations.failed.get shouldBe an[IllegalArgumentException] + step.tryNext shouldBe a[Failure[_]] } it("Succeeds if RP ID is the same.") { val steps = finishAssertion() - val step: FinishAssertionSteps#Step11 = steps.begin.next.next.next.next.next.next.next.next.next.next.next + val step: FinishAssertionSteps#Step11 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next - step.validations shouldBe a [Success[_]] - step.tryNext shouldBe a [Success[_]] + step.validations shouldBe a[Success[_]] + step.tryNext shouldBe a[Success[_]] } describe("When using the appid extension, it") { val appid = new AppId("https://test.example.org/foo") - val extensions = AssertionExtensionInputs.builder() + val extensions = AssertionExtensionInputs + .builder() .appid(Some(appid).asJava) .build() it("fails if RP ID is different.") { val steps = finishAssertion( requestedExtensions = extensions, - authenticatorData = new ByteArray(Array.fill[Byte](32)(0) ++ Defaults.authenticatorData.getBytes.drop(32)) + authenticatorData = new ByteArray( + Array.fill[Byte](32)(0) ++ Defaults.authenticatorData.getBytes + .drop(32) + ), ) - val step: FinishAssertionSteps#Step11 = steps.begin.next.next.next.next.next.next.next.next.next.next.next + val step: FinishAssertionSteps#Step11 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next - step.validations shouldBe a [Failure[_]] - step.validations.failed.get shouldBe an [IllegalArgumentException] - step.tryNext shouldBe a [Failure[_]] + step.validations shouldBe a[Failure[_]] + step.validations.failed.get shouldBe an[IllegalArgumentException] + step.tryNext shouldBe a[Failure[_]] } it("succeeds if RP ID is the SHA-256 hash of the standard RP ID.") { val steps = finishAssertion(requestedExtensions = extensions) - val step: FinishAssertionSteps#Step11 = steps.begin.next.next.next.next.next.next.next.next.next.next.next + val step: FinishAssertionSteps#Step11 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next - step.validations shouldBe a [Success[_]] - step.tryNext shouldBe a [Success[_]] + step.validations shouldBe a[Success[_]] + step.tryNext shouldBe a[Success[_]] } it("succeeds if RP ID is the SHA-256 hash of the appid.") { val steps = finishAssertion( requestedExtensions = extensions, - authenticatorData = new ByteArray(sha256(appid.getId).getBytes ++ Defaults.authenticatorData.getBytes.drop(32)) + authenticatorData = new ByteArray( + sha256( + appid.getId + ).getBytes ++ Defaults.authenticatorData.getBytes.drop(32) + ), ) - val step: FinishAssertionSteps#Step11 = steps.begin.next.next.next.next.next.next.next.next.next.next.next + val step: FinishAssertionSteps#Step11 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next - step.validations shouldBe a [Success[_]] - step.tryNext shouldBe a [Success[_]] + step.validations shouldBe a[Success[_]] + step.tryNext shouldBe a[Success[_]] } } } { - def checks[Next <: FinishAssertionSteps.Step[_], Step <: FinishAssertionSteps.Step[Next]](stepsToStep: FinishAssertionSteps => Step) = { - def check[Ret] - (stepsToStep: FinishAssertionSteps => Step) - (chk: Step => Ret) - (uvr: UserVerificationRequirement, authData: ByteArray) - : Ret = { + def checks[Next <: FinishAssertionSteps.Step[ + _ + ], Step <: FinishAssertionSteps.Step[Next]]( + stepsToStep: FinishAssertionSteps => Step + ) = { + def check[Ret](stepsToStep: FinishAssertionSteps => Step)( + chk: Step => Ret + )(uvr: UserVerificationRequirement, authData: ByteArray): Ret = { val steps = finishAssertion( userVerificationRequirement = uvr, - authenticatorData = authData + authenticatorData = authData, ) chk(stepsToStep(steps)) } - def checkFailsWith(stepsToStep: FinishAssertionSteps => Step): (UserVerificationRequirement, ByteArray) => Unit = check(stepsToStep) { step => - step.validations shouldBe a [Failure[_]] - step.validations.failed.get shouldBe an [IllegalArgumentException] - step.tryNext shouldBe a [Failure[_]] - } - def checkSucceedsWith(stepsToStep: FinishAssertionSteps => Step): (UserVerificationRequirement, ByteArray) => Unit = check(stepsToStep) { step => - step.validations shouldBe a [Success[_]] - step.tryNext shouldBe a [Success[_]] - } + def checkFailsWith( + stepsToStep: FinishAssertionSteps => Step + ): (UserVerificationRequirement, ByteArray) => Unit = + check(stepsToStep) { step => + step.validations shouldBe a[Failure[_]] + step.validations.failed.get shouldBe an[ + IllegalArgumentException + ] + step.tryNext shouldBe a[Failure[_]] + } + def checkSucceedsWith( + stepsToStep: FinishAssertionSteps => Step + ): (UserVerificationRequirement, ByteArray) => Unit = + check(stepsToStep) { step => + step.validations shouldBe a[Success[_]] + step.tryNext shouldBe a[Success[_]] + } (checkFailsWith(stepsToStep), checkSucceedsWith(stepsToStep)) } describe("12. Verify that the User Present bit of the flags in authData is set.") { - val flagOn: ByteArray = new ByteArray(Defaults.authenticatorData.getBytes.toVector.updated(32, (Defaults.authenticatorData.getBytes.toVector(32) | 0x04 | 0x01).toByte).toArray) - val flagOff: ByteArray = new ByteArray(Defaults.authenticatorData.getBytes.toVector.updated(32, ((Defaults.authenticatorData.getBytes.toVector(32) | 0x04) & 0xfe).toByte).toArray) - val (checkFails, checkSucceeds) = checks[FinishAssertionSteps#Step13, FinishAssertionSteps#Step12](_.begin.next.next.next.next.next.next.next.next.next.next.next.next) + val flagOn: ByteArray = new ByteArray( + Defaults.authenticatorData.getBytes.toVector + .updated( + 32, + (Defaults.authenticatorData.getBytes + .toVector(32) | 0x04 | 0x01).toByte, + ) + .toArray + ) + val flagOff: ByteArray = new ByteArray( + Defaults.authenticatorData.getBytes.toVector + .updated( + 32, + ((Defaults.authenticatorData.getBytes + .toVector(32) | 0x04) & 0xfe).toByte, + ) + .toArray + ) + val (checkFails, checkSucceeds) = + checks[FinishAssertionSteps#Step13, FinishAssertionSteps#Step12]( + _.begin.next.next.next.next.next.next.next.next.next.next.next.next + ) it("Fails if UV is discouraged and flag is not set.") { checkFails(UserVerificationRequirement.DISCOURAGED, flagOff) @@ -938,9 +1163,28 @@ class RelyingPartyAssertionSpec extends FunSpec with Matchers with ScalaCheckDri } describe("13. If user verification is required for this assertion, verify that the User Verified bit of the flags in authData is set.") { - val flagOn: ByteArray = new ByteArray(Defaults.authenticatorData.getBytes.toVector.updated(32, (Defaults.authenticatorData.getBytes.toVector(32) | 0x04).toByte).toArray) - val flagOff: ByteArray = new ByteArray(Defaults.authenticatorData.getBytes.toVector.updated(32, (Defaults.authenticatorData.getBytes.toVector(32) & 0xfb).toByte).toArray) - val (checkFails, checkSucceeds) = checks[FinishAssertionSteps#Step14, FinishAssertionSteps#Step13](_.begin.next.next.next.next.next.next.next.next.next.next.next.next.next) + val flagOn: ByteArray = new ByteArray( + Defaults.authenticatorData.getBytes.toVector + .updated( + 32, + (Defaults.authenticatorData.getBytes + .toVector(32) | 0x04).toByte, + ) + .toArray + ) + val flagOff: ByteArray = new ByteArray( + Defaults.authenticatorData.getBytes.toVector + .updated( + 32, + (Defaults.authenticatorData.getBytes + .toVector(32) & 0xfb).toByte, + ) + .toArray + ) + val (checkFails, checkSucceeds) = + checks[FinishAssertionSteps#Step14, FinishAssertionSteps#Step13]( + _.begin.next.next.next.next.next.next.next.next.next.next.next.next.next + ) it("Succeeds if UV is discouraged and flag is not set.") { checkSucceeds(UserVerificationRequirement.DISCOURAGED, flagOff) @@ -973,66 +1217,156 @@ class RelyingPartyAssertionSpec extends FunSpec with Matchers with ScalaCheckDri describe("client extension outputs in clientExtensionResults are as expected, considering the client extension input values that were given as the extensions option in the get() call. In particular, any extension identifier values in the clientExtensionResults MUST be also be present as extension identifier values in the extensions member of options, i.e., no extensions are present that were not requested. In the general case, the meaning of \"are as expected\" is specific to the Relying Party and which extensions are in use.") { it("Fails if clientExtensionResults is not a subset of the extensions requested by the Relying Party.") { val extensionInputs = AssertionExtensionInputs.builder().build() - val clientExtensionOutputs = ClientAssertionExtensionOutputs.builder().appid(true).build() + val clientExtensionOutputs = + ClientAssertionExtensionOutputs.builder().appid(true).build() // forAll(unrequestedAssertionExtensions, minSuccessful(1)) { case (extensionInputs, clientExtensionOutputs) => - val steps = finishAssertion( - requestedExtensions = extensionInputs, - clientExtensionResults = clientExtensionOutputs - ) - val step: FinishAssertionSteps#Step14 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next + val steps = finishAssertion( + requestedExtensions = extensionInputs, + clientExtensionResults = clientExtensionOutputs, + ) + val step: FinishAssertionSteps#Step14 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next + + step.validations shouldBe a[Failure[_]] + step.validations.failed.get shouldBe an[IllegalArgumentException] + step.tryNext shouldBe a[Failure[_]] + // } + } + + it("Succeeds if clientExtensionResults is not a subset of the extensions requested by the Relying Party, but the Relying Party has enabled allowing unrequested extensions.") { + val extensionInputs = AssertionExtensionInputs.builder().build() + val clientExtensionOutputs = + ClientAssertionExtensionOutputs.builder().appid(true).build() + + // forAll(unrequestedAssertionExtensions, minSuccessful(1)) { case (extensionInputs, clientExtensionOutputs) => + val steps = finishAssertion( + allowUnrequestedExtensions = true, + requestedExtensions = extensionInputs, + clientExtensionResults = clientExtensionOutputs, + ) + val step: FinishAssertionSteps#Step14 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next - step.validations shouldBe a [Failure[_]] - step.validations.failed.get shouldBe an [IllegalArgumentException] - step.tryNext shouldBe a [Failure[_]] + step.validations shouldBe a[Success[_]] + step.tryNext shouldBe a[Success[_]] // } } it("Succeeds if clientExtensionResults is a subset of the extensions requested by the Relying Party.") { - forAll(subsetAssertionExtensions) { case (extensionInputs, clientExtensionOutputs) => - val steps = finishAssertion( - requestedExtensions = extensionInputs, - clientExtensionResults = clientExtensionOutputs - ) - val step: FinishAssertionSteps#Step14 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next + forAll(subsetAssertionExtensions) { + case (extensionInputs, clientExtensionOutputs) => + val steps = finishAssertion( + requestedExtensions = extensionInputs, + clientExtensionResults = clientExtensionOutputs, + ) + val step: FinishAssertionSteps#Step14 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next - step.validations shouldBe a [Success[_]] - step.tryNext shouldBe a [Success[_]] + step.validations shouldBe a[Success[_]] + step.tryNext shouldBe a[Success[_]] } } } describe("authenticator extension outputs in the extensions in authData are as expected, considering the client extension input values that were given as the extensions option in the get() call. In particular, any extension identifier values in the extensions in authData MUST be also be present as extension identifier values in the extensions member of options, i.e., no extensions are present that were not requested. In the general case, the meaning of \"are as expected\" is specific to the Relying Party and which extensions are in use.") { it("Fails if authenticator extensions is not a subset of the extensions requested by the Relying Party.") { - forAll(anyAuthenticatorExtensions[AssertionExtensionInputs]) { case (extensionInputs: AssertionExtensionInputs, authenticatorExtensionOutputs: ObjectNode) => - whenever(authenticatorExtensionOutputs.fieldNames().asScala.exists(id => !extensionInputs.getExtensionIds.contains(id))) { - val steps = finishAssertion( - requestedExtensions = extensionInputs, - authenticatorData = TestAuthenticator.makeAuthDataBytes( - extensionsCborBytes = Some(new ByteArray(JacksonCodecs.cbor.writeValueAsBytes(authenticatorExtensionOutputs))) + forAll(anyAuthenticatorExtensions[AssertionExtensionInputs]) { + case ( + extensionInputs: AssertionExtensionInputs, + authenticatorExtensionOutputs: ObjectNode, + ) => + whenever( + authenticatorExtensionOutputs + .fieldNames() + .asScala + .exists(id => + !extensionInputs.getExtensionIds.contains(id) + ) + ) { + val steps = finishAssertion( + requestedExtensions = extensionInputs, + authenticatorData = TestAuthenticator.makeAuthDataBytes( + extensionsCborBytes = Some( + new ByteArray( + JacksonCodecs.cbor.writeValueAsBytes( + authenticatorExtensionOutputs + ) + ) + ) + ), ) - ) - val step: FinishAssertionSteps#Step14 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next + val step: FinishAssertionSteps#Step14 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next + + step.validations shouldBe a[Failure[_]] + step.validations.failed.get shouldBe an[ + IllegalArgumentException + ] + step.tryNext shouldBe a[Failure[_]] + } + } + } - step.validations shouldBe a [Failure[_]] - step.validations.failed.get shouldBe an [IllegalArgumentException] - step.tryNext shouldBe a [Failure[_]] - } + it("Succeeds if authenticator extensions is not a subset of the extensions requested by the Relying Party, but the Relying Party has enabled allowing unrequested extensions.") { + forAll(anyAuthenticatorExtensions[AssertionExtensionInputs]) { + case ( + extensionInputs: AssertionExtensionInputs, + authenticatorExtensionOutputs: ObjectNode, + ) => + whenever( + authenticatorExtensionOutputs + .fieldNames() + .asScala + .exists(id => + !extensionInputs.getExtensionIds.contains(id) + ) + ) { + val steps = finishAssertion( + allowUnrequestedExtensions = true, + requestedExtensions = extensionInputs, + authenticatorData = TestAuthenticator.makeAuthDataBytes( + extensionsCborBytes = Some( + new ByteArray( + JacksonCodecs.cbor.writeValueAsBytes( + authenticatorExtensionOutputs + ) + ) + ) + ), + ) + val step: FinishAssertionSteps#Step14 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next + + step.validations shouldBe a[Success[_]] + step.tryNext shouldBe a[Success[_]] + } } } it("Succeeds if authenticator extensions is a subset of the extensions requested by the Relying Party.") { - forAll(subsetAuthenticatorExtensions[AssertionExtensionInputs]) { case (extensionInputs: AssertionExtensionInputs, authenticatorExtensionOutputs: ObjectNode) => - val steps = finishAssertion( - requestedExtensions = extensionInputs, - authenticatorData = TestAuthenticator.makeAuthDataBytes( - extensionsCborBytes = Some(new ByteArray(JacksonCodecs.cbor.writeValueAsBytes(authenticatorExtensionOutputs))) + forAll(subsetAuthenticatorExtensions[AssertionExtensionInputs]) { + case ( + extensionInputs: AssertionExtensionInputs, + authenticatorExtensionOutputs: ObjectNode, + ) => + val steps = finishAssertion( + requestedExtensions = extensionInputs, + authenticatorData = TestAuthenticator.makeAuthDataBytes( + extensionsCborBytes = Some( + new ByteArray( + JacksonCodecs.cbor.writeValueAsBytes( + authenticatorExtensionOutputs + ) + ) + ) + ), ) - ) - val step: FinishAssertionSteps#Step14 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next + val step: FinishAssertionSteps#Step14 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next - step.validations shouldBe a [Success[_]] - step.tryNext shouldBe a [Success[_]] + step.validations shouldBe a[Success[_]] + step.tryNext shouldBe a[Success[_]] } } } @@ -1041,72 +1375,101 @@ class RelyingPartyAssertionSpec extends FunSpec with Matchers with ScalaCheckDri it("15. Let hash be the result of computing a hash over the cData using SHA-256.") { val steps = finishAssertion() - val step: FinishAssertionSteps#Step15 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next - - step.validations shouldBe a [Success[_]] - step.tryNext shouldBe a [Success[_]] - step.clientDataJsonHash should equal (new ByteArray(MessageDigest.getInstance("SHA-256").digest(Defaults.clientDataJsonBytes.getBytes))) + val step: FinishAssertionSteps#Step15 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next + + step.validations shouldBe a[Success[_]] + step.tryNext shouldBe a[Success[_]] + step.clientDataJsonHash should equal( + new ByteArray( + MessageDigest + .getInstance("SHA-256") + .digest(Defaults.clientDataJsonBytes.getBytes) + ) + ) } describe("16. Using the credential public key looked up in step 3, verify that sig is a valid signature over the binary concatenation of authData and hash.") { it("The default test case succeeds.") { val steps = finishAssertion() - val step: FinishAssertionSteps#Step16 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next + val step: FinishAssertionSteps#Step16 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next - step.validations shouldBe a [Success[_]] - step.tryNext shouldBe a [Success[_]] + step.validations shouldBe a[Success[_]] + step.tryNext shouldBe a[Success[_]] step.signedBytes should not be null } it("A mutated clientDataJSON fails verification.") { val steps = finishAssertion( clientDataJson = JacksonCodecs.json.writeValueAsString( - JacksonCodecs.json.readTree(Defaults.clientDataJson).asInstanceOf[ObjectNode] + JacksonCodecs.json + .readTree(Defaults.clientDataJson) + .asInstanceOf[ObjectNode] .set("foo", jsonFactory.textNode("bar")) ) ) - val step: FinishAssertionSteps#Step16 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next + val step: FinishAssertionSteps#Step16 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next - step.validations shouldBe a [Failure[_]] - step.validations.failed.get shouldBe an [IllegalArgumentException] - step.tryNext shouldBe a [Failure[_]] + step.validations shouldBe a[Failure[_]] + step.validations.failed.get shouldBe an[IllegalArgumentException] + step.tryNext shouldBe a[Failure[_]] } it("A test case with a different signed RP ID hash fails.") { val rpId = "ARGHABLARGHLER" - val rpIdHash: ByteArray = Crypto.hash(rpId) + val rpIdHash: ByteArray = Crypto.sha256(rpId) val steps = finishAssertion( - authenticatorData = new ByteArray((rpIdHash.getBytes.toVector ++ Defaults.authenticatorData.getBytes.toVector.drop(32)).toArray), + authenticatorData = new ByteArray( + (rpIdHash.getBytes.toVector ++ Defaults.authenticatorData.getBytes.toVector + .drop(32)).toArray + ), rpId = Defaults.rpId.toBuilder.id(rpId).build(), - origins = Some(Set("https://localhost")) + 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 + val step: FinishAssertionSteps#Step16 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next - step.validations shouldBe a [Failure[_]] - step.validations.failed.get shouldBe an [IllegalArgumentException] - step.tryNext shouldBe a [Failure[_]] + step.validations shouldBe a[Failure[_]] + step.validations.failed.get shouldBe an[IllegalArgumentException] + step.tryNext shouldBe a[Failure[_]] } it("A test case with a different signed flags field fails.") { val steps = finishAssertion( - authenticatorData = new ByteArray(Defaults.authenticatorData.getBytes.toVector.updated(32, (Defaults.authenticatorData.getBytes.toVector(32) | 0x02).toByte).toArray) + authenticatorData = new ByteArray( + Defaults.authenticatorData.getBytes.toVector + .updated( + 32, + (Defaults.authenticatorData.getBytes + .toVector(32) | 0x02).toByte, + ) + .toArray + ) ) - val step: FinishAssertionSteps#Step16 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next + val step: FinishAssertionSteps#Step16 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next - step.validations shouldBe a [Failure[_]] - step.validations.failed.get shouldBe an [IllegalArgumentException] - step.tryNext shouldBe a [Failure[_]] + step.validations shouldBe a[Failure[_]] + step.validations.failed.get shouldBe an[IllegalArgumentException] + step.tryNext shouldBe a[Failure[_]] } it("A test case with a different signed signature counter fails.") { val steps = finishAssertion( - authenticatorData = new ByteArray(Defaults.authenticatorData.getBytes.toVector.updated(33, 42.toByte).toArray) + authenticatorData = new ByteArray( + Defaults.authenticatorData.getBytes.toVector + .updated(33, 42.toByte) + .toArray + ) ) - val step: FinishAssertionSteps#Step16 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next + val step: FinishAssertionSteps#Step16 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next - step.validations shouldBe a [Failure[_]] - step.validations.failed.get shouldBe an [IllegalArgumentException] - step.tryNext shouldBe a [Failure[_]] + step.validations shouldBe a[Failure[_]] + step.validations.failed.get shouldBe an[IllegalArgumentException] + step.tryNext shouldBe a[Failure[_]] } } @@ -1115,17 +1478,30 @@ class RelyingPartyAssertionSpec extends FunSpec with Matchers with ScalaCheckDri def credentialRepository(signatureCount: Long) = Helpers.CredentialRepository.withUser( Defaults.user, - RegisteredCredential.builder() + RegisteredCredential + .builder() .credentialId(Defaults.credentialId) .userHandle(Defaults.userHandle) .publicKeyCose(getPublicKeyBytes(Defaults.credentialKey)) .signatureCount(signatureCount) - .build() + .build(), ) - describe("zero, then the stored signature counter value must also be zero.") { - val authenticatorData = new ByteArray(Defaults.authenticatorData.getBytes.updated(33, 0: Byte).updated(34, 0: Byte).updated(35, 0: Byte).updated(36, 0: Byte)) - val signature = TestAuthenticator.makeAssertionSignature(authenticatorData, Crypto.hash(Defaults.clientDataJsonBytes), Defaults.credentialKey.getPrivate) + describe( + "zero, then the stored signature counter value must also be zero." + ) { + val authenticatorData = new ByteArray( + Defaults.authenticatorData.getBytes + .updated(33, 0: Byte) + .updated(34, 0: Byte) + .updated(35, 0: Byte) + .updated(36, 0: Byte) + ) + val signature = TestAuthenticator.makeAssertionSignature( + authenticatorData, + Crypto.sha256(Defaults.clientDataJsonBytes), + Defaults.credentialKey.getPrivate, + ) it("Succeeds if the stored signature counter value is zero.") { val cr = credentialRepository(0) @@ -1133,14 +1509,15 @@ class RelyingPartyAssertionSpec extends FunSpec with Matchers with ScalaCheckDri authenticatorData = authenticatorData, signature = signature, credentialRepository = Some(cr), - validateSignatureCounter = true + validateSignatureCounter = true, ) - val step: FinishAssertionSteps#Step17 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next + val step: FinishAssertionSteps#Step17 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next - step.validations shouldBe a [Success[_]] - step.tryNext shouldBe a [Success[_]] - step.next.result.get.isSignatureCounterValid should be (true) - step.next.result.get.getSignatureCount should be (0) + step.validations shouldBe a[Success[_]] + step.tryNext shouldBe a[Success[_]] + step.next.result.get.isSignatureCounterValid should be(true) + step.next.result.get.getSignatureCount should be(0) } it("Fails if the stored signature counter value is nonzero.") { @@ -1149,13 +1526,16 @@ class RelyingPartyAssertionSpec extends FunSpec with Matchers with ScalaCheckDri authenticatorData = authenticatorData, signature = signature, credentialRepository = Some(cr), - validateSignatureCounter = true + validateSignatureCounter = true, ) - val step: FinishAssertionSteps#Step17 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next - - step.validations shouldBe a [Failure[_]] - step.tryNext shouldBe a [Failure[_]] - step.tryNext.failed.get shouldBe an [InvalidSignatureCountException] + val step: FinishAssertionSteps#Step17 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next + + step.validations shouldBe a[Failure[_]] + step.tryNext shouldBe a[Failure[_]] + step.tryNext.failed.get shouldBe an[ + InvalidSignatureCountException + ] } } @@ -1166,14 +1546,15 @@ class RelyingPartyAssertionSpec extends FunSpec with Matchers with ScalaCheckDri it("An increasing signature counter always succeeds.") { val steps = finishAssertion( credentialRepository = Some(cr), - validateSignatureCounter = true + validateSignatureCounter = true, ) - val step: FinishAssertionSteps#Step17 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next + val step: FinishAssertionSteps#Step17 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next - step.validations shouldBe a [Success[_]] - step.tryNext shouldBe a [Success[_]] - step.next.result.get.isSignatureCounterValid should be (true) - step.next.result.get.getSignatureCount should be (1337) + step.validations shouldBe a[Success[_]] + step.tryNext shouldBe a[Success[_]] + step.next.result.get.isSignatureCounterValid should be(true) + step.next.result.get.getSignatureCount should be(1337) } } } @@ -1185,12 +1566,13 @@ class RelyingPartyAssertionSpec extends FunSpec with Matchers with ScalaCheckDri it("If signature counter validation is disabled, a nonincreasing signature counter succeeds.") { val steps = finishAssertion( credentialRepository = Some(cr), - validateSignatureCounter = false + validateSignatureCounter = false, ) - val step: FinishAssertionSteps#Step17 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next + val step: FinishAssertionSteps#Step17 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next - step.validations shouldBe a [Success[_]] - step.tryNext shouldBe a [Success[_]] + step.validations shouldBe a[Success[_]] + step.tryNext shouldBe a[Success[_]] step.next.result.get.isSignatureCounterValid should be(false) step.next.result.get.getSignatureCount should be(1337) } @@ -1198,20 +1580,29 @@ class RelyingPartyAssertionSpec extends FunSpec with Matchers with ScalaCheckDri it("If signature counter validation is enabled, a nonincreasing signature counter fails.") { val steps = finishAssertion( credentialRepository = Some(cr), - validateSignatureCounter = true + validateSignatureCounter = true, ) - val step: FinishAssertionSteps#Step17 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next + val step: FinishAssertionSteps#Step17 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next val result = Try(step.run()) - step.validations shouldBe a [Failure[_]] - step.validations.failed.get shouldBe an [InvalidSignatureCountException] - step.tryNext shouldBe a [Failure[_]] - - result shouldBe a [Failure[_]] - result.failed.get shouldBe an [InvalidSignatureCountException] - result.failed.get.asInstanceOf[InvalidSignatureCountException].getExpectedMinimum should equal (1338) - result.failed.get.asInstanceOf[InvalidSignatureCountException].getReceived should equal (1337) - result.failed.get.asInstanceOf[InvalidSignatureCountException].getCredentialId should equal (Defaults.credentialId) + step.validations shouldBe a[Failure[_]] + step.validations.failed.get shouldBe an[ + InvalidSignatureCountException + ] + step.tryNext shouldBe a[Failure[_]] + + result shouldBe a[Failure[_]] + result.failed.get shouldBe an[InvalidSignatureCountException] + result.failed.get + .asInstanceOf[InvalidSignatureCountException] + .getExpectedMinimum should equal(1338) + result.failed.get + .asInstanceOf[InvalidSignatureCountException] + .getReceived should equal(1337) + result.failed.get + .asInstanceOf[InvalidSignatureCountException] + .getCredentialId should equal(Defaults.credentialId) } } } @@ -1220,14 +1611,15 @@ class RelyingPartyAssertionSpec extends FunSpec with Matchers with ScalaCheckDri it("18. If all the above steps are successful, continue with the authentication ceremony as appropriate. Otherwise, fail the authentication ceremony.") { val steps = finishAssertion() - val step: FinishAssertionSteps#Finished = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next + val step: FinishAssertionSteps#Finished = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next - step.validations shouldBe a [Success[_]] - Try(steps.run) shouldBe a [Success[_]] + step.validations shouldBe a[Success[_]] + Try(steps.run) shouldBe a[Success[_]] - step.result.get.isSuccess should be (true) - step.result.get.getCredentialId should equal (Defaults.credentialId) - step.result.get.getUserHandle should equal (Defaults.userHandle) + step.result.get.isSuccess should be(true) + step.result.get.getCredentialId should equal(Defaults.credentialId) + step.result.get.getUserHandle should equal(Defaults.userHandle) } } @@ -1238,12 +1630,16 @@ class RelyingPartyAssertionSpec extends FunSpec with Matchers with ScalaCheckDri it("a real RSA key.") { val testData = RegistrationTestData.Packed.BasicAttestationRsaReal - val credData = testData.response.getResponse.getAttestation.getAuthenticatorData.getAttestedCredentialData.get + val credData = + testData.response.getResponse.getAttestation.getAuthenticatorData.getAttestedCredentialData.get val credId: ByteArray = credData.getCredentialId val publicKeyBytes: ByteArray = credData.getCredentialPublicKey - val request: AssertionRequest = AssertionRequest.builder() - .publicKeyCredentialRequestOptions(JacksonCodecs.json.readValue("""{ + val request: AssertionRequest = AssertionRequest + .builder() + .publicKeyCredentialRequestOptions( + JacksonCodecs.json.readValue( + """{ "challenge": "drdVqKT0T-9PyQfkceSE94Q8ruW2I-w1gsamBisjuMw", "rpId": "demo3.yubico.test", "userVerification": "preferred", @@ -1251,12 +1647,16 @@ class RelyingPartyAssertionSpec extends FunSpec with Matchers with ScalaCheckDri "appid": "https://demo3.yubico.test:8443" } }""", - classOf[PublicKeyCredentialRequestOptions] - )) + classOf[PublicKeyCredentialRequestOptions], + ) + ) .username(testData.userId.getName) .build() - val response: PublicKeyCredential[AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs] = JacksonCodecs.json.readValue( + val response: PublicKeyCredential[ + AuthenticatorAssertionResponse, + ClientAssertionExtensionOutputs, + ] = JacksonCodecs.json.readValue( """{ "type": "public-key", "id": "ClvGfsNH8ulYnrKNd4fEgQ", @@ -1270,38 +1670,53 @@ class RelyingPartyAssertionSpec extends FunSpec with Matchers with ScalaCheckDri "appid": false } }""", - new TypeReference[PublicKeyCredential[AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs]](){} + new TypeReference[PublicKeyCredential[ + AuthenticatorAssertionResponse, + ClientAssertionExtensionOutputs, + ]]() {}, ) val credRepo = Helpers.CredentialRepository.withUser( testData.userId, - RegisteredCredential.builder() + RegisteredCredential + .builder() .credentialId(testData.response.getId) .userHandle(testData.userId.getId) .publicKeyCose(publicKeyBytes) - .build() + .build(), ) - val rp = RelyingParty.builder() - .identity(RelyingPartyIdentity.builder().id("demo3.yubico.test").name("Yubico WebAuthn demo").build()) + val rp = RelyingParty + .builder() + .identity( + RelyingPartyIdentity + .builder() + .id("demo3.yubico.test") + .name("Yubico WebAuthn demo") + .build() + ) .credentialRepository(credRepo) .origins(Set("https://demo3.yubico.test:8443").asJava) .build() - val result = rp.finishAssertion(FinishAssertionOptions.builder() - .request(request) - .response(response) - .build() + val result = rp.finishAssertion( + FinishAssertionOptions + .builder() + .request(request) + .response(response) + .build() ) - result.isSuccess should be (true) - result.getUserHandle should equal (testData.userId.getId) - result.getCredentialId should equal (credId) + result.isSuccess should be(true) + result.getUserHandle should equal(testData.userId.getId) + result.getCredentialId should equal(credId) } it("an Ed25519 key.") { - val registrationRequest = JacksonCodecs.json().readValue( - """ + val registrationRequest = JacksonCodecs + .json() + .readValue( + """ |{ | "rp": { | "name": "Yubico WebAuthn demo", @@ -1328,9 +1743,10 @@ class RelyingPartyAssertionSpec extends FunSpec with Matchers with ScalaCheckDri | "extensions": {} |} """.stripMargin, - classOf[PublicKeyCredentialCreationOptions]) - val registrationResponse = PublicKeyCredential.parseRegistrationResponseJson( - """ + classOf[PublicKeyCredentialCreationOptions], + ) + val registrationResponse = + PublicKeyCredential.parseRegistrationResponseJson(""" |{ | "type": "public-key", | "id": "PMEuc5FHylmDzH9BgG0lf_YqsOKKspino-b5ybq8CD0mpwU3Q4S4oUMQd_CgQsJOR3qyv3HirclQM2lNIiyi3dytZ6p-zbfBxDCH637qWTTZTZfKPxKBsdEOVPMBPopU_9uNXKh9dTxqe4mpSuznjxV-cEMF3BU3CSnJDU1BOCM", @@ -1343,8 +1759,10 @@ class RelyingPartyAssertionSpec extends FunSpec with Matchers with ScalaCheckDri | """.stripMargin) - val assertionRequest = JacksonCodecs.json().readValue( - """{ + val assertionRequest = JacksonCodecs + .json() + .readValue( + """{ | "challenge": "YK17iD3fpOQKPSU6bxIU-TFBj1HNVSrX5bX5Pzj-SHQ", | "rpId": "demo3.yubico.test", | "allowCredentials": [ @@ -1359,7 +1777,8 @@ class RelyingPartyAssertionSpec extends FunSpec with Matchers with ScalaCheckDri | } |} |""".stripMargin, - classOf[PublicKeyCredentialRequestOptions]) + classOf[PublicKeyCredentialRequestOptions], + ) val assertionResponse = PublicKeyCredential.parseAssertionResponseJson( """ |{ @@ -1378,89 +1797,136 @@ class RelyingPartyAssertionSpec extends FunSpec with Matchers with ScalaCheckDri """.stripMargin ) - val credData = registrationResponse.getResponse.getAttestation.getAuthenticatorData.getAttestedCredentialData.get + val credData = + registrationResponse.getResponse.getAttestation.getAuthenticatorData.getAttestedCredentialData.get val credId: ByteArray = credData.getCredentialId val publicKeyBytes: ByteArray = credData.getCredentialPublicKey val credRepo = Helpers.CredentialRepository.withUser( registrationRequest.getUser, - RegisteredCredential.builder() + RegisteredCredential + .builder() .credentialId(registrationResponse.getId) .userHandle(registrationRequest.getUser.getId) .publicKeyCose(publicKeyBytes) - .build() + .build(), ) - val rp = RelyingParty.builder() - .identity(RelyingPartyIdentity.builder().id("demo3.yubico.test").name("Yubico WebAuthn demo").build()) + val rp = RelyingParty + .builder() + .identity( + RelyingPartyIdentity + .builder() + .id("demo3.yubico.test") + .name("Yubico WebAuthn demo") + .build() + ) .credentialRepository(credRepo) .origins(Set("https://demo3.yubico.test:8443").asJava) .build() - val result = rp.finishAssertion(FinishAssertionOptions.builder() - .request(AssertionRequest.builder() - .publicKeyCredentialRequestOptions(assertionRequest) - .username(registrationRequest.getUser.getName) - .build()) - .response(assertionResponse) - .build() + val result = rp.finishAssertion( + FinishAssertionOptions + .builder() + .request( + AssertionRequest + .builder() + .publicKeyCredentialRequestOptions(assertionRequest) + .username(registrationRequest.getUser.getName) + .build() + ) + .response(assertionResponse) + .build() ) - result.isSuccess should be (true) - result.getUserHandle should equal (registrationRequest.getUser.getId) - result.getCredentialId should equal (credId) + result.isSuccess should be(true) + result.getUserHandle should equal(registrationRequest.getUser.getId) + result.getCredentialId should equal(credId) } it("a generated Ed25519 key.") { - val registrationTestData = RegistrationTestData.Packed.BasicAttestationEdDsa + val registrationTestData = + RegistrationTestData.Packed.BasicAttestationEdDsa val testData = registrationTestData.assertion.get - val rp = RelyingParty.builder() - .identity(RelyingPartyIdentity.builder().id("localhost").name("Test RP").build()) - .credentialRepository(Helpers.CredentialRepository.withUser(registrationTestData.userId, RegisteredCredential.builder() - .credentialId(registrationTestData.response.getId) - .userHandle(registrationTestData.userId.getId) - .publicKeyCose(registrationTestData.response.getResponse.getParsedAuthenticatorData.getAttestedCredentialData.get.getCredentialPublicKey) - .signatureCount(0) - .build())) + val rp = RelyingParty + .builder() + .identity( + RelyingPartyIdentity.builder().id("localhost").name("Test RP").build() + ) + .credentialRepository( + Helpers.CredentialRepository.withUser( + 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() + 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) + 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(Helpers.CredentialRepository.withUser(registrationTestData.userId, RegisteredCredential.builder() - .credentialId(registrationTestData.response.getId) - .userHandle(registrationTestData.userId.getId) - .publicKeyCose(registrationTestData.response.getResponse.getParsedAuthenticatorData.getAttestedCredentialData.get.getCredentialPublicKey) - .signatureCount(0) - .build())) + val rp = RelyingParty + .builder() + .identity( + RelyingPartyIdentity + .builder() + .id("localhost") + .name("Test RP") + .build() + ) + .credentialRepository( + Helpers.CredentialRepository.withUser( + 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() + 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) + 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.") { diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyCeremoniesSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyCeremoniesSpec.scala index 5707e3d73..3415dc688 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyCeremoniesSpec.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyCeremoniesSpec.scala @@ -37,63 +37,95 @@ import org.scalatestplus.junit.JUnitRunner import scala.jdk.CollectionConverters._ - @RunWith(classOf[JUnitRunner]) -class RelyingPartyCeremoniesSpec extends FunSpec with Matchers with TestWithEachProvider { +class RelyingPartyCeremoniesSpec + extends FunSpec + with Matchers + with TestWithEachProvider { - private def newRp(testData: RealExamples.Example, credentialRepo: CredentialRepository): RelyingParty = - RelyingParty.builder() + private def newRp( + testData: RealExamples.Example, + credentialRepo: CredentialRepository, + ): RelyingParty = + RelyingParty + .builder() .identity(testData.rp) .credentialRepository(credentialRepo) .build() testWithEachProvider { it => - describe("The default RelyingParty settings") { describe("can register and then authenticate") { def check(testData: RealExamples.Example): Unit = { - val registrationRp = newRp(testData, Helpers.CredentialRepository.empty) + val registrationRp = + newRp(testData, Helpers.CredentialRepository.empty) - val registrationResult = registrationRp.finishRegistration(FinishRegistrationOptions.builder() - .request(PublicKeyCredentialCreationOptions.builder() - .rp(testData.rp) - .user(testData.user) - .challenge(testData.attestation.challenge) - .pubKeyCredParams(List(PublicKeyCredentialParameters.ES256).asJava) - .build()) - .response(testData.attestation.credential) - .build()); + val registrationResult = registrationRp.finishRegistration( + FinishRegistrationOptions + .builder() + .request( + PublicKeyCredentialCreationOptions + .builder() + .rp(testData.rp) + .user(testData.user) + .challenge(testData.attestation.challenge) + .pubKeyCredParams( + List(PublicKeyCredentialParameters.ES256).asJava + ) + .build() + ) + .response(testData.attestation.credential) + .build() + ); - registrationResult.getKeyId.getId should equal (testData.attestation.credential.getId) - registrationResult.isAttestationTrusted should be (false) - registrationResult.getAttestationMetadata.isPresent should be (false) + registrationResult.getKeyId.getId should equal( + testData.attestation.credential.getId + ) + registrationResult.isAttestationTrusted should be(false) + registrationResult.getAttestationMetadata.isPresent should be(false) val assertionRp = newRp( testData, Helpers.CredentialRepository.withUser( testData.user, - Helpers.toRegisteredCredential(testData.user, registrationResult) - ) + Helpers.toRegisteredCredential(testData.user, registrationResult), + ), ) - val assertionResult = assertionRp.finishAssertion(FinishAssertionOptions.builder() - .request(AssertionRequest.builder() - .publicKeyCredentialRequestOptions(PublicKeyCredentialRequestOptions.builder() - .challenge(testData.assertion.challenge) - .allowCredentials(List(PublicKeyCredentialDescriptor.builder().id(testData.assertion.id).build()).asJava) - .build()) - .username(testData.user.getName) - .build()) - .response(testData.assertion.credential) - .build()) + val assertionResult = assertionRp.finishAssertion( + FinishAssertionOptions + .builder() + .request( + AssertionRequest + .builder() + .publicKeyCredentialRequestOptions( + PublicKeyCredentialRequestOptions + .builder() + .challenge(testData.assertion.challenge) + .allowCredentials( + List( + PublicKeyCredentialDescriptor + .builder() + .id(testData.assertion.id) + .build() + ).asJava + ) + .build() + ) + .username(testData.user.getName) + .build() + ) + .response(testData.assertion.credential) + .build() + ) - assertionResult.isSuccess should be (true) - assertionResult.getCredentialId should equal (testData.assertion.id) - assertionResult.getUserHandle should equal (testData.user.getId) - assertionResult.getUsername should equal (testData.user.getName) + assertionResult.isSuccess should be(true) + assertionResult.getCredentialId should equal(testData.assertion.id) + assertionResult.getUserHandle should equal(testData.user.getId) + assertionResult.getUsername should equal(testData.user.getName) assertionResult.getSignatureCount should be >= testData.attestation.authenticatorData.getSignatureCounter - assertionResult.isSignatureCounterValid should be (true) + assertionResult.isSignatureCounterValid should be(true) } it("a YubiKey NEO.") { 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 3597c2340..64cf95e8f 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,20 +24,6 @@ package com.yubico.webauthn -import java.io.IOException -import java.nio.charset.Charset -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 javax.security.auth.x500.X500Principal -import scala.jdk.CollectionConverters._ -import scala.util.Failure -import scala.util.Success -import scala.util.Try - import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.node.JsonNodeFactory import com.fasterxml.jackson.databind.node.ObjectNode @@ -62,6 +48,7 @@ import com.yubico.webauthn.data.RelyingPartyIdentity import com.yubico.webauthn.data.UserIdentity import com.yubico.webauthn.data.UserVerificationRequirement import com.yubico.webauthn.test.Helpers +import com.yubico.webauthn.test.RealExamples import com.yubico.webauthn.test.Util.toStepWithUtilities import org.bouncycastle.asn1.DEROctetString import org.bouncycastle.asn1.x500.X500Name @@ -73,37 +60,69 @@ import org.scalatest.Matchers import org.scalatestplus.junit.JUnitRunner import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks +import java.io.IOException +import java.nio.charset.Charset +import java.security.KeyPair +import java.security.MessageDigest +import java.security.PrivateKey +import java.security.SignatureException +import java.security.cert.X509Certificate +import java.security.interfaces.RSAPublicKey +import javax.security.auth.x500.X500Principal +import scala.jdk.CollectionConverters._ +import scala.util.Failure +import scala.util.Success +import scala.util.Try + @RunWith(classOf[JUnitRunner]) -class RelyingPartyRegistrationSpec extends FunSpec with Matchers with ScalaCheckDrivenPropertyChecks with TestWithEachProvider { +class RelyingPartyRegistrationSpec + extends FunSpec + with Matchers + with ScalaCheckDrivenPropertyChecks + with TestWithEachProvider { private def jsonFactory: JsonNodeFactory = JsonNodeFactory.instance - private def toJsonObject(obj: Map[String, JsonNode]): JsonNode = jsonFactory.objectNode().setAll(obj.asJava) - private def toJson(obj: Map[String, String]): JsonNode = toJsonObject(obj.view.mapValues(jsonFactory.textNode).toMap) + private def toJsonObject(obj: Map[String, JsonNode]): JsonNode = + jsonFactory.objectNode().setAll(obj.asJava) + private def toJson(obj: Map[String, String]): JsonNode = + toJsonObject(obj.view.mapValues(jsonFactory.textNode).toMap) - private def sha256(bytes: ByteArray): ByteArray = Crypto.hash(bytes) + private def sha256(bytes: ByteArray): ByteArray = Crypto.sha256(bytes) - def flipByte(index: Int, bytes: ByteArray): ByteArray = editByte(bytes, index, b => (0xff ^ b).toByte) - def editByte(bytes: ByteArray, index: Int, updater: Byte => Byte): ByteArray = new ByteArray(bytes.getBytes.updated(index, updater(bytes.getBytes()(index)))) + def flipByte(index: Int, bytes: ByteArray): ByteArray = + editByte(bytes, index, b => (0xff ^ b).toByte) + def editByte(bytes: ByteArray, index: Int, updater: Byte => Byte): ByteArray = + new ByteArray( + bytes.getBytes.updated(index, updater(bytes.getBytes()(index))) + ) private def finishRegistration( - allowOriginPort: Boolean = false, - allowOriginSubdomain: Boolean = false, - allowUntrustedAttestation: Boolean = false, - callerTokenBindingId: Option[ByteArray] = None, - credentialId: Option[ByteArray] = None, - credentialRepository: CredentialRepository = Helpers.CredentialRepository.unimplemented, - 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 + allowOriginPort: Boolean = false, + allowOriginSubdomain: Boolean = false, + allowUnrequestedExtensions: Boolean = false, + allowUntrustedAttestation: Boolean = false, + callerTokenBindingId: Option[ByteArray] = None, + credentialId: Option[ByteArray] = None, + credentialRepository: CredentialRepository = + Helpers.CredentialRepository.unimplemented, + 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 = { - val builder = RelyingParty.builder() + val builder = RelyingParty + .builder() .identity(rp) .credentialRepository(credentialRepository) .preferredPubkeyParams(preferredPubkeyParams.asJava) .allowOriginPort(allowOriginPort) .allowOriginSubdomain(allowOriginSubdomain) + .allowUnrequestedExtensions(allowUnrequestedExtensions) .allowUntrustedAttestation(allowUntrustedAttestation) .metadataService(metadataService.asJava) @@ -111,18 +130,25 @@ class RelyingPartyRegistrationSpec extends FunSpec with Matchers with ScalaCheck builder .build() - ._finishRegistration(testData.request, testData.response, callerTokenBindingId.asJava) + ._finishRegistration( + testData.request, + testData.response, + callerTokenBindingId.asJava, + ) } - class TestMetadataService(private val attestation: Option[Attestation] = None) extends MetadataService { - override def getAttestation(attestationCertificateChain: java.util.List[X509Certificate]): Attestation = attestation match { - case None => Attestation.builder().trusted(false).build() - case Some(a) => a - } + class TestMetadataService(private val attestation: Option[Attestation] = None) + extends MetadataService { + override def getAttestation( + attestationCertificateChain: java.util.List[X509Certificate] + ): Attestation = + attestation match { + case None => Attestation.builder().trusted(false).build() + case Some(a) => a + } } testWithEachProvider { it => - describe("§7.1. Registering a new credential") { describe("When registering a new credential, represented by an AuthenticatorAttestationResponse structure response and an AuthenticationExtensionsClientOutputs structure clientExtensionResults, as part of a registration ceremony, a Relying Party MUST proceed as follows:") { @@ -134,59 +160,64 @@ class RelyingPartyRegistrationSpec extends FunSpec with Matchers with ScalaCheck describe("2. Let C, the client data claimed as collected during the credential creation, be the result of running an implementation-specific JSON parser on JSONtext.") { it("Fails if clientDataJson is not valid JSON.") { - an [IOException] should be thrownBy new CollectedClientData(new ByteArray("{".getBytes(Charset.forName("UTF-8")))) - an [IOException] should be thrownBy finishRegistration( - testData = RegistrationTestData.FidoU2f.BasicAttestation.copy(clientDataJson = "{") + an[IOException] should be thrownBy new CollectedClientData( + new ByteArray("{".getBytes(Charset.forName("UTF-8"))) + ) + an[IOException] should be thrownBy finishRegistration( + testData = RegistrationTestData.FidoU2f.BasicAttestation + .copy(clientDataJson = "{") ) } it("Succeeds if clientDataJson is valid JSON.") { val steps = finishRegistration( testData = RegistrationTestData.FidoU2f.BasicAttestation.copy( - clientDataJson = - """{ + clientDataJson = """{ "challenge": "", "origin": "", "type": "" }""", - overrideRequest = Some(RegistrationTestData.FidoU2f.BasicAttestation.request) + overrideRequest = + Some(RegistrationTestData.FidoU2f.BasicAttestation.request), ) ) val step: FinishRegistrationSteps#Step2 = steps.begin.next - step.validations shouldBe a [Success[_]] + step.validations shouldBe a[Success[_]] step.clientData should not be null - step.tryNext shouldBe a [Success[_]] + step.tryNext shouldBe a[Success[_]] } } describe("3. Verify that the value of C.type is webauthn.create.") { it("The default test case succeeds.") { - val steps = finishRegistration(testData = RegistrationTestData.FidoU2f.BasicAttestation) + val steps = finishRegistration(testData = + RegistrationTestData.FidoU2f.BasicAttestation + ) val step: FinishRegistrationSteps#Step3 = steps.begin.next.next - step.validations shouldBe a [Success[_]] + step.validations shouldBe a[Success[_]] } - def assertFails(typeString: String): Unit = { val steps = finishRegistration( - testData = RegistrationTestData.FidoU2f.BasicAttestation.editClientData("type", typeString) + testData = RegistrationTestData.FidoU2f.BasicAttestation + .editClientData("type", typeString) ) val step: FinishRegistrationSteps#Step3 = steps.begin.next.next - step.validations shouldBe a [Failure[_]] - step.validations.failed.get shouldBe an [IllegalArgumentException] + step.validations shouldBe a[Failure[_]] + step.validations.failed.get shouldBe an[IllegalArgumentException] } it("""Any value other than "webauthn.create" fails.""") { forAll { (typeString: String) => - whenever (typeString != "webauthn.create") { + whenever(typeString != "webauthn.create") { assertFails(typeString) } } forAll(Gen.alphaNumStr) { (typeString: String) => - whenever (typeString != "webauthn.create") { + whenever(typeString != "webauthn.create") { assertFails(typeString) } } @@ -196,53 +227,61 @@ class RelyingPartyRegistrationSpec extends FunSpec with Matchers with ScalaCheck it("4. Verify that the value of C.challenge matches the challenge that was sent to the authenticator in the create() call.") { val steps = finishRegistration( testData = RegistrationTestData.FidoU2f.BasicAttestation.copy( - overrideRequest = Some(RegistrationTestData.FidoU2f.BasicAttestation.request.toBuilder.challenge(new ByteArray(Array.fill(16)(0))).build()) + overrideRequest = Some( + RegistrationTestData.FidoU2f.BasicAttestation.request.toBuilder + .challenge(new ByteArray(Array.fill(16)(0))) + .build() + ) ) ) val step: FinishRegistrationSteps#Step4 = steps.begin.next.next.next - step.validations shouldBe a [Failure[_]] - step.validations.failed.get shouldBe an [IllegalArgumentException] - step.tryNext shouldBe a [Failure[_]] + step.validations shouldBe a[Failure[_]] + step.validations.failed.get shouldBe an[IllegalArgumentException] + step.tryNext shouldBe a[Failure[_]] } describe("5. 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 + origin: String, + origins: Option[Set[String]] = None, + allowOriginPort: Boolean = false, + allowOriginSubdomain: Boolean = false, ): Unit = { val steps = finishRegistration( - testData = RegistrationTestData.FidoU2f.BasicAttestation.editClientData("origin", origin), + testData = RegistrationTestData.FidoU2f.BasicAttestation + .editClientData("origin", origin), origins = origins, allowOriginPort = allowOriginPort, - allowOriginSubdomain = allowOriginSubdomain + allowOriginSubdomain = allowOriginSubdomain, ) - val step: FinishRegistrationSteps#Step5 = steps.begin.next.next.next.next + val step: FinishRegistrationSteps#Step5 = + steps.begin.next.next.next.next - step.validations shouldBe a [Success[_]] - step.tryNext shouldBe a [Success[_]] + 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 + origin: String, + origins: Option[Set[String]] = None, + allowOriginPort: Boolean = false, + allowOriginSubdomain: Boolean = false, ): Unit = { val steps = finishRegistration( - testData = RegistrationTestData.FidoU2f.BasicAttestation.editClientData("origin", origin), + testData = RegistrationTestData.FidoU2f.BasicAttestation + .editClientData("origin", origin), origins = origins, allowOriginPort = allowOriginPort, - allowOriginSubdomain = allowOriginSubdomain + allowOriginSubdomain = allowOriginSubdomain, ) - val step: FinishRegistrationSteps#Step5 = steps.begin.next.next.next.next + 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[_]] + step.validations shouldBe a[Failure[_]] + step.validations.failed.get shouldBe an[IllegalArgumentException] + step.tryNext shouldBe a[Failure[_]] } it("Fails if origin is different.") { @@ -273,32 +312,44 @@ class RelyingPartyRegistrationSpec extends FunSpec with Matchers with ScalaCheck } describe("Subdomains and explicit ports at the same time are") { - val origin = "https://foo.localhost:8080" + 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) + checkRejected( + origin = origin, + allowOriginPort = false, + allowOriginSubdomain = true, + ) } it("not allowed if only explicit ports are allowed.") { - checkRejected(origin = origin, allowOriginPort = true, allowOriginSubdomain = false) + checkRejected( + origin = origin, + allowOriginPort = true, + allowOriginSubdomain = false, + ) } it("allowed if RP opts in to both.") { - checkAccepted(origin = origin, allowOriginPort = true, allowOriginSubdomain = true) + 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 + origins: Set[String], + acceptOrigins: Iterable[String], + rejectOrigins: Iterable[String], + allowOriginPort: Boolean = false, + allowOriginSubdomain: Boolean = false, ): Unit = { for { origin <- acceptOrigins } { it(s"${origin} is accepted.") { @@ -306,7 +357,7 @@ class RelyingPartyRegistrationSpec extends FunSpec with Matchers with ScalaCheck origin = origin, origins = Some(origins), allowOriginPort = allowOriginPort, - allowOriginSubdomain = allowOriginSubdomain + allowOriginSubdomain = allowOriginSubdomain, ) } } @@ -317,14 +368,18 @@ class RelyingPartyRegistrationSpec extends FunSpec with Matchers with ScalaCheck origin = origin, origins = Some(origins), allowOriginPort = allowOriginPort, - allowOriginSubdomain = allowOriginSubdomain + allowOriginSubdomain = allowOriginSubdomain, ) } } } describe("For allowOriginPort:") { - val origins = Set("https://example.org", "https://accounts.example.org", "https://acme.com:8443") + val origins = Set( + "https://example.org", + "https://accounts.example.org", + "https://acme.com:8443", + ) describe("false,") { check( @@ -332,15 +387,15 @@ class RelyingPartyRegistrationSpec extends FunSpec with Matchers with ScalaCheck acceptOrigins = List( "https://example.org", "https://accounts.example.org", - "https://acme.com:8443" + "https://acme.com:8443", ), rejectOrigins = List( "https://example.org:8443", "https://shop.example.org", "https://acme.com", - "https://acme.com:9000" + "https://acme.com:9000", ), - allowOriginPort = false + allowOriginPort = false, ) } @@ -353,12 +408,12 @@ class RelyingPartyRegistrationSpec extends FunSpec with Matchers with ScalaCheck "https://accounts.example.org", "https://acme.com", "https://acme.com:8443", - "https://acme.com:9000" + "https://acme.com:9000", ), rejectOrigins = List( "https://shop.example.org" ), - allowOriginPort = true + allowOriginPort = true, ) } } @@ -371,15 +426,15 @@ class RelyingPartyRegistrationSpec extends FunSpec with Matchers with ScalaCheck origins = origins, acceptOrigins = List( "https://example.org", - "https://acme.com:8443" + "https://acme.com:8443", ), rejectOrigins = List( "https://example.org:8443", "https://accounts.example.org", "https://acme.com", - "https://shop.acme.com:8443" + "https://shop.acme.com:8443", ), - allowOriginSubdomain = false + allowOriginSubdomain = false, ) } @@ -390,13 +445,13 @@ class RelyingPartyRegistrationSpec extends FunSpec with Matchers with ScalaCheck "https://example.org", "https://accounts.example.org", "https://acme.com:8443", - "https://shop.acme.com:8443" + "https://shop.acme.com:8443", ), rejectOrigins = List( "https://example.org:8443", - "https://acme.com" + "https://acme.com", ), - allowOriginSubdomain = true + allowOriginSubdomain = true, ) } } @@ -405,147 +460,210 @@ class RelyingPartyRegistrationSpec extends FunSpec with Matchers with ScalaCheck 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.") { it("Verification succeeds if neither side uses token binding ID.") { - val steps = finishRegistration(testData = RegistrationTestData.FidoU2f.BasicAttestation) - val step: FinishRegistrationSteps#Step6 = steps.begin.next.next.next.next.next + val steps = finishRegistration(testData = + RegistrationTestData.FidoU2f.BasicAttestation + ) + val step: FinishRegistrationSteps#Step6 = + steps.begin.next.next.next.next.next - step.validations shouldBe a [Success[_]] - step.tryNext shouldBe a [Success[_]] + step.validations shouldBe a[Success[_]] + step.tryNext shouldBe a[Success[_]] } it("Verification succeeds if client data specifies token binding is unsupported, and RP does not use it.") { - val steps = finishRegistration(testData = RegistrationTestData.FidoU2f.BasicAttestation - .editClientData(_.without[ObjectNode]("tokenBinding")) + val steps = finishRegistration(testData = + RegistrationTestData.FidoU2f.BasicAttestation + .editClientData(_.without[ObjectNode]("tokenBinding")) ) - val step: FinishRegistrationSteps#Step6 = steps.begin.next.next.next.next.next + val step: FinishRegistrationSteps#Step6 = + steps.begin.next.next.next.next.next - step.validations shouldBe a [Success[_]] - step.tryNext shouldBe a [Success[_]] + step.validations shouldBe a[Success[_]] + step.tryNext shouldBe a[Success[_]] } it("Verification succeeds if client data specifies token binding is supported, and RP does not use it.") { - val steps = finishRegistration(testData = RegistrationTestData.FidoU2f.BasicAttestation - .editClientData("tokenBinding", toJson(Map("status" -> "supported"))) + val steps = finishRegistration(testData = + RegistrationTestData.FidoU2f.BasicAttestation + .editClientData( + "tokenBinding", + toJson(Map("status" -> "supported")), + ) ) - val step: FinishRegistrationSteps#Step6 = steps.begin.next.next.next.next.next + val step: FinishRegistrationSteps#Step6 = + steps.begin.next.next.next.next.next - step.validations shouldBe a [Success[_]] - step.tryNext shouldBe a [Success[_]] + step.validations shouldBe a[Success[_]] + step.tryNext shouldBe a[Success[_]] } it("Verification fails if client data does not specify token binding status and RP specifies token binding ID.") { val steps = finishRegistration( - callerTokenBindingId = Some(ByteArray.fromBase64Url("YELLOWSUBMARINE")), - testData = RegistrationTestData.FidoU2f.BasicAttestation.editClientData(_.without[ObjectNode]("tokenBinding")) + callerTokenBindingId = + Some(ByteArray.fromBase64Url("YELLOWSUBMARINE")), + testData = RegistrationTestData.FidoU2f.BasicAttestation + .editClientData(_.without[ObjectNode]("tokenBinding")), ) - val step: FinishRegistrationSteps#Step6 = steps.begin.next.next.next.next.next + val step: FinishRegistrationSteps#Step6 = + steps.begin.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[Failure[_]] + step.validations.failed.get shouldBe an[IllegalArgumentException] + step.tryNext shouldBe a[Failure[_]] } it("Verification succeeds if client data does not specify token binding status and RP does not specify token binding ID.") { val steps = finishRegistration( callerTokenBindingId = None, - testData = RegistrationTestData.FidoU2f.BasicAttestation.editClientData(_.without[ObjectNode]("tokenBinding")) + testData = RegistrationTestData.FidoU2f.BasicAttestation + .editClientData(_.without[ObjectNode]("tokenBinding")), ) - val step: FinishRegistrationSteps#Step6 = steps.begin.next.next.next.next.next + val step: FinishRegistrationSteps#Step6 = + steps.begin.next.next.next.next.next - step.validations shouldBe a [Success[_]] - step.tryNext shouldBe a [Success[_]] + step.validations shouldBe a[Success[_]] + step.tryNext shouldBe a[Success[_]] } it("Verification fails if client data specifies token binding ID but RP does not.") { val steps = finishRegistration( callerTokenBindingId = None, - testData = RegistrationTestData.FidoU2f.BasicAttestation.editClientData("tokenBinding", toJson(Map("status" -> "present", "id" -> "YELLOWSUBMARINE"))) + testData = + RegistrationTestData.FidoU2f.BasicAttestation.editClientData( + "tokenBinding", + toJson(Map("status" -> "present", "id" -> "YELLOWSUBMARINE")), + ), ) - val step: FinishRegistrationSteps#Step6 = steps.begin.next.next.next.next.next + val step: FinishRegistrationSteps#Step6 = + steps.begin.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[Failure[_]] + step.validations.failed.get shouldBe an[IllegalArgumentException] + step.tryNext shouldBe a[Failure[_]] } describe("If Token Binding was used on that TLS connection, also verify that C.tokenBinding.id matches the base64url encoding of the Token Binding ID for the connection.") { it("Verification succeeds if both sides specify the same token binding ID.") { val steps = finishRegistration( - callerTokenBindingId = Some(ByteArray.fromBase64Url("YELLOWSUBMARINE")), - testData = RegistrationTestData.FidoU2f.BasicAttestation.editClientData("tokenBinding", toJson(Map("status" -> "present", "id" -> "YELLOWSUBMARINE"))) + callerTokenBindingId = + Some(ByteArray.fromBase64Url("YELLOWSUBMARINE")), + testData = + RegistrationTestData.FidoU2f.BasicAttestation.editClientData( + "tokenBinding", + toJson( + Map("status" -> "present", "id" -> "YELLOWSUBMARINE") + ), + ), ) - val step: FinishRegistrationSteps#Step6 = steps.begin.next.next.next.next.next + val step: FinishRegistrationSteps#Step6 = + steps.begin.next.next.next.next.next - step.validations shouldBe a [Success[_]] - step.tryNext shouldBe a [Success[_]] + step.validations shouldBe a[Success[_]] + step.tryNext shouldBe a[Success[_]] } it("Verification fails if ID is missing from tokenBinding in client data.") { val steps = finishRegistration( - callerTokenBindingId = Some(ByteArray.fromBase64Url("YELLOWSUBMARINE")), - testData = RegistrationTestData.FidoU2f.BasicAttestation.editClientData("tokenBinding", toJson(Map("status" -> "present"))) + callerTokenBindingId = + Some(ByteArray.fromBase64Url("YELLOWSUBMARINE")), + testData = + RegistrationTestData.FidoU2f.BasicAttestation.editClientData( + "tokenBinding", + toJson(Map("status" -> "present")), + ), ) - val step: FinishRegistrationSteps#Step6 = steps.begin.next.next.next.next.next + val step: FinishRegistrationSteps#Step6 = + steps.begin.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[Failure[_]] + step.validations.failed.get shouldBe an[IllegalArgumentException] + step.tryNext shouldBe a[Failure[_]] } it("Verification fails if RP specifies token binding ID but client does not support it.") { val steps = finishRegistration( - callerTokenBindingId = Some(ByteArray.fromBase64Url("YELLOWSUBMARINE")), - testData = RegistrationTestData.FidoU2f.BasicAttestation.editClientData(_.without[ObjectNode]("tokenBinding")) + callerTokenBindingId = + Some(ByteArray.fromBase64Url("YELLOWSUBMARINE")), + testData = RegistrationTestData.FidoU2f.BasicAttestation + .editClientData(_.without[ObjectNode]("tokenBinding")), ) - val step: FinishRegistrationSteps#Step6 = steps.begin.next.next.next.next.next + val step: FinishRegistrationSteps#Step6 = + steps.begin.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[Failure[_]] + step.validations.failed.get shouldBe an[IllegalArgumentException] + step.tryNext shouldBe a[Failure[_]] } it("Verification fails if RP specifies token binding ID but client does not use it.") { val steps = finishRegistration( - callerTokenBindingId = Some(ByteArray.fromBase64Url("YELLOWSUBMARINE")), - testData = RegistrationTestData.FidoU2f.BasicAttestation.editClientData("tokenBinding", toJson(Map("status" -> "supported"))) + callerTokenBindingId = + Some(ByteArray.fromBase64Url("YELLOWSUBMARINE")), + testData = + RegistrationTestData.FidoU2f.BasicAttestation.editClientData( + "tokenBinding", + toJson(Map("status" -> "supported")), + ), ) - val step: FinishRegistrationSteps#Step6 = steps.begin.next.next.next.next.next + val step: FinishRegistrationSteps#Step6 = + steps.begin.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[Failure[_]] + step.validations.failed.get shouldBe an[IllegalArgumentException] + step.tryNext shouldBe a[Failure[_]] } it("Verification fails if client data and RP specify different token binding IDs.") { val steps = finishRegistration( - callerTokenBindingId = Some(ByteArray.fromBase64Url("ORANGESUBMARINE")), - testData = RegistrationTestData.FidoU2f.BasicAttestation.editClientData("tokenBinding", toJson(Map("status" -> "supported", "id" -> "YELLOWSUBMARINE"))) + callerTokenBindingId = + Some(ByteArray.fromBase64Url("ORANGESUBMARINE")), + testData = + RegistrationTestData.FidoU2f.BasicAttestation.editClientData( + "tokenBinding", + toJson( + Map("status" -> "supported", "id" -> "YELLOWSUBMARINE") + ), + ), ) - val step: FinishRegistrationSteps#Step6 = steps.begin.next.next.next.next.next + val step: FinishRegistrationSteps#Step6 = + steps.begin.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[Failure[_]] + step.validations.failed.get shouldBe an[IllegalArgumentException] + step.tryNext shouldBe a[Failure[_]] } } } it("7. Compute the hash of response.clientDataJSON using SHA-256.") { - val steps = finishRegistration(testData = RegistrationTestData.FidoU2f.BasicAttestation) - val step: FinishRegistrationSteps#Step7 = steps.begin.next.next.next.next.next.next + val steps = finishRegistration(testData = + RegistrationTestData.FidoU2f.BasicAttestation + ) + val step: FinishRegistrationSteps#Step7 = + steps.begin.next.next.next.next.next.next val digest = MessageDigest.getInstance("SHA-256") - step.validations shouldBe a [Success[_]] - step.tryNext shouldBe a [Success[_]] - step.clientDataJsonHash should equal (new ByteArray(digest.digest(RegistrationTestData.FidoU2f.BasicAttestation.clientDataJsonBytes.getBytes))) + step.validations shouldBe a[Success[_]] + step.tryNext shouldBe a[Success[_]] + step.clientDataJsonHash should equal( + new ByteArray( + digest.digest( + RegistrationTestData.FidoU2f.BasicAttestation.clientDataJsonBytes.getBytes + ) + ) + ) } it("8. Perform CBOR decoding on the attestationObject field of the AuthenticatorAttestationResponse structure to obtain the attestation statement format fmt, the authenticator data authData, and the attestation statement attStmt.") { - val steps = finishRegistration(testData = RegistrationTestData.FidoU2f.BasicAttestation) - val step: FinishRegistrationSteps#Step8 = steps.begin.next.next.next.next.next.next.next + val steps = finishRegistration(testData = + RegistrationTestData.FidoU2f.BasicAttestation + ) + val step: FinishRegistrationSteps#Step8 = + steps.begin.next.next.next.next.next.next.next - step.validations shouldBe a [Success[_]] - step.tryNext shouldBe a [Success[_]] - step.attestation.getFormat should equal ("fido-u2f") + step.validations shouldBe a[Success[_]] + step.tryNext shouldBe a[Success[_]] + step.attestation.getFormat should equal("fido-u2f") step.attestation.getAuthenticatorData should not be null step.attestation.getAttestationStatement should not be null } @@ -553,63 +671,109 @@ class RelyingPartyRegistrationSpec extends FunSpec with Matchers with ScalaCheck describe("9. Verify that the rpIdHash in authData is the SHA-256 hash of the RP ID expected by the Relying Party.") { it("Fails if RP ID is different.") { val steps = finishRegistration( - testData = RegistrationTestData.FidoU2f.BasicAttestation.editAuthenticatorData { authData: ByteArray => - new ByteArray(Array.fill[Byte](32)(0) ++ authData.getBytes.drop(32)) - } + testData = RegistrationTestData.FidoU2f.BasicAttestation + .editAuthenticatorData { authData: ByteArray => + new ByteArray( + Array.fill[Byte](32)(0) ++ authData.getBytes.drop(32) + ) + } ) - val step: FinishRegistrationSteps#Step9 = steps.begin.next.next.next.next.next.next.next.next + val step: FinishRegistrationSteps#Step9 = + steps.begin.next.next.next.next.next.next.next.next - step.validations shouldBe a [Failure[_]] - step.validations.failed.get shouldBe an [IllegalArgumentException] - step.tryNext shouldBe a [Failure[_]] + step.validations shouldBe a[Failure[_]] + step.validations.failed.get shouldBe an[IllegalArgumentException] + step.tryNext shouldBe a[Failure[_]] } it("Succeeds if RP ID is the same.") { - val steps = finishRegistration(testData = RegistrationTestData.FidoU2f.BasicAttestation) - val step: FinishRegistrationSteps#Step9 = steps.begin.next.next.next.next.next.next.next.next + val steps = finishRegistration(testData = + RegistrationTestData.FidoU2f.BasicAttestation + ) + val step: FinishRegistrationSteps#Step9 = + steps.begin.next.next.next.next.next.next.next.next - step.validations shouldBe a [Success[_]] - step.tryNext shouldBe a [Success[_]] + step.validations shouldBe a[Success[_]] + step.tryNext shouldBe a[Success[_]] } } { val testData = RegistrationTestData.Packed.BasicAttestation - def upOn(authData: ByteArray): ByteArray = new ByteArray(authData.getBytes.updated(32, (authData.getBytes()(32) | 0x01).toByte)) - def upOff(authData: ByteArray): ByteArray = new ByteArray(authData.getBytes.updated(32, (authData.getBytes()(32) & 0xfe).toByte)) + def upOn(authData: ByteArray): ByteArray = + new ByteArray( + authData.getBytes + .updated(32, (authData.getBytes()(32) | 0x01).toByte) + ) + def upOff(authData: ByteArray): ByteArray = + new ByteArray( + authData.getBytes + .updated(32, (authData.getBytes()(32) & 0xfe).toByte) + ) - def uvOn(authData: ByteArray): ByteArray = new ByteArray(authData.getBytes.updated(32, (authData.getBytes()(32) | 0x04).toByte)) - def uvOff(authData: ByteArray): ByteArray = new ByteArray(authData.getBytes.updated(32, (authData.getBytes()(32) & 0xfb).toByte)) + def uvOn(authData: ByteArray): ByteArray = + new ByteArray( + authData.getBytes + .updated(32, (authData.getBytes()(32) | 0x04).toByte) + ) + def uvOff(authData: ByteArray): ByteArray = + new ByteArray( + authData.getBytes + .updated(32, (authData.getBytes()(32) & 0xfb).toByte) + ) - def checks[Next <: FinishRegistrationSteps.Step[_], Step <: FinishRegistrationSteps.Step[Next]](stepsToStep: FinishRegistrationSteps => Step) = { - def check[B] - (stepsToStep: FinishRegistrationSteps => Step) - (chk: Step => B) - (uvr: UserVerificationRequirement, authDataEdit: ByteArray => ByteArray) - : B = { + def checks[Next <: FinishRegistrationSteps.Step[ + _ + ], Step <: FinishRegistrationSteps.Step[Next]]( + stepsToStep: FinishRegistrationSteps => Step + ) = { + def check[B]( + stepsToStep: FinishRegistrationSteps => Step + )(chk: Step => B)( + uvr: UserVerificationRequirement, + authDataEdit: ByteArray => ByteArray, + ): B = { val steps = finishRegistration( - testData = testData.copy( - authenticatorSelection = Some(AuthenticatorSelectionCriteria.builder().userVerification(uvr).build()) - ).editAuthenticatorData(authDataEdit) + testData = testData + .copy( + authenticatorSelection = Some( + AuthenticatorSelectionCriteria + .builder() + .userVerification(uvr) + .build() + ) + ) + .editAuthenticatorData(authDataEdit) ) chk(stepsToStep(steps)) } - def checkFailsWith(stepsToStep: FinishRegistrationSteps => Step): (UserVerificationRequirement, ByteArray => ByteArray) => Unit = check(stepsToStep) { step => - step.validations shouldBe a [Failure[_]] - step.validations.failed.get shouldBe an [IllegalArgumentException] - step.tryNext shouldBe a [Failure[_]] - } - def checkSucceedsWith(stepsToStep: FinishRegistrationSteps => Step): (UserVerificationRequirement, ByteArray => ByteArray) => Unit = check(stepsToStep) { step => - step.validations shouldBe a [Success[_]] - step.tryNext shouldBe a [Success[_]] - } + def checkFailsWith( + stepsToStep: FinishRegistrationSteps => Step + ): (UserVerificationRequirement, ByteArray => ByteArray) => Unit = + check(stepsToStep) { step => + step.validations shouldBe a[Failure[_]] + step.validations.failed.get shouldBe an[ + IllegalArgumentException + ] + step.tryNext shouldBe a[Failure[_]] + } + def checkSucceedsWith( + stepsToStep: FinishRegistrationSteps => Step + ): (UserVerificationRequirement, ByteArray => ByteArray) => Unit = + check(stepsToStep) { step => + step.validations shouldBe a[Success[_]] + step.tryNext shouldBe a[Success[_]] + } (checkFailsWith(stepsToStep), checkSucceedsWith(stepsToStep)) } describe("10. Verify that the User Present bit of the flags in authData is set.") { - val (checkFails, checkSucceeds) = checks[FinishRegistrationSteps#Step11, FinishRegistrationSteps#Step10](_.begin.next.next.next.next.next.next.next.next.next) + val (checkFails, checkSucceeds) = checks[ + FinishRegistrationSteps#Step11, + FinishRegistrationSteps#Step10, + ](_.begin.next.next.next.next.next.next.next.next.next) it("Fails if UV is discouraged and flag is not set.") { checkFails(UserVerificationRequirement.DISCOURAGED, upOff) @@ -628,16 +792,25 @@ class RelyingPartyRegistrationSpec extends FunSpec with Matchers with ScalaCheck } it("Fails if UV is required and flag is not set.") { - checkFails(UserVerificationRequirement.REQUIRED, upOff _ andThen uvOn) + checkFails( + UserVerificationRequirement.REQUIRED, + upOff _ andThen uvOn, + ) } it("Succeeds if UV is required and flag is set.") { - checkSucceeds(UserVerificationRequirement.REQUIRED, upOn _ andThen uvOn) + checkSucceeds( + UserVerificationRequirement.REQUIRED, + upOn _ andThen uvOn, + ) } } describe("11. If user verification is required for this registration, verify that the User Verified bit of the flags in authData is set.") { - val (checkFails, checkSucceeds) = checks[FinishRegistrationSteps#Step12, FinishRegistrationSteps#Step11](_.begin.next.next.next.next.next.next.next.next.next.next) + val (checkFails, checkSucceeds) = checks[ + FinishRegistrationSteps#Step12, + FinishRegistrationSteps#Step11, + ](_.begin.next.next.next.next.next.next.next.next.next.next) it("Succeeds if UV is discouraged and flag is not set.") { checkSucceeds(UserVerificationRequirement.DISCOURAGED, uvOff) @@ -669,78 +842,190 @@ class RelyingPartyRegistrationSpec extends FunSpec with Matchers with ScalaCheck describe("client extension outputs in clientExtensionResults are as expected, considering the client extension input values that were given as the extensions option in the create() call. In particular, any extension identifier values in the clientExtensionResults MUST be also be present as extension identifier values in the extensions member of options, i.e., no extensions are present that were not requested. In the general case, the meaning of \"are as expected\" is specific to the Relying Party and which extensions are in use.") { ignore("Fails if clientExtensionResults is not a subset of the extensions requested by the Relying Party.") { - forAll(anyRegistrationExtensions) { case (extensionInputs, clientExtensionOutputs) => - whenever(clientExtensionOutputs.getExtensionIds.asScala.exists(id => !extensionInputs.getExtensionIds.contains(id))) { - val steps = finishRegistration( - testData = RegistrationTestData.Packed.BasicAttestation.copy( - requestedExtensions = extensionInputs, - clientExtensionResults = clientExtensionOutputs + forAll(anyRegistrationExtensions) { + case (extensionInputs, clientExtensionOutputs) => + whenever( + clientExtensionOutputs.getExtensionIds.asScala.exists(id => + !extensionInputs.getExtensionIds.contains(id) ) - ) - val step: FinishRegistrationSteps#Step12 = steps.begin.next.next.next.next.next.next.next.next.next.next.next + ) { + val steps = finishRegistration( + testData = + RegistrationTestData.Packed.BasicAttestation.copy( + requestedExtensions = extensionInputs, + clientExtensionResults = clientExtensionOutputs, + ) + ) + val step: FinishRegistrationSteps#Step12 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next + + step.validations shouldBe a[Failure[_]] + step.validations.failed.get shouldBe an[ + IllegalArgumentException + ] + step.tryNext shouldBe a[Failure[_]] + } + } + } - step.validations shouldBe a [Failure[_]] - step.validations.failed.get shouldBe an [IllegalArgumentException] - step.tryNext shouldBe a [Failure[_]] - } + ignore("Succeeds if clientExtensionResults is not a subset of the extensions requested by the Relying Party, but the Relying Party has enabled allowing unrequested extensions.") { + forAll(anyRegistrationExtensions) { + case (extensionInputs, clientExtensionOutputs) => + whenever( + clientExtensionOutputs.getExtensionIds.asScala.exists(id => + !extensionInputs.getExtensionIds.contains(id) + ) + ) { + val steps = finishRegistration( + allowUnrequestedExtensions = true, + testData = + RegistrationTestData.Packed.BasicAttestation.copy( + requestedExtensions = extensionInputs, + clientExtensionResults = clientExtensionOutputs, + ), + ) + val step: FinishRegistrationSteps#Step12 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next + + step.validations shouldBe a[Success[_]] + step.tryNext shouldBe a[Success[_]] + } } } it("Succeeds if clientExtensionResults is a subset of the extensions requested by the Relying Party.") { - forAll(subsetRegistrationExtensions) { case (extensionInputs, clientExtensionOutputs) => - val steps = finishRegistration( - testData = RegistrationTestData.Packed.BasicAttestation.copy( - requestedExtensions = extensionInputs, - clientExtensionResults = clientExtensionOutputs + forAll(subsetRegistrationExtensions) { + case (extensionInputs, clientExtensionOutputs) => + val steps = finishRegistration( + testData = + RegistrationTestData.Packed.BasicAttestation.copy( + requestedExtensions = extensionInputs, + clientExtensionResults = clientExtensionOutputs, + ) ) - ) - val step: FinishRegistrationSteps#Step12 = steps.begin.next.next.next.next.next.next.next.next.next.next.next + val step: FinishRegistrationSteps#Step12 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next - step.validations shouldBe a [Success[_]] - step.tryNext shouldBe a [Success[_]] + step.validations shouldBe a[Success[_]] + step.tryNext shouldBe a[Success[_]] } } } describe("authenticator extension outputs in the extensions in authData are as expected, considering the client extension input values that were given as the extensions option in the create() call. In particular, any extension identifier values in the extensions in authData MUST be also be present as extension identifier values in the extensions member of options, i.e., no extensions are present that were not requested. In the general case, the meaning of \"are as expected\" is specific to the Relying Party and which extensions are in use.") { it("Fails if authenticator extensions is not a subset of the extensions requested by the Relying Party.") { - forAll(anyAuthenticatorExtensions[RegistrationExtensionInputs]) { case (extensionInputs: RegistrationExtensionInputs, authenticatorExtensionOutputs: ObjectNode) => - whenever(authenticatorExtensionOutputs.fieldNames().asScala.exists(id => !extensionInputs.getExtensionIds.contains(id))) { - val steps = finishRegistration( - testData = RegistrationTestData.Packed.BasicAttestation.copy( - requestedExtensions = extensionInputs - ).editAuthenticatorData( - authData => new ByteArray( - authData.getBytes.updated(32, (authData.getBytes()(32) | 0x80).toByte) ++ - JacksonCodecs.cbor.writeValueAsBytes(authenticatorExtensionOutputs) + forAll(anyAuthenticatorExtensions[RegistrationExtensionInputs]) { + case ( + extensionInputs: RegistrationExtensionInputs, + authenticatorExtensionOutputs: ObjectNode, + ) => + whenever( + authenticatorExtensionOutputs + .fieldNames() + .asScala + .exists(id => + !extensionInputs.getExtensionIds.contains(id) ) + ) { + val steps = finishRegistration( + testData = RegistrationTestData.Packed.BasicAttestation + .copy( + requestedExtensions = extensionInputs + ) + .editAuthenticatorData(authData => + new ByteArray( + authData.getBytes.updated( + 32, + (authData.getBytes()(32) | 0x80).toByte, + ) ++ + JacksonCodecs.cbor.writeValueAsBytes( + authenticatorExtensionOutputs + ) + ) + ) ) - ) - val step: FinishRegistrationSteps#Step12 = steps.begin.next.next.next.next.next.next.next.next.next.next.next + val step: FinishRegistrationSteps#Step12 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next + + step.validations shouldBe a[Failure[_]] + step.validations.failed.get shouldBe an[ + IllegalArgumentException + ] + step.tryNext shouldBe a[Failure[_]] + } + } + } - step.validations shouldBe a [Failure[_]] - step.validations.failed.get shouldBe an [IllegalArgumentException] - step.tryNext shouldBe a [Failure[_]] - } + it("Succeeds if authenticator extensions is not a subset of the extensions requested by the Relying Party, but the Relying Party has enabled allowing unrequested extensions.") { + forAll(anyAuthenticatorExtensions[RegistrationExtensionInputs]) { + case ( + extensionInputs: RegistrationExtensionInputs, + authenticatorExtensionOutputs: ObjectNode, + ) => + whenever( + authenticatorExtensionOutputs + .fieldNames() + .asScala + .exists(id => + !extensionInputs.getExtensionIds.contains(id) + ) + ) { + val steps = finishRegistration( + allowUnrequestedExtensions = true, + testData = RegistrationTestData.Packed.BasicAttestation + .copy( + requestedExtensions = extensionInputs + ) + .editAuthenticatorData(authData => + new ByteArray( + authData.getBytes.updated( + 32, + (authData.getBytes()(32) | 0x80).toByte, + ) ++ + JacksonCodecs.cbor.writeValueAsBytes( + authenticatorExtensionOutputs + ) + ) + ), + ) + val step: FinishRegistrationSteps#Step12 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next + + step.validations shouldBe a[Success[_]] + step.tryNext shouldBe a[Success[_]] + } } } it("Succeeds if authenticator extensions is a subset of the extensions requested by the Relying Party.") { - forAll(subsetAuthenticatorExtensions[RegistrationExtensionInputs]) { case (extensionInputs: RegistrationExtensionInputs, authenticatorExtensionOutputs: ObjectNode) => - val steps = finishRegistration( - testData = RegistrationTestData.Packed.BasicAttestation.copy( - requestedExtensions = extensionInputs - ).editAuthenticatorData( - authData => new ByteArray( - authData.getBytes.updated(32, (authData.getBytes()(32) | 0x80).toByte) ++ - JacksonCodecs.cbor.writeValueAsBytes(authenticatorExtensionOutputs) - ) + forAll( + subsetAuthenticatorExtensions[RegistrationExtensionInputs] + ) { + case ( + extensionInputs: RegistrationExtensionInputs, + authenticatorExtensionOutputs: ObjectNode, + ) => + val steps = finishRegistration( + testData = RegistrationTestData.Packed.BasicAttestation + .copy( + requestedExtensions = extensionInputs + ) + .editAuthenticatorData(authData => + new ByteArray( + authData.getBytes.updated( + 32, + (authData.getBytes()(32) | 0x80).toByte, + ) ++ + JacksonCodecs.cbor + .writeValueAsBytes(authenticatorExtensionOutputs) + ) + ) ) - ) - val step: FinishRegistrationSteps#Step12 = steps.begin.next.next.next.next.next.next.next.next.next.next.next + val step: FinishRegistrationSteps#Step12 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next - step.validations shouldBe a [Success[_]] - step.tryNext shouldBe a [Success[_]] + step.validations shouldBe a[Success[_]] + step.tryNext shouldBe a[Success[_]] } } } @@ -750,18 +1035,20 @@ class RelyingPartyRegistrationSpec extends FunSpec with Matchers with ScalaCheck describe("13. Determine the attestation statement format by performing a USASCII case-sensitive match on fmt against the set of supported WebAuthn Attestation Statement Format Identifier values. An up-to-date list of registered WebAuthn Attestation Statement Format Identifier values is maintained in the IANA registry of the same name [WebAuthn-Registries].") { def setup(format: String): FinishRegistrationSteps = { finishRegistration( - testData = RegistrationTestData.FidoU2f.BasicAttestation.setAttestationStatementFormat(format) + testData = RegistrationTestData.FidoU2f.BasicAttestation + .setAttestationStatementFormat(format) ) } def checkUnknown(format: String): Unit = { it(s"""Returns no known attestation statement verifier if fmt is "${format}".""") { val steps = setup(format) - val step: FinishRegistrationSteps#Step13 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next + val step: FinishRegistrationSteps#Step13 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next - step.validations shouldBe a [Success[_]] - step.tryNext shouldBe a [Success[_]] - step.format should equal (format) + step.validations shouldBe a[Success[_]] + step.tryNext shouldBe a[Success[_]] + step.format should equal(format) step.attestationStatementVerifier.asScala shouldBe empty } } @@ -769,11 +1056,12 @@ class RelyingPartyRegistrationSpec extends FunSpec with Matchers with ScalaCheck def checkKnown(format: String): Unit = { it(s"""Returns a known attestation statement verifier if fmt is "${format}".""") { val steps = setup(format) - val step: FinishRegistrationSteps#Step13 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next + val step: FinishRegistrationSteps#Step13 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next - step.validations shouldBe a [Success[_]] - step.tryNext shouldBe a [Success[_]] - step.format should equal (format) + step.validations shouldBe a[Success[_]] + step.tryNext shouldBe a[Success[_]] + step.format should equal(format) step.attestationStatementVerifier.asScala should not be empty } } @@ -795,213 +1083,318 @@ class RelyingPartyRegistrationSpec extends FunSpec with Matchers with ScalaCheck describe("If allowUntrustedAttestation is set,") { it("a fido-u2f attestation is still rejected if invalid.") { - val testData = RegistrationTestData.FidoU2f.BasicAttestation.updateAttestationObject("attStmt", { attStmtNode: JsonNode => - attStmtNode.asInstanceOf[ObjectNode] - .set[ObjectNode]("sig", jsonFactory.binaryNode(Array(0, 0, 0, 0))) - }) + val testData = RegistrationTestData.FidoU2f.BasicAttestation + .updateAttestationObject( + "attStmt", + { attStmtNode: JsonNode => + attStmtNode + .asInstanceOf[ObjectNode] + .set[ObjectNode]( + "sig", + jsonFactory.binaryNode(Array(0, 0, 0, 0)), + ) + }, + ) val steps = finishRegistration( testData = testData, - allowUntrustedAttestation = true + allowUntrustedAttestation = true, ) - val step: FinishRegistrationSteps#Step14 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next + val step: FinishRegistrationSteps#Step14 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next - step.validations shouldBe a [Failure[_]] - step.validations.failed.get.getCause shouldBe a [SignatureException] - step.tryNext shouldBe a [Failure[_]] + step.validations shouldBe a[Failure[_]] + step.validations.failed.get.getCause shouldBe a[ + SignatureException + ] + step.tryNext shouldBe a[Failure[_]] } } describe("For the fido-u2f statement format,") { it("the default test case is a valid basic attestation.") { - val steps = finishRegistration(testData = RegistrationTestData.FidoU2f.BasicAttestation) - val step: FinishRegistrationSteps#Step14 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next + val steps = finishRegistration(testData = + RegistrationTestData.FidoU2f.BasicAttestation + ) + val step: FinishRegistrationSteps#Step14 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next - step.validations shouldBe a [Success[_]] - step.attestationType should equal (AttestationType.BASIC) - step.tryNext shouldBe a [Success[_]] + step.validations shouldBe a[Success[_]] + step.attestationType should equal(AttestationType.BASIC) + step.tryNext shouldBe a[Success[_]] } it("a test case with self attestation is valid.") { - val steps = finishRegistration(testData = RegistrationTestData.FidoU2f.SelfAttestation) - val step: FinishRegistrationSteps#Step14 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next + val steps = finishRegistration(testData = + RegistrationTestData.FidoU2f.SelfAttestation + ) + val step: FinishRegistrationSteps#Step14 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next - step.validations shouldBe a [Success[_]] - step.attestationType should equal (AttestationType.SELF_ATTESTATION) - step.tryNext shouldBe a [Success[_]] + step.validations shouldBe a[Success[_]] + step.attestationType should equal( + AttestationType.SELF_ATTESTATION + ) + step.tryNext shouldBe a[Success[_]] } it("a test case with different signed client data is not valid.") { val testData = RegistrationTestData.FidoU2f.SelfAttestation - val steps = finishRegistration(testData = RegistrationTestData.FidoU2f.BasicAttestation) + val steps = finishRegistration(testData = + RegistrationTestData.FidoU2f.BasicAttestation + ) val step: FinishRegistrationSteps#Step14 = new steps.Step14( - Crypto.hash(new ByteArray(testData.clientDataJsonBytes.getBytes.updated(20, (testData.clientDataJsonBytes.getBytes()(20) + 1).toByte))), + Crypto.sha256( + new ByteArray( + testData.clientDataJsonBytes.getBytes.updated( + 20, + (testData.clientDataJsonBytes.getBytes()(20) + 1).toByte, + ) + ) + ), new AttestationObject(testData.attestationObject), Some(new FidoU2fAttestationStatementVerifier).asJava, - Nil.asJava + Nil.asJava, ) - step.validations shouldBe a [Failure[_]] - step.validations.failed.get shouldBe an [IllegalArgumentException] - step.tryNext shouldBe a [Failure[_]] + step.validations shouldBe a[Failure[_]] + step.validations.failed.get shouldBe an[IllegalArgumentException] + step.tryNext shouldBe a[Failure[_]] } def checkByteFlipFails(index: Int): Unit = { - val testData = RegistrationTestData.FidoU2f.BasicAttestation.editAuthenticatorData { flipByte(index, _) } + val testData = RegistrationTestData.FidoU2f.BasicAttestation + .editAuthenticatorData { flipByte(index, _) } val steps = finishRegistration( testData = testData, - credentialId = Some(new ByteArray(Array.fill(16)(0))) + credentialId = Some(new ByteArray(Array.fill(16)(0))), ) val step: FinishRegistrationSteps#Step14 = new steps.Step14( - Crypto.hash(testData.clientDataJsonBytes), + Crypto.sha256(testData.clientDataJsonBytes), new AttestationObject(testData.attestationObject), Some(new FidoU2fAttestationStatementVerifier).asJava, - Nil.asJava + Nil.asJava, ) - step.validations shouldBe a [Failure[_]] - step.validations.failed.get shouldBe an [IllegalArgumentException] - step.tryNext shouldBe a [Failure[_]] + step.validations shouldBe a[Failure[_]] + step.validations.failed.get shouldBe an[IllegalArgumentException] + step.tryNext shouldBe a[Failure[_]] } it("a test case with a different signed RP ID hash is not valid.") { checkByteFlipFails(0) } - it("a test case with a different signed credential ID is not valid.") { + it( + "a test case with a different signed credential ID is not valid." + ) { checkByteFlipFails(32 + 1 + 4 + 16 + 2 + 1) } it("a test case with a different signed credential public key is not valid.") { - val testData = RegistrationTestData.FidoU2f.BasicAttestation.editAuthenticatorData { authenticatorData => - val decoded = new AuthenticatorData(authenticatorData) - val L = decoded.getAttestedCredentialData.get.getCredentialId.getBytes.length - val evilPublicKey: ByteArray = - WebAuthnTestCodecs.publicKeyToCose( - TestAuthenticator.generateKeypair( - WebAuthnTestCodecs.getCoseAlgId(decoded.getAttestedCredentialData.get.getCredentialPublicKey) - ).getPublic - ) + val testData = RegistrationTestData.FidoU2f.BasicAttestation + .editAuthenticatorData { authenticatorData => + val decoded = new AuthenticatorData(authenticatorData) + val L = + decoded.getAttestedCredentialData.get.getCredentialId.getBytes.length + val evilPublicKey: ByteArray = + WebAuthnTestCodecs.publicKeyToCose( + TestAuthenticator + .generateKeypair( + WebAuthnTestCodecs.getCoseAlgId( + decoded.getAttestedCredentialData.get.getCredentialPublicKey + ) + ) + .getPublic + ) - new ByteArray(authenticatorData.getBytes.take(32 + 1 + 4 + 16 + 2 + L) ++ evilPublicKey.getBytes) - } + new ByteArray( + authenticatorData.getBytes.take( + 32 + 1 + 4 + 16 + 2 + L + ) ++ evilPublicKey.getBytes + ) + } val steps = finishRegistration( testData = testData, - credentialId = Some(new ByteArray(Array.fill(16)(0))) + credentialId = Some(new ByteArray(Array.fill(16)(0))), ) val step: FinishRegistrationSteps#Step14 = new steps.Step14( - Crypto.hash(testData.clientDataJsonBytes), + Crypto.sha256(testData.clientDataJsonBytes), new AttestationObject(testData.attestationObject), Some(new FidoU2fAttestationStatementVerifier).asJava, - Nil.asJava + Nil.asJava, ) - step.validations shouldBe a [Failure[_]] - step.validations.failed.get shouldBe an [IllegalArgumentException] - step.tryNext shouldBe a [Failure[_]] + step.validations shouldBe a[Failure[_]] + step.validations.failed.get shouldBe an[IllegalArgumentException] + step.tryNext shouldBe a[Failure[_]] } describe("if x5c is not a certificate for an ECDSA public key over the P-256 curve, stop verification and return an error.") { val testAuthenticator = TestAuthenticator - def checkRejected(attestationAlg: COSEAlgorithmIdentifier, keypair: KeyPair): Unit = { - val (credential, _) = testAuthenticator.createBasicAttestedCredential(attestationMaker = AttestationMaker.fidoU2f(new AttestationCert(attestationAlg, testAuthenticator.generateAttestationCertificate(attestationAlg, Some(keypair))))) + 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") + attestationObject = + credential.getResponse.getAttestationObject, + clientDataJson = new String( + credential.getResponse.getClientDataJSON.getBytes, + "UTF-8", + ), ), - credentialId = Some(credential.getId) + credentialId = Some(credential.getId), ) - val step: FinishRegistrationSteps#Step14 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next + val step: FinishRegistrationSteps#Step14 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next val standaloneVerification = Try { - new FidoU2fAttestationStatementVerifier().verifyAttestationSignature( - credential.getResponse.getAttestation, - Crypto.hash(credential.getResponse.getClientDataJSON) - ) + new FidoU2fAttestationStatementVerifier() + .verifyAttestationSignature( + credential.getResponse.getAttestation, + Crypto.sha256(credential.getResponse.getClientDataJSON), + ) } - step.validations shouldBe a [Failure[_]] - step.validations.failed.get shouldBe an [IllegalArgumentException] - step.tryNext shouldBe a [Failure[_]] + step.validations shouldBe a[Failure[_]] + step.validations.failed.get shouldBe an[ + IllegalArgumentException + ] + step.tryNext shouldBe a[Failure[_]] - standaloneVerification shouldBe a [Failure[_]] - standaloneVerification.failed.get shouldBe an [IllegalArgumentException] + standaloneVerification shouldBe a[Failure[_]] + standaloneVerification.failed.get shouldBe an[ + IllegalArgumentException + ] } - def checkAccepted(attestationAlg: COSEAlgorithmIdentifier, keypair: KeyPair): Unit = { - val (credential, _) = testAuthenticator.createBasicAttestedCredential(attestationMaker = AttestationMaker.fidoU2f(new AttestationCert(attestationAlg, testAuthenticator.generateAttestationCertificate(attestationAlg, Some(keypair))))) + 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") + attestationObject = + credential.getResponse.getAttestationObject, + clientDataJson = new String( + credential.getResponse.getClientDataJSON.getBytes, + "UTF-8", + ), ), - credentialId = Some(credential.getId) + credentialId = Some(credential.getId), ) - val step: FinishRegistrationSteps#Step14 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next + val step: FinishRegistrationSteps#Step14 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next val standaloneVerification = Try { - new FidoU2fAttestationStatementVerifier().verifyAttestationSignature( - credential.getResponse.getAttestation, - Crypto.hash(credential.getResponse.getClientDataJSON) - ) + new FidoU2fAttestationStatementVerifier() + .verifyAttestationSignature( + credential.getResponse.getAttestation, + Crypto.sha256(credential.getResponse.getClientDataJSON), + ) } - step.validations shouldBe a [Success[_]] - step.tryNext shouldBe a [Success[_]] + step.validations shouldBe a[Success[_]] + step.tryNext shouldBe a[Success[_]] - standaloneVerification should equal (Success(true)) + standaloneVerification should equal(Success(true)) } it("An RSA attestation certificate is rejected.") { - checkRejected(COSEAlgorithmIdentifier.RS256, testAuthenticator.generateRsaKeypair()) + checkRejected( + COSEAlgorithmIdentifier.RS256, + testAuthenticator.generateRsaKeypair(), + ) } it("A secp256r1 attestation certificate is accepted.") { - checkAccepted(COSEAlgorithmIdentifier.ES256, testAuthenticator.generateEcKeypair(curve = "secp256r1")) + checkAccepted( + COSEAlgorithmIdentifier.ES256, + testAuthenticator.generateEcKeypair(curve = "secp256r1"), + ) } it("A secp256k1 attestation certificate is rejected.") { - checkRejected(COSEAlgorithmIdentifier.ES256, testAuthenticator.generateEcKeypair(curve = "secp256k1")) + checkRejected( + COSEAlgorithmIdentifier.ES256, + testAuthenticator.generateEcKeypair(curve = "secp256k1"), + ) } } } describe("For the none statement format,") { - def flipByte(index: Int, bytes: ByteArray): ByteArray = new ByteArray(bytes.getBytes.updated(index, (0xff ^ bytes.getBytes()(index)).toByte)) + def flipByte(index: Int, bytes: ByteArray): ByteArray = + new ByteArray( + bytes.getBytes + .updated(index, (0xff ^ bytes.getBytes()(index)).toByte) + ) - def checkByteFlipSucceeds(mutationDescription: String, index: Int): Unit = { + def checkByteFlipSucceeds( + mutationDescription: String, + index: Int, + ): Unit = { it(s"the default test case with mutated ${mutationDescription} is accepted.") { - val testData = RegistrationTestData.NoneAttestation.Default.editAuthenticatorData { - flipByte(index, _) - } + val testData = RegistrationTestData.NoneAttestation.Default + .editAuthenticatorData { + flipByte(index, _) + } val steps = finishRegistration(testData = testData) val step: FinishRegistrationSteps#Step14 = new steps.Step14( - Crypto.hash(testData.clientDataJsonBytes), + Crypto.sha256(testData.clientDataJsonBytes), new AttestationObject(testData.attestationObject), Some(new NoneAttestationStatementVerifier).asJava, - Nil.asJava + Nil.asJava, ) - step.validations shouldBe a [Success[_]] - step.attestationType should equal (AttestationType.NONE) - step.tryNext shouldBe a [Success[_]] + step.validations shouldBe a[Success[_]] + step.attestationType should equal(AttestationType.NONE) + step.tryNext shouldBe a[Success[_]] } } it("the default test case is accepted.") { - val steps = finishRegistration(testData = RegistrationTestData.NoneAttestation.Default) - val step: FinishRegistrationSteps#Step14 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next + val steps = finishRegistration(testData = + RegistrationTestData.NoneAttestation.Default + ) + val step: FinishRegistrationSteps#Step14 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next - step.validations shouldBe a [Success[_]] - step.attestationType should equal (AttestationType.NONE) - step.tryNext shouldBe a [Success[_]] + step.validations shouldBe a[Success[_]] + step.attestationType should equal(AttestationType.NONE) + step.tryNext shouldBe a[Success[_]] } checkByteFlipSucceeds("signature counter", 32 + 1) @@ -1013,10 +1406,15 @@ class RelyingPartyRegistrationSpec extends FunSpec with Matchers with ScalaCheck val verifier = new PackedAttestationStatementVerifier it("the attestation statement verifier implementation is PackedAttestationStatementVerifier.") { - val steps = finishRegistration(testData = RegistrationTestData.Packed.BasicAttestation) - val step: FinishRegistrationSteps#Step14 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next + val steps = finishRegistration(testData = + RegistrationTestData.Packed.BasicAttestation + ) + val step: FinishRegistrationSteps#Step14 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next - step.getAttestationStatementVerifier.get shouldBe a [PackedAttestationStatementVerifier] + step.getAttestationStatementVerifier.get shouldBe a[ + PackedAttestationStatementVerifier + ] } describe("the verification procedure is:") { @@ -1024,75 +1422,103 @@ class RelyingPartyRegistrationSpec extends FunSpec with Matchers with ScalaCheck it("Fails if attStmt.sig is a text value.") { val testData = RegistrationTestData.Packed.BasicAttestation - .editAttestationObject("attStmt", jsonFactory.objectNode().set("sig", jsonFactory.textNode("foo"))) + .editAttestationObject( + "attStmt", + jsonFactory + .objectNode() + .set("sig", jsonFactory.textNode("foo")), + ) - val result: Try[Boolean] = Try(verifier.verifyAttestationSignature( - new AttestationObject(testData.attestationObject), - testData.clientDataJsonHash - )) + val result: Try[Boolean] = Try( + verifier.verifyAttestationSignature( + new AttestationObject(testData.attestationObject), + testData.clientDataJsonHash, + ) + ) - result shouldBe a [Failure[_]] - result.failed.get shouldBe an [IllegalArgumentException] + result shouldBe a[Failure[_]] + result.failed.get shouldBe an[IllegalArgumentException] } it("Fails if attStmt.sig is missing.") { val testData = RegistrationTestData.Packed.BasicAttestation - .editAttestationObject("attStmt", jsonFactory.objectNode().set("x5c", jsonFactory.arrayNode())) + .editAttestationObject( + "attStmt", + jsonFactory + .objectNode() + .set("x5c", jsonFactory.arrayNode()), + ) - val result: Try[Boolean] = Try(verifier.verifyAttestationSignature( - new AttestationObject(testData.attestationObject), - testData.clientDataJsonHash - )) + val result: Try[Boolean] = Try( + verifier.verifyAttestationSignature( + new AttestationObject(testData.attestationObject), + testData.clientDataJsonHash, + ) + ) - result shouldBe a [Failure[_]] - result.failed.get shouldBe an [IllegalArgumentException] + result shouldBe a[Failure[_]] + result.failed.get shouldBe an[IllegalArgumentException] } } describe("2. If x5c is present, this indicates that the attestation type is not ECDAA. In this case:") { it("The attestation type is identified as Basic.") { - val steps = finishRegistration(testData = RegistrationTestData.Packed.BasicAttestation) - val step: FinishRegistrationSteps#Step14 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next + val steps = finishRegistration(testData = + RegistrationTestData.Packed.BasicAttestation + ) + val step: FinishRegistrationSteps#Step14 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next - step.validations shouldBe a [Success[_]] - step.tryNext shouldBe a [Success[_]] - step.attestationType should be (AttestationType.BASIC) + step.validations shouldBe a[Success[_]] + step.tryNext shouldBe a[Success[_]] + step.attestationType should be(AttestationType.BASIC) } describe("1. Verify that sig is a valid signature over the concatenation of authenticatorData and clientDataHash using the attestation public key in attestnCert with the algorithm specified in alg.") { it("Succeeds for the default test case.") { val testData = RegistrationTestData.Packed.BasicAttestation - val result: Try[Boolean] = Try(verifier.verifyAttestationSignature( - new AttestationObject(testData.attestationObject), - testData.clientDataJsonHash - )) - result should equal (Success(true)) + val result: Try[Boolean] = Try( + verifier.verifyAttestationSignature( + new AttestationObject(testData.attestationObject), + testData.clientDataJsonHash, + ) + ) + result should equal(Success(true)) } it("Succeeds for an RS1 test case.") { - val testData = RegistrationTestData.Packed.BasicAttestationRs1 + val testData = + RegistrationTestData.Packed.BasicAttestationRs1 val result = verifier.verifyAttestationSignature( new AttestationObject(testData.attestationObject), - testData.clientDataJsonHash + testData.clientDataJsonHash, ) - result should equal (true) + result should equal(true) } it("Fail if the default test case is mutated.") { val testData = RegistrationTestData.Packed.BasicAttestation - val result: Try[Boolean] = Try(verifier.verifyAttestationSignature( - new AttestationObject( - testData - .editAuthenticatorData({ authData: ByteArray => - new ByteArray(authData.getBytes.updated(16, if (authData.getBytes()(16) == 0) 1: Byte else 0: Byte)) - }) - .attestationObject - ), - testData.clientDataJsonHash - )) - result should equal (Success(false)) + val result: Try[Boolean] = Try( + verifier.verifyAttestationSignature( + new AttestationObject( + testData + .editAuthenticatorData({ authData: ByteArray => + new ByteArray( + authData.getBytes.updated( + 16, + if (authData.getBytes()(16) == 0) 1: Byte + else 0: Byte, + ) + ) + }) + .attestationObject + ), + testData.clientDataJsonHash, + ) + ) + result should equal(Success(false)) } } @@ -1100,26 +1526,37 @@ class RelyingPartyRegistrationSpec extends FunSpec with Matchers with ScalaCheck it("Fails for an attestation signature with an invalid country code.") { val authenticator = TestAuthenticator val alg = COSEAlgorithmIdentifier.ES256 - val (badCert, key): (X509Certificate, PrivateKey) = authenticator.generateAttestationCertificate( - alg = alg, - name = new X500Name("O=Yubico, C=AA, OU=Authenticator Attestation") - ) - val (credential, _) = authenticator.createBasicAttestedCredential( - attestationMaker = AttestationMaker.packed(new AttestationCert(alg, (badCert, key))), + val (badCert, key): (X509Certificate, PrivateKey) = + authenticator.generateAttestationCertificate( + alg = alg, + name = new X500Name( + "O=Yubico, C=AA, OU=Authenticator Attestation" + ), + ) + val (credential, _) = + authenticator.createBasicAttestedCredential( + attestationMaker = AttestationMaker.packed( + new AttestationCert(alg, (badCert, key)) + ) + ) + val result = Try( + verifier.verifyAttestationSignature( + credential.getResponse.getAttestation, + sha256(credential.getResponse.getClientDataJSON), + ) ) - val result = Try(verifier.verifyAttestationSignature(credential.getResponse.getAttestation, sha256(credential.getResponse.getClientDataJSON))) - result shouldBe a [Failure[_]] - result.failed.get shouldBe an [IllegalArgumentException] + result shouldBe a[Failure[_]] + result.failed.get shouldBe an[IllegalArgumentException] } it("succeeds for the default test case.") { val testData = RegistrationTestData.Packed.BasicAttestation val result = verifier.verifyAttestationSignature( new AttestationObject(testData.attestationObject), - testData.clientDataJsonHash + testData.clientDataJsonHash, ) - result should equal (true) + result should equal(true) } } @@ -1128,36 +1565,42 @@ class RelyingPartyRegistrationSpec extends FunSpec with Matchers with ScalaCheck val testData = RegistrationTestData.Packed.BasicAttestation val result = verifier.verifyAttestationSignature( new AttestationObject(testData.attestationObject), - testData.clientDataJsonHash + testData.clientDataJsonHash, ) - testData.packedAttestationCert.getNonCriticalExtensionOIDs.asScala should equal (Set("1.3.6.1.4.1.45724.1.1.4")) - result should equal (true) + testData.packedAttestationCert.getNonCriticalExtensionOIDs.asScala should equal( + Set("1.3.6.1.4.1.45724.1.1.4") + ) + result should equal(true) } it("Succeeds if the attestation certificate does not have the extension.") { - val testData = RegistrationTestData.Packed.BasicAttestationWithoutAaguidExtension + val testData = + RegistrationTestData.Packed.BasicAttestationWithoutAaguidExtension val result = verifier.verifyAttestationSignature( new AttestationObject(testData.attestationObject), - testData.clientDataJsonHash + testData.clientDataJsonHash, ) testData.packedAttestationCert.getNonCriticalExtensionOIDs shouldBe null - result should equal (true) + result should equal(true) } it("Fails if the attestation certificate has the extension and it does not match the AAGUID.") { - val testData = RegistrationTestData.Packed.BasicAttestationWithWrongAaguidExtension + val testData = + RegistrationTestData.Packed.BasicAttestationWithWrongAaguidExtension - val result = Try(verifier.verifyAttestationSignature( - new AttestationObject(testData.attestationObject), - testData.clientDataJsonHash - )) + val result = Try( + verifier.verifyAttestationSignature( + new AttestationObject(testData.attestationObject), + testData.clientDataJsonHash, + ) + ) testData.packedAttestationCert.getNonCriticalExtensionOIDs should not be empty - result shouldBe a [Failure[_]] - result.failed.get shouldBe an [IllegalArgumentException] + result shouldBe a[Failure[_]] + result.failed.get shouldBe an[IllegalArgumentException] } } @@ -1168,13 +1611,19 @@ class RelyingPartyRegistrationSpec extends FunSpec with Matchers with ScalaCheck it("5. If successful, return implementation-specific values representing attestation type Basic, AttCA or uncertainty, and attestation trust path x5c.") { val testData = RegistrationTestData.Packed.BasicAttestation val steps = finishRegistration(testData = testData) - val step: FinishRegistrationSteps#Step14 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next + val step: FinishRegistrationSteps#Step14 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next - step.validations shouldBe a [Success[_]] - step.tryNext shouldBe a [Success[_]] - step.attestationType should be (AttestationType.BASIC) + step.validations shouldBe a[Success[_]] + 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, testData.attestationCaCert.get)) + step.attestationTrustPath.get.asScala should be( + List( + testData.packedAttestationCert, + testData.attestationCaCert.get, + ) + ) } } @@ -1193,55 +1642,112 @@ class RelyingPartyRegistrationSpec extends FunSpec with Matchers with ScalaCheck it("The attestation type is identified as SelfAttestation.") { val steps = finishRegistration(testData = testDataBase) - val step: FinishRegistrationSteps#Step14 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next + val step: FinishRegistrationSteps#Step14 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next - step.validations shouldBe a [Success[_]] - step.tryNext shouldBe a [Success[_]] - step.attestationType should be (AttestationType.SELF_ATTESTATION) + step.validations shouldBe a[Success[_]] + step.tryNext shouldBe a[Success[_]] + step.attestationType should be( + AttestationType.SELF_ATTESTATION + ) } describe("1. Validate that alg matches the algorithm of the credentialPublicKey in authenticatorData.") { it("Succeeds for the default test case.") { val result = verifier.verifyAttestationSignature( new AttestationObject(testDataBase.attestationObject), - testDataBase.clientDataJsonHash + testDataBase.clientDataJsonHash, ) - CBORObject.DecodeFromBytes(new AttestationObject(testDataBase.attestationObject).getAuthenticatorData.getAttestedCredentialData.get.getCredentialPublicKey.getBytes).get(CBORObject.FromObject(3)).AsInt64 should equal (-7) - new AttestationObject(testDataBase.attestationObject).getAttestationStatement.get("alg").longValue should equal (-7) - result should equal (true) + CBORObject + .DecodeFromBytes( + new AttestationObject( + testDataBase.attestationObject + ).getAuthenticatorData.getAttestedCredentialData.get.getCredentialPublicKey.getBytes + ) + .get(CBORObject.FromObject(3)) + .AsInt64 should equal(-7) + new AttestationObject( + testDataBase.attestationObject + ).getAttestationStatement.get("alg").longValue should equal( + -7 + ) + result should equal(true) } it("Fails if the alg is a different value.") { - def modifyAuthdataPubkeyAlg(authDataBytes: Array[Byte]): Array[Byte] = { - val authData = new AuthenticatorData(new ByteArray(authDataBytes)) - val key = WebAuthnCodecs.importCosePublicKey(authData.getAttestedCredentialData.get.getCredentialPublicKey).asInstanceOf[RSAPublicKey] - val reencodedKey = WebAuthnTestCodecs.rsaPublicKeyToCose(key, COSEAlgorithmIdentifier.RS256) - new ByteArray(java.util.Arrays.copyOfRange(authDataBytes, 0, 32 + 1 + 4 + 16 + 2)) - .concat(authData.getAttestedCredentialData.get.getCredentialId) + 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()))) - )) + 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 testData = + RegistrationTestData.Packed.SelfAttestationRs1 + val attObj = new AttestationObject( + modifyAttobjPubkeyAlg( + testData.response.getResponse.getAttestationObject + ) + ) - val result = Try(verifier.verifyAttestationSignature( - attObj, - testData.clientDataJsonHash - )) + val result = Try( + verifier.verifyAttestationSignature( + attObj, + testData.clientDataJsonHash, + ) + ) - 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] + 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] } } @@ -1249,117 +1755,195 @@ class RelyingPartyRegistrationSpec extends FunSpec with Matchers with ScalaCheck it("Succeeds for the default test case.") { val result = verifier.verifyAttestationSignature( new AttestationObject(testDataBase.attestationObject), - testDataBase.clientDataJsonHash + testDataBase.clientDataJsonHash, ) - result should equal (true) + 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 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 + testData.clientDataJsonHash, ) - result should equal (true) + 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 testData = testDataBase.editAuthenticatorData { + authData: ByteArray => + new ByteArray( + authData.getBytes.updated( + 16, + if (authData.getBytes()(16) == 0) 1: Byte + else 0: Byte, + ) + ) + } val result = verifier.verifyAttestationSignature( new AttestationObject(testData.attestationObject), - testData.clientDataJsonHash + testData.clientDataJsonHash, ) - result should equal (false) + result should equal(false) } it("Fails if the client data is mutated.") { val result = verifier.verifyAttestationSignature( new AttestationObject(testDataBase.attestationObject), - sha256(new ByteArray(testDataBase.clientDataJson.updated(4, 'ä').getBytes("UTF-8"))) + sha256( + new ByteArray( + testDataBase.clientDataJson + .updated(4, 'ä') + .getBytes("UTF-8") + ) + ), ) - result should equal (false) + result should equal(false) } it("Fails if the client data hash is mutated.") { val result = verifier.verifyAttestationSignature( new AttestationObject(testDataBase.attestationObject), - new ByteArray(testDataBase.clientDataJsonHash.getBytes.updated(7, if (testDataBase.clientDataJsonHash.getBytes()(7) == 0) 1: Byte else 0: Byte))) - result should equal (false) + new ByteArray( + testDataBase.clientDataJsonHash.getBytes.updated( + 7, + if ( + testDataBase.clientDataJsonHash.getBytes()(7) == 0 + ) 1: Byte + else 0: Byte, + ) + ), + ) + result should equal(false) } } it("3. If successful, return implementation-specific values representing attestation type Self and an empty attestation trust path.") { val testData = RegistrationTestData.Packed.SelfAttestation val steps = finishRegistration(testData = testData) - val step: FinishRegistrationSteps#Step14 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next + val step: FinishRegistrationSteps#Step14 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next - step.validations shouldBe a [Success[_]] - step.tryNext shouldBe a [Success[_]] - step.attestationType should be (AttestationType.SELF_ATTESTATION) + step.validations shouldBe a[Success[_]] + step.tryNext shouldBe a[Success[_]] + step.attestationType should be( + AttestationType.SELF_ATTESTATION + ) step.attestationTrustPath.asScala shouldBe empty } } } - describe("8.2.1. Packed Attestation Statement Certificate Requirements") { + describe( + "8.2.1. Packed Attestation Statement Certificate Requirements" + ) { val testDataBase = RegistrationTestData.Packed.BasicAttestation describe("The attestation certificate MUST have the following fields/extensions:") { it("Version MUST be set to 3 (which is indicated by an ASN.1 INTEGER with value 2).") { val badCert = Mockito.mock(classOf[X509Certificate]) - val principal = new X500Principal("O=Yubico, C=SE, OU=Authenticator Attestation") + val principal = new X500Principal( + "O=Yubico, C=SE, OU=Authenticator Attestation" + ) Mockito.when(badCert.getVersion) thenReturn 2 - Mockito.when(badCert.getSubjectX500Principal) thenReturn principal + Mockito.when( + badCert.getSubjectX500Principal + ) thenReturn principal Mockito.when(badCert.getBasicConstraints) thenReturn -1 - val result = Try(verifier.verifyX5cRequirements(badCert, testDataBase.aaguid)) + val result = Try( + verifier.verifyX5cRequirements(badCert, testDataBase.aaguid) + ) - result shouldBe a [Failure[_]] - result.failed.get shouldBe an [IllegalArgumentException] + result shouldBe a[Failure[_]] + result.failed.get shouldBe an[IllegalArgumentException] - verifier.verifyX5cRequirements(testDataBase.packedAttestationCert, testDataBase.aaguid) should equal (true) + verifier.verifyX5cRequirements( + testDataBase.packedAttestationCert, + testDataBase.aaguid, + ) should equal(true) } describe("Subject field MUST be set to:") { it("Subject-C: ISO 3166 code specifying the country where the Authenticator vendor is incorporated (PrintableString)") { - val badCert: X509Certificate = TestAuthenticator.generateAttestationCertificate( - name = new X500Name("O=Yubico, C=AA, OU=Authenticator Attestation") - )._1 - val result = Try(verifier.verifyX5cRequirements(badCert, testDataBase.aaguid)) + val badCert: X509Certificate = TestAuthenticator + .generateAttestationCertificate( + name = new X500Name( + "O=Yubico, C=AA, OU=Authenticator Attestation" + ) + ) + ._1 + val result = Try( + verifier.verifyX5cRequirements( + badCert, + testDataBase.aaguid, + ) + ) - result shouldBe a [Failure[_]] - result.failed.get shouldBe an [IllegalArgumentException] + result shouldBe a[Failure[_]] + result.failed.get shouldBe an[IllegalArgumentException] - verifier.verifyX5cRequirements(testDataBase.packedAttestationCert, testDataBase.aaguid) should equal (true) + verifier.verifyX5cRequirements( + testDataBase.packedAttestationCert, + testDataBase.aaguid, + ) should equal(true) } it("Subject-O: Legal name of the Authenticator vendor (UTF8String)") { - val badCert: X509Certificate = TestAuthenticator.generateAttestationCertificate( - name = new X500Name("C=SE, OU=Authenticator Attestation") - )._1 - val result = Try(verifier.verifyX5cRequirements(badCert, testDataBase.aaguid)) + val badCert: X509Certificate = TestAuthenticator + .generateAttestationCertificate( + name = + new X500Name("C=SE, OU=Authenticator Attestation") + ) + ._1 + val result = Try( + verifier.verifyX5cRequirements( + badCert, + testDataBase.aaguid, + ) + ) - result shouldBe a [Failure[_]] - result.failed.get shouldBe an [IllegalArgumentException] + result shouldBe a[Failure[_]] + result.failed.get shouldBe an[IllegalArgumentException] - verifier.verifyX5cRequirements(testDataBase.packedAttestationCert, testDataBase.aaguid) should equal(true) + verifier.verifyX5cRequirements( + testDataBase.packedAttestationCert, + testDataBase.aaguid, + ) should equal(true) } it("""Subject-OU: Literal string "Authenticator Attestation" (UTF8String)""") { - val badCert: X509Certificate = TestAuthenticator.generateAttestationCertificate( - name = new X500Name("O=Yubico, C=SE, OU=Foo") - )._1 - val result = Try(verifier.verifyX5cRequirements(badCert, testDataBase.aaguid)) + val badCert: X509Certificate = TestAuthenticator + .generateAttestationCertificate( + name = new X500Name("O=Yubico, C=SE, OU=Foo") + ) + ._1 + val result = Try( + verifier.verifyX5cRequirements( + badCert, + testDataBase.aaguid, + ) + ) - result shouldBe a [Failure[_]] - result.failed.get shouldBe an [IllegalArgumentException] + result shouldBe a[Failure[_]] + result.failed.get shouldBe an[IllegalArgumentException] - verifier.verifyX5cRequirements(testDataBase.packedAttestationCert, testDataBase.aaguid) should equal(true) + verifier.verifyX5cRequirements( + testDataBase.packedAttestationCert, + testDataBase.aaguid, + ) should equal(true) } - describe("Subject-CN: A UTF8String of the vendor’s choosing") { + describe( + "Subject-CN: A UTF8String of the vendor’s choosing" + ) { it("Nothing to test") {} } } @@ -1367,43 +1951,89 @@ class RelyingPartyRegistrationSpec extends FunSpec with Matchers with ScalaCheck it("If the related attestation root certificate is used for multiple authenticator models, the Extension OID 1.3.6.1.4.1.45724.1.1.4 (id-fido-gen-ce-aaguid) MUST be present, containing the AAGUID as a 16-byte OCTET STRING. The extension MUST NOT be marked as critical.") { val idFidoGenCeAaguid = "1.3.6.1.4.1.45724.1.1.4" - val badCert: X509Certificate = TestAuthenticator.generateAttestationCertificate( - name = new X500Name("O=Yubico, C=SE, OU=Authenticator Attestation"), - extensions = List((idFidoGenCeAaguid, false, new DEROctetString(Array[Byte](0, 1, 2, 3)))) - )._1 - val result = Try(verifier.verifyX5cRequirements(badCert, testDataBase.aaguid)) + val badCert: X509Certificate = TestAuthenticator + .generateAttestationCertificate( + name = new X500Name( + "O=Yubico, C=SE, OU=Authenticator Attestation" + ), + extensions = List( + ( + idFidoGenCeAaguid, + false, + new DEROctetString(Array[Byte](0, 1, 2, 3)), + ) + ), + ) + ._1 + val result = Try( + verifier.verifyX5cRequirements(badCert, testDataBase.aaguid) + ) - result shouldBe a [Failure[_]] - result.failed.get shouldBe an [IllegalArgumentException] + result shouldBe a[Failure[_]] + result.failed.get shouldBe an[IllegalArgumentException] - val badCertCritical: X509Certificate = TestAuthenticator.generateAttestationCertificate( - name = new X500Name("O=Yubico, C=SE, OU=Authenticator Attestation"), - extensions = List((idFidoGenCeAaguid, true, new DEROctetString(testDataBase.aaguid.getBytes))) - )._1 - val resultCritical = Try(verifier.verifyX5cRequirements(badCertCritical, testDataBase.aaguid)) + val badCertCritical: X509Certificate = TestAuthenticator + .generateAttestationCertificate( + name = new X500Name( + "O=Yubico, C=SE, OU=Authenticator Attestation" + ), + extensions = List( + ( + idFidoGenCeAaguid, + true, + new DEROctetString(testDataBase.aaguid.getBytes), + ) + ), + ) + ._1 + val resultCritical = Try( + verifier.verifyX5cRequirements( + badCertCritical, + testDataBase.aaguid, + ) + ) - resultCritical shouldBe a [Failure[_]] - resultCritical.failed.get shouldBe an [IllegalArgumentException] + resultCritical shouldBe a[Failure[_]] + resultCritical.failed.get shouldBe an[ + IllegalArgumentException + ] - val goodCert: X509Certificate = TestAuthenticator.generateAttestationCertificate( - name = new X500Name("O=Yubico, C=SE, OU=Authenticator Attestation"), - extensions = Nil - )._1 - val goodResult = Try(verifier.verifyX5cRequirements(badCert, testDataBase.aaguid)) + val goodCert: X509Certificate = TestAuthenticator + .generateAttestationCertificate( + name = new X500Name( + "O=Yubico, C=SE, OU=Authenticator Attestation" + ), + extensions = Nil, + ) + ._1 + val goodResult = Try( + verifier.verifyX5cRequirements(badCert, testDataBase.aaguid) + ) - goodResult shouldBe a [Failure[_]] - goodResult.failed.get shouldBe an [IllegalArgumentException] + goodResult shouldBe a[Failure[_]] + goodResult.failed.get shouldBe an[IllegalArgumentException] - verifier.verifyX5cRequirements(testDataBase.packedAttestationCert, testDataBase.aaguid) should equal(true) + verifier.verifyX5cRequirements( + testDataBase.packedAttestationCert, + testDataBase.aaguid, + ) should equal(true) } it("The Basic Constraints extension MUST have the CA component set to false.") { - val result = Try(verifier.verifyX5cRequirements(testDataBase.attestationCaCert.get, testDataBase.aaguid)) + val result = Try( + verifier.verifyX5cRequirements( + testDataBase.attestationCaCert.get, + testDataBase.aaguid, + ) + ) - result shouldBe a [Failure[_]] - result.failed.get shouldBe an [IllegalArgumentException] + result shouldBe a[Failure[_]] + result.failed.get shouldBe an[IllegalArgumentException] - verifier.verifyX5cRequirements(testDataBase.packedAttestationCert, testDataBase.aaguid) should equal (true) + verifier.verifyX5cRequirements( + testDataBase.packedAttestationCert, + testDataBase.aaguid, + ) should equal(true) } describe("An Authority Information Access (AIA) extension with entry id-ad-ocsp and a CRL Distribution Point extension [RFC5280] are both OPTIONAL as the status of many attestation certificates is available through authenticator metadata services. See, for example, the FIDO Metadata Service [FIDOMetadataService].") { @@ -1414,19 +2044,24 @@ class RelyingPartyRegistrationSpec extends FunSpec with Matchers with ScalaCheck } ignore("The tpm statement format is supported.") { - val steps = finishRegistration(testData = RegistrationTestData.Tpm.PrivacyCa) - val step: FinishRegistrationSteps#Step14 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next + val steps = + finishRegistration(testData = RegistrationTestData.Tpm.PrivacyCa) + val step: FinishRegistrationSteps#Step14 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next - step.validations shouldBe a [Success[_]] - step.tryNext shouldBe a [Success[_]] + step.validations shouldBe a[Success[_]] + step.tryNext shouldBe a[Success[_]] } ignore("The android-key statement format is supported.") { - val steps = finishRegistration(testData = RegistrationTestData.AndroidKey.BasicAttestation) - val step: FinishRegistrationSteps#Step14 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next + val steps = finishRegistration(testData = + RegistrationTestData.AndroidKey.BasicAttestation + ) + val step: FinishRegistrationSteps#Step14 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next - step.validations shouldBe a [Success[_]] - step.tryNext shouldBe a [Success[_]] + step.validations shouldBe a[Success[_]] + step.tryNext shouldBe a[Success[_]] } describe("For the android-safetynet attestation statement format") { @@ -1438,46 +2073,82 @@ class RelyingPartyRegistrationSpec extends FunSpec with Matchers with ScalaCheck val steps = finishRegistration( testData = defaultTestData, allowUntrustedAttestation = false, - rp = defaultTestData.rpId + rp = defaultTestData.rpId, ) - val step: FinishRegistrationSteps#Step14 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next + val step: FinishRegistrationSteps#Step14 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next - step.getAttestationStatementVerifier.get shouldBe an [AndroidSafetynetAttestationStatementVerifier] + step.getAttestationStatementVerifier.get shouldBe an[ + AndroidSafetynetAttestationStatementVerifier + ] } describe("the verification procedure is:") { def checkFails(testData: RegistrationTestData): Unit = { - val result: Try[Boolean] = Try(verifier.verifyAttestationSignature( - new AttestationObject(testData.attestationObject), - testData.clientDataJsonHash - )) + val result: Try[Boolean] = Try( + verifier.verifyAttestationSignature( + new AttestationObject(testData.attestationObject), + testData.clientDataJsonHash, + ) + ) - result shouldBe a [Failure[_]] - result.failed.get shouldBe an [IllegalArgumentException] + result shouldBe a[Failure[_]] + result.failed.get shouldBe an[IllegalArgumentException] } describe("1. Verify that attStmt is valid CBOR conforming to the syntax defined above and perform CBOR decoding on it to extract the contained fields.") { it("Fails if attStmt.ver is a number value.") { val testData = defaultTestData - .updateAttestationObject("attStmt", attStmt => attStmt.asInstanceOf[ObjectNode].set[ObjectNode]("ver", jsonFactory.numberNode(123))) + .updateAttestationObject( + "attStmt", + attStmt => + attStmt + .asInstanceOf[ObjectNode] + .set[ObjectNode]("ver", jsonFactory.numberNode(123)), + ) checkFails(testData) } it("Fails if attStmt.ver is missing.") { val testData = defaultTestData - .updateAttestationObject("attStmt", attStmt => attStmt.asInstanceOf[ObjectNode].without[ObjectNode]("ver")) + .updateAttestationObject( + "attStmt", + attStmt => + attStmt + .asInstanceOf[ObjectNode] + .without[ObjectNode]("ver"), + ) checkFails(testData) } it("Fails if attStmt.response is a text value.") { val testData = defaultTestData - .updateAttestationObject("attStmt", attStmt => attStmt.asInstanceOf[ObjectNode].set[ObjectNode]("response", jsonFactory.textNode(new ByteArray(attStmt.get("response").binaryValue()).getBase64Url))) + .updateAttestationObject( + "attStmt", + attStmt => + attStmt + .asInstanceOf[ObjectNode] + .set[ObjectNode]( + "response", + jsonFactory.textNode( + new ByteArray( + attStmt.get("response").binaryValue() + ).getBase64Url + ), + ), + ) checkFails(testData) } it("Fails if attStmt.response is missing.") { val testData = defaultTestData - .updateAttestationObject("attStmt", attStmt => attStmt.asInstanceOf[ObjectNode].without[ObjectNode]("response")) + .updateAttestationObject( + "attStmt", + attStmt => + attStmt + .asInstanceOf[ObjectNode] + .without[ObjectNode]("response"), + ) checkFails(testData) } } @@ -1485,26 +2156,49 @@ class RelyingPartyRegistrationSpec extends FunSpec with Matchers with ScalaCheck describe("2. Verify that response is a valid SafetyNet response of version ver.") { it("Fails if there's a difference in the signature.") { val testData = defaultTestData - .updateAttestationObject("attStmt", attStmt => attStmt.asInstanceOf[ObjectNode].set[ObjectNode]("response", jsonFactory.binaryNode(editByte(new ByteArray(attStmt.get("response").binaryValue()), 2000, b => ((b + 1) % 26 + 0x41).toByte).getBytes))) + .updateAttestationObject( + "attStmt", + attStmt => + attStmt + .asInstanceOf[ObjectNode] + .set[ObjectNode]( + "response", + jsonFactory.binaryNode( + editByte( + new ByteArray( + attStmt.get("response").binaryValue() + ), + 2000, + b => ((b + 1) % 26 + 0x41).toByte, + ).getBytes + ), + ), + ) - val result: Try[Boolean] = Try(verifier.verifyAttestationSignature( - new AttestationObject(testData.attestationObject), - testData.clientDataJsonHash - )) + val result: Try[Boolean] = Try( + verifier.verifyAttestationSignature( + new AttestationObject(testData.attestationObject), + testData.clientDataJsonHash, + ) + ) - result shouldBe a [Success[_]] - result.get should be (false) + result shouldBe a[Success[_]] + result.get should be(false) } } describe("3. Verify that the nonce in the response is identical to the Base64 encoding of the SHA-256 hash of the concatenation of authenticatorData and clientDataHash.") { - it("Fails if an additional property is added to the client data.") { + it( + "Fails if an additional property is added to the client data." + ) { val testData = defaultTestData.editClientData("foo", "bar") checkFails(testData) } } - describe("4. Let attestationCert be the attestation certificate.") { + describe( + "4. Let attestationCert be the attestation certificate." + ) { it("Nothing to test.") {} } @@ -1520,26 +2214,30 @@ class RelyingPartyRegistrationSpec extends FunSpec with Matchers with ScalaCheck it("The real example succeeds.") { val steps = finishRegistration( testData = testDataContainer.RealExample, - rp = testDataContainer.RealExample.rpId + rp = testDataContainer.RealExample.rpId, ) - val step: FinishRegistrationSteps#Step14 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next + val step: FinishRegistrationSteps#Step14 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next - step.validations shouldBe a [Success[_]] - step.tryNext shouldBe a [Success[_]] - step.attestationType() should be (AttestationType.BASIC) + step.validations shouldBe a[Success[_]] + step.tryNext shouldBe a[Success[_]] + step.attestationType() should be(AttestationType.BASIC) step.attestationTrustPath().get should not be empty - step.attestationTrustPath().get.size should be (2) + step.attestationTrustPath().get.size should be(2) } it("The default test case succeeds.") { - val steps = finishRegistration(testData = testDataContainer.BasicAttestation) - val step: FinishRegistrationSteps#Step14 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next + val steps = finishRegistration(testData = + testDataContainer.BasicAttestation + ) + val step: FinishRegistrationSteps#Step14 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next - step.validations shouldBe a [Success[_]] - step.tryNext shouldBe a [Success[_]] - step.attestationType() should be (AttestationType.BASIC) + step.validations shouldBe a[Success[_]] + step.tryNext shouldBe a[Success[_]] + step.attestationType() should be(AttestationType.BASIC) step.attestationTrustPath().get should not be empty - step.attestationTrustPath().get.size should be (1) + step.attestationTrustPath().get.size should be(1) } } } @@ -1548,21 +2246,30 @@ class RelyingPartyRegistrationSpec extends FunSpec with Matchers with ScalaCheck it("The android-safetynet statement format is supported.") { val steps = finishRegistration( testData = RegistrationTestData.AndroidSafetynet.RealExample, - rp = RelyingPartyIdentity.builder().id("demo.yubico.com").name("").build() + rp = RelyingPartyIdentity + .builder() + .id("demo.yubico.com") + .name("") + .build(), ) - val step: FinishRegistrationSteps#Step14 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next + val step: FinishRegistrationSteps#Step14 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next - step.validations shouldBe a [Success[_]] - step.tryNext shouldBe a [Success[_]] + step.validations shouldBe a[Success[_]] + step.tryNext shouldBe a[Success[_]] } it("Unknown attestation statement formats are identified as such.") { - val steps = finishRegistration(testData = RegistrationTestData.FidoU2f.BasicAttestation.setAttestationStatementFormat("urgel")) - val step: FinishRegistrationSteps#Step14 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next + val steps = finishRegistration(testData = + RegistrationTestData.FidoU2f.BasicAttestation + .setAttestationStatementFormat("urgel") + ) + val step: FinishRegistrationSteps#Step14 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next - step.validations shouldBe a [Success[_]] - step.tryNext shouldBe a [Success[_]] - step.attestationType should be (AttestationType.UNKNOWN) + step.validations shouldBe a[Success[_]] + step.tryNext shouldBe a[Success[_]] + step.attestationType should be(AttestationType.UNKNOWN) step.attestationTrustPath.asScala shouldBe empty } } @@ -1575,61 +2282,73 @@ class RelyingPartyRegistrationSpec extends FunSpec with Matchers with ScalaCheck val steps = finishRegistration( testData = RegistrationTestData.AndroidSafetynet.RealExample, metadataService = Some(metadataService), - rp = RegistrationTestData.AndroidSafetynet.RealExample.rpId + rp = RegistrationTestData.AndroidSafetynet.RealExample.rpId, ) - val step: FinishRegistrationSteps#Step15 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next + val step: FinishRegistrationSteps#Step15 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next - step.validations shouldBe a [Success[_]] + step.validations shouldBe a[Success[_]] step.trustResolver.get should not be null - step.tryNext shouldBe a [Success[_]] + step.tryNext shouldBe a[Success[_]] } } describe("For the fido-u2f statement format") { it("with self attestation, no trust anchors are returned.") { - val steps = finishRegistration(testData = RegistrationTestData.FidoU2f.SelfAttestation) - val step: FinishRegistrationSteps#Step15 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next + val steps = finishRegistration(testData = + RegistrationTestData.FidoU2f.SelfAttestation + ) + val step: FinishRegistrationSteps#Step15 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next - step.validations shouldBe a [Success[_]] + step.validations shouldBe a[Success[_]] step.trustResolver.asScala shouldBe empty - step.tryNext shouldBe a [Success[_]] + step.tryNext shouldBe a[Success[_]] } it("with basic attestation, a trust resolver is returned.") { val metadataService: MetadataService = new TestMetadataService() val steps = finishRegistration( testData = RegistrationTestData.FidoU2f.BasicAttestation, - metadataService = Some(metadataService) + metadataService = Some(metadataService), ) - val step: FinishRegistrationSteps#Step15 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next + val step: FinishRegistrationSteps#Step15 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next - step.validations shouldBe a [Success[_]] + step.validations shouldBe a[Success[_]] step.trustResolver.get should not be null - step.tryNext shouldBe a [Success[_]] + step.tryNext shouldBe a[Success[_]] } } describe("For the none statement format") { it("no trust anchors are returned.") { - val steps = finishRegistration(testData = RegistrationTestData.NoneAttestation.Default) - val step: FinishRegistrationSteps#Step15 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next + val steps = finishRegistration(testData = + RegistrationTestData.NoneAttestation.Default + ) + val step: FinishRegistrationSteps#Step15 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next - step.validations shouldBe a [Success[_]] + step.validations shouldBe a[Success[_]] step.trustResolver.asScala shouldBe empty - step.tryNext shouldBe a [Success[_]] + step.tryNext shouldBe a[Success[_]] } } describe("For unknown attestation statement formats") { it("no trust anchors are returned.") { - val steps = finishRegistration(testData = RegistrationTestData.FidoU2f.BasicAttestation.setAttestationStatementFormat("urgel")) - val step: FinishRegistrationSteps#Step15 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next + val steps = finishRegistration(testData = + RegistrationTestData.FidoU2f.BasicAttestation + .setAttestationStatementFormat("urgel") + ) + val step: FinishRegistrationSteps#Step15 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next - step.validations shouldBe a [Success[_]] + step.validations shouldBe a[Success[_]] step.trustResolver.asScala shouldBe empty - step.tryNext shouldBe a [Success[_]] + step.tryNext shouldBe a[Success[_]] } } } @@ -1641,57 +2360,66 @@ class RelyingPartyRegistrationSpec extends FunSpec with Matchers with ScalaCheck it("is rejected if untrusted attestation is not allowed.") { val steps = finishRegistration( testData = RegistrationTestData.NoneAttestation.Default, - allowUntrustedAttestation = false + allowUntrustedAttestation = false, ) - val step: FinishRegistrationSteps#Step16 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next - - step.validations shouldBe a [Failure[_]] - step.validations.failed.get shouldBe an [IllegalArgumentException] - step.attestationTrusted should be (false) - step.tryNext shouldBe a [Failure[_]] + val step: FinishRegistrationSteps#Step16 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next + + step.validations shouldBe a[Failure[_]] + step.validations.failed.get shouldBe an[ + IllegalArgumentException + ] + step.attestationTrusted should be(false) + step.tryNext shouldBe a[Failure[_]] } it("is accepted if untrusted attestation is allowed.") { val steps = finishRegistration( testData = RegistrationTestData.NoneAttestation.Default, - allowUntrustedAttestation = true + allowUntrustedAttestation = true, ) - val step: FinishRegistrationSteps#Step16 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next + val step: FinishRegistrationSteps#Step16 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next - step.validations shouldBe a [Success[_]] - step.attestationTrusted should be (false) - step.tryNext shouldBe a [Success[_]] + step.validations shouldBe a[Success[_]] + step.attestationTrusted should be(false) + step.tryNext shouldBe a[Success[_]] } } } describe("If an unknown attestation statement format was used, check if no attestation is acceptable under Relying Party policy.") { - val testData = RegistrationTestData.FidoU2f.BasicAttestation.setAttestationStatementFormat("urgel") + val testData = RegistrationTestData.FidoU2f.BasicAttestation + .setAttestationStatementFormat("urgel") describe("The default test case") { it("is rejected if untrusted attestation is not allowed.") { val steps = finishRegistration( testData = testData, - allowUntrustedAttestation = false + allowUntrustedAttestation = false, ) - val step: FinishRegistrationSteps#Step16 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next - - step.validations shouldBe a [Failure[_]] - step.validations.failed.get shouldBe an [IllegalArgumentException] - step.attestationTrusted should be (false) - step.tryNext shouldBe a [Failure[_]] + val step: FinishRegistrationSteps#Step16 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next + + step.validations shouldBe a[Failure[_]] + step.validations.failed.get shouldBe an[ + IllegalArgumentException + ] + step.attestationTrusted should be(false) + step.tryNext shouldBe a[Failure[_]] } it("is accepted if untrusted attestation is allowed.") { val steps = finishRegistration( testData = testData, - allowUntrustedAttestation = true + allowUntrustedAttestation = true, ) - val step: FinishRegistrationSteps#Step16 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next + val step: FinishRegistrationSteps#Step16 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next - step.validations shouldBe a [Success[_]] - step.attestationTrusted should be (false) - step.tryNext shouldBe a [Success[_]] + step.validations shouldBe a[Success[_]] + step.attestationTrusted should be(false) + step.tryNext shouldBe a[Success[_]] } } } @@ -1702,26 +2430,30 @@ class RelyingPartyRegistrationSpec extends FunSpec with Matchers with ScalaCheck it("is rejected if untrusted attestation is not allowed.") { val steps = finishRegistration( testData = RegistrationTestData.FidoU2f.SelfAttestation, - allowUntrustedAttestation = false + allowUntrustedAttestation = false, ) - val step: FinishRegistrationSteps#Step16 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next - - step.validations shouldBe a [Failure[_]] - step.validations.failed.get shouldBe an [IllegalArgumentException] - step.attestationTrusted should be (false) - step.tryNext shouldBe a [Failure[_]] + val step: FinishRegistrationSteps#Step16 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next + + step.validations shouldBe a[Failure[_]] + step.validations.failed.get shouldBe an[ + IllegalArgumentException + ] + step.attestationTrusted should be(false) + step.tryNext shouldBe a[Failure[_]] } it("is accepted if untrusted attestation is allowed.") { val steps = finishRegistration( testData = RegistrationTestData.FidoU2f.SelfAttestation, - allowUntrustedAttestation = true + allowUntrustedAttestation = true, ) - val step: FinishRegistrationSteps#Step16 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next + val step: FinishRegistrationSteps#Step16 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next - step.validations shouldBe a [Success[_]] - step.attestationTrusted should be (false) - step.tryNext shouldBe a [Success[_]] + step.validations shouldBe a[Success[_]] + step.attestationTrusted should be(false) + step.tryNext shouldBe a[Success[_]] } } } @@ -1739,15 +2471,16 @@ class RelyingPartyRegistrationSpec extends FunSpec with Matchers with ScalaCheck allowUntrustedAttestation = false, testData = testData, metadataService = Some(metadataService), - rp = testData.rpId + rp = testData.rpId, ) - val step: FinishRegistrationSteps#Step16 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next + val step: FinishRegistrationSteps#Step16 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next - step.validations shouldBe a [Failure[_]] - step.attestationTrusted should be (false) + step.validations shouldBe a[Failure[_]] + step.attestationTrusted should be(false) step.attestationMetadata.asScala should not be empty step.attestationMetadata.get.getMetadataIdentifier.asScala shouldBe empty - step.tryNext shouldBe a [Failure[_]] + step.tryNext shouldBe a[Failure[_]] } it("is accepted if untrusted attestation is allowed and the metadata service does not trust it.") { @@ -1756,20 +2489,23 @@ class RelyingPartyRegistrationSpec extends FunSpec with Matchers with ScalaCheck allowUntrustedAttestation = true, testData = testData, metadataService = Some(metadataService), - rp = testData.rpId + rp = testData.rpId, ) - val step: FinishRegistrationSteps#Step16 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next + val step: FinishRegistrationSteps#Step16 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next - step.validations shouldBe a [Success[_]] - step.attestationTrusted should be (false) + step.validations shouldBe a[Success[_]] + step.attestationTrusted should be(false) step.attestationMetadata.asScala should not be empty step.attestationMetadata.get.getMetadataIdentifier.asScala shouldBe empty - step.tryNext shouldBe a [Success[_]] + step.tryNext shouldBe a[Success[_]] } it("is accepted if the metadata service trusts it.") { - val metadataService: MetadataService = new TestMetadataService(Some( - Attestation.builder() + val metadataService: MetadataService = new TestMetadataService( + Some( + Attestation + .builder() .trusted(true) .metadataIdentifier(Some("Test attestation CA").asJava) .build() @@ -1779,15 +2515,18 @@ class RelyingPartyRegistrationSpec extends FunSpec with Matchers with ScalaCheck val steps = finishRegistration( testData = testData, metadataService = Some(metadataService), - rp = testData.rpId + rp = testData.rpId, ) - val step: FinishRegistrationSteps#Step16 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next + val step: FinishRegistrationSteps#Step16 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next - step.validations shouldBe a [Success[_]] - step.attestationTrusted should be (true) + step.validations shouldBe a[Success[_]] + step.attestationTrusted should be(true) step.attestationMetadata.asScala should not be empty - step.attestationMetadata.get.getMetadataIdentifier.asScala should equal (Some("Test attestation CA")) - step.tryNext shouldBe a [Success[_]] + step.attestationMetadata.get.getMetadataIdentifier.asScala should equal( + Some("Test attestation CA") + ) + step.tryNext shouldBe a[Success[_]] } } @@ -1798,15 +2537,21 @@ class RelyingPartyRegistrationSpec extends FunSpec with Matchers with ScalaCheck } describe("An android-safetynet basic attestation") { - generateTests(testData = RegistrationTestData.AndroidSafetynet.RealExample) + generateTests(testData = + RegistrationTestData.AndroidSafetynet.RealExample + ) } describe("A fido-u2f basic attestation") { - generateTests(testData = RegistrationTestData.FidoU2f.BasicAttestation) + generateTests(testData = + RegistrationTestData.FidoU2f.BasicAttestation + ) } describe("A packed basic attestation") { - generateTests(testData = RegistrationTestData.Packed.BasicAttestation) + generateTests(testData = + RegistrationTestData.Packed.BasicAttestation + ) } } @@ -1817,38 +2562,44 @@ class RelyingPartyRegistrationSpec extends FunSpec with Matchers with ScalaCheck val testData = RegistrationTestData.FidoU2f.SelfAttestation it("Registration is aborted if the given credential ID is already registered.") { - val credentialRepository = com.yubico.webauthn.test.Helpers.CredentialRepository.withUser( - testData.userId, - RegisteredCredential.builder() - .credentialId(testData.response.getId) - .userHandle(testData.userId.getId) - .publicKeyCose(testData.response.getResponse.getAttestation.getAuthenticatorData.getAttestedCredentialData.get.getCredentialPublicKey) - .signatureCount(1337) - .build() - ) + val credentialRepository = + com.yubico.webauthn.test.Helpers.CredentialRepository.withUser( + testData.userId, + RegisteredCredential + .builder() + .credentialId(testData.response.getId) + .userHandle(testData.userId.getId) + .publicKeyCose( + testData.response.getResponse.getAttestation.getAuthenticatorData.getAttestedCredentialData.get.getCredentialPublicKey + ) + .signatureCount(1337) + .build(), + ) val steps = finishRegistration( allowUntrustedAttestation = true, testData = testData, - credentialRepository = 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 + val step: FinishRegistrationSteps#Step17 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next - step.validations shouldBe a [Failure[_]] - step.validations.failed.get shouldBe an [IllegalArgumentException] - step.tryNext shouldBe an [Failure[_]] + step.validations shouldBe a[Failure[_]] + step.validations.failed.get shouldBe an[IllegalArgumentException] + step.tryNext shouldBe an[Failure[_]] } it("Registration proceeds if the given credential ID is not already registered.") { val steps = finishRegistration( allowUntrustedAttestation = true, testData = testData, - credentialRepository = Helpers.CredentialRepository.empty + credentialRepository = Helpers.CredentialRepository.empty, ) - val step: FinishRegistrationSteps#Step17 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next + val step: FinishRegistrationSteps#Step17 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next - step.validations shouldBe a [Success[_]] - step.tryNext shouldBe a [Success[_]] + step.validations shouldBe a[Success[_]] + step.tryNext shouldBe a[Success[_]] } } @@ -1857,11 +2608,15 @@ class RelyingPartyRegistrationSpec extends FunSpec with Matchers with ScalaCheck val testData = RegistrationTestData.FidoU2f.BasicAttestation val steps = finishRegistration( testData = testData, - metadataService = Some(new TestMetadataService(Some(Attestation.builder().trusted(true).build()))), - credentialRepository = Helpers.CredentialRepository.empty + metadataService = Some( + new TestMetadataService( + Some(Attestation.builder().trusted(true).build()) + ) + ), + credentialRepository = Helpers.CredentialRepository.empty, ) - steps.run.getKeyId.getId should be (testData.response.getId) - steps.run.isAttestationTrusted should be (true) + steps.run.getKeyId.getId should be(testData.response.getId) + steps.run.isAttestationTrusted should be(true) } } @@ -1871,25 +2626,26 @@ class RelyingPartyRegistrationSpec extends FunSpec with Matchers with ScalaCheck val steps = finishRegistration( testData = testData, allowUntrustedAttestation = true, - credentialRepository = Helpers.CredentialRepository.empty + credentialRepository = Helpers.CredentialRepository.empty, ) - steps.run.getKeyId.getId should be (testData.response.getId) - steps.run.isAttestationTrusted should be (false) + steps.run.getKeyId.getId should be(testData.response.getId) + steps.run.isAttestationTrusted should be(false) } describe("The test case with unknown attestation") { - val testData = RegistrationTestData.FidoU2f.BasicAttestation.setAttestationStatementFormat("urgel") + val testData = RegistrationTestData.FidoU2f.BasicAttestation + .setAttestationStatementFormat("urgel") it("passes if the RP allows untrusted attestation.") { val steps = finishRegistration( testData = testData, allowUntrustedAttestation = true, - credentialRepository = Helpers.CredentialRepository.empty + credentialRepository = Helpers.CredentialRepository.empty, ) val result = Try(steps.run) - result shouldBe a [Success[_]] - result.get.isAttestationTrusted should be (false) - result.get.getAttestationType should be (AttestationType.UNKNOWN) + result shouldBe a[Success[_]] + result.get.isAttestationTrusted should be(false) + result.get.getAttestationType should be(AttestationType.UNKNOWN) result.get.getAttestationMetadata.asScala shouldBe empty } @@ -1897,11 +2653,11 @@ class RelyingPartyRegistrationSpec extends FunSpec with Matchers with ScalaCheck val steps = finishRegistration( testData = testData, allowUntrustedAttestation = false, - credentialRepository = Helpers.CredentialRepository.empty + credentialRepository = Helpers.CredentialRepository.empty, ) val result = Try(steps.run) - result shouldBe a [Failure[_]] - result.failed.get shouldBe an [IllegalArgumentException] + result shouldBe a[Failure[_]] + result.failed.get shouldBe an[IllegalArgumentException] } } @@ -1910,17 +2666,18 @@ class RelyingPartyRegistrationSpec extends FunSpec with Matchers with ScalaCheck } def testUntrusted(testData: RegistrationTestData): Unit = { - val fmt = new AttestationObject(testData.attestationObject).getFormat + val fmt = + new AttestationObject(testData.attestationObject).getFormat it(s"""A test case with good "${fmt}" attestation but no metadata service succeeds, but reports attestation as not trusted.""") { val testData = RegistrationTestData.FidoU2f.BasicAttestation val steps = finishRegistration( testData = testData, metadataService = None, allowUntrustedAttestation = true, - credentialRepository = Helpers.CredentialRepository.empty + credentialRepository = Helpers.CredentialRepository.empty, ) - steps.run.getKeyId.getId should be (testData.response.getId) - steps.run.isAttestationTrusted should be (false) + steps.run.getKeyId.getId should be(testData.response.getId) + steps.run.isAttestationTrusted should be(false) } } @@ -1932,132 +2689,289 @@ class RelyingPartyRegistrationSpec extends FunSpec with Matchers with ScalaCheck } it("(Deleted) If verification of the attestation statement failed, the Relying Party MUST fail the registration ceremony.") { - val steps = finishRegistration(testData = RegistrationTestData.FidoU2f.BasicAttestation.editClientData("foo", "bar")) - val step14: FinishRegistrationSteps#Step14 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next + val steps = finishRegistration(testData = + RegistrationTestData.FidoU2f.BasicAttestation + .editClientData("foo", "bar") + ) + val step14: FinishRegistrationSteps#Step14 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next val step15: Try[FinishRegistrationSteps#Step15] = Try(step14.next) - step14.validations shouldBe a [Failure[_]] - Try(step14.next) shouldBe a [Failure[_]] + step14.validations shouldBe a[Failure[_]] + Try(step14.next) shouldBe a[Failure[_]] - step15 shouldBe a [Failure[_]] - step15.failed.get shouldBe an [IllegalArgumentException] + step15 shouldBe a[Failure[_]] + step15.failed.get shouldBe an[IllegalArgumentException] - Try(steps.run) shouldBe a [Failure[_]] - Try(steps.run).failed.get shouldBe an [IllegalArgumentException] + Try(steps.run) shouldBe a[Failure[_]] + Try(steps.run).failed.get shouldBe an[IllegalArgumentException] } describe("The default RelyingParty settings") { - val rp = RelyingParty.builder() - .identity(RelyingPartyIdentity.builder().id("localhost").name("Test party").build()) + val rp = RelyingParty + .builder() + .identity( + RelyingPartyIdentity + .builder() + .id("localhost") + .name("Test party") + .build() + ) .credentialRepository(Helpers.CredentialRepository.empty) .build() - val request = rp.startRegistration(StartRegistrationOptions.builder() - .user(UserIdentity.builder().name("test").displayName("Test Testsson").id(new ByteArray(Array())).build()) - .build() - ).toBuilder() - .challenge(RegistrationTestData.NoneAttestation.Default.clientData.getChallenge) + val request = rp + .startRegistration( + StartRegistrationOptions + .builder() + .user( + UserIdentity + .builder() + .name("test") + .displayName("Test Testsson") + .id(new ByteArray(Array())) + .build() + ) + .build() + ) + .toBuilder() + .challenge( + RegistrationTestData.NoneAttestation.Default.clientData.getChallenge + ) .build() it("accept registrations with no attestation.") { - val result = rp.finishRegistration(FinishRegistrationOptions.builder() + val result = rp.finishRegistration( + FinishRegistrationOptions + .builder() .request(request) .response(RegistrationTestData.NoneAttestation.Default.response) .build() ) - result.isAttestationTrusted should be (false) - result.getAttestationType should be (AttestationType.NONE) - result.getKeyId.getId should equal (RegistrationTestData.NoneAttestation.Default.response.getId) + result.isAttestationTrusted should be(false) + result.getAttestationType should be(AttestationType.NONE) + result.getKeyId.getId should equal( + RegistrationTestData.NoneAttestation.Default.response.getId + ) } - it("accept registrations with unknown attestation statement format.") { - val testData = RegistrationTestData.FidoU2f.BasicAttestation.setAttestationStatementFormat("urgel") - val result = rp.finishRegistration(FinishRegistrationOptions.builder() - .request(request) - .response(testData.response) - .build() + it( + "accept registrations with unknown attestation statement format." + ) { + val testData = RegistrationTestData.FidoU2f.BasicAttestation + .setAttestationStatementFormat("urgel") + val result = rp.finishRegistration( + FinishRegistrationOptions + .builder() + .request(request) + .response(testData.response) + .build() ) - result.isAttestationTrusted should be (false) - result.getAttestationType should be (AttestationType.UNKNOWN) - result.getKeyId.getId should equal (testData.response.getId) + result.isAttestationTrusted should be(false) + result.getAttestationType should be(AttestationType.UNKNOWN) + result.getKeyId.getId should equal(testData.response.getId) } it("accept android-key attestations but report they're untrusted.") { - val result = rp.finishRegistration(FinishRegistrationOptions.builder() - .request(request) - .response(RegistrationTestData.AndroidKey.BasicAttestation.response) - .build() + val result = rp.finishRegistration( + FinishRegistrationOptions + .builder() + .request(request) + .response( + RegistrationTestData.AndroidKey.BasicAttestation.response + ) + .build() ) - result.isAttestationTrusted should be (false) - result.getKeyId.getId should equal (RegistrationTestData.AndroidKey.BasicAttestation.response.getId) + result.isAttestationTrusted should be(false) + result.getKeyId.getId should equal( + RegistrationTestData.AndroidKey.BasicAttestation.response.getId + ) } it("accept TPM attestations but report they're untrusted.") { - val result = rp.finishRegistration(FinishRegistrationOptions.builder() - .request(request) - .response(RegistrationTestData.Tpm.PrivacyCa.response) - .build() + val result = rp.finishRegistration( + FinishRegistrationOptions + .builder() + .request(request) + .response(RegistrationTestData.Tpm.PrivacyCa.response) + .build() ) - result.isAttestationTrusted should be (false) - result.getKeyId.getId should equal (RegistrationTestData.Tpm.PrivacyCa.response.getId) + result.isAttestationTrusted should be(false) + result.getKeyId.getId should equal( + RegistrationTestData.Tpm.PrivacyCa.response.getId + ) } - 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(Helpers.CredentialRepository.empty) - testData.origin.foreach({ o => builder.origins(Set(o).asJava) }) - builder.build() - } + describe("accept apple attestations but report they're untrusted:") { + it("iOS") { + val result = rp + .toBuilder() + .identity(RealExamples.AppleAttestationIos.rp) + .origins( + Set( + RealExamples.AppleAttestationIos.attestation.collectedClientData.getOrigin + ).asJava + ) + .build() + .finishRegistration( + FinishRegistrationOptions + .builder() + .request( + request + .toBuilder() + .challenge( + RealExamples.AppleAttestationIos.attestation.collectedClientData.getChallenge + ) + .build() + ) + .response( + RealExamples.AppleAttestationIos.attestation.credential + ) + .build() + ) - val result = rp.finishRegistration(FinishRegistrationOptions.builder() - .request(testData.request) - .response(testData.response) - .build() + result.isAttestationTrusted should be(false) + RealExamples.AppleAttestationIos.attestation.credential.getResponse.getAttestation.getFormat should be( + "apple" + ) + result.getAttestationType should be( + AttestationType.ANONYMIZATION_CA + ) + result.getKeyId.getId should equal( + RealExamples.AppleAttestationIos.attestation.credential.getId + ) + } + + it("MacOS") { + val result = rp + .toBuilder() + .identity(RealExamples.AppleAttestationMacos.rp) + .origins( + Set( + RealExamples.AppleAttestationMacos.attestation.collectedClientData.getOrigin + ).asJava + ) + .build() + .finishRegistration( + FinishRegistrationOptions + .builder() + .request( + request + .toBuilder() + .challenge( + RealExamples.AppleAttestationMacos.attestation.collectedClientData.getChallenge + ) + .build() + ) + .response( + RealExamples.AppleAttestationMacos.attestation.credential + ) + .build() ) - result.getKeyId.getId should equal (testData.response.getId) - } + result.isAttestationTrusted should be(false) + RealExamples.AppleAttestationMacos.attestation.credential.getResponse.getAttestation.getFormat should be( + "apple" + ) + result.getAttestationType should be( + AttestationType.ANONYMIZATION_CA + ) + result.getKeyId.getId should equal( + RealExamples.AppleAttestationMacos.attestation.credential.getId + ) } } + describe("accept all test examples in the validExamples list.") { + RegistrationTestData.defaultSettingsValidExamples.zipWithIndex + .foreach { + case (testData, i) => + it(s"Succeeds for example index ${i}.") { + val rp = { + val builder = RelyingParty + .builder() + .identity(testData.rpId) + .credentialRepository( + Helpers.CredentialRepository.empty + ) + testData.origin.foreach({ o => + builder.origins(Set(o).asJava) + }) + builder.build() + } + + val result = rp.finishRegistration( + FinishRegistrationOptions + .builder() + .request(testData.request) + .response(testData.response) + .build() + ) + + 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()) + val rp = RelyingParty + .builder() + .identity( + RelyingPartyIdentity + .builder() + .id("localhost") + .name("Test RP") + .build() + ) .credentialRepository(Helpers.CredentialRepository.empty) .build() - val pkcco = rp.startRegistration(StartRegistrationOptions.builder() - .user(UserIdentity.builder() - .name("foo") - .displayName("Foo") - .id(ByteArray.fromHex("aabbccdd")) - .build()) - .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) + 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) + 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) + pubKeyCredParams should contain( + PublicKeyCredentialParameters.RS256 + ) + pubKeyCredParams map (_.getAlg) should contain( + COSEAlgorithmIdentifier.RS256 + ) } } @@ -2072,21 +2986,30 @@ class RelyingPartyRegistrationSpec extends FunSpec with Matchers with ScalaCheck describe("RelyingParty supports registering") { it("a real packed attestation with an RSA key.") { - val rp = RelyingParty.builder() - .identity(RelyingPartyIdentity.builder().id("demo3.yubico.test").name("Yubico WebAuthn demo").build()) + val rp = RelyingParty + .builder() + .identity( + RelyingPartyIdentity + .builder() + .id("demo3.yubico.test") + .name("Yubico WebAuthn demo") + .build() + ) .credentialRepository(Helpers.CredentialRepository.empty) .origins(Set("https://demo3.yubico.test:8443").asJava) .build() val testData = RegistrationTestData.Packed.BasicAttestationRsaReal - val result = rp.finishRegistration(FinishRegistrationOptions.builder() - .request(testData.request) - .response(testData.response) - .build() + val result = rp.finishRegistration( + FinishRegistrationOptions + .builder() + .request(testData.request) + .response(testData.response) + .build() ) - result.isAttestationTrusted should be (false) - result.getKeyId.getId should equal (testData.response.getId) + result.isAttestationTrusted should be(false) + result.getKeyId.getId should equal(testData.response.getId) } } @@ -2102,7 +3025,7 @@ class RelyingPartyRegistrationSpec extends FunSpec with Matchers with ScalaCheck val result = finishRegistration( testData = testData, credentialRepository = Helpers.CredentialRepository.empty, - allowUntrustedAttestation = true + allowUntrustedAttestation = true, ).run result should not be null @@ -2111,19 +3034,27 @@ class RelyingPartyRegistrationSpec extends FunSpec with Matchers with ScalaCheck it("An ES256 key fails if only RSA and EdDSA are allowed.") { val testData = RegistrationTestData.FidoU2f.BasicAttestation - val result = Try(finishRegistration( - testData = testData.copy( - overrideRequest = Some(testData.request.toBuilder - .pubKeyCredParams(List(PublicKeyCredentialParameters.EdDSA, PublicKeyCredentialParameters.RS256).asJava) - .build() - ) - ), - credentialRepository = Helpers.CredentialRepository.empty, - allowUntrustedAttestation = true - ).run) + val result = Try( + finishRegistration( + testData = testData.copy( + overrideRequest = Some( + testData.request.toBuilder + .pubKeyCredParams( + List( + PublicKeyCredentialParameters.EdDSA, + PublicKeyCredentialParameters.RS256, + ).asJava + ) + .build() + ) + ), + credentialRepository = Helpers.CredentialRepository.empty, + allowUntrustedAttestation = true, + ).run + ) - result shouldBe a [Failure[_]] - result.failed.get shouldBe an [IllegalArgumentException] + result shouldBe a[Failure[_]] + result.failed.get shouldBe an[IllegalArgumentException] } } diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyStartOperationSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyStartOperationSpec.scala index c5569f242..be09a6f7d 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyStartOperationSpec.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyStartOperationSpec.scala @@ -24,13 +24,12 @@ package com.yubico.webauthn -import java.util.Optional - import com.yubico.internal.util.scala.JavaConverters._ import com.yubico.scalacheck.gen.JavaGenerators._ import com.yubico.webauthn.data.AuthenticatorAttachment import com.yubico.webauthn.data.AuthenticatorSelectionCriteria import com.yubico.webauthn.data.ByteArray +import com.yubico.webauthn.data.Generators._ import com.yubico.webauthn.data.PublicKeyCredentialDescriptor import com.yubico.webauthn.data.PublicKeyCredentialParameters import com.yubico.webauthn.data.RelyingPartyIdentity @@ -43,42 +42,62 @@ import org.scalatest.FunSpec import org.scalatest.Matchers import org.scalatestplus.junit.JUnitRunner import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks -import com.yubico.webauthn.data.Generators._ +import java.util.Optional import scala.jdk.CollectionConverters._ - @RunWith(classOf[JUnitRunner]) -class RelyingPartyStartOperationSpec extends FunSpec with Matchers with ScalaCheckDrivenPropertyChecks { - - def credRepo(credentials: Set[PublicKeyCredentialDescriptor]): CredentialRepository = new CredentialRepository { - override def getCredentialIdsForUsername(username: String): java.util.Set[PublicKeyCredentialDescriptor] = credentials.asJava - 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): java.util.Set[RegisteredCredential] = ??? - } +class RelyingPartyStartOperationSpec + extends FunSpec + with Matchers + with ScalaCheckDrivenPropertyChecks { + + def credRepo( + credentials: Set[PublicKeyCredentialDescriptor] + ): CredentialRepository = + new CredentialRepository { + override def getCredentialIdsForUsername( + username: String + ): java.util.Set[PublicKeyCredentialDescriptor] = credentials.asJava + 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 + ): java.util.Set[RegisteredCredential] = ??? + } def relyingParty( - appId: Optional[AppId] = None.asJava, - credentials: Set[PublicKeyCredentialDescriptor] = Set.empty - ): RelyingParty = RelyingParty.builder() - .identity(rpId) - .credentialRepository(credRepo(credentials)) - .preferredPubkeyParams(List(PublicKeyCredentialParameters.ES256).asJava) - .origins(Set.empty.asJava) - .appId(appId) - .build() - - val rpId = RelyingPartyIdentity.builder() + appId: Optional[AppId] = None.asJava, + credentials: Set[PublicKeyCredentialDescriptor] = Set.empty, + ): RelyingParty = + RelyingParty + .builder() + .identity(rpId) + .credentialRepository(credRepo(credentials)) + .preferredPubkeyParams(List(PublicKeyCredentialParameters.ES256).asJava) + .origins(Set.empty.asJava) + .appId(appId) + .build() + + val rpId = RelyingPartyIdentity + .builder() .id("localhost") .name("Test") .build() - val userId = UserIdentity.builder() + val userId = UserIdentity + .builder() .name("foo") .displayName("Foo") - .id(new ByteArray(Array(0, 1 ,2, 3))) + .id(new ByteArray(Array(0, 1, 2, 3))) .build() describe("RelyingParty.startRegistration") { @@ -86,20 +105,28 @@ class RelyingPartyStartOperationSpec extends FunSpec with Matchers with ScalaChe it("sets excludeCredentials automatically.") { forAll { credentials: Set[PublicKeyCredentialDescriptor] => val rp = relyingParty(credentials = credentials) - val result = rp.startRegistration(StartRegistrationOptions.builder() - .user(userId) - .build() + val result = rp.startRegistration( + StartRegistrationOptions + .builder() + .user(userId) + .build() ) - result.getExcludeCredentials.asScala.map(_.asScala) should equal (Some(credentials)) + result.getExcludeCredentials.asScala.map(_.asScala) should equal( + Some(credentials) + ) } } it("sets challenge randomly.") { val rp = relyingParty() - val request1 = rp.startRegistration(StartRegistrationOptions.builder().user(userId).build()) - val request2 = rp.startRegistration(StartRegistrationOptions.builder().user(userId).build()) + val request1 = rp.startRegistration( + StartRegistrationOptions.builder().user(userId).build() + ) + val request2 = rp.startRegistration( + StartRegistrationOptions.builder().user(userId).build() + ) request1.getChallenge should not equal request2.getChallenge request1.getChallenge.size should be >= 32 @@ -107,25 +134,30 @@ class RelyingPartyStartOperationSpec extends FunSpec with Matchers with ScalaChe } it("allows setting authenticatorSelection.") { - val authnrSel = AuthenticatorSelectionCriteria.builder() + val authnrSel = AuthenticatorSelectionCriteria + .builder() .authenticatorAttachment(AuthenticatorAttachment.CROSS_PLATFORM) .requireResidentKey(true) .build() val pkcco = relyingParty().startRegistration( - StartRegistrationOptions.builder() + StartRegistrationOptions + .builder() .user(userId) .authenticatorSelection(authnrSel) - .build()) - pkcco.getAuthenticatorSelection.asScala should equal (Some(authnrSel)) + .build() + ) + pkcco.getAuthenticatorSelection.asScala should equal(Some(authnrSel)) } it("allows setting the timeout to empty.") { val pkcco = relyingParty().startRegistration( - StartRegistrationOptions.builder() + StartRegistrationOptions + .builder() .user(userId) .timeout(Optional.empty[java.lang.Long]) - .build()) + .build() + ) pkcco.getTimeout.asScala shouldBe empty } @@ -134,37 +166,43 @@ class RelyingPartyStartOperationSpec extends FunSpec with Matchers with ScalaChe forAll(Gen.posNum[Long]) { timeout: Long => val pkcco = rp.startRegistration( - StartRegistrationOptions.builder() + StartRegistrationOptions + .builder() .user(userId) .timeout(timeout) - .build()) + .build() + ) - pkcco.getTimeout.asScala should equal (Some(timeout)) + pkcco.getTimeout.asScala should equal(Some(timeout)) } } it("does not allow setting the timeout to zero or negative.") { - an [IllegalArgumentException] should be thrownBy { - StartRegistrationOptions.builder() + an[IllegalArgumentException] should be thrownBy { + StartRegistrationOptions + .builder() .user(userId) .timeout(0) } - an [IllegalArgumentException] should be thrownBy { - StartRegistrationOptions.builder() + an[IllegalArgumentException] should be thrownBy { + StartRegistrationOptions + .builder() .user(userId) .timeout(Optional.of[java.lang.Long](0L)) } forAll(Gen.negNum[Long]) { timeout: Long => - an [IllegalArgumentException] should be thrownBy { - StartRegistrationOptions.builder() + an[IllegalArgumentException] should be thrownBy { + StartRegistrationOptions + .builder() .user(userId) .timeout(timeout) } - an [IllegalArgumentException] should be thrownBy { - StartRegistrationOptions.builder() + an[IllegalArgumentException] should be thrownBy { + StartRegistrationOptions + .builder() .user(userId) .timeout(Optional.of[java.lang.Long](timeout)) } @@ -186,12 +224,15 @@ class RelyingPartyStartOperationSpec extends FunSpec with Matchers with ScalaChe it("sets allowCredentials automatically if given a username.") { forAll { credentials: Set[PublicKeyCredentialDescriptor] => val rp = relyingParty(credentials = credentials) - val result = rp.startAssertion(StartAssertionOptions.builder() - .username(userId.getName) - .build() + val result = rp.startAssertion( + StartAssertionOptions + .builder() + .username(userId.getName) + .build() ) - result.getPublicKeyCredentialRequestOptions.getAllowCredentials.asScala.map(_.asScala.toSet) should equal (Some(credentials)) + result.getPublicKeyCredentialRequestOptions.getAllowCredentials.asScala + .map(_.asScala.toSet) should equal(Some(credentials)) } } @@ -209,20 +250,26 @@ class RelyingPartyStartOperationSpec extends FunSpec with Matchers with ScalaChe it("sets the appid extension if the RP instance is given an AppId.") { forAll { appId: Optional[AppId] => val rp = relyingParty(appId = appId) - val result = rp.startAssertion(StartAssertionOptions.builder() - .username(userId.getName) - .build() + val result = rp.startAssertion( + StartAssertionOptions + .builder() + .username(userId.getName) + .build() ) - result.getPublicKeyCredentialRequestOptions.getExtensions.getAppid should equal (appId) + result.getPublicKeyCredentialRequestOptions.getExtensions.getAppid should equal( + appId + ) } } it("allows setting the timeout to empty.") { val req = relyingParty().startAssertion( - StartAssertionOptions.builder() + StartAssertionOptions + .builder() .timeout(Optional.empty[java.lang.Long]) - .build()) + .build() + ) req.getPublicKeyCredentialRequestOptions.getTimeout.asScala shouldBe empty } @@ -231,33 +278,41 @@ class RelyingPartyStartOperationSpec extends FunSpec with Matchers with ScalaChe forAll(Gen.posNum[Long]) { timeout: Long => val req = rp.startAssertion( - StartAssertionOptions.builder() + StartAssertionOptions + .builder() .timeout(timeout) - .build()) + .build() + ) - req.getPublicKeyCredentialRequestOptions.getTimeout.asScala should equal (Some(timeout)) + req.getPublicKeyCredentialRequestOptions.getTimeout.asScala should equal( + Some(timeout) + ) } } it("does not allow setting the timeout to zero or negative.") { - an [IllegalArgumentException] should be thrownBy { - StartAssertionOptions.builder() + an[IllegalArgumentException] should be thrownBy { + StartAssertionOptions + .builder() .timeout(0) } - an [IllegalArgumentException] should be thrownBy { - StartAssertionOptions.builder() + an[IllegalArgumentException] should be thrownBy { + StartAssertionOptions + .builder() .timeout(Optional.of[java.lang.Long](0L)) } forAll(Gen.negNum[Long]) { timeout: Long => - an [IllegalArgumentException] should be thrownBy { - StartAssertionOptions.builder() + an[IllegalArgumentException] should be thrownBy { + StartAssertionOptions + .builder() .timeout(timeout) } - an [IllegalArgumentException] should be thrownBy { - StartAssertionOptions.builder() + an[IllegalArgumentException] should be thrownBy { + StartAssertionOptions + .builder() .timeout(Optional.of[java.lang.Long](timeout)) } } 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 512deb5bb..91668eef8 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 @@ -24,10 +24,6 @@ package com.yubico.webauthn -import java.security.KeyPair -import java.security.interfaces.ECPublicKey -import java.util.Optional - import com.fasterxml.jackson.databind.node.JsonNodeFactory import com.fasterxml.jackson.databind.node.ObjectNode import com.yubico.internal.util.scala.JavaConverters._ @@ -43,45 +39,63 @@ import org.scalatest.FunSpec import org.scalatest.Matchers import org.scalatestplus.junit.JUnitRunner +import java.security.KeyPair +import java.security.interfaces.ECPublicKey +import java.util.Optional import scala.jdk.CollectionConverters._ import scala.util.Failure import scala.util.Success import scala.util.Try - @RunWith(classOf[JUnitRunner]) -class RelyingPartyUserIdentificationSpec extends FunSpec with Matchers { +class RelyingPartyUserIdentificationSpec extends FunSpec with Matchers { private def jsonFactory: JsonNodeFactory = JsonNodeFactory.instance private object Defaults { - val rpId = RelyingPartyIdentity.builder().id("localhost").name("Test party").build() + val rpId = + RelyingPartyIdentity.builder().id("localhost").name("Test party").build() // These values were generated using TestAuthenticator.makeCredentialExample(TestAuthenticator.createCredential()) - 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 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 credentialKey: KeyPair = TestAuthenticator.importEcKeypair( - privateBytes = ByteArray.fromHex("308193020100301306072a8648ce3d020106082a8648ce3d0301070479307702010104206a88f478910df685bc0cfcc2077e64fb3a8ba770fb23fbbcd1f6572ce35cf360a00a06082a8648ce3d030107a14403420004d8020a2ec718c2c595bb890fcdaf9b81cc742118efdbb8812ac4a9dd5ace2990ec22a48faf1544df0fe5fe0e2e7a69720e63a83d7f46aa022f1323eaf7967762"), - publicBytes = ByteArray.fromHex("3059301306072a8648ce3d020106082a8648ce3d03010703420004d8020a2ec718c2c595bb890fcdaf9b81cc742118efdbb8812ac4a9dd5ace2990ec22a48faf1544df0fe5fe0e2e7a69720e63a83d7f46aa022f1323eaf7967762") + privateBytes = + ByteArray.fromHex("308193020100301306072a8648ce3d020106082a8648ce3d0301070479307702010104206a88f478910df685bc0cfcc2077e64fb3a8ba770fb23fbbcd1f6572ce35cf360a00a06082a8648ce3d030107a14403420004d8020a2ec718c2c595bb890fcdaf9b81cc742118efdbb8812ac4a9dd5ace2990ec22a48faf1544df0fe5fe0e2e7a69720e63a83d7f46aa022f1323eaf7967762"), + publicBytes = + ByteArray.fromHex("3059301306072a8648ce3d020106082a8648ce3d03010703420004d8020a2ec718c2c595bb890fcdaf9b81cc742118efdbb8812ac4a9dd5ace2990ec22a48faf1544df0fe5fe0e2e7a69720e63a83d7f46aa022f1323eaf7967762"), ) - val signature: ByteArray = ByteArray.fromHex("30450221008d478e4c24894d261c7fd3790363ba9687facf4dd1d59610933a2c292cffc3d902205069264c167833d239d6af4c7bf7326c4883fb8c3517a2c86318aa3060d8b441") + val signature: ByteArray = + ByteArray.fromHex("30450221008d478e4c24894d261c7fd3790363ba9687facf4dd1d59610933a2c292cffc3d902205069264c167833d239d6af4c7bf7326c4883fb8c3517a2c86318aa3060d8b441") // These values are not signed over - val userHandle: ByteArray = ByteArray.fromHex("6d8972d9603ce4f3fa5d520ce6d024bf") + val userHandle: ByteArray = + ByteArray.fromHex("6d8972d9603ce4f3fa5d520ce6d024bf") // These values are defined by the attestationObject and clientDataJson above - val clientDataJsonBytes: ByteArray = new ByteArray(clientDataJson.getBytes("UTF-8")) + val clientDataJsonBytes: ByteArray = new ByteArray( + clientDataJson.getBytes("UTF-8") + ) val clientData = new CollectedClientData(clientDataJsonBytes) val challenge: ByteArray = clientData.getChallenge val requestedExtensions: Option[ObjectNode] = None - val clientExtensionResults: ClientAssertionExtensionOutputs = ClientAssertionExtensionOutputs.builder().build() - - val publicKeyCredential: PublicKeyCredential[AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs] = PublicKeyCredential.builder() + val clientExtensionResults: ClientAssertionExtensionOutputs = + ClientAssertionExtensionOutputs.builder().build() + + val publicKeyCredential: PublicKeyCredential[ + AuthenticatorAssertionResponse, + ClientAssertionExtensionOutputs, + ] = PublicKeyCredential + .builder() .id(credentialId) .response( - AuthenticatorAssertionResponse.builder() + AuthenticatorAssertionResponse + .builder() .authenticatorData(authenticatorData) .clientDataJSON(clientDataJsonBytes) .signature(signature) @@ -93,9 +107,10 @@ class RelyingPartyUserIdentificationSpec extends FunSpec with Matchers { val username = "foo-user" def defaultResponse( - userHandle: Option[ByteArray] = None + userHandle: Option[ByteArray] = None ): AuthenticatorAssertionResponse = - AuthenticatorAssertionResponse.builder() + AuthenticatorAssertionResponse + .builder() .authenticatorData(authenticatorData) .clientDataJSON(clientDataJsonBytes) .signature(signature) @@ -103,11 +118,15 @@ class RelyingPartyUserIdentificationSpec extends FunSpec with Matchers { .build() def defaultPublicKeyCredential( - credentialId: ByteArray = Defaults.credentialId, - response: Option[AuthenticatorAssertionResponse] = None, - userHandle: Option[ByteArray] = None - ): PublicKeyCredential[AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs] = - PublicKeyCredential.builder() + credentialId: ByteArray = Defaults.credentialId, + response: Option[AuthenticatorAssertionResponse] = None, + userHandle: Option[ByteArray] = None, + ): PublicKeyCredential[ + AuthenticatorAssertionResponse, + ClientAssertionExtensionOutputs, + ] = + PublicKeyCredential + .builder() .id(credentialId) .response(response getOrElse defaultResponse(userHandle = userHandle)) .clientExtensionResults(clientExtensionResults) @@ -116,35 +135,49 @@ class RelyingPartyUserIdentificationSpec extends FunSpec with Matchers { describe("The assertion ceremony") { - val rp = RelyingParty.builder() + val rp = RelyingParty + .builder() .identity(Defaults.rpId) .credentialRepository( new CredentialRepository { override def getCredentialIdsForUsername(username: String) = if (username == Defaults.username) - Set(PublicKeyCredentialDescriptor.builder().id(Defaults.credentialId).build()).asJava + Set( + PublicKeyCredentialDescriptor + .builder() + .id(Defaults.credentialId) + .build() + ).asJava else Set.empty.asJava override def lookup(credId: ByteArray, lookupUserHandle: ByteArray) = if (credId == Defaults.credentialId) - Some(RegisteredCredential.builder() - .credentialId(Defaults.credentialId) - .userHandle(Defaults.userHandle) - .publicKeyCose(WebAuthnTestCodecs.ecPublicKeyToCose(Defaults.credentialKey.getPublic.asInstanceOf[ECPublicKey])) - .signatureCount(0) - .build() + Some( + RegisteredCredential + .builder() + .credentialId(Defaults.credentialId) + .userHandle(Defaults.userHandle) + .publicKeyCose( + WebAuthnTestCodecs.ecPublicKeyToCose( + Defaults.credentialKey.getPublic.asInstanceOf[ECPublicKey] + ) + ) + .signatureCount(0) + .build() ).asJava else None.asJava override def lookupAll(credId: ByteArray) = ??? - override def getUserHandleForUsername(username: String): Optional[ByteArray] = + override def getUserHandleForUsername(username: String) + : Optional[ByteArray] = if (username == Defaults.username) Some(Defaults.userHandle).asJava else None.asJava - override def getUsernameForUserHandle(userHandle: ByteArray): Optional[String] = + override def getUsernameForUserHandle(userHandle: ByteArray) + : Optional[String] = if (userHandle == Defaults.userHandle) Some(Defaults.username).asJava else @@ -158,60 +191,87 @@ class RelyingPartyUserIdentificationSpec extends FunSpec with Matchers { .build() it("succeeds for the default test case if a username was given.") { - val request = rp.startAssertion(StartAssertionOptions.builder() + val request = rp.startAssertion( + StartAssertionOptions + .builder() .username(Defaults.username) - .build()) + .build() + ) val deterministicRequest = - request.toBuilder.publicKeyCredentialRequestOptions( - request.getPublicKeyCredentialRequestOptions.toBuilder.challenge(Defaults.challenge).build() - ) - .build() - - val result = Try(rp.finishAssertion(FinishAssertionOptions.builder() - .request(deterministicRequest) - .response(Defaults.publicKeyCredential) + request.toBuilder + .publicKeyCredentialRequestOptions( + request.getPublicKeyCredentialRequestOptions.toBuilder + .challenge(Defaults.challenge) + .build() + ) .build() - )) - result shouldBe a [Success[_]] + val result = Try( + rp.finishAssertion( + FinishAssertionOptions + .builder() + .request(deterministicRequest) + .response(Defaults.publicKeyCredential) + .build() + ) + ) + + result shouldBe a[Success[_]] } it("succeeds if username was not given but userHandle was returned.") { val request = rp.startAssertion(StartAssertionOptions.builder().build()) val deterministicRequest = - request.toBuilder.publicKeyCredentialRequestOptions( - request.getPublicKeyCredentialRequestOptions.toBuilder.challenge(Defaults.challenge).build() - ) - .build() + request.toBuilder + .publicKeyCredentialRequestOptions( + request.getPublicKeyCredentialRequestOptions.toBuilder + .challenge(Defaults.challenge) + .build() + ) + .build() - val response: PublicKeyCredential[AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs] = Defaults.defaultPublicKeyCredential( + val response: PublicKeyCredential[ + AuthenticatorAssertionResponse, + ClientAssertionExtensionOutputs, + ] = Defaults.defaultPublicKeyCredential( userHandle = Some(Defaults.userHandle) ) - val result = Try(rp.finishAssertion(FinishAssertionOptions.builder() - .request(deterministicRequest) - .response(response) - .build() - )) + val result = Try( + rp.finishAssertion( + FinishAssertionOptions + .builder() + .request(deterministicRequest) + .response(response) + .build() + ) + ) - result shouldBe a [Success[_]] + result shouldBe a[Success[_]] } it("fails for the default test case if no username was given and no userHandle returned.") { val request = rp.startAssertion(StartAssertionOptions.builder().build()) val deterministicRequest = - request.toBuilder.publicKeyCredentialRequestOptions( - request.getPublicKeyCredentialRequestOptions.toBuilder.challenge(Defaults.challenge).build() - ) - .build() - - val result = Try(rp.finishAssertion(FinishAssertionOptions.builder() - .request(deterministicRequest) - .response(Defaults.publicKeyCredential) + request.toBuilder + .publicKeyCredentialRequestOptions( + request.getPublicKeyCredentialRequestOptions.toBuilder + .challenge(Defaults.challenge) + .build() + ) .build() - )) - result shouldBe a [Failure[_]] + val result = Try( + rp.finishAssertion( + FinishAssertionOptions + .builder() + .request(deterministicRequest) + .response(Defaults.publicKeyCredential) + .build() + ) + ) + + result shouldBe a[Failure[_]] } } diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/TestAuthenticator.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/TestAuthenticator.scala index ff7fa0d89..0c6ec2d39 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 @@ -24,32 +24,6 @@ package com.yubico.webauthn -import java.io.BufferedReader -import java.io.InputStream -import java.io.InputStreamReader -import java.math.BigInteger -import java.nio.charset.StandardCharsets -import java.security.KeyFactory -import java.security.KeyPair -import java.security.KeyPairGenerator -import java.security.MessageDigest -import java.security.PrivateKey -import java.security.PublicKey -import java.security.SecureRandom -import java.security.Signature -import java.security.cert.X509Certificate -import java.security.interfaces.ECPublicKey -import java.security.interfaces.RSAPublicKey -import java.security.spec.ECGenParameterSpec -import java.security.spec.ECPoint -import java.security.spec.ECPublicKeySpec -import java.security.spec.PKCS8EncodedKeySpec -import java.security.spec.X509EncodedKeySpec -import java.time.Instant -import java.util.Date -import scala.jdk.CollectionConverters._ -import scala.util.Try - import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.node.JsonNodeFactory @@ -72,6 +46,8 @@ import com.yubico.webauthn.test.Util import org.bouncycastle.asn1.ASN1ObjectIdentifier import org.bouncycastle.asn1.ASN1Primitive import org.bouncycastle.asn1.DEROctetString +import org.bouncycastle.asn1.DERSequence +import org.bouncycastle.asn1.DERTaggedObject import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x509.BasicConstraints import org.bouncycastle.asn1.x509.Extension @@ -89,24 +65,67 @@ import org.bouncycastle.openssl.PEMParser import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder +import java.io.BufferedReader +import java.io.InputStream +import java.io.InputStreamReader +import java.math.BigInteger +import java.nio.charset.StandardCharsets +import java.security.KeyFactory +import java.security.KeyPair +import java.security.KeyPairGenerator +import java.security.MessageDigest +import java.security.PrivateKey +import java.security.PublicKey +import java.security.SecureRandom +import java.security.Signature +import java.security.cert.X509Certificate +import java.security.interfaces.ECPublicKey +import java.security.interfaces.RSAPublicKey +import java.security.spec.ECGenParameterSpec +import java.security.spec.ECPoint +import java.security.spec.ECPublicKeySpec +import java.security.spec.PKCS8EncodedKeySpec +import java.security.spec.X509EncodedKeySpec +import java.time.Instant +import java.util.Date +import scala.jdk.CollectionConverters._ +import scala.util.Try object TestAuthenticator { def main(args: Array[String]): Unit = { - val attestationCertBytes: ByteArray = ByteArray.fromHex("308201313081d8a003020102020441c4567d300a06082a8648ce3d0403023021311f301d0603550403131646697265666f782055324620536f667420546f6b656e301e170d3137303930353134303030345a170d3137303930373134303030345a3021311f301d0603550403131646697265666f782055324620536f667420546f6b656e3059301306072a8648ce3d020106082a8648ce3d03010703420004f9b7dfc17c8a7dcaacdaaad402c7f1f8570e3e9165f6ce2b9b9a4f64333405e1b952c516560bbe7d304d2da3b6582734dadd980e379b0f86a3e42cc657cffe84300a06082a8648ce3d0403020348003045022067fd4da98db1ddbcef53041d3cfd15ed6b8315cb4116889c2eabe6b50b7f985f02210098842f6835ee18181acc765f642fa124556121f418e108c5ec1bb22e9c28b76b") - val publicKeyHex: String = "04f9b7dfc17c8a7dcaacdaaad402c7f1f8570e3e9165f6ce2b9b9a4f64333405e1b952c516560bbe7d304d2da3b6582734dadd980e379b0f86a3e42cc657cffe84" - val signedDataBytes: ByteArray = ByteArray.fromHex("0049960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d976354543ac68315afe4cd7947adf5f7e8e7dc87ddf4582ef6e7fb467e5cad098af50008f926c96b3248cb3733c70a10e3e0995af0892220d6293780335390594e35a73a3743ed97c8e4fd9c0e183d60ccb764edac2fcbdb84b6b940089be98744673db427ce9d4f09261d4f6535bf52dcd216d9ba81a88f2ed5d7fa04bb25e641a3cd7ef9922fdb8d7d4b9f81a55f661b74f26d97a9382dda9a6b62c378cf6603b9f1218a87c158d88bf1ac51b0e4343657de0e9a6b6d60289fed2b46239abe00947e6a04c6733148283cb5786a678afc959262a71be0925da9992354ba6438022d68ae573285e5564196d62edfc46432cba9393c6138882856a0296b41f5b4b97e00e935") - val signatureBytes: ByteArray = ByteArray.fromHex("3046022100a78ca2cb9feb402acc9f50d16d96487821122bbbdf70c8745a6d37161a16de09022100e10db1bf39b73b18acf9236f758558a7811e04a7901d12f7f34f503b171fe51e") - - verifyU2fExampleWithCert(attestationCertBytes, signedDataBytes, signatureBytes) - verifyU2fExampleWithExplicitParams(publicKeyHex, signedDataBytes, signatureBytes) + val attestationCertBytes: ByteArray = + ByteArray.fromHex("308201313081d8a003020102020441c4567d300a06082a8648ce3d0403023021311f301d0603550403131646697265666f782055324620536f667420546f6b656e301e170d3137303930353134303030345a170d3137303930373134303030345a3021311f301d0603550403131646697265666f782055324620536f667420546f6b656e3059301306072a8648ce3d020106082a8648ce3d03010703420004f9b7dfc17c8a7dcaacdaaad402c7f1f8570e3e9165f6ce2b9b9a4f64333405e1b952c516560bbe7d304d2da3b6582734dadd980e379b0f86a3e42cc657cffe84300a06082a8648ce3d0403020348003045022067fd4da98db1ddbcef53041d3cfd15ed6b8315cb4116889c2eabe6b50b7f985f02210098842f6835ee18181acc765f642fa124556121f418e108c5ec1bb22e9c28b76b") + val publicKeyHex: String = + "04f9b7dfc17c8a7dcaacdaaad402c7f1f8570e3e9165f6ce2b9b9a4f64333405e1b952c516560bbe7d304d2da3b6582734dadd980e379b0f86a3e42cc657cffe84" + val signedDataBytes: ByteArray = + ByteArray.fromHex("0049960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d976354543ac68315afe4cd7947adf5f7e8e7dc87ddf4582ef6e7fb467e5cad098af50008f926c96b3248cb3733c70a10e3e0995af0892220d6293780335390594e35a73a3743ed97c8e4fd9c0e183d60ccb764edac2fcbdb84b6b940089be98744673db427ce9d4f09261d4f6535bf52dcd216d9ba81a88f2ed5d7fa04bb25e641a3cd7ef9922fdb8d7d4b9f81a55f661b74f26d97a9382dda9a6b62c378cf6603b9f1218a87c158d88bf1ac51b0e4343657de0e9a6b6d60289fed2b46239abe00947e6a04c6733148283cb5786a678afc959262a71be0925da9992354ba6438022d68ae573285e5564196d62edfc46432cba9393c6138882856a0296b41f5b4b97e00e935") + val signatureBytes: ByteArray = + ByteArray.fromHex("3046022100a78ca2cb9feb402acc9f50d16d96487821122bbbdf70c8745a6d37161a16de09022100e10db1bf39b73b18acf9236f758558a7811e04a7901d12f7f34f503b171fe51e") + + verifyU2fExampleWithCert( + attestationCertBytes, + signedDataBytes, + signatureBytes, + ) + verifyU2fExampleWithExplicitParams( + publicKeyHex, + signedDataBytes, + signatureBytes, + ) println(generateAttestationCertificate()) - val (credential, _) = createBasicAttestedCredential(attestationMaker = AttestationMaker.packed(AttestationSigner.selfsigned(COSEAlgorithmIdentifier.ES256))) + val (credential, _) = createBasicAttestedCredential(attestationMaker = + AttestationMaker.packed( + AttestationSigner.selfsigned(COSEAlgorithmIdentifier.ES256) + ) + ) println(credential) - println(s"Client data: ${new String(credential.getResponse.getClientDataJSON.getBytes, "UTF-8")}") + println( + s"Client data: ${new String(credential.getResponse.getClientDataJSON.getBytes, "UTF-8")}" + ) println(s"Client data: ${credential.getResponse.getClientDataJSON.getHex}") println(s"Client data: ${credential.getResponse.getClientData}") println(s"Attestation object: ${credential.getResponse.getAttestationObject.getHex}") @@ -119,16 +138,26 @@ object TestAuthenticator { println(s"Private key: ${BinaryUtil.toHex(Defaults.credentialKey.getPrivate.getEncoded)}") val assertion = createAssertion() - println(s"Assertion signature: ${assertion.getResponse.getSignature.getHex}") + println( + s"Assertion signature: ${assertion.getResponse.getSignature.getHex}" + ) println(s"Authenticator data: ${assertion.getResponse.getAuthenticatorData.getHex}") println(s"Client data: ${assertion.getResponse.getClientDataJSON.getHex}") - println(s"Client data: ${new String(assertion.getResponse.getClientDataJSON.getBytes, "UTF-8")}") + println( + s"Client data: ${new String(assertion.getResponse.getClientDataJSON.getBytes, "UTF-8")}" + ) } object Defaults { - val aaguid: ByteArray = new ByteArray(Array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15)) - val challenge: ByteArray = new ByteArray(Array(0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 16, 105, 121, 98, 91)) - val credentialId: ByteArray = new ByteArray(((0 to 31).toVector map { _.toByte }).toArray) + val aaguid: ByteArray = new ByteArray( + Array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15) + ) + val challenge: ByteArray = new ByteArray( + Array(0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 16, 105, 121, 98, 91) + ) + val credentialId: ByteArray = new ByteArray( + ((0 to 31).toVector map { _.toByte }).toArray + ) val keyAlgorithm = COSEAlgorithmIdentifier.ES256 val rpId = "localhost" val origin = "https://" + rpId @@ -142,70 +171,163 @@ object TestAuthenticator { private def jsonFactory: JsonNodeFactory = JsonNodeFactory.instance private def toBytes(s: String): ByteArray = new ByteArray(s.getBytes("UTF-8")) - private def toJson(node: JsonNode): String = new ObjectMapper().writeValueAsString(node) + private def toJson(node: JsonNode): String = + new ObjectMapper().writeValueAsString(node) private def sha256(s: String): ByteArray = sha256(toBytes(s)) - private def sha256(b: ByteArray): ByteArray = new ByteArray(MessageDigest.getInstance("SHA-256").digest(b.getBytes)) - + private def sha256(b: ByteArray): ByteArray = + new ByteArray(MessageDigest.getInstance("SHA-256").digest(b.getBytes)) sealed trait AttestationMaker { val format: String - def makeAttestationStatement(authDataBytes: ByteArray, clientDataJson: String): JsonNode + def makeAttestationStatement( + authDataBytes: ByteArray, + clientDataJson: String, + ): JsonNode def attestationCert: Option[X509Certificate] = ??? def makeAttestationObjectBytes( - authDataBytes: ByteArray, - clientDataJson: String, + authDataBytes: ByteArray, + clientDataJson: String, ): ByteArray = { val f = JsonNodeFactory.instance - val attObj = f.objectNode().setAll[ObjectNode](Map( - "authData" -> f.binaryNode(authDataBytes.getBytes), - "fmt" -> f.textNode(format), - "attStmt" -> makeAttestationStatement(authDataBytes, clientDataJson), - ).asJava) + val attObj = f + .objectNode() + .setAll[ObjectNode]( + 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 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 apple( + addNonceExtension: Boolean = true, + nonceValue: Option[ByteArray] = None, + certSubjectPublicKey: Option[PublicKey] = None, + ): (AttestationMaker, X509Certificate, PrivateKey) = { + val (caCert, caKey) = + generateAttestationCertificate( + COSEAlgorithmIdentifier.ES256, + name = new X500Name( + "CN=Yubico WebAuthn unit tests CA, O=Yubico, OU=Apple Attestation" + ), + ) - 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() + ( + new AttestationMaker { + override val format = "apple" + override def makeAttestationStatement( + authDataBytes: ByteArray, + clientDataJson: String, + ): JsonNode = + makeAppleAttestationStatement( + caCert, + caKey, + authDataBytes, + clientDataJson, + addNonceExtension, + nonceValue, + certSubjectPublicKey, + ) + }, + caCert, + caKey, + ) } + + 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 { + 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 + 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) + 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) + 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)) } @@ -215,109 +337,159 @@ object TestAuthenticator { } } - - def makeCreateCredentialExample(publicKeyCredential: PublicKeyCredential[AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs]): String = + 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 (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 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}") + | 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, - attestationMaker: AttestationMaker, - authenticatorExtensions: Option[JsonNode] = None, - challenge: ByteArray = Defaults.challenge, - clientData: Option[JsonNode] = None, - clientExtensions: ClientRegistrationExtensionOutputs = ClientRegistrationExtensionOutputs.builder().build(), - credentialKeypair: Option[KeyPair] = None, - keyAlgorithm: COSEAlgorithmIdentifier = Defaults.keyAlgorithm, - origin: String = Defaults.origin, - rpId: String = Defaults.rpId, - 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(), - ): (data.PublicKeyCredential[data.AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs], KeyPair) = { - - val clientDataJson: String = JacksonCodecs.json.writeValueAsString(clientData getOrElse { - val json: ObjectNode = jsonFactory.objectNode() - - json.setAll(Map( - "challenge" -> jsonFactory.textNode(challenge.getBase64Url), - "origin" -> jsonFactory.textNode(origin), - "type" -> jsonFactory.textNode("webauthn.create") - ).asJava) - - json.set( - "tokenBinding", - { - val tokenBinding = jsonFactory.objectNode() - tokenBinding.set("status", jsonFactory.textNode(tokenBindingStatus)) - tokenBindingId foreach { id => tokenBinding.set("id", jsonFactory.textNode(id)) } - tokenBinding - } - ) + aaguid: ByteArray = Defaults.aaguid, + attestationMaker: AttestationMaker, + authenticatorExtensions: Option[JsonNode] = None, + challenge: ByteArray = Defaults.challenge, + clientData: Option[JsonNode] = None, + clientExtensions: ClientRegistrationExtensionOutputs = + ClientRegistrationExtensionOutputs.builder().build(), + credentialKeypair: Option[KeyPair] = None, + keyAlgorithm: COSEAlgorithmIdentifier = Defaults.keyAlgorithm, + origin: String = Defaults.origin, + rpId: String = Defaults.rpId, + 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(), + ): ( + data.PublicKeyCredential[ + data.AuthenticatorAttestationResponse, + ClientRegistrationExtensionOutputs, + ], + KeyPair, + ) = { + + val clientDataJson: String = + JacksonCodecs.json.writeValueAsString(clientData getOrElse { + val json: ObjectNode = jsonFactory.objectNode() + + json.setAll( + Map( + "challenge" -> jsonFactory.textNode(challenge.getBase64Url), + "origin" -> jsonFactory.textNode(origin), + "type" -> jsonFactory.textNode("webauthn.create"), + ).asJava + ) - json.set("clientExtensions", JacksonCodecs.json().readTree(JacksonCodecs.json().writeValueAsString(clientExtensions))) - authenticatorExtensions foreach { extensions => json.set("authenticatorExtensions", extensions) } + json.set( + "tokenBinding", { + val tokenBinding = jsonFactory.objectNode() + tokenBinding.set("status", jsonFactory.textNode(tokenBindingStatus)) + tokenBindingId foreach { id => + tokenBinding.set("id", jsonFactory.textNode(id)) + } + tokenBinding + }, + ) - json - }) + json.set( + "clientExtensions", + JacksonCodecs + .json() + .readTree(JacksonCodecs.json().writeValueAsString(clientExtensions)), + ) + authenticatorExtensions foreach { extensions => + json.set("authenticatorExtensions", extensions) + } + + json + }) val clientDataJsonBytes = toBytes(clientDataJson) - val keypair = credentialKeypair.getOrElse(generateKeypair(algorithm = keyAlgorithm)) + val keypair = + credentialKeypair.getOrElse(generateKeypair(algorithm = keyAlgorithm)) val publicKeyCose = keypair.getPublic match { - case pub: ECPublicKey => WebAuthnTestCodecs.ecPublicKeyToCose(pub) + case pub: ECPublicKey => WebAuthnTestCodecs.ecPublicKeyToCose(pub) case pub: BCEdDSAPublicKey => WebAuthnTestCodecs.eddsaPublicKeyToCose(pub) - case pub: RSAPublicKey => WebAuthnTestCodecs.rsaPublicKeyToCose(pub, keyAlgorithm) + case pub: RSAPublicKey => + WebAuthnTestCodecs.rsaPublicKeyToCose(pub, keyAlgorithm) } val authDataBytes: ByteArray = makeAuthDataBytes( rpId = Defaults.rpId, - attestedCredentialDataBytes = Some(makeAttestedCredentialDataBytes( - aaguid = aaguid, - publicKeyCose = publicKeyCose, - rpId = Defaults.rpId - )) + attestedCredentialDataBytes = Some( + makeAttestedCredentialDataBytes( + aaguid = aaguid, + publicKeyCose = publicKeyCose, + rpId = Defaults.rpId, + ) + ), ) - val attestationObjectBytes = attestationMaker.makeAttestationObjectBytes(authDataBytes, clientDataJson) + val attestationObjectBytes = + attestationMaker.makeAttestationObjectBytes(authDataBytes, clientDataJson) - val response = AuthenticatorAttestationResponse.builder() + val response = AuthenticatorAttestationResponse + .builder() .attestationObject(attestationObjectBytes) .clientDataJSON(clientDataJsonBytes) .build() ( - PublicKeyCredential.builder() - .id(response.getAttestation.getAuthenticatorData.getAttestedCredentialData.get.getCredentialId) + PublicKeyCredential + .builder() + .id( + response.getAttestation.getAuthenticatorData.getAttestedCredentialData.get.getCredentialId + ) .response(response) .clientExtensionResults(clientExtensions) .build(), - keypair + keypair, ) } def createBasicAttestedCredential( - aaguid: ByteArray = Defaults.aaguid, - attestationMaker: AttestationMaker, - keyAlgorithm: COSEAlgorithmIdentifier = Defaults.keyAlgorithm, - ): (data.PublicKeyCredential[data.AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs], KeyPair) = + aaguid: ByteArray = Defaults.aaguid, + attestationMaker: AttestationMaker, + keyAlgorithm: COSEAlgorithmIdentifier = Defaults.keyAlgorithm, + ): ( + data.PublicKeyCredential[ + data.AuthenticatorAttestationResponse, + ClientRegistrationExtensionOutputs, + ], + KeyPair, + ) = createCredential( aaguid = aaguid, attestationMaker = attestationMaker, @@ -325,29 +497,44 @@ object TestAuthenticator { ) def createSelfAttestedCredential( - attestationMaker: SelfAttestation => AttestationMaker, - keyAlgorithm: COSEAlgorithmIdentifier = Defaults.keyAlgorithm, - ): (data.PublicKeyCredential[data.AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs], KeyPair) = { + 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 + keyAlgorithm = keyAlgorithm, ) } - def createUnattestedCredential(): (PublicKeyCredential[AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs], KeyPair) = + 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] = { + 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, @@ -365,63 +552,80 @@ object TestAuthenticator { } def createAssertion( - alg: COSEAlgorithmIdentifier = COSEAlgorithmIdentifier.ES256, - authenticatorExtensions: Option[JsonNode] = None, - challenge: ByteArray = Defaults.challenge, - clientData: Option[JsonNode] = None, - clientExtensions: ClientAssertionExtensionOutputs = ClientAssertionExtensionOutputs.builder().build(), - credentialId: ByteArray = Defaults.credentialId, - credentialKey: KeyPair = Defaults.credentialKey, - origin: String = Defaults.origin, - rpId: String = Defaults.rpId, - tokenBindingStatus: String = Defaults.TokenBinding.status, - tokenBindingId: Option[String] = Defaults.TokenBinding.id, - userHandle: Option[ByteArray] = None, - ): data.PublicKeyCredential[data.AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs] = { - - val clientDataJson: String = JacksonCodecs.json.writeValueAsString(clientData getOrElse { - val json: ObjectNode = jsonFactory.objectNode() - - json.setAll(Map( - "challenge" -> jsonFactory.textNode(challenge.getBase64Url), - "origin" -> jsonFactory.textNode(origin), - "type" -> jsonFactory.textNode("webauthn.get") - ).asJava) - - json.set( - "tokenBinding", - { - val tokenBinding = jsonFactory.objectNode() - tokenBinding.set("status", jsonFactory.textNode(tokenBindingStatus)) - tokenBindingId foreach { id => tokenBinding.set("id", jsonFactory.textNode(id)) } - tokenBinding - } - ) + alg: COSEAlgorithmIdentifier = COSEAlgorithmIdentifier.ES256, + authenticatorExtensions: Option[JsonNode] = None, + challenge: ByteArray = Defaults.challenge, + clientData: Option[JsonNode] = None, + clientExtensions: ClientAssertionExtensionOutputs = + ClientAssertionExtensionOutputs.builder().build(), + credentialId: ByteArray = Defaults.credentialId, + credentialKey: KeyPair = Defaults.credentialKey, + origin: String = Defaults.origin, + rpId: String = Defaults.rpId, + tokenBindingStatus: String = Defaults.TokenBinding.status, + tokenBindingId: Option[String] = Defaults.TokenBinding.id, + userHandle: Option[ByteArray] = None, + ): data.PublicKeyCredential[ + data.AuthenticatorAssertionResponse, + ClientAssertionExtensionOutputs, + ] = { + + val clientDataJson: String = + JacksonCodecs.json.writeValueAsString(clientData getOrElse { + val json: ObjectNode = jsonFactory.objectNode() + + json.setAll( + Map( + "challenge" -> jsonFactory.textNode(challenge.getBase64Url), + "origin" -> jsonFactory.textNode(origin), + "type" -> jsonFactory.textNode("webauthn.get"), + ).asJava + ) - json.set("clientExtensions", JacksonCodecs.json().readTree(JacksonCodecs.json().writeValueAsString(clientExtensions))) - authenticatorExtensions foreach { extensions => json.set("authenticatorExtensions", extensions) } + json.set( + "tokenBinding", { + val tokenBinding = jsonFactory.objectNode() + tokenBinding.set("status", jsonFactory.textNode(tokenBindingStatus)) + tokenBindingId foreach { id => + tokenBinding.set("id", jsonFactory.textNode(id)) + } + tokenBinding + }, + ) - json - }) + json.set( + "clientExtensions", + JacksonCodecs + .json() + .readTree(JacksonCodecs.json().writeValueAsString(clientExtensions)), + ) + authenticatorExtensions foreach { extensions => + json.set("authenticatorExtensions", extensions) + } + + json + }) val clientDataJsonBytes = toBytes(clientDataJson) val authDataBytes: ByteArray = makeAuthDataBytes(rpId = Defaults.rpId) - val response = AuthenticatorAssertionResponse.builder() + val response = AuthenticatorAssertionResponse + .builder() .authenticatorData(authDataBytes) .clientDataJSON(clientDataJsonBytes) .signature( makeAssertionSignature( authDataBytes, - Crypto.hash(clientDataJsonBytes), + Crypto.sha256(clientDataJsonBytes), credentialKey.getPrivate, - alg + alg, ) ) .userHandle(userHandle.asJava) .build() - PublicKeyCredential.builder() + PublicKeyCredential + .builder() .id(credentialId) .response(response) .clientExtensionResults(clientExtensions) @@ -429,150 +633,283 @@ object TestAuthenticator { } def makeU2fAttestationStatement( - authDataBytes: ByteArray, - clientDataJson: String, - signer: AttestationSigner, + authDataBytes: ByteArray, + clientDataJson: String, + signer: AttestationSigner, ): JsonNode = { val authData = new AuthenticatorData(authDataBytes) def makeSignedData( - rpIdHash: ByteArray, - clientDataJson: String, - credentialId: ByteArray, - credentialPublicKeyRawBytes: ByteArray + 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) + new ByteArray( + (Vector[Byte](0) + ++ rpIdHash.getBytes + ++ Crypto.sha256(clientDataJson).getBytes + ++ credentialId.getBytes + ++ credentialPublicKeyRawBytes.getBytes).toArray + ) } val signedData = makeSignedData( authData.getRpIdHash, clientDataJson, authData.getAttestedCredentialData.get.getCredentialId, - WebAuthnCodecs.ecPublicKeyToRaw(WebAuthnCodecs.importCosePublicKey(authData.getAttestedCredentialData.get.getCredentialPublicKey).asInstanceOf[ECPublicKey]) + WebAuthnCodecs.ecPublicKeyToRaw( + WebAuthnCodecs + .importCosePublicKey( + authData.getAttestedCredentialData.get.getCredentialPublicKey + ) + .asInstanceOf[ECPublicKey] + ), ) val f = JsonNodeFactory.instance - f.objectNode().setAll(Map( - "x5c" -> f.arrayNode().add(f.binaryNode(signer.cert.getEncoded)), - "sig" -> f.binaryNode( - sign( - signedData, - signer.key, - signer.alg - ).getBytes + f.objectNode() + .setAll( + Map( + "x5c" -> f.arrayNode().add(f.binaryNode(signer.cert.getEncoded)), + "sig" -> f.binaryNode( + sign( + signedData, + signer.key, + signer.alg, + ).getBytes + ), + ).asJava ) - ).asJava) } - def makeNoneAttestationStatement(): JsonNode = JsonNodeFactory.instance.objectNode() + def makeNoneAttestationStatement(): JsonNode = + JsonNodeFactory.instance.objectNode() def makePackedAttestationStatement( - authDataBytes: ByteArray, - clientDataJson: String, - signer: AttestationSigner, + authDataBytes: ByteArray, + clientDataJson: String, + signer: AttestationSigner, ): JsonNode = { - val signedData = new ByteArray(authDataBytes.getBytes ++ Crypto.hash(clientDataJson).getBytes) + val signedData = new ByteArray( + authDataBytes.getBytes ++ Crypto.sha256(clientDataJson).getBytes + ) val signature = signer match { - case SelfAttestation(keypair, alg) => sign(signedData, keypair.getPrivate, alg) + 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( - "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 - ) + f.objectNode() + .setAll( + ( + 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 + ) } def makeAndroidSafetynetAttestationStatement( - authDataBytes: ByteArray, - clientDataJson: String, - cert: AttestationCert, - ctsProfileMatch: Boolean = true + authDataBytes: ByteArray, + clientDataJson: String, + cert: AttestationCert, + ctsProfileMatch: Boolean = true, ): JsonNode = { - val nonce = Crypto.hash(authDataBytes concat Crypto.hash(clientDataJson)) + val nonce = + Crypto.sha256(authDataBytes concat Crypto.sha256(clientDataJson)) val f = JsonNodeFactory.instance - val jwsHeader = f.objectNode().setAll(Map( - "alg" -> f.textNode("RS256"), - "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 - - val jwsPayload = f.objectNode().setAll(Map( - "nonce" -> f.textNode(nonce.getBase64), - "timestampMs" -> f.numberNode(Instant.now().toEpochMilli), - "apkPackageName" -> f.textNode("com.yubico.webauthn.test"), - "apkDigestSha256" -> f.textNode(Crypto.hash("foo").getBase64), - "ctsProfileMatch" -> f.booleanNode(ctsProfileMatch), - "aplCertificateDigestSha256" -> f.arrayNode().add(f.textNode(Crypto.hash("foo").getBase64)), - "basicIntegrity" -> f.booleanNode(true) - ).asJava) - val jwsPayloadBase64 = new ByteArray(JacksonCodecs.json().writeValueAsBytes(jwsPayload)).getBase64Url + val jwsHeader = f + .objectNode() + .setAll( + Map( + "alg" -> f.textNode("RS256"), + "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 + + val jwsPayload = f + .objectNode() + .setAll( + Map( + "nonce" -> f.textNode(nonce.getBase64), + "timestampMs" -> f.numberNode(Instant.now().toEpochMilli), + "apkPackageName" -> f.textNode("com.yubico.webauthn.test"), + "apkDigestSha256" -> f.textNode(Crypto.sha256("foo").getBase64), + "ctsProfileMatch" -> f.booleanNode(ctsProfileMatch), + "aplCertificateDigestSha256" -> f + .arrayNode() + .add(f.textNode(Crypto.sha256("foo").getBase64)), + "basicIntegrity" -> f.booleanNode(true), + ).asJava + ) + val jwsPayloadBase64 = new ByteArray( + JacksonCodecs.json().writeValueAsBytes(jwsPayload) + ).getBase64Url val jwsSignedCompact = jwsHeaderBase64 + "." + jwsPayloadBase64 - val jwsSignedBytes = new ByteArray(jwsSignedCompact.getBytes(StandardCharsets.UTF_8)) + val jwsSignedBytes = new ByteArray( + jwsSignedCompact.getBytes(StandardCharsets.UTF_8) + ) val jwsSignature = sign(jwsSignedBytes, cert.key, cert.alg) val jwsCompact = jwsSignedCompact + "." + jwsSignature.getBase64Url - val attStmt = f.objectNode().setAll(Map( - "ver" -> f.textNode("14799021"), - "response" -> f.binaryNode(jwsCompact.getBytes(StandardCharsets.UTF_8)) - ).asJava) + val attStmt = f + .objectNode() + .setAll( + Map( + "ver" -> f.textNode("14799021"), + "response" -> f.binaryNode( + jwsCompact.getBytes(StandardCharsets.UTF_8) + ), + ).asJava + ) attStmt } + def makeAppleAttestationStatement( + caCert: X509Certificate, + caKey: PrivateKey, + authDataBytes: ByteArray, + clientDataJson: String, + addNonceExtension: Boolean = true, + nonceValue: Option[ByteArray] = None, + certSubjectPublicKey: Option[PublicKey] = None, + ): JsonNode = { + val clientDataJSON = new ByteArray( + clientDataJson.getBytes(StandardCharsets.UTF_8) + ) + val clientDataJsonHash = Crypto.sha256(clientDataJSON) + val nonceToHash = authDataBytes.concat(clientDataJsonHash) + val nonce = Crypto.sha256(nonceToHash) + + val subjectCert = buildCertificate( + certSubjectPublicKey.getOrElse( + WebAuthnTestCodecs.importCosePublicKey( + new AuthenticatorData( + authDataBytes + ).getAttestedCredentialData.get.getCredentialPublicKey + ) + ), + new X500Name( + "CN=Yubico WebAuthn unit tests CA, O=Yubico, OU=Apple Attestation" + ), + new X500Name( + "CN=Apple attestation test credential, O=Yubico, OU=Apple Attestation" + ), + caKey, + COSEAlgorithmIdentifier.ES256, + extensions = if (addNonceExtension) { + List( + ( + "1.2.840.113635.100.8.2", + false, + new DERSequence( + new DERTaggedObject( + 1, + new DEROctetString(nonceValue.getOrElse(nonce).getBytes), + ) + ), + ) + ) + } else Nil, + ) + + val f = JsonNodeFactory.instance + f.objectNode() + .setAll( + Map( + "x5c" -> f + .arrayNode() + .addAll( + List(subjectCert, caCert) + .map(crt => f.binaryNode(crt.getEncoded)) + .asJava + ) + ).asJava + ) + } + def makeAuthDataBytes( - rpId: String = Defaults.rpId, - counterBytes: ByteArray = ByteArray.fromHex("00000539"), - attestedCredentialDataBytes: Option[ByteArray] = None, - extensionsCborBytes: Option[ByteArray] = None + rpId: String = Defaults.rpId, + counterBytes: ByteArray = ByteArray.fromHex("00000539"), + attestedCredentialDataBytes: Option[ByteArray] = None, + extensionsCborBytes: Option[ByteArray] = None, ): ByteArray = - new ByteArray((Vector[Byte]() - ++ sha256(rpId).getBytes.toVector - ++ Some[Byte]((0x01 | (if (attestedCredentialDataBytes.isDefined) 0x40 else 0x00) | (if (extensionsCborBytes.isDefined) 0x80 else 0x00)).toByte) - ++ counterBytes.getBytes.toVector - ++ (attestedCredentialDataBytes map { _.getBytes.toVector } getOrElse Nil) - ++ (extensionsCborBytes map { _.getBytes.toVector } getOrElse Nil) - ).toArray) + new ByteArray( + (Vector[Byte]() + ++ sha256(rpId).getBytes.toVector + ++ Some[Byte]( + (0x01 | (if (attestedCredentialDataBytes.isDefined) 0x40 + else 0x00) | (if (extensionsCborBytes.isDefined) 0x80 + else 0x00)).toByte + ) + ++ counterBytes.getBytes.toVector + ++ (attestedCredentialDataBytes map { + _.getBytes.toVector + } getOrElse Nil) + ++ (extensionsCborBytes map { + _.getBytes.toVector + } getOrElse Nil)).toArray + ) def makeAttestedCredentialDataBytes( - publicKeyCose: ByteArray, - rpId: String = Defaults.rpId, - counterBytes: ByteArray = ByteArray.fromHex("0539"), - aaguid: ByteArray = Defaults.aaguid + publicKeyCose: ByteArray, + rpId: String = Defaults.rpId, + counterBytes: ByteArray = ByteArray.fromHex("0539"), + aaguid: ByteArray = Defaults.aaguid, ): ByteArray = { val credentialId = sha256(publicKeyCose) - new ByteArray((Vector[Byte]() - ++ aaguid.getBytes.toVector - ++ BinaryUtil.fromHex("0020").toVector - ++ credentialId.getBytes.toVector - ++ publicKeyCose.getBytes.toVector - ).toArray) + new ByteArray( + (Vector[Byte]() + ++ aaguid.getBytes.toVector + ++ BinaryUtil.fromHex("0020").toVector + ++ credentialId.getBytes.toVector + ++ publicKeyCose.getBytes.toVector).toArray + ) } - def makeAssertionSignature(authenticatorData: ByteArray, clientDataHash: ByteArray, key: PrivateKey, alg: COSEAlgorithmIdentifier = COSEAlgorithmIdentifier.ES256): ByteArray = + 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, alg: COSEAlgorithmIdentifier): ByteArray = { + def sign( + data: ByteArray, + key: PrivateKey, + alg: COSEAlgorithmIdentifier, + ): ByteArray = { val jAlg = WebAuthnCodecs.getJavaAlgorithmName(alg) // Need to use BouncyCastle provider here because JDK15 standard providers do not support secp256k1 @@ -583,18 +920,20 @@ object TestAuthenticator { new ByteArray(sig.sign()) } - def generateKeypair(algorithm: COSEAlgorithmIdentifier): KeyPair = algorithm match { - case COSEAlgorithmIdentifier.EdDSA => generateEddsaKeypair() - case COSEAlgorithmIdentifier.ES256 => generateEcKeypair() - case COSEAlgorithmIdentifier.RS256 => generateRsaKeypair() - case COSEAlgorithmIdentifier.RS1 => generateRsaKeypair() - } + def generateKeypair(algorithm: COSEAlgorithmIdentifier): KeyPair = + algorithm match { + case COSEAlgorithmIdentifier.EdDSA => generateEddsaKeypair() + case COSEAlgorithmIdentifier.ES256 => generateEcKeypair() + case COSEAlgorithmIdentifier.RS256 => generateRsaKeypair() + case COSEAlgorithmIdentifier.RS1 => generateRsaKeypair() + } def generateEcKeypair(curve: String = "secp256r1"): KeyPair = { val ecSpec = new ECGenParameterSpec(curve) // Need to use BouncyCastle provider here because JDK15 standard providers do not support secp256k1 - val g: KeyPairGenerator = KeyPairGenerator.getInstance("EC", new BouncyCastleProvider()) + val g: KeyPairGenerator = + KeyPairGenerator.getInstance("EC", new BouncyCastleProvider()) g.initialize(ecSpec, new SecureRandom()) @@ -607,12 +946,15 @@ object TestAuthenticator { keyPairGenerator.generateKeyPair() } - def importEcKeypair(privateBytes: ByteArray, publicBytes: ByteArray): KeyPair = { + def importEcKeypair( + privateBytes: ByteArray, + publicBytes: ByteArray, + ): KeyPair = { val keyFactory: KeyFactory = KeyFactory.getInstance("EC") new KeyPair( keyFactory.generatePublic(new X509EncodedKeySpec(publicBytes.getBytes)), - keyFactory.generatePrivate(new PKCS8EncodedKeySpec(privateBytes.getBytes)) + keyFactory.generatePrivate(new PKCS8EncodedKeySpec(privateBytes.getBytes)), ) } @@ -623,9 +965,9 @@ object TestAuthenticator { } def verifyEcSignature( - pubKey: PublicKey, - signedDataBytes: ByteArray, - signatureBytes: ByteArray + pubKey: PublicKey, + signedDataBytes: ByteArray, + signatureBytes: ByteArray, ): Boolean = { val alg = "SHA256withECDSA" val sig: Signature = Signature.getInstance(alg) @@ -633,86 +975,115 @@ object TestAuthenticator { sig.update(signedDataBytes.getBytes) sig.verify(signatureBytes.getBytes) && - Crypto.verifySignature(pubKey, signedDataBytes, signatureBytes, COSEAlgorithmIdentifier.ES256) + Crypto.verifySignature( + pubKey, + signedDataBytes, + signatureBytes, + COSEAlgorithmIdentifier.ES256, + ) } def verifyU2fExampleWithCert( - attestationCertBytes: ByteArray, - signedDataBytes: ByteArray, - signatureBytes: ByteArray + attestationCertBytes: ByteArray, + signedDataBytes: ByteArray, + signatureBytes: ByteArray, ): Unit = { - val attestationCert: X509Certificate = CertificateParser.parseDer(attestationCertBytes.getBytes) + val attestationCert: X509Certificate = + CertificateParser.parseDer(attestationCertBytes.getBytes) val pubKey: PublicKey = attestationCert.getPublicKey verifyEcSignature(pubKey, signedDataBytes, signatureBytes) } def verifyU2fExampleWithExplicitParams( - publicKeyHex: String, - signedDataBytes: ByteArray, - signatureBytes: ByteArray + publicKeyHex: String, + signedDataBytes: ByteArray, + signatureBytes: ByteArray, ): Unit = { - val pubKeyPoint = new ECPoint(new BigInteger(publicKeyHex drop 2 take 64, 16), new BigInteger(publicKeyHex drop 2 drop 64, 16)) + val pubKeyPoint = new ECPoint( + new BigInteger(publicKeyHex drop 2 take 64, 16), + new BigInteger(publicKeyHex drop 2 drop 64, 16), + ) 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 curveSpec: ECNamedCurveSpec = new ECNamedCurveSpec( + "P-256", + namedSpec.getCurve, + namedSpec.getG, + namedSpec.getN, + ) + val pubKeySpec: ECPublicKeySpec = + new ECPublicKeySpec(pubKeyPoint, curveSpec) val keyFactory: KeyFactory = KeyFactory.getInstance("EC") val pubKey: PublicKey = keyFactory.generatePublic(pubKeySpec) verifyEcSignature(pubKey, signedDataBytes, signatureBytes) } def generateAttestationCaCertificate( - 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 + 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 = actualKeypair.getPublic, - issuerName = superCa map (_._1) map JcaX500NameUtil.getSubject getOrElse name, + issuerName = + superCa map (_._1) map JcaX500NameUtil.getSubject getOrElse name, subjectName = name, signingKey = superCa map (_._2) getOrElse actualKeypair.getPrivate, signingAlg = signingAlg, isCa = true, - extensions = extensions + extensions = extensions, ), - actualKeypair.getPrivate + actualKeypair.getPrivate, ) } def generateAttestationCertificate( - 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, + 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, ): (X509Certificate, PrivateKey) = { val actualKeypair = keypair.getOrElse(generateKeypair(alg)) ( buildCertificate( publicKey = actualKeypair.getPublic, - issuerName = caCertAndKey.map(_._1).map(JcaX500NameUtil.getSubject).getOrElse(name), + issuerName = caCertAndKey + .map(_._1) + .map(JcaX500NameUtil.getSubject) + .getOrElse(name), subjectName = name, signingKey = caCertAndKey.map(_._2).getOrElse(actualKeypair.getPrivate), signingAlg = alg, isCa = false, - extensions = extensions + extensions = extensions, ), - actualKeypair.getPrivate + actualKeypair.getPrivate, ) } private def buildCertificate( - publicKey: PublicKey, - issuerName: X500Name, - subjectName: X500Name, - signingKey: PrivateKey, - signingAlg: COSEAlgorithmIdentifier, - isCa: Boolean = false, - extensions: Iterable[(String, Boolean, ASN1Primitive)] = Nil + publicKey: PublicKey, + issuerName: X500Name, + subjectName: X500Name, + signingKey: PrivateKey, + signingAlg: COSEAlgorithmIdentifier, + isCa: Boolean = false, + extensions: Iterable[(String, Boolean, ASN1Primitive)] = Nil, ): X509Certificate = { CertificateParser.parseDer({ val builder = new X509v3CertificateBuilder( @@ -721,7 +1092,7 @@ object TestAuthenticator { Date.from(Instant.parse("2018-09-06T17:42:00Z")), Date.from(Instant.parse("2018-09-06T17:42:00Z")), subjectName, - SubjectPublicKeyInfo.getInstance(publicKey.getEncoded) + SubjectPublicKeyInfo.getInstance(publicKey.getEncoded), ) for { (oid, critical, value) <- extensions } { @@ -729,11 +1100,19 @@ object TestAuthenticator { } if (isCa) { - builder.addExtension(Extension.basicConstraints, true, new BasicConstraints(true)); + builder.addExtension( + Extension.basicConstraints, + true, + new BasicConstraints(true), + ); } - val signerBuilder = new JcaContentSignerBuilder(WebAuthnCodecs.getJavaAlgorithmName(signingAlg)) - .setProvider(new BouncyCastleProvider()) // Needed because JDK15 standard providers do not support secp256k1 + val signerBuilder = new JcaContentSignerBuilder( + WebAuthnCodecs.getJavaAlgorithmName(signingAlg) + ) + .setProvider( + new BouncyCastleProvider() + ) // Needed because JDK15 standard providers do not support secp256k1 builder.build(signerBuilder.build(signingKey)).getEncoded }) @@ -742,17 +1121,23 @@ object TestAuthenticator { def generateRsaCertificate(): (X509Certificate, PrivateKey) = generateAttestationCertificate(COSEAlgorithmIdentifier.RS256) - def importCertAndKeyFromPem(certPem: InputStream, keyPem: InputStream): (X509Certificate, PrivateKey) = { + def importCertAndKeyFromPem( + certPem: InputStream, + keyPem: InputStream, + ): (X509Certificate, PrivateKey) = { val cert: X509Certificate = Util.importCertFromPem(certPem) - val priKeyParser = new PEMParser(new BufferedReader(new InputStreamReader(keyPem))) + val priKeyParser = new PEMParser( + new BufferedReader(new InputStreamReader(keyPem)) + ) priKeyParser.readObject() // Throw away the EC params part val converter = new JcaPEMKeyConverter() val key: PrivateKey = converter .getKeyPair( - priKeyParser.readObject() + priKeyParser + .readObject() .asInstanceOf[PEMKeyPair] ) .getPrivate @@ -762,10 +1147,11 @@ object TestAuthenticator { def coseAlgorithmOfJavaKey(key: PrivateKey): COSEAlgorithmIdentifier = Try(COSEAlgorithmIdentifier.valueOf(key.getAlgorithm)) getOrElse - key match { - case key: BCECPrivateKey => key.getParameters.getCurve match { - case _: SecP256R1Curve => COSEAlgorithmIdentifier.valueOf("ES256") - } + key match { + case key: BCECPrivateKey => + key.getParameters.getCurve match { + case _: SecP256R1Curve => COSEAlgorithmIdentifier.valueOf("ES256") } + } } diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/TestWithEachProvider.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/TestWithEachProvider.scala index c67d66bd8..f6cf5ab0c 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/TestWithEachProvider.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/TestWithEachProvider.scala @@ -1,42 +1,50 @@ package com.yubico.webauthn -import java.security.Provider -import java.security.Security - import org.bouncycastle.jce.provider.BouncyCastleProvider import org.scalatest.FunSpec import org.scalatest.Matchers +import java.security.Provider +import java.security.Security + trait TestWithEachProvider extends Matchers { this: FunSpec => - def wrapItFunctionWithProviderContext(providerSetName: String, providers: List[Provider], testSetupFun: (String => (=> Any) => Unit) => Any): Any = { + def wrapItFunctionWithProviderContext( + providerSetName: String, + providers: List[Provider], + testSetupFun: (String => (=> Any) => Unit) => Any, + ): Any = { /** - * Wrapper around the standard [[FunSpec#it]] that sets the JCA [[Security]] providers before running the test, - * and then resets the providers to the original state after the test. - * - * This is needed because ScalaTest shared tests work by taking fixture parameters as lexical context, - * but JCA providers are set in the dynamic context. - * The [[FunSpec#it]] call does not immediately run the test, instead it registers a test to be run later. - * This helper ensures that the dynamic context matches the lexical context at the time the test runs. - */ + * Wrapper around the standard [[FunSpec#it]] that sets the JCA [[Security]] providers before running the test, + * and then resets the providers to the original state after the test. + * + * This is needed because ScalaTest shared tests work by taking fixture parameters as lexical context, + * but JCA providers are set in the dynamic context. + * The [[FunSpec#it]] call does not immediately run the test, instead it registers a test to be run later. + * This helper ensures that the dynamic context matches the lexical context at the time the test runs. + */ def it(testName: String)(testFun: => Any): Unit = { this.it.apply(testName) { val originalProviders = Security.getProviders.toList - Security.getProviders.foreach(prov => Security.removeProvider(prov.getName)) + Security.getProviders.foreach(prov => + Security.removeProvider(prov.getName) + ) providers.foreach(Security.addProvider) testFun - Security.getProviders.foreach(prov => Security.removeProvider(prov.getName)) + Security.getProviders.foreach(prov => + Security.removeProvider(prov.getName) + ) originalProviders.foreach(Security.addProvider) } } describe(s"""With providers "${providerSetName}":""") { it(s"This test runs with the right security providers: ${providers}.") { - Security.getProviders.toSet should equal (providers.toSet) + Security.getProviders.toSet should equal(providers.toSet) } testSetupFun(it _) @@ -44,28 +52,38 @@ trait TestWithEachProvider extends Matchers { } /** - * Register tests in a modified DSL environment where the `it` "keyword" - * is modified to set the JCA [[Security]] providers before running the test, - * and then reset the providers to the original state after the test. - * - * The caller SHOULD name the callback parameter `it`, - * in order to shadow the standard [[FunSpec#it]] from ScalaTest. - * - * This is needed because ScalaTest shared tests work by taking fixture parameters as lexical context, - * but JCA providers are set in the dynamic context. - * The [[FunSpec#it]] call does not immediately run the test, instead it registers a test to be run later. - * This helper ensures that the dynamic context matches the lexical context at the time the test runs. - */ - def testWithEachProvider(registerTests: (String => (=> Any) => Unit) => Any): Unit = { + * Register tests in a modified DSL environment where the `it` "keyword" + * is modified to set the JCA [[Security]] providers before running the test, + * and then reset the providers to the original state after the test. + * + * The caller SHOULD name the callback parameter `it`, + * in order to shadow the standard [[FunSpec#it]] from ScalaTest. + * + * This is needed because ScalaTest shared tests work by taking fixture parameters as lexical context, + * but JCA providers are set in the dynamic context. + * The [[FunSpec#it]] call does not immediately run the test, instead it registers a test to be run later. + * This helper ensures that the dynamic context matches the lexical context at the time the test runs. + */ + def testWithEachProvider( + registerTests: (String => (=> Any) => Unit) => Any + ): Unit = { val defaultProviders: List[Provider] = Security.getProviders.toList // TODO: Uncomment this in the next major version //it should behave like wrapItFunctionWithProviderContext("default", defaultProviders, registerTests) - it should behave like wrapItFunctionWithProviderContext("BouncyCastle", List(new BouncyCastleProvider()), registerTests) + it should behave like wrapItFunctionWithProviderContext( + "BouncyCastle", + List(new BouncyCastleProvider()), + registerTests, + ) // TODO: Delete this in the next major version - it should behave like wrapItFunctionWithProviderContext("default and BouncyCastle", defaultProviders.appended(new BouncyCastleProvider()), registerTests) + it should behave like wrapItFunctionWithProviderContext( + "default and BouncyCastle", + defaultProviders.appended(new BouncyCastleProvider()), + registerTests, + ) } } 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 2a2e79e61..490da14ae 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 @@ -24,9 +24,6 @@ package com.yubico.webauthn -import java.security.interfaces.ECPublicKey -import scala.util.Try - import com.yubico.webauthn.data.ByteArray import com.yubico.webauthn.test.Util import org.junit.runner.RunWith @@ -37,28 +34,40 @@ import org.scalatest.Matchers import org.scalatestplus.junit.JUnitRunner import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks +import java.security.interfaces.ECPublicKey +import scala.util.Try @RunWith(classOf[JUnitRunner]) -class WebAuthnCodecsSpec extends FunSpec with Matchers with ScalaCheckDrivenPropertyChecks with TestWithEachProvider { - - implicit def arbitraryEcPublicKey: Arbitrary[ECPublicKey] = Arbitrary( - for { - ySign: Byte <- Gen.oneOf(0x02: Byte, 0x03: Byte) - rawBytes: Seq[Byte] <- Gen.listOfN[Byte](32, Arbitrary.arbitrary[Byte]) - key = Try(Util.decodePublicKey(new ByteArray((ySign +: rawBytes).toArray)).asInstanceOf[ECPublicKey]) - if key.isSuccess - } yield key.get - ) +class WebAuthnCodecsSpec + extends FunSpec + with Matchers + with ScalaCheckDrivenPropertyChecks + with TestWithEachProvider { + + implicit def arbitraryEcPublicKey: Arbitrary[ECPublicKey] = + Arbitrary( + for { + ySign: Byte <- Gen.oneOf(0x02: Byte, 0x03: Byte) + rawBytes: Seq[Byte] <- Gen.listOfN[Byte](32, Arbitrary.arbitrary[Byte]) + key = Try( + Util + .decodePublicKey(new ByteArray((ySign +: rawBytes).toArray)) + .asInstanceOf[ECPublicKey] + ) + if key.isSuccess + } yield key.get + ) testWithEachProvider { it => describe("The ecPublicKeyToRaw method") { it("outputs the correct x and y values") { - forAll (minSuccessful(500)) { pubkey: ECPublicKey => - val rawkey: Array[Byte] = WebAuthnCodecs.ecPublicKeyToRaw(pubkey).getBytes + forAll(minSuccessful(500)) { pubkey: ECPublicKey => + val rawkey: Array[Byte] = + WebAuthnCodecs.ecPublicKeyToRaw(pubkey).getBytes - rawkey.length should equal (65) - rawkey(0) should equal (0x04: Byte) + rawkey.length should equal(65) + rawkey(0) should equal(0x04: Byte) val x = rawkey.slice(1, 33) val y = rawkey.slice(33, 65) @@ -66,8 +75,12 @@ class WebAuthnCodecsSpec extends FunSpec with Matchers with ScalaCheckDrivenProp val expectedX = pubkey.getW.getAffineX.toByteArray.toVector val expectedY = pubkey.getW.getAffineY.toByteArray.toVector - x.dropWhile(_ == (0: Byte)) should equal (expectedX.dropWhile(_ == (0: Byte))) - y.dropWhile(_ == (0: Byte)) should equal (expectedY.dropWhile(_ == (0: Byte))) + x.dropWhile(_ == (0: Byte)) should equal( + expectedX.dropWhile(_ == (0: Byte)) + ) + y.dropWhile(_ == (0: Byte)) should equal( + expectedY.dropWhile(_ == (0: Byte)) + ) } } @@ -81,10 +94,13 @@ class WebAuthnCodecsSpec extends FunSpec with Matchers with ScalaCheckDrivenProp val coseKey = WebAuthnTestCodecs.rawEcdaKeyToCose(rawKey) - val importedPubkey: ECPublicKey = WebAuthnCodecs.importCosePublicKey(coseKey).asInstanceOf[ECPublicKey] - val rawImportedPubkey = WebAuthnCodecs.ecPublicKeyToRaw(importedPubkey) + val importedPubkey: ECPublicKey = WebAuthnCodecs + .importCosePublicKey(coseKey) + .asInstanceOf[ECPublicKey] + val rawImportedPubkey = + WebAuthnCodecs.ecPublicKeyToRaw(importedPubkey) - rawImportedPubkey should equal (rawKey) + rawImportedPubkey should equal(rawKey) } } @@ -98,10 +114,13 @@ class WebAuthnCodecsSpec extends FunSpec with Matchers with ScalaCheckDrivenProp val coseKey = WebAuthnTestCodecs.ecPublicKeyToCose(originalPubkey) - val importedPubkey: ECPublicKey = WebAuthnCodecs.importCosePublicKey(coseKey).asInstanceOf[ECPublicKey] - val rawImportedPubkey = WebAuthnCodecs.ecPublicKeyToRaw(importedPubkey) + val importedPubkey: ECPublicKey = WebAuthnCodecs + .importCosePublicKey(coseKey) + .asInstanceOf[ECPublicKey] + val rawImportedPubkey = + WebAuthnCodecs.ecPublicKeyToRaw(importedPubkey) - rawImportedPubkey should equal (rawKey) + rawImportedPubkey should equal(rawKey) } } 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 462514d83..4b408af31 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,5 +1,10 @@ package com.yubico.webauthn +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 + import java.security.KeyFactory import java.security.PrivateKey import java.security.PublicKey @@ -7,25 +12,24 @@ import java.security.interfaces.ECPublicKey import java.security.interfaces.RSAPublicKey import java.security.spec.PKCS8EncodedKeySpec -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 + * Re-exports from [[WebAuthnCodecs]] and [[Crypto]] so tests can use it */ object WebAuthnTestCodecs { + def sha256(bytes: ByteArray): ByteArray = Crypto.sha256(bytes) + def ecPublicKeyToRaw = WebAuthnCodecs.ecPublicKeyToRaw _ def importCosePublicKey = WebAuthnCodecs.importCosePublicKey _ - def ecPublicKeyToCose(key: ECPublicKey): ByteArray = rawEcdaKeyToCose(ecPublicKeyToRaw(key)) + 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))) { + 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)}" ) @@ -40,9 +44,15 @@ object WebAuthnTestCodecs { 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( + -2L, + java.util.Arrays.copyOfRange(keyBytes, start, start + 32), + ) // x - coseKey.put(-3L, java.util.Arrays.copyOfRange(keyBytes, start + 32, start + 64)) // y + coseKey.put( + -3L, + java.util.Arrays.copyOfRange(keyBytes, start + 32, start + 64), + ) // y new ByteArray(CBORObject.FromObject(coseKey).EncodeToBytes) } @@ -50,26 +60,33 @@ object WebAuthnTestCodecs { def publicKeyToCose(key: PublicKey): ByteArray = { key match { case k: ECPublicKey => ecPublicKeyToCose(k) - case other => throw new UnsupportedOperationException("Unknown key type: " + other.getClass.getCanonicalName) + case other => + throw new UnsupportedOperationException( + "Unknown key type: " + other.getClass.getCanonicalName + ) } } - def importPrivateKey(encodedKey: ByteArray, alg: COSEAlgorithmIdentifier): PrivateKey = alg match { - case COSEAlgorithmIdentifier.ES256 => - val keyFactory: KeyFactory = KeyFactory.getInstance("EC") - val spec = new PKCS8EncodedKeySpec(encodedKey.getBytes) - keyFactory.generatePrivate(spec) - - case COSEAlgorithmIdentifier.EdDSA => - val keyFactory: KeyFactory = KeyFactory.getInstance("EdDSA") - val spec = new PKCS8EncodedKeySpec(encodedKey.getBytes) - keyFactory.generatePrivate(spec) - - case COSEAlgorithmIdentifier.RS256 | COSEAlgorithmIdentifier.RS1 => - val keyFactory: KeyFactory = KeyFactory.getInstance("RSA") - val spec = new PKCS8EncodedKeySpec(encodedKey.getBytes) - keyFactory.generatePrivate(spec) - } + def importPrivateKey( + encodedKey: ByteArray, + alg: COSEAlgorithmIdentifier, + ): PrivateKey = + alg match { + case COSEAlgorithmIdentifier.ES256 => + val keyFactory: KeyFactory = KeyFactory.getInstance("EC") + val spec = new PKCS8EncodedKeySpec(encodedKey.getBytes) + keyFactory.generatePrivate(spec) + + case COSEAlgorithmIdentifier.EdDSA => + val keyFactory: KeyFactory = KeyFactory.getInstance("EdDSA") + val spec = new PKCS8EncodedKeySpec(encodedKey.getBytes) + keyFactory.generatePrivate(spec) + + case COSEAlgorithmIdentifier.RS256 | COSEAlgorithmIdentifier.RS1 => + val keyFactory: KeyFactory = KeyFactory.getInstance("RSA") + val spec = new PKCS8EncodedKeySpec(encodedKey.getBytes) + keyFactory.generatePrivate(spec) + } def importEcdsaPrivateKey(encodedKey: ByteArray): PrivateKey = { val keyFactory: KeyFactory = KeyFactory.getInstance("EC") @@ -88,7 +105,10 @@ object WebAuthnTestCodecs { new ByteArray(CBORObject.FromObject(coseKey).EncodeToBytes) } - def rsaPublicKeyToCose(key: RSAPublicKey, alg: COSEAlgorithmIdentifier): ByteArray = { + 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 @@ -103,7 +123,8 @@ object WebAuthnTestCodecs { def getCoseAlgId(encodedPublicKey: ByteArray): COSEAlgorithmIdentifier = { importCosePublicKey(encodedPublicKey).getAlgorithm match { case "EC" => COSEAlgorithmIdentifier.ES256 - case other => throw new UnsupportedOperationException("Unknown algorithm: " + other) + case other => + throw new UnsupportedOperationException("Unknown algorithm: " + other) } } diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/attestation/Generators.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/attestation/Generators.scala index 9b32a37ff..e52015398 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/attestation/Generators.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/attestation/Generators.scala @@ -24,27 +24,29 @@ package com.yubico.webauthn.attestation -import java.util.Optional - import com.yubico.scalacheck.gen.JavaGenerators._ import org.scalacheck.Arbitrary import org.scalacheck.Arbitrary.arbitrary +import java.util.Optional object Generators { - implicit val arbitraryAttestation: Arbitrary[Attestation] = Arbitrary(for { - trusted <- arbitrary[Boolean] - deviceProperties <- arbitrary[Optional[java.util.Map[String, String]]] - metadataIdentifier <- arbitrary[Optional[String]] - transports <- arbitrary[Optional[java.util.Set[Transport]]] - vendorProperties <- arbitrary[Optional[java.util.Map[String, String]]] - } yield Attestation.builder() - .trusted(trusted) - .deviceProperties(deviceProperties) - .metadataIdentifier(metadataIdentifier) - .transports(transports) - .vendorProperties(vendorProperties) - .build()) + implicit val arbitraryAttestation: Arbitrary[Attestation] = Arbitrary( + for { + trusted <- arbitrary[Boolean] + deviceProperties <- arbitrary[Optional[java.util.Map[String, String]]] + metadataIdentifier <- arbitrary[Optional[String]] + transports <- arbitrary[Optional[java.util.Set[Transport]]] + vendorProperties <- arbitrary[Optional[java.util.Map[String, String]]] + } yield Attestation + .builder() + .trusted(trusted) + .deviceProperties(deviceProperties) + .metadataIdentifier(metadataIdentifier) + .transports(transports) + .vendorProperties(vendorProperties) + .build() + ) } diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/AuthenticatorAttestationResponseSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/AuthenticatorAttestationResponseSpec.scala index 2032ceecf..344db8bac 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/AuthenticatorAttestationResponseSpec.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/AuthenticatorAttestationResponseSpec.scala @@ -25,17 +25,17 @@ package com.yubico.webauthn.data import org.junit.runner.RunWith -import org.scalatest.Matchers import org.scalatest.FunSpec +import org.scalatest.Matchers import org.scalatestplus.junit.JUnitRunner - @RunWith(classOf[JUnitRunner]) class AuthenticatorAttestationResponseSpec extends FunSpec with Matchers { describe("AuthenticatorAttestationResponse") { - val exampleAttestation = ByteArray.fromHex("a368617574684461746159012c49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d976341000000000000000000000000000000000000000000a20008dce8bdc3fc2c734a29a20ddb6509bceb721d7381859ab2548ae350fdb1962df68f1ebc08dbb5263c653b4e855b45b7df85b4926ed4572f2af78da28028143d6a6de8c0afcc6c6fbb648ce0bac022ba0a2303d2fced0d9772fcc0d32e281c8563082820e9bfd2e76241637ccbc36aebd85f398f6b6863d3d6755e398e05faf101e467c201219a83b2bf4269efc6e82f2c95dbfbc2a979ea2b78dea9b9fe467a2fa363616c6765455332353661785820c5df3292ce78ea68322b36073fd3b012a35cc9352cba7abd5ed2c287f6112b5361795820a83b6a518319bee86dccd1c8d54b3acb4f590e2cf7d26616aad3e7aa49fc8b4c63666d74686669646f2d7532666761747453746d74a26378356381590136308201323081d9a003020102020500a5427a1d300a06082a8648ce3d0403023021311f301d0603550403131646697265666f782055324620536f667420546f6b656e301e170d3137303833303134353130365a170d3137303930313134353130365a3021311f301d0603550403131646697265666f782055324620536f667420546f6b656e3059301306072a8648ce3d020106082a8648ce3d0301070342000409b9c8303e3a9f1cc0c4bb83c6d56a223699137387ad27dd01ad9c8e0c80addce10e52e622197576f756e38d5965bf98d53ece5af4b0ec003ad08f932bd84c1e300a06082a8648ce3d040302034800304502210083239a57e0fa99224b2c7989998cf833d5c1562df38d285d46cab1d6cf46ae9e02204cfd5deb11de1fdafc4e899f8d03388164beaff2e4263a82210ccc38906981236373696758463044022049c439848ec81672461cc0ea629f297cc7228450a6b0d08872ab969364ec6a6202200ea1acec627fd0e616d23da3e8bfa38a5527f2007cfe3fed63e5f3e2f7e25b11") + val exampleAttestation = + ByteArray.fromHex("a368617574684461746159012c49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d976341000000000000000000000000000000000000000000a20008dce8bdc3fc2c734a29a20ddb6509bceb721d7381859ab2548ae350fdb1962df68f1ebc08dbb5263c653b4e855b45b7df85b4926ed4572f2af78da28028143d6a6de8c0afcc6c6fbb648ce0bac022ba0a2303d2fced0d9772fcc0d32e281c8563082820e9bfd2e76241637ccbc36aebd85f398f6b6863d3d6755e398e05faf101e467c201219a83b2bf4269efc6e82f2c95dbfbc2a979ea2b78dea9b9fe467a2fa363616c6765455332353661785820c5df3292ce78ea68322b36073fd3b012a35cc9352cba7abd5ed2c287f6112b5361795820a83b6a518319bee86dccd1c8d54b3acb4f590e2cf7d26616aad3e7aa49fc8b4c63666d74686669646f2d7532666761747453746d74a26378356381590136308201323081d9a003020102020500a5427a1d300a06082a8648ce3d0403023021311f301d0603550403131646697265666f782055324620536f667420546f6b656e301e170d3137303833303134353130365a170d3137303930313134353130365a3021311f301d0603550403131646697265666f782055324620536f667420546f6b656e3059301306072a8648ce3d020106082a8648ce3d0301070342000409b9c8303e3a9f1cc0c4bb83c6d56a223699137387ad27dd01ad9c8e0c80addce10e52e622197576f756e38d5965bf98d53ece5af4b0ec003ad08f932bd84c1e300a06082a8648ce3d040302034800304502210083239a57e0fa99224b2c7989998cf833d5c1562df38d285d46cab1d6cf46ae9e02204cfd5deb11de1fdafc4e899f8d03388164beaff2e4263a82210ccc38906981236373696758463044022049c439848ec81672461cc0ea629f297cc7228450a6b0d08872ab969364ec6a6202200ea1acec627fd0e616d23da3e8bfa38a5527f2007cfe3fed63e5f3e2f7e25b11") val booExtension = "far" val challenge = ByteArray.fromBase64Url("HfpNmDkOp66Edjd5-uvwlg") @@ -56,35 +56,41 @@ class AuthenticatorAttestationResponseSpec extends FunSpec with Matchers { describe("has a clientDataJSON field which") { it("can be parsed as JSON.") { - val clientData = AuthenticatorAttestationResponse.builder() + val clientData = AuthenticatorAttestationResponse + .builder() .attestationObject(exampleAttestation) .clientDataJSON(exampleJson) .build() .getClientData - clientData.getChallenge should equal (challenge) + clientData.getChallenge should equal(challenge) } describe("defines attributes on the contained CollectedClientData:") { - val response = AuthenticatorAttestationResponse.builder() + val response = AuthenticatorAttestationResponse + .builder() .attestationObject(exampleAttestation) .clientDataJSON(exampleJson) .build() it("challenge") { - response.getClientData.getChallenge should equal (challenge) + response.getClientData.getChallenge should equal(challenge) } it("origin") { - response.getClientData.getOrigin should equal (origin) + response.getClientData.getOrigin should equal(origin) } describe("tokenBinding") { it("status") { - response.getClientData.getTokenBinding.get.getStatus should equal (tokenBindingStatus) + response.getClientData.getTokenBinding.get.getStatus should equal( + tokenBindingStatus + ) } it("id") { - response.getClientData.getTokenBinding.get.getId.get should equal (tokenBindingId) + response.getClientData.getTokenBinding.get.getId.get should equal( + tokenBindingId + ) } } @@ -93,12 +99,13 @@ class AuthenticatorAttestationResponseSpec extends FunSpec with Matchers { } it("can decode its attestationObject as CBOR.") { - val response = AuthenticatorAttestationResponse.builder() + val response = AuthenticatorAttestationResponse + .builder() .attestationObject(exampleAttestation) .clientDataJSON(exampleJson) .build() - response.getAttestation.getFormat should be ("fido-u2f") + response.getAttestation.getFormat should be("fido-u2f") } } diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/AuthenticatorDataFlagsSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/AuthenticatorDataFlagsSpec.scala index c21bfd2aa..ec1aaff3b 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/AuthenticatorDataFlagsSpec.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/AuthenticatorDataFlagsSpec.scala @@ -30,45 +30,45 @@ import org.scalatest.FunSpec import org.scalatest.Matchers import org.scalatestplus.junit.JUnitRunner - @RunWith(classOf[JUnitRunner]) class AuthenticatorDataFlagsSpec extends FunSpec with Matchers { describe("AuthenticatorDataFlags") { describe("decodes") { - def decode(hex: String) = new AuthenticatorDataFlags(BinaryUtil.fromHex(hex).head) + def decode(hex: String) = + new AuthenticatorDataFlags(BinaryUtil.fromHex(hex).head) it("0x01 to UP.") { val flags = decode("01") - flags.UP should be (true) - flags.UV should be (false) - flags.AT should be (false) - flags.ED should be (false) + flags.UP should be(true) + flags.UV should be(false) + flags.AT should be(false) + flags.ED should be(false) } it("0x04 to UV.") { val flags = decode("04") - flags.UP should be (false) - flags.UV should be (true) - flags.AT should be (false) - flags.ED should be (false) + flags.UP should be(false) + flags.UV should be(true) + flags.AT should be(false) + flags.ED should be(false) } it("0x40 to AT.") { val flags = decode("40") - flags.UP should be (false) - flags.UV should be (false) - flags.AT should be (true) - flags.ED should be (false) + flags.UP should be(false) + flags.UV should be(false) + flags.AT should be(true) + flags.ED should be(false) } it("0x80 to ED.") { val flags = decode("80") - flags.UP should be (false) - flags.UV should be (false) - flags.AT should be (false) - flags.ED should be (true) + flags.UP should be(false) + flags.UV should be(false) + flags.AT should be(false) + flags.ED should be(true) } } diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/AuthenticatorDataSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/AuthenticatorDataSpec.scala index 769f8e257..8ba9dd57b 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/AuthenticatorDataSpec.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/AuthenticatorDataSpec.scala @@ -24,8 +24,6 @@ package com.yubico.webauthn.data -import java.security.interfaces.ECPublicKey - import com.upokecenter.cbor.CBORObject import com.yubico.internal.util.scala.JavaConverters._ import com.yubico.webauthn.WebAuthnTestCodecs @@ -34,54 +32,81 @@ import org.scalatest.FunSpec import org.scalatest.Matchers import org.scalatestplus.junit.JUnitRunner +import java.security.interfaces.ECPublicKey import scala.util.Failure import scala.util.Try @RunWith(classOf[JUnitRunner]) class AuthenticatorDataSpec extends FunSpec with Matchers { - def jsonToCbor(json: String): ByteArray = new ByteArray(CBORObject.FromJSONString(json).EncodeToBytes) + def jsonToCbor(json: String): ByteArray = + new ByteArray(CBORObject.FromJSONString(json).EncodeToBytes) describe("AuthenticatorData") { - def generateTests(authDataHex: String, hasAttestation: Boolean = false, hasExtensions: Boolean = false): Unit = { + def generateTests( + authDataHex: String, + hasAttestation: Boolean = false, + hasExtensions: Boolean = false, + ): Unit = { val authData = new AuthenticatorData(ByteArray.fromHex(authDataHex)) it("gets the correct RP ID hash from the raw bytes.") { - authData.getRpIdHash.getHex should equal("49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d9763") + authData.getRpIdHash.getHex should equal( + "49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d9763" + ) } it("gets the correct flags from the raw bytes.") { - authData.getFlags.UP should be (true) - authData.getFlags.UV should be (false) - authData.getFlags.AT should equal (hasAttestation) - authData.getFlags.ED should equal (hasExtensions) + authData.getFlags.UP should be(true) + authData.getFlags.UV should be(false) + authData.getFlags.AT should equal(hasAttestation) + authData.getFlags.ED should equal(hasExtensions) } it("gets the correct signature counter from the raw bytes.") { authData.getSignatureCounter should equal(1337) - val evilBytes = ByteArray.fromHex("49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d976301ffffffff") - new AuthenticatorData(evilBytes).getSignatureCounter should equal(0xffffffffL) - new AuthenticatorData(evilBytes).getSignatureCounter should be > Int.MaxValue.toLong + val evilBytes = + ByteArray.fromHex("49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d976301ffffffff") + new AuthenticatorData(evilBytes).getSignatureCounter should equal( + 0xffffffffL + ) + new AuthenticatorData( + evilBytes + ).getSignatureCounter should be > Int.MaxValue.toLong } if (hasAttestation) { it("gets the correct attestation data from the raw bytes.") { authData.getAttestedCredentialData.asScala shouldBe defined - authData.getAttestedCredentialData.get.getAaguid.getHex should equal ("000102030405060708090a0b0c0d0e0f") - authData.getAttestedCredentialData.get.getCredentialId.getHex should equal ("7137c4e57894dce742723f9966c1e71c7c966f14e9429d5b2a2098a68416deec") - - val pubkey: ByteArray = WebAuthnTestCodecs.ecPublicKeyToRaw(WebAuthnTestCodecs.importCosePublicKey(authData.getAttestedCredentialData.get.getCredentialPublicKey).asInstanceOf[ECPublicKey]) - pubkey should equal (ByteArray.fromHex("04DAFE0DE5312BA080A5CCDF6B483B10EF19A2454D1E17A8350311A0B7FF0566EF8EC6324D2C81398D2E80BC985B910B26970A0F408C9DE19BECCF39899A41674D")) + authData.getAttestedCredentialData.get.getAaguid.getHex should equal( + "000102030405060708090a0b0c0d0e0f" + ) + authData.getAttestedCredentialData.get.getCredentialId.getHex should equal( + "7137c4e57894dce742723f9966c1e71c7c966f14e9429d5b2a2098a68416deec" + ) + + val pubkey: ByteArray = WebAuthnTestCodecs.ecPublicKeyToRaw( + WebAuthnTestCodecs + .importCosePublicKey( + authData.getAttestedCredentialData.get.getCredentialPublicKey + ) + .asInstanceOf[ECPublicKey] + ) + pubkey should equal( + ByteArray.fromHex("04DAFE0DE5312BA080A5CCDF6B483B10EF19A2454D1E17A8350311A0B7FF0566EF8EC6324D2C81398D2E80BC985B910B26970A0F408C9DE19BECCF39899A41674D") + ) } } if (hasExtensions) { it("gets the correct extension data from the raw bytes.") { authData.getExtensions.asScala shouldBe defined - new ByteArray(authData.getExtensions.get.EncodeToBytes()) should equal (jsonToCbor("""{ "foo": "bar" }""")) + new ByteArray( + authData.getExtensions.get.EncodeToBytes() + ) should equal(jsonToCbor("""{ "foo": "bar" }""")) } } } @@ -89,22 +114,22 @@ class AuthenticatorDataSpec extends FunSpec with Matchers { describe("with neither attestation data nor extensions") { generateTests( "49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d9763" // RP ID hash - + "01" // Flags - + "00000539" // Signature count + + "01" // Flags + + "00000539" // Signature count ) } describe("with only attestation data") { generateTests( "49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d9763" // RP ID hash - + "41" // Flags - + "00000539" // Signature count - + "000102030405060708090a0b0c0d0e0f" // AAGUID - + "0020" // Credential ID length - + "7137c4e57894dce742723f9966c1e71c7c966f14e9429d5b2a2098a68416deec" // Credential ID - + "a52258208ec6324d2c81398d2e80bc985b910b26970a0f408c9de19beccf39899a41674d03260102215820dafe0de5312ba080a5ccdf6b483b10ef19a2454d1e17a8350311a0b7ff0566ef2001" // Credential public key COSE_key + + "41" // Flags + + "00000539" // Signature count + + "000102030405060708090a0b0c0d0e0f" // AAGUID + + "0020" // Credential ID length + + "7137c4e57894dce742723f9966c1e71c7c966f14e9429d5b2a2098a68416deec" // Credential ID + + "a52258208ec6324d2c81398d2e80bc985b910b26970a0f408c9de19beccf39899a41674d03260102215820dafe0de5312ba080a5ccdf6b483b10ef19a2454d1e17a8350311a0b7ff0566ef2001" // Credential public key COSE_key , - hasAttestation = true + hasAttestation = true, ) } @@ -112,11 +137,11 @@ class AuthenticatorDataSpec extends FunSpec with Matchers { describe("with only extensions") { generateTests( "49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d9763" // RP ID hash - + "81" // Flags - + "00000539" // Signature count - + "a163666f6f63626172" // Extensions + + "81" // Flags + + "00000539" // Signature count + + "a163666f6f63626172" // Extensions , - hasExtensions = true + hasExtensions = true, ) } @@ -132,35 +157,37 @@ class AuthenticatorDataSpec extends FunSpec with Matchers { + "a163666f6f63626172" // Extensions , hasAttestation = true, - hasExtensions = true + hasExtensions = true, ) } - describe("rejects a byte array with both attestation data and extensions if") { + describe( + "rejects a byte array with both attestation data and extensions if" + ) { def authDataBytes(flags: String): ByteArray = ByteArray.fromHex( "49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d9763" // RP ID hash - + flags - + "00000539" // Signature count - + "000102030405060708090a0b0c0d0e0f" // AAGUID - + "0020" // Credential ID length - + "7137c4e57894dce742723f9966c1e71c7c966f14e9429d5b2a2098a68416deec" // Credential ID - + "a52258208ec6324d2c81398d2e80bc985b910b26970a0f408c9de19beccf39899a41674d03260102215820dafe0de5312ba080a5ccdf6b483b10ef19a2454d1e17a8350311a0b7ff0566ef2001" // Credential public key COSE_key - + "a163666f6f63626172" // Extensions + + flags + + "00000539" // Signature count + + "000102030405060708090a0b0c0d0e0f" // AAGUID + + "0020" // Credential ID length + + "7137c4e57894dce742723f9966c1e71c7c966f14e9429d5b2a2098a68416deec" // Credential ID + + "a52258208ec6324d2c81398d2e80bc985b910b26970a0f408c9de19beccf39899a41674d03260102215820dafe0de5312ba080a5ccdf6b483b10ef19a2454d1e17a8350311a0b7ff0566ef2001" // Credential public key COSE_key + + "a163666f6f63626172" // Extensions ) it("flags indicate only attestation data") { val authData = Try(new AuthenticatorData(authDataBytes("41"))) - authData shouldBe a [Failure[_]] - authData.failed.get shouldBe an [IllegalArgumentException] + authData shouldBe a[Failure[_]] + authData.failed.get shouldBe an[IllegalArgumentException] } it("flags indicate only extensions") { val authData = Try(new AuthenticatorData(authDataBytes("81"))) - authData shouldBe a [Failure[_]] - authData.failed.get shouldBe an [IllegalArgumentException] + authData shouldBe a[Failure[_]] + authData.failed.get shouldBe an[IllegalArgumentException] } } 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 index f2b7ad1cf..2e56f8539 100644 --- 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 @@ -28,39 +28,51 @@ import org.junit.runner.RunWith import org.scalatest.FunSpec import org.scalatest.Matchers import org.scalatestplus.junit.JUnitRunner -import org.scalatest.prop.GeneratorDrivenPropertyChecks import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks @RunWith(classOf[JUnitRunner]) -class AuthenticatorTransportSpec extends FunSpec with Matchers with ScalaCheckDrivenPropertyChecks { +class AuthenticatorTransportSpec + extends FunSpec + with Matchers + with ScalaCheckDrivenPropertyChecks { describe("The AuthenticatorTransport type") { describe("has the constant") { it("USB.") { - AuthenticatorTransport.USB.getId should equal ("usb") + AuthenticatorTransport.USB.getId should equal("usb") } it("NFC.") { - AuthenticatorTransport.NFC.getId should equal ("nfc") + AuthenticatorTransport.NFC.getId should equal("nfc") } it("BLE.") { - AuthenticatorTransport.BLE.getId should equal ("ble") + AuthenticatorTransport.BLE.getId should equal("ble") } it("INTERNAL.") { - AuthenticatorTransport.INTERNAL.getId should equal ("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()) + 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 + 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") } @@ -68,15 +80,19 @@ class AuthenticatorTransportSpec extends FunSpec with Matchers with ScalaCheckDr it("can contain any value.") { forAll { transport: String => - AuthenticatorTransport.of(transport).getId should equal (transport) + 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 + 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/BuildersSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/BuildersSpec.scala index a82514e36..2541603ad 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/BuildersSpec.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/BuildersSpec.scala @@ -41,21 +41,25 @@ import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks import scala.language.reflectiveCalls - @RunWith(classOf[JUnitRunner]) -class BuildersSpec extends FunSpec with Matchers with ScalaCheckDrivenPropertyChecks { +class BuildersSpec + extends FunSpec + with Matchers + with ScalaCheckDrivenPropertyChecks { type ToBuilder[A] = { def toBuilder(): { def build(): A } } describe("The class") { - def test[A <: ToBuilder[A]](tpe: TypeReference[A])(implicit a: Arbitrary[A]): Unit = { + def test[A <: ToBuilder[A]](tpe: TypeReference[A])(implicit + a: Arbitrary[A] + ): Unit = { val cn = tpe.getType.getTypeName describe(s"${cn}") { it("has a working .toBuilder() method.") { forAll { value: A => val rebuilt = value.toBuilder().build() - rebuilt should equal (value) + rebuilt should equal(value) } } } @@ -71,8 +75,18 @@ class BuildersSpec extends FunSpec with Matchers with ScalaCheckDrivenPropertyCh test(new TypeReference[AuthenticatorSelectionCriteria]() {}) test(new TypeReference[ClientAssertionExtensionOutputs]() {}) test(new TypeReference[ClientRegistrationExtensionOutputs]() {}) - test(new TypeReference[PublicKeyCredential[AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs]]() {}) - test(new TypeReference[PublicKeyCredential[AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs]]() {}) + test( + new TypeReference[PublicKeyCredential[ + AuthenticatorAssertionResponse, + ClientAssertionExtensionOutputs, + ]]() {} + ) + test( + new TypeReference[PublicKeyCredential[ + AuthenticatorAttestationResponse, + ClientRegistrationExtensionOutputs, + ]]() {} + ) test(new TypeReference[PublicKeyCredentialCreationOptions]() {}) test(new TypeReference[PublicKeyCredentialDescriptor]() {}) test(new TypeReference[PublicKeyCredentialParameters]() {}) diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/CollectedClientDataSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/CollectedClientDataSpec.scala index dc15d6222..5117eed53 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/CollectedClientDataSpec.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/CollectedClientDataSpec.scala @@ -27,21 +27,23 @@ package com.yubico.webauthn.data import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.node.ObjectNode import com.yubico.internal.util.JacksonCodecs -import com.yubico.webauthn.WebAuthnCodecs import org.junit.runner.RunWith import org.scalatest.FunSpec import org.scalatest.Matchers import org.scalatestplus.junit.JUnitRunner - @RunWith(classOf[JUnitRunner]) class CollectedClientDataSpec extends FunSpec with Matchers { - def parse(json: JsonNode): CollectedClientData = new CollectedClientData(new ByteArray(JacksonCodecs.json().writeValueAsBytes(json))) + def parse(json: JsonNode): CollectedClientData = + new CollectedClientData( + new ByteArray(JacksonCodecs.json().writeValueAsBytes(json)) + ) describe("CollectedClientData") { - val defaultJson: ObjectNode = JacksonCodecs.json.readTree("""{ + val defaultJson: ObjectNode = JacksonCodecs.json + .readTree("""{ "challenge": "aaaa", "origin": "example.org", "type": "webauthn.create", @@ -55,32 +57,46 @@ class CollectedClientDataSpec extends FunSpec with Matchers { "status": "present", "id": "bbbb" } - }""").asInstanceOf[ObjectNode] + }""") + .asInstanceOf[ObjectNode] it("can be parsed from JSON.") { val cd = parse(defaultJson) - cd.getChallenge.getBase64Url should equal ("aaaa") - cd.getOrigin should equal ("example.org") - cd.getType should equal ("webauthn.create") - cd.getTokenBinding.get should equal (TokenBindingInfo.present(ByteArray.fromBase64Url("bbbb"))) + cd.getChallenge.getBase64Url should equal("aaaa") + cd.getOrigin should equal("example.org") + cd.getType should equal("webauthn.create") + cd.getTokenBinding.get should equal( + TokenBindingInfo.present(ByteArray.fromBase64Url("bbbb")) + ) } - describe("forbids null value for") { it("field: challenge") { - an [IllegalArgumentException] should be thrownBy parse(defaultJson.set("challenge", defaultJson.nullNode())) - an [IllegalArgumentException] should be thrownBy parse(defaultJson.remove("challenge")) + an[IllegalArgumentException] should be thrownBy parse( + defaultJson.set("challenge", defaultJson.nullNode()) + ) + an[IllegalArgumentException] should be thrownBy parse( + defaultJson.remove("challenge") + ) } it("field: origin") { - an [IllegalArgumentException] should be thrownBy parse(defaultJson.set("origin", defaultJson.nullNode())) - an [IllegalArgumentException] should be thrownBy parse(defaultJson.remove("origin")) + an[IllegalArgumentException] should be thrownBy parse( + defaultJson.set("origin", defaultJson.nullNode()) + ) + an[IllegalArgumentException] should be thrownBy parse( + defaultJson.remove("origin") + ) } it("field: type") { - an [IllegalArgumentException] should be thrownBy parse(defaultJson.set("type", defaultJson.nullNode())) - an [IllegalArgumentException] should be thrownBy parse(defaultJson.remove("type")) + an[IllegalArgumentException] should be thrownBy parse( + defaultJson.set("type", defaultJson.nullNode()) + ) + an[IllegalArgumentException] should be thrownBy parse( + defaultJson.remove("type") + ) } } } 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 ef90a5c0f..618eff093 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 @@ -24,32 +24,30 @@ package com.yubico.webauthn.data -import java.net.URL -import java.security.interfaces.ECPublicKey -import java.util.Optional - import com.fasterxml.jackson.databind.node.JsonNodeFactory import com.fasterxml.jackson.databind.node.ObjectNode import com.upokecenter.cbor.CBOREncodeOptions import com.upokecenter.cbor.CBORObject import com.yubico.internal.util.BinaryUtil -import com.yubico.internal.util.scala.JavaConverters._ import com.yubico.internal.util.JacksonCodecs +import com.yubico.internal.util.scala.JavaConverters._ import com.yubico.scalacheck.gen.JacksonGenerators import com.yubico.scalacheck.gen.JacksonGenerators._ import com.yubico.scalacheck.gen.JavaGenerators._ import com.yubico.webauthn.AssertionRequest import com.yubico.webauthn.TestAuthenticator +import com.yubico.webauthn.WebAuthnTestCodecs import com.yubico.webauthn.extension.appid.AppId import com.yubico.webauthn.extension.appid.Generators._ -import com.yubico.webauthn.WebAuthnTestCodecs import org.scalacheck.Arbitrary import org.scalacheck.Arbitrary.arbitrary import org.scalacheck.Gen +import java.net.URL +import java.security.interfaces.ECPublicKey +import java.util.Optional import scala.jdk.CollectionConverters._ - object Generators { private def jsonFactory: JsonNodeFactory = JsonNodeFactory.instance @@ -60,300 +58,503 @@ object Generators { else (flags & (mask ^ (-0x01).toByte)).toByte - implicit val arbitraryAssertionExtensionInputs: Arbitrary[AssertionExtensionInputs] = Arbitrary(for { - appid <- arbitrary[Optional[AppId]] - } yield AssertionExtensionInputs.builder() - .appid(appid) - .build()) - - implicit val arbitraryAssertionRequest: Arbitrary[AssertionRequest] = Arbitrary(for { - publicKeyCredentialRequestOptions <- arbitrary[PublicKeyCredentialRequestOptions] - username <- arbitrary[Optional[String]] - } yield AssertionRequest.builder() - .publicKeyCredentialRequestOptions(publicKeyCredentialRequestOptions) - .username(username) - .build()) - - implicit val arbitraryAttestedCredentialData: Arbitrary[AttestedCredentialData] = Arbitrary(for { - aaguid <- byteArray(16) - credentialId <- arbitrary[ByteArray] - credentialPublicKey <- Gen.delay(Gen.const(TestAuthenticator.generateEcKeypair().getPublic.asInstanceOf[ECPublicKey])) - credentialPublicKeyCose = WebAuthnTestCodecs.ecPublicKeyToCose(credentialPublicKey) - } yield AttestedCredentialData.builder() - .aaguid(aaguid) - .credentialId(credentialId) - .credentialPublicKey(credentialPublicKeyCose) - .build()) - def attestedCredentialDataBytes: Gen[ByteArray] = for { - attestedCredentialData <- arbitrary[AttestedCredentialData] - } yield new ByteArray( - attestedCredentialData.getAaguid.getBytes - ++ BinaryUtil.encodeUint16(attestedCredentialData.getCredentialId.getBytes.length) - ++ attestedCredentialData.getCredentialId.getBytes - ++ attestedCredentialData.getCredentialPublicKey.getBytes + implicit val arbitraryAssertionExtensionInputs + : Arbitrary[AssertionExtensionInputs] = Arbitrary( + for { + appid <- arbitrary[Optional[AppId]] + } yield AssertionExtensionInputs + .builder() + .appid(appid) + .build() ) - implicit val arbitraryAttestationObject: Arbitrary[AttestationObject] = Arbitrary(for { - bytes <- attestationObjectBytes - } yield new AttestationObject(bytes)) - def attestationObjectBytes: Gen[ByteArray] = Gen.oneOf(packedAttestationObject, fidoU2fAttestationObject) - - def packedAttestationObject: Gen[ByteArray] = for { - authData <- authenticatorDataBytes - alg <- arbitrary[COSEAlgorithmIdentifier] - sig <- arbitrary[ByteArray] - x5c <- arbitrary[List[ByteArray]] - attStmt = jsonFactory.objectNode().setAll[ObjectNode](Map( - "alg" -> jsonFactory.numberNode(alg.getId), - "sig" -> jsonFactory.binaryNode(sig.getBytes), - "x5c" -> jsonFactory.arrayNode().addAll(x5c.map(cert => jsonFactory.binaryNode(cert.getBytes)).asJava) - ).asJava) - attObj = jsonFactory.objectNode().setAll[ObjectNode](Map( - "authData" -> jsonFactory.binaryNode(authData.getBytes), - "fmt" -> jsonFactory.textNode("packed"), - "attStmt" -> attStmt - ).asJava) - } yield new ByteArray(JacksonCodecs.cbor().writeValueAsBytes(attObj)) - - def fidoU2fAttestationObject: Gen[ByteArray] = for { - authData <- authenticatorDataBytes - alg <- arbitrary[COSEAlgorithmIdentifier] - sig <- arbitrary[ByteArray] - x5c <- arbitrary[List[ByteArray]] - attStmt = jsonFactory.objectNode().setAll[ObjectNode](Map( - "sig" -> jsonFactory.binaryNode(sig.getBytes), - "x5c" -> jsonFactory.arrayNode().addAll(x5c.map(cert => jsonFactory.binaryNode(cert.getBytes)).asJava) - ).asJava) - attObj = jsonFactory.objectNode().setAll[ObjectNode](Map( - "authData" -> jsonFactory.binaryNode(authData.getBytes), - "fmt" -> jsonFactory.textNode("fido-u2f"), - "attStmt" -> attStmt - ).asJava) - } yield new ByteArray(JacksonCodecs.cbor().writeValueAsBytes(attObj)) - - implicit val arbitraryAuthenticatorDataFlags: Arbitrary[AuthenticatorDataFlags] = Arbitrary(for { + implicit val arbitraryAssertionRequest: Arbitrary[AssertionRequest] = + Arbitrary( + for { + publicKeyCredentialRequestOptions <- + arbitrary[PublicKeyCredentialRequestOptions] + username <- arbitrary[Optional[String]] + } yield AssertionRequest + .builder() + .publicKeyCredentialRequestOptions(publicKeyCredentialRequestOptions) + .username(username) + .build() + ) + + implicit val arbitraryAttestedCredentialData + : Arbitrary[AttestedCredentialData] = Arbitrary( + for { + aaguid <- byteArray(16) + credentialId <- arbitrary[ByteArray] + credentialPublicKey <- Gen.delay( + Gen.const( + TestAuthenticator + .generateEcKeypair() + .getPublic + .asInstanceOf[ECPublicKey] + ) + ) + credentialPublicKeyCose = + WebAuthnTestCodecs.ecPublicKeyToCose(credentialPublicKey) + } yield AttestedCredentialData + .builder() + .aaguid(aaguid) + .credentialId(credentialId) + .credentialPublicKey(credentialPublicKeyCose) + .build() + ) + def attestedCredentialDataBytes: Gen[ByteArray] = + for { + attestedCredentialData <- arbitrary[AttestedCredentialData] + } yield new ByteArray( + attestedCredentialData.getAaguid.getBytes + ++ BinaryUtil.encodeUint16( + attestedCredentialData.getCredentialId.getBytes.length + ) + ++ attestedCredentialData.getCredentialId.getBytes + ++ attestedCredentialData.getCredentialPublicKey.getBytes + ) + + implicit val arbitraryAttestationObject: Arbitrary[AttestationObject] = + Arbitrary(for { + bytes <- attestationObjectBytes + } yield new AttestationObject(bytes)) + def attestationObjectBytes: Gen[ByteArray] = + Gen.oneOf(packedAttestationObject, fidoU2fAttestationObject) + + def packedAttestationObject: Gen[ByteArray] = + for { + authData <- authenticatorDataBytes + alg <- arbitrary[COSEAlgorithmIdentifier] + sig <- arbitrary[ByteArray] + x5c <- arbitrary[List[ByteArray]] + attStmt = + jsonFactory + .objectNode() + .setAll[ObjectNode]( + Map( + "alg" -> jsonFactory.numberNode(alg.getId), + "sig" -> jsonFactory.binaryNode(sig.getBytes), + "x5c" -> jsonFactory + .arrayNode() + .addAll( + x5c.map(cert => jsonFactory.binaryNode(cert.getBytes)).asJava + ), + ).asJava + ) + attObj = + jsonFactory + .objectNode() + .setAll[ObjectNode]( + Map( + "authData" -> jsonFactory.binaryNode(authData.getBytes), + "fmt" -> jsonFactory.textNode("packed"), + "attStmt" -> attStmt, + ).asJava + ) + } yield new ByteArray(JacksonCodecs.cbor().writeValueAsBytes(attObj)) + + def fidoU2fAttestationObject: Gen[ByteArray] = + for { + authData <- authenticatorDataBytes + alg <- arbitrary[COSEAlgorithmIdentifier] + sig <- arbitrary[ByteArray] + x5c <- arbitrary[List[ByteArray]] + attStmt = + jsonFactory + .objectNode() + .setAll[ObjectNode]( + Map( + "sig" -> jsonFactory.binaryNode(sig.getBytes), + "x5c" -> jsonFactory + .arrayNode() + .addAll( + x5c.map(cert => jsonFactory.binaryNode(cert.getBytes)).asJava + ), + ).asJava + ) + attObj = + jsonFactory + .objectNode() + .setAll[ObjectNode]( + Map( + "authData" -> jsonFactory.binaryNode(authData.getBytes), + "fmt" -> jsonFactory.textNode("fido-u2f"), + "attStmt" -> attStmt, + ).asJava + ) + } yield new ByteArray(JacksonCodecs.cbor().writeValueAsBytes(attObj)) + + implicit val arbitraryAuthenticatorDataFlags + : Arbitrary[AuthenticatorDataFlags] = Arbitrary(for { value <- arbitrary[Byte] } yield new AuthenticatorDataFlags(value)) - implicit val arbitraryAuthenticatorAssertionResponse: Arbitrary[AuthenticatorAssertionResponse] = Arbitrary(for { - authenticatorData <- authenticatorDataBytes - clientDataJson <- clientDataJsonBytes - signature <- arbitrary[ByteArray] - userHandle <- arbitrary[Option[ByteArray]] - } yield AuthenticatorAssertionResponse.builder() - .authenticatorData(authenticatorData) - .clientDataJSON(clientDataJson) - .signature(signature) - .userHandle(userHandle.asJava) - .build() + implicit val arbitraryAuthenticatorAssertionResponse + : Arbitrary[AuthenticatorAssertionResponse] = Arbitrary( + for { + authenticatorData <- authenticatorDataBytes + clientDataJson <- clientDataJsonBytes + signature <- arbitrary[ByteArray] + userHandle <- arbitrary[Option[ByteArray]] + } yield AuthenticatorAssertionResponse + .builder() + .authenticatorData(authenticatorData) + .clientDataJSON(clientDataJson) + .signature(signature) + .userHandle(userHandle.asJava) + .build() ) - implicit val arbitraryAuthenticatorAttestationResponse: Arbitrary[AuthenticatorAttestationResponse] = Arbitrary(for { - attestationObject <- attestationObjectBytes - clientDataJSON <- clientDataJsonBytes - } yield AuthenticatorAttestationResponse.builder() - .attestationObject(attestationObject) - .clientDataJSON(clientDataJSON) - .build() + implicit val arbitraryAuthenticatorAttestationResponse + : Arbitrary[AuthenticatorAttestationResponse] = Arbitrary( + for { + attestationObject <- attestationObjectBytes + clientDataJSON <- clientDataJsonBytes + } yield AuthenticatorAttestationResponse + .builder() + .attestationObject(attestationObject) + .clientDataJSON(clientDataJSON) + .build() ) - implicit val arbitraryAuthenticatorData: Arbitrary[AuthenticatorData] = Arbitrary(authenticatorDataBytes map (new AuthenticatorData(_))) - def authenticatorDataBytes: Gen[ByteArray] = for { - fixedBytes <- byteArray(37) - attestedCredentialDataBytes <- Gen.option(attestedCredentialDataBytes) - extensions <- arbitrary[Option[CBORObject]] - - 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) - } yield new ByteArray( - fixedBytes.getBytes.updated(32, flagsByte) - ++ attestedCredentialDataBytes.map(_.getBytes).getOrElse(Array.empty) - ++ extensionsBytes.map(_.getBytes).getOrElse(Array.empty) + implicit val arbitraryAuthenticatorData: Arbitrary[AuthenticatorData] = + Arbitrary(authenticatorDataBytes map (new AuthenticatorData(_))) + def authenticatorDataBytes: Gen[ByteArray] = + for { + fixedBytes <- byteArray(37) + attestedCredentialDataBytes <- Gen.option(attestedCredentialDataBytes) + extensions <- arbitrary[Option[CBORObject]] + + 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, + ) + } yield new ByteArray( + fixedBytes.getBytes.updated(32, flagsByte) + ++ attestedCredentialDataBytes.map(_.getBytes).getOrElse(Array.empty) + ++ extensionsBytes.map(_.getBytes).getOrElse(Array.empty) + ) + + implicit val arbitraryAuthenticatorSelectionCriteria + : Arbitrary[AuthenticatorSelectionCriteria] = Arbitrary( + for { + authenticatorAttachment <- arbitrary[Optional[AuthenticatorAttachment]] + requireResidentKey <- arbitrary[Boolean] + userVerification <- arbitrary[UserVerificationRequirement] + } yield AuthenticatorSelectionCriteria + .builder() + .authenticatorAttachment(authenticatorAttachment) + .requireResidentKey(requireResidentKey) + .userVerification(userVerification) + .build() ) - implicit val arbitraryAuthenticatorSelectionCriteria: Arbitrary[AuthenticatorSelectionCriteria] = Arbitrary(for { - authenticatorAttachment <- arbitrary[Optional[AuthenticatorAttachment]] - requireResidentKey <- arbitrary[Boolean] - userVerification <- arbitrary[UserVerificationRequirement] - } yield AuthenticatorSelectionCriteria.builder() - .authenticatorAttachment(authenticatorAttachment) - .requireResidentKey(requireResidentKey) - .userVerification(userVerification) - .build()) - - implicit val arbitraryAuthenticatorTransport: Arbitrary[AuthenticatorTransport] = Arbitrary( + implicit val arbitraryAuthenticatorTransport + : Arbitrary[AuthenticatorTransport] = Arbitrary( Gen.oneOf( Gen.oneOf(AuthenticatorTransport.values().toIndexedSeq), - arbitrary[String] map AuthenticatorTransport.of - )) + 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)) + 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)) - implicit val arbitraryClientAssertionExtensionOutputs: Arbitrary[ClientAssertionExtensionOutputs] = Arbitrary(for { - appid <- arbitrary[Optional[java.lang.Boolean]] - } yield ClientAssertionExtensionOutputs.builder() - .appid(appid) - .build()) + def flipOneBit(bytes: ByteArray): Gen[ByteArray] = + for { + byteIndex: Int <- Gen.choose(0, bytes.size() - 1) + bitIndex: Int <- Gen.choose(0, 7) + flipMask: Byte = (1 << bitIndex).toByte + } yield new ByteArray( + bytes.getBytes + .updated(byteIndex, (bytes.getBytes()(byteIndex) ^ flipMask).toByte) + ) + + implicit val arbitraryClientAssertionExtensionOutputs + : Arbitrary[ClientAssertionExtensionOutputs] = Arbitrary( + for { + appid <- arbitrary[Optional[java.lang.Boolean]] + } yield ClientAssertionExtensionOutputs + .builder() + .appid(appid) + .build() + ) def clientAssertionExtensionOutputs( - appid: Gen[Optional[java.lang.Boolean]] = arbitrary[Optional[java.lang.Boolean]] - ): Gen[ClientAssertionExtensionOutputs] = for { - appid <- appid - } yield ClientAssertionExtensionOutputs.builder() - .appid(appid) - .build() - - implicit val arbitraryClientRegistrationExtensionOutputs: Arbitrary[ClientRegistrationExtensionOutputs] = Arbitrary(Gen.const(ClientRegistrationExtensionOutputs.builder().build())) - - implicit val arbitraryCollectedClientData: Arbitrary[CollectedClientData] = Arbitrary(clientDataJsonBytes map (new CollectedClientData(_))) - def clientDataJsonBytes: Gen[ByteArray] = for { - jsonBase <- arbitrary[ObjectNode] - challenge <- arbitrary[ByteArray] - origin <- arbitrary[URL] - tpe <- Gen.alphaNumStr - tokenBinding <- arbitrary[Optional[TokenBindingInfo]] - authenticatorExtensions <- arbitrary[Optional[ObjectNode]] - clientExtensions <- arbitrary[Optional[ObjectNode]] - json = { - val json = jsonBase - .set("challenge", jsonFactory.textNode(challenge.getBase64Url)).asInstanceOf[ObjectNode] - .set("origin", jsonFactory.textNode(origin.toExternalForm)).asInstanceOf[ObjectNode] - .set("type", jsonFactory.textNode(tpe)).asInstanceOf[ObjectNode] - - tokenBinding.asScala foreach { tb => - json.set[ObjectNode]("tokenBinding", JacksonCodecs.json().readTree(JacksonCodecs.json().writeValueAsString(tb))) - } + appid: Gen[Optional[java.lang.Boolean]] = + arbitrary[Optional[java.lang.Boolean]] + ): Gen[ClientAssertionExtensionOutputs] = + for { + appid <- appid + } yield ClientAssertionExtensionOutputs + .builder() + .appid(appid) + .build() + + implicit val arbitraryClientRegistrationExtensionOutputs + : Arbitrary[ClientRegistrationExtensionOutputs] = Arbitrary( + Gen.const(ClientRegistrationExtensionOutputs.builder().build()) + ) - authenticatorExtensions.asScala foreach { ae => - json.set[ObjectNode]("authenticatorExtensions", JacksonCodecs.json().readTree(JacksonCodecs.json().writeValueAsString(ae))) + implicit val arbitraryCollectedClientData: Arbitrary[CollectedClientData] = + Arbitrary(clientDataJsonBytes map (new CollectedClientData(_))) + def clientDataJsonBytes: Gen[ByteArray] = + for { + jsonBase <- arbitrary[ObjectNode] + challenge <- arbitrary[ByteArray] + origin <- arbitrary[URL] + tpe <- Gen.alphaNumStr + tokenBinding <- arbitrary[Optional[TokenBindingInfo]] + authenticatorExtensions <- arbitrary[Optional[ObjectNode]] + clientExtensions <- arbitrary[Optional[ObjectNode]] + json = { + val json = jsonBase + .set("challenge", jsonFactory.textNode(challenge.getBase64Url)) + .asInstanceOf[ObjectNode] + .set("origin", jsonFactory.textNode(origin.toExternalForm)) + .asInstanceOf[ObjectNode] + .set("type", jsonFactory.textNode(tpe)) + .asInstanceOf[ObjectNode] + + tokenBinding.asScala foreach { tb => + json.set[ObjectNode]( + "tokenBinding", + JacksonCodecs + .json() + .readTree(JacksonCodecs.json().writeValueAsString(tb)), + ) + } + + authenticatorExtensions.asScala foreach { ae => + json.set[ObjectNode]( + "authenticatorExtensions", + JacksonCodecs + .json() + .readTree(JacksonCodecs.json().writeValueAsString(ae)), + ) + } + + clientExtensions.asScala foreach { ce => + json.set[ObjectNode]( + "clientExtensions", + JacksonCodecs + .json() + .readTree(JacksonCodecs.json().writeValueAsString(ce)), + ) + } + + json } + } yield new ByteArray(JacksonCodecs.json().writeValueAsBytes(json)) - clientExtensions.asScala foreach { ce => - json.set[ObjectNode]("clientExtensions", JacksonCodecs.json().readTree(JacksonCodecs.json().writeValueAsString(ce))) - } + implicit val arbitraryCOSEAlgorithmIdentifier + : Arbitrary[COSEAlgorithmIdentifier] = Arbitrary( + Gen.oneOf(COSEAlgorithmIdentifier.values().toIndexedSeq) + ) - json - } - } yield new ByteArray(JacksonCodecs.json().writeValueAsBytes(json)) - - implicit val arbitraryCOSEAlgorithmIdentifier: Arbitrary[COSEAlgorithmIdentifier] = Arbitrary(Gen.oneOf(COSEAlgorithmIdentifier.values().toIndexedSeq)) - - implicit val arbitraryPublicKeyCredentialWithAssertion: Arbitrary[PublicKeyCredential[AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs]] = Arbitrary(for { - id <- arbitrary[ByteArray] - response <- arbitrary[AuthenticatorAssertionResponse] - clientExtensionResults <- arbitrary[ClientAssertionExtensionOutputs] - } yield PublicKeyCredential.builder().id(id).response(response).clientExtensionResults(clientExtensionResults).build()) - - implicit val arbitraryPublicKeyCredentialWithAttestation: Arbitrary[PublicKeyCredential[AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs]] = Arbitrary(for { - id <- arbitrary[ByteArray] - response <- arbitrary[AuthenticatorAttestationResponse] - clientExtensionResults <- arbitrary[ClientRegistrationExtensionOutputs] - } yield PublicKeyCredential.builder().id(id).response(response).clientExtensionResults(clientExtensionResults).build()) - - implicit val arbitraryPublicKeyCredentialCreationOptions: Arbitrary[PublicKeyCredentialCreationOptions] = Arbitrary(for { - attestation <- arbitrary[AttestationConveyancePreference] - authenticatorSelection <- arbitrary[Optional[AuthenticatorSelectionCriteria]] - challenge <- arbitrary[ByteArray] - excludeCredentials <- arbitrary[Optional[java.util.Set[PublicKeyCredentialDescriptor]]] - extensions <- arbitrary[RegistrationExtensionInputs] - pubKeyCredParams <- arbitrary[java.util.List[PublicKeyCredentialParameters]] - rp <- arbitrary[RelyingPartyIdentity] - timeout <- arbitrary[Optional[java.lang.Long]] - user <- arbitrary[UserIdentity] - } yield PublicKeyCredentialCreationOptions.builder() - .rp(rp) - .user(user) - .challenge(challenge) - .pubKeyCredParams(pubKeyCredParams) - .attestation(attestation) - .authenticatorSelection(authenticatorSelection) - .excludeCredentials(excludeCredentials) - .extensions(extensions) - .timeout(timeout) - .build()) - - implicit val arbitraryPublicKeyCredentialDescriptor: Arbitrary[PublicKeyCredentialDescriptor] = Arbitrary(for { - id <- arbitrary[ByteArray] - transports <- arbitrary[Optional[java.util.Set[AuthenticatorTransport]]] - tpe <- arbitrary[PublicKeyCredentialType] - } yield PublicKeyCredentialDescriptor.builder() - .id(id) - .transports(transports) - .`type`(tpe) - .build()) - - implicit val arbitraryPublicKeyCredentialParameters: Arbitrary[PublicKeyCredentialParameters] = Arbitrary(for { - alg <- arbitrary[COSEAlgorithmIdentifier] - tpe <- arbitrary[PublicKeyCredentialType] - } yield PublicKeyCredentialParameters.builder() - .alg(alg) - .`type`(tpe) - .build()) - - implicit val arbitraryPublicKeyCredentialRequestOptions: Arbitrary[PublicKeyCredentialRequestOptions] = Arbitrary(for { - allowCredentials <- arbitrary[Optional[java.util.List[PublicKeyCredentialDescriptor]]] - challenge <- arbitrary[ByteArray] - extensions <- arbitrary[AssertionExtensionInputs] - rpId <- arbitrary[Optional[String]] - timeout <- arbitrary[Optional[java.lang.Long]] - userVerification <- arbitrary[UserVerificationRequirement] - } yield PublicKeyCredentialRequestOptions.builder() - .challenge(challenge) - .allowCredentials(allowCredentials) - .extensions(extensions) - .rpId(rpId) - .timeout(timeout) - .userVerification(userVerification) - .build()) - - implicit val arbitraryRegistrationExtensionInputs: Arbitrary[RegistrationExtensionInputs] = Arbitrary(Gen.const(RegistrationExtensionInputs.builder().build())) - - implicit val arbitraryRelyingPartyIdentity: Arbitrary[RelyingPartyIdentity] = Arbitrary(for { - icon <- arbitrary[Optional[URL]] - id <- arbitrary[String] - name <- arbitrary[String] - } yield RelyingPartyIdentity.builder() - .id(id) - .name(name) - .icon(icon) - .build()) - - implicit val arbitraryTokenBindingInfo: Arbitrary[TokenBindingInfo] = Arbitrary(Gen.oneOf( - Gen.const(TokenBindingInfo.supported()), - arbitrary[ByteArray] map TokenBindingInfo.present - )) - - implicit val arbitraryUserIdentity: Arbitrary[UserIdentity] = Arbitrary(for { - displayName <- arbitrary[String] - name <- arbitrary[String] - icon <- arbitrary[Optional[URL]] - id <- arbitrary[ByteArray] - name <- arbitrary[String] - } yield UserIdentity.builder() - .name(name) - .displayName(displayName) - .id(id) - .icon(icon) - .build()) - - def knownExtensionId: Gen[String] = Gen.oneOf("appid", "txAuthSimple", "txAuthGeneric", "authnSel", "exts", "uvi", "loc", "uvm", "biometricPerfBounds") - - def anyAuthenticatorExtensions[A <: ExtensionInputs](implicit a: Arbitrary[A]): Gen[(A, ObjectNode)] = + implicit val arbitraryPublicKeyCredentialWithAssertion + : Arbitrary[PublicKeyCredential[ + AuthenticatorAssertionResponse, + ClientAssertionExtensionOutputs, + ]] = Arbitrary( + for { + id <- arbitrary[ByteArray] + response <- arbitrary[AuthenticatorAssertionResponse] + clientExtensionResults <- arbitrary[ClientAssertionExtensionOutputs] + } yield PublicKeyCredential + .builder() + .id(id) + .response(response) + .clientExtensionResults(clientExtensionResults) + .build() + ) + + implicit val arbitraryPublicKeyCredentialWithAttestation + : Arbitrary[PublicKeyCredential[ + AuthenticatorAttestationResponse, + ClientRegistrationExtensionOutputs, + ]] = Arbitrary( + for { + id <- arbitrary[ByteArray] + response <- arbitrary[AuthenticatorAttestationResponse] + clientExtensionResults <- arbitrary[ClientRegistrationExtensionOutputs] + } yield PublicKeyCredential + .builder() + .id(id) + .response(response) + .clientExtensionResults(clientExtensionResults) + .build() + ) + + implicit val arbitraryPublicKeyCredentialCreationOptions + : Arbitrary[PublicKeyCredentialCreationOptions] = Arbitrary( + for { + attestation <- arbitrary[AttestationConveyancePreference] + authenticatorSelection <- + arbitrary[Optional[AuthenticatorSelectionCriteria]] + challenge <- arbitrary[ByteArray] + excludeCredentials <- + arbitrary[Optional[java.util.Set[PublicKeyCredentialDescriptor]]] + extensions <- arbitrary[RegistrationExtensionInputs] + pubKeyCredParams <- + arbitrary[java.util.List[PublicKeyCredentialParameters]] + rp <- arbitrary[RelyingPartyIdentity] + timeout <- arbitrary[Optional[java.lang.Long]] + user <- arbitrary[UserIdentity] + } yield PublicKeyCredentialCreationOptions + .builder() + .rp(rp) + .user(user) + .challenge(challenge) + .pubKeyCredParams(pubKeyCredParams) + .attestation(attestation) + .authenticatorSelection(authenticatorSelection) + .excludeCredentials(excludeCredentials) + .extensions(extensions) + .timeout(timeout) + .build() + ) + + implicit val arbitraryPublicKeyCredentialDescriptor + : Arbitrary[PublicKeyCredentialDescriptor] = Arbitrary( + for { + id <- arbitrary[ByteArray] + transports <- arbitrary[Optional[java.util.Set[AuthenticatorTransport]]] + tpe <- arbitrary[PublicKeyCredentialType] + } yield PublicKeyCredentialDescriptor + .builder() + .id(id) + .transports(transports) + .`type`(tpe) + .build() + ) + + implicit val arbitraryPublicKeyCredentialParameters + : Arbitrary[PublicKeyCredentialParameters] = Arbitrary( + for { + alg <- arbitrary[COSEAlgorithmIdentifier] + tpe <- arbitrary[PublicKeyCredentialType] + } yield PublicKeyCredentialParameters + .builder() + .alg(alg) + .`type`(tpe) + .build() + ) + + implicit val arbitraryPublicKeyCredentialRequestOptions + : Arbitrary[PublicKeyCredentialRequestOptions] = Arbitrary( + for { + allowCredentials <- + arbitrary[Optional[java.util.List[PublicKeyCredentialDescriptor]]] + challenge <- arbitrary[ByteArray] + extensions <- arbitrary[AssertionExtensionInputs] + rpId <- arbitrary[Optional[String]] + timeout <- arbitrary[Optional[java.lang.Long]] + userVerification <- arbitrary[UserVerificationRequirement] + } yield PublicKeyCredentialRequestOptions + .builder() + .challenge(challenge) + .allowCredentials(allowCredentials) + .extensions(extensions) + .rpId(rpId) + .timeout(timeout) + .userVerification(userVerification) + .build() + ) + + implicit val arbitraryRegistrationExtensionInputs + : Arbitrary[RegistrationExtensionInputs] = Arbitrary( + Gen.const(RegistrationExtensionInputs.builder().build()) + ) + + implicit val arbitraryRelyingPartyIdentity: Arbitrary[RelyingPartyIdentity] = + Arbitrary( + for { + icon <- arbitrary[Optional[URL]] + id <- arbitrary[String] + name <- arbitrary[String] + } yield RelyingPartyIdentity + .builder() + .id(id) + .name(name) + .icon(icon) + .build() + ) + + implicit val arbitraryTokenBindingInfo: Arbitrary[TokenBindingInfo] = + Arbitrary( + Gen.oneOf( + Gen.const(TokenBindingInfo.supported()), + arbitrary[ByteArray] map TokenBindingInfo.present, + ) + ) + + implicit val arbitraryUserIdentity: Arbitrary[UserIdentity] = Arbitrary( + for { + displayName <- arbitrary[String] + name <- arbitrary[String] + icon <- arbitrary[Optional[URL]] + id <- arbitrary[ByteArray] + name <- arbitrary[String] + } yield UserIdentity + .builder() + .name(name) + .displayName(displayName) + .id(id) + .icon(icon) + .build() + ) + + def knownExtensionId: Gen[String] = + Gen.oneOf( + "appid", + "txAuthSimple", + "txAuthGeneric", + "authnSel", + "exts", + "uvi", + "loc", + "uvm", + "biometricPerfBounds", + ) + + def anyAuthenticatorExtensions[A <: ExtensionInputs](implicit + a: Arbitrary[A] + ): Gen[(A, ObjectNode)] = for { requested <- arbitrary[A] - returned: ObjectNode <- JacksonGenerators.objectNode(names = Gen.oneOf(knownExtensionId, Gen.alphaNumStr)) + returned: ObjectNode <- JacksonGenerators.objectNode(names = + Gen.oneOf(knownExtensionId, Gen.alphaNumStr) + ) } yield (requested, returned) - def subsetAuthenticatorExtensions[A <: ExtensionInputs](implicit a: Arbitrary[A]): Gen[(A, ObjectNode)] = + def subsetAuthenticatorExtensions[A <: ExtensionInputs](implicit + a: Arbitrary[A] + ): Gen[(A, ObjectNode)] = for { requested <- arbitrary[A] - returned: ObjectNode <- JacksonGenerators.objectNode(names = Gen.oneOf(knownExtensionId, Gen.alphaNumStr)) + returned: ObjectNode <- JacksonGenerators.objectNode(names = + Gen.oneOf(knownExtensionId, Gen.alphaNumStr) + ) } yield { - val toRemove: Set[String] = returned.fieldNames().asScala.filter({ extId: String => - (requested.getExtensionIds contains extId) == false - }).toSet + val toRemove: Set[String] = returned + .fieldNames() + .asScala + .filter({ extId: String => + (requested.getExtensionIds contains extId) == false + }) + .toSet for { extId <- toRemove } { returned.remove(extId) @@ -362,39 +563,50 @@ object Generators { (requested, returned) } - def anyAssertionExtensions: Gen[(AssertionExtensionInputs, ClientAssertionExtensionOutputs)] = + def anyAssertionExtensions + : Gen[(AssertionExtensionInputs, ClientAssertionExtensionOutputs)] = for { requested <- arbitrary[AssertionExtensionInputs] returned <- arbitrary[ClientAssertionExtensionOutputs] } yield (requested, returned) - def anyRegistrationExtensions: Gen[(RegistrationExtensionInputs, ClientRegistrationExtensionOutputs)] = + def anyRegistrationExtensions + : Gen[(RegistrationExtensionInputs, ClientRegistrationExtensionOutputs)] = for { requested <- arbitrary[RegistrationExtensionInputs] returned <- arbitrary[ClientRegistrationExtensionOutputs] } yield (requested, returned) - def unrequestedAssertionExtensions: Gen[(AssertionExtensionInputs, ClientAssertionExtensionOutputs)] = + def unrequestedAssertionExtensions + : Gen[(AssertionExtensionInputs, ClientAssertionExtensionOutputs)] = for { requested <- arbitrary[AssertionExtensionInputs] - returned <- arbitrary[ClientAssertionExtensionOutputs] suchThat { returned => - ! returned.getExtensionIds.asScala.subsetOf(requested.getExtensionIds.asScala) + returned <- arbitrary[ClientAssertionExtensionOutputs] suchThat { + returned => + !returned.getExtensionIds.asScala.subsetOf( + requested.getExtensionIds.asScala + ) } } yield { (requested, returned) } - def subsetAssertionExtensions: Gen[(AssertionExtensionInputs, ClientAssertionExtensionOutputs)] = + def subsetAssertionExtensions + : Gen[(AssertionExtensionInputs, ClientAssertionExtensionOutputs)] = for { requested <- arbitrary[AssertionExtensionInputs] returned <- clientAssertionExtensionOutputs( - appid = if (requested.getAppid.isPresent) arbitrary[Optional[java.lang.Boolean]] else Gen.const(Optional.empty[java.lang.Boolean]) + appid = + if (requested.getAppid.isPresent) + arbitrary[Optional[java.lang.Boolean]] + else Gen.const(Optional.empty[java.lang.Boolean]) ) } yield { (requested, returned) } - def subsetRegistrationExtensions: Gen[(RegistrationExtensionInputs, ClientRegistrationExtensionOutputs)] = + def subsetRegistrationExtensions + : Gen[(RegistrationExtensionInputs, ClientRegistrationExtensionOutputs)] = for { requested <- arbitrary[RegistrationExtensionInputs] returned <- arbitrary[ClientRegistrationExtensionOutputs] diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/JsonIoSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/JsonIoSpec.scala index 7e7e22f18..6f4ef3bbd 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/JsonIoSpec.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/JsonIoSpec.scala @@ -51,15 +51,18 @@ import org.scalatest.Matchers import org.scalatestplus.junit.JUnitRunner import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks - @RunWith(classOf[JUnitRunner]) -class JsonIoSpec extends FunSpec with Matchers with ScalaCheckDrivenPropertyChecks { +class JsonIoSpec + extends FunSpec + with Matchers + with ScalaCheckDrivenPropertyChecks { - def json: ObjectMapper = new ObjectMapper() - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true) - .configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false) - .setSerializationInclusion(Include.NON_ABSENT) - .registerModule(new Jdk8Module()) + def json: ObjectMapper = + new ObjectMapper() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true) + .configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false) + .setSerializationInclusion(Include.NON_ABSENT) + .registerModule(new Jdk8Module()) describe("The class") { @@ -79,7 +82,7 @@ class JsonIoSpec extends FunSpec with Matchers with ScalaCheckDrivenPropertyChec val encoded: String = json.writeValueAsString(value) val decoded: A = json.readValue(encoded, tpe) - decoded should equal (value) + decoded should equal(value) } } @@ -89,8 +92,8 @@ class JsonIoSpec extends FunSpec with Matchers with ScalaCheckDrivenPropertyChec val decoded: A = json.readValue(encoded, tpe) val recoded: String = json.writeValueAsString(decoded) - decoded should equal (value) - recoded should equal (encoded) + decoded should equal(value) + recoded should equal(encoded) } } } @@ -116,8 +119,18 @@ class JsonIoSpec extends FunSpec with Matchers with ScalaCheckDrivenPropertyChec test(new TypeReference[ClientRegistrationExtensionOutputs]() {}) test(new TypeReference[CollectedClientData]() {}) test(new TypeReference[COSEAlgorithmIdentifier]() {}) - test(new TypeReference[PublicKeyCredential[AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs]]() {}) - test(new TypeReference[PublicKeyCredential[AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs]]() {}) + test( + new TypeReference[PublicKeyCredential[ + AuthenticatorAssertionResponse, + ClientAssertionExtensionOutputs, + ]]() {} + ) + test( + new TypeReference[PublicKeyCredential[ + AuthenticatorAttestationResponse, + ClientRegistrationExtensionOutputs, + ]]() {} + ) test(new TypeReference[PublicKeyCredentialCreationOptions]() {}) test(new TypeReference[PublicKeyCredentialDescriptor]() {}) test(new TypeReference[PublicKeyCredentialParameters]() {}) @@ -135,92 +148,152 @@ class JsonIoSpec extends FunSpec with Matchers with ScalaCheckDrivenPropertyChec } describe("The class PublicKeyCredential") { - it("has an alternative parseRegistrationResponseJson function as an alias.") { + it( + "has an alternative parseRegistrationResponseJson function as an alias." + ) { def test[A](tpe: TypeReference[A])(implicit a: Arbitrary[A]): Unit = { forAll { value: A => val encoded: String = json.writeValueAsString(value) val decoded: A = json.readValue(encoded, tpe) - val altDecoded = PublicKeyCredential.parseRegistrationResponseJson(encoded) + val altDecoded = + PublicKeyCredential.parseRegistrationResponseJson(encoded) val altRecoded: String = json.writeValueAsString(altDecoded) - altDecoded should equal (decoded) - altRecoded should equal (encoded) + altDecoded should equal(decoded) + altRecoded should equal(encoded) } } - test(new TypeReference[PublicKeyCredential[AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs]](){}) + test( + new TypeReference[PublicKeyCredential[ + AuthenticatorAttestationResponse, + ClientRegistrationExtensionOutputs, + ]]() {} + ) } - it("has an alternative parseAuthenticationResponseJson function as an alias.") { + it( + "has an alternative parseAuthenticationResponseJson function as an alias." + ) { def test[A](tpe: TypeReference[A])(implicit a: Arbitrary[A]): Unit = { forAll { value: A => val encoded: String = json.writeValueAsString(value) val decoded: A = json.readValue(encoded, tpe) - val altDecoded = PublicKeyCredential.parseAssertionResponseJson(encoded) + val altDecoded = + PublicKeyCredential.parseAssertionResponseJson(encoded) val altRecoded: String = json.writeValueAsString(altDecoded) - altDecoded should equal (decoded) - altRecoded should equal (encoded) + altDecoded should equal(decoded) + altRecoded should equal(encoded) } } - test(new TypeReference[PublicKeyCredential[AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs]](){}) + test( + new TypeReference[PublicKeyCredential[ + AuthenticatorAssertionResponse, + ClientAssertionExtensionOutputs, + ]]() {} + ) } it("allows rawId to be present without id.") { - def test[P <: PublicKeyCredential[_, _]](tpe: TypeReference[P])(implicit a: Arbitrary[P]): Unit = { + def test[P <: PublicKeyCredential[_, _]](tpe: TypeReference[P])(implicit + a: Arbitrary[P] + ): Unit = { forAll { value: P => val encoded: String = json.writeValueAsString(value) val decoded = json.readTree(encoded) - decoded.asInstanceOf[ObjectNode] + decoded + .asInstanceOf[ObjectNode] .set[ObjectNode]("rawId", new TextNode(value.getId.getBase64Url)) .remove("id") val reencoded = json.writeValueAsString(decoded) val restored: P = json.readValue(reencoded, tpe) - restored.getId should equal (value.getId) - restored should equal (value) + restored.getId should equal(value.getId) + restored should equal(value) } } - test(new TypeReference[PublicKeyCredential[AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs]](){}) - test(new TypeReference[PublicKeyCredential[AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs]](){}) + test( + new TypeReference[PublicKeyCredential[ + AuthenticatorAssertionResponse, + ClientAssertionExtensionOutputs, + ]]() {} + ) + test( + new TypeReference[PublicKeyCredential[ + AuthenticatorAttestationResponse, + ClientRegistrationExtensionOutputs, + ]]() {} + ) } it("allows id to be present without rawId.") { - def test[P <: PublicKeyCredential[_, _]](tpe: TypeReference[P])(implicit a: Arbitrary[P]): Unit = { + def test[P <: PublicKeyCredential[_, _]](tpe: TypeReference[P])(implicit + a: Arbitrary[P] + ): Unit = { forAll { value: P => val encoded: String = json.writeValueAsString(value) val decoded = json.readTree(encoded) - decoded.asInstanceOf[ObjectNode] + decoded + .asInstanceOf[ObjectNode] .set[ObjectNode]("id", new TextNode(value.getId.getBase64Url)) .remove("rawId") val reencoded = json.writeValueAsString(decoded) val restored: P = json.readValue(reencoded, tpe) - restored should equal (value) + restored should equal(value) } } - test(new TypeReference[PublicKeyCredential[AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs]](){}) - test(new TypeReference[PublicKeyCredential[AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs]](){}) + test( + new TypeReference[PublicKeyCredential[ + AuthenticatorAssertionResponse, + ClientAssertionExtensionOutputs, + ]]() {} + ) + test( + new TypeReference[PublicKeyCredential[ + AuthenticatorAttestationResponse, + ClientRegistrationExtensionOutputs, + ]]() {} + ) } it("allows both id and rawId to be present if equal.") { - def test[P <: PublicKeyCredential[_, _]](tpe: TypeReference[P])(implicit a: Arbitrary[P]): Unit = { + def test[P <: PublicKeyCredential[_, _]](tpe: TypeReference[P])(implicit + a: Arbitrary[P] + ): Unit = { forAll { value: P => val encoded: String = json.writeValueAsString(value) val decoded = json.readTree(encoded) - decoded.asInstanceOf[ObjectNode].set("id", new TextNode(value.getId.getBase64Url)) - decoded.asInstanceOf[ObjectNode].set("rawId", new TextNode(value.getId.getBase64Url)) + decoded + .asInstanceOf[ObjectNode] + .set("id", new TextNode(value.getId.getBase64Url)) + decoded + .asInstanceOf[ObjectNode] + .set("rawId", new TextNode(value.getId.getBase64Url)) val reencoded = json.writeValueAsString(decoded) val restored: P = json.readValue(reencoded, tpe) - restored should equal (value) + restored should equal(value) } } - test(new TypeReference[PublicKeyCredential[AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs]](){}) - test(new TypeReference[PublicKeyCredential[AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs]](){}) + test( + new TypeReference[PublicKeyCredential[ + AuthenticatorAssertionResponse, + ClientAssertionExtensionOutputs, + ]]() {} + ) + test( + new TypeReference[PublicKeyCredential[ + AuthenticatorAttestationResponse, + ClientRegistrationExtensionOutputs, + ]]() {} + ) } it("does not allow both id and rawId to be absent.") { - def test[P <: PublicKeyCredential[_, _]](tpe: TypeReference[P])(implicit a: Arbitrary[P]): Unit = { + def test[P <: PublicKeyCredential[_, _]](tpe: TypeReference[P])(implicit + a: Arbitrary[P] + ): Unit = { forAll { value: P => val encoded: String = json.writeValueAsString(value) val decoded = json.readTree(encoded).asInstanceOf[ObjectNode] @@ -228,41 +301,65 @@ class JsonIoSpec extends FunSpec with Matchers with ScalaCheckDrivenPropertyChec decoded.remove("rawId") val reencoded = json.writeValueAsString(decoded) - an [ValueInstantiationException] should be thrownBy { + an[ValueInstantiationException] should be thrownBy { json.readValue(reencoded, tpe) } } } - test(new TypeReference[PublicKeyCredential[AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs]](){}) - test(new TypeReference[PublicKeyCredential[AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs]](){}) + test( + new TypeReference[PublicKeyCredential[ + AuthenticatorAssertionResponse, + ClientAssertionExtensionOutputs, + ]]() {} + ) + test( + new TypeReference[PublicKeyCredential[ + AuthenticatorAttestationResponse, + ClientRegistrationExtensionOutputs, + ]]() {} + ) } it("does not allow both id and rawId to be present and not equal.") { - def test[P <: PublicKeyCredential[_, _]](tpe: TypeReference[P])(implicit a: Arbitrary[P]): Unit = { + def test[P <: PublicKeyCredential[_, _]](tpe: TypeReference[P])(implicit + a: Arbitrary[P] + ): Unit = { forAll { value: P => val modId = new ByteArray( if (value.getId.getBytes.isEmpty) Array(0) else - value.getId.getBytes.updated(0, (value.getId.getBytes()(0) + 1 % 127).byteValue) + value.getId.getBytes + .updated(0, (value.getId.getBytes()(0) + 1 % 127).byteValue) ) val encoded: String = json.writeValueAsString(value) val decoded = json.readTree(encoded) - decoded.asInstanceOf[ObjectNode] + decoded + .asInstanceOf[ObjectNode] .set[ObjectNode]("id", new TextNode(value.getId.getBase64Url)) .set[ObjectNode]("rawId", new TextNode(modId.getBase64Url)) val reencoded = json.writeValueAsString(decoded) - an [ValueInstantiationException] should be thrownBy { + an[ValueInstantiationException] should be thrownBy { json.readValue(reencoded, tpe) } } } - test(new TypeReference[PublicKeyCredential[AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs]](){}) - test(new TypeReference[PublicKeyCredential[AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs]](){}) + test( + new TypeReference[PublicKeyCredential[ + AuthenticatorAssertionResponse, + ClientAssertionExtensionOutputs, + ]]() {} + ) + test( + new TypeReference[PublicKeyCredential[ + AuthenticatorAttestationResponse, + ClientRegistrationExtensionOutputs, + ]]() {} + ) } } diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/PublicKeyCredentialDescriptorSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/PublicKeyCredentialDescriptorSpec.scala index fc4f97978..0b7218289 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/PublicKeyCredentialDescriptorSpec.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/PublicKeyCredentialDescriptorSpec.scala @@ -29,8 +29,10 @@ import org.scalatest.FunSpec import org.scalatest.Matchers import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks - -class PublicKeyCredentialDescriptorSpec extends FunSpec with Matchers with ScalaCheckDrivenPropertyChecks { +class PublicKeyCredentialDescriptorSpec + extends FunSpec + with Matchers + with ScalaCheckDrivenPropertyChecks { describe("PublicKeyCredentialDescriptor") { @@ -38,29 +40,38 @@ class PublicKeyCredentialDescriptorSpec extends FunSpec with Matchers with Scala describe("which is consistent with") { - implicit val generatorDrivenConfig = PropertyCheckConfiguration(minSuccessful = 300) + implicit val generatorDrivenConfig = + PropertyCheckConfiguration(minSuccessful = 300) it("equals.") { - forAll { (a: PublicKeyCredentialDescriptor, b: PublicKeyCredentialDescriptor) => - val comparison = a.compareTo(b) - - if (a == b) { - comparison should equal (0) - } else { - comparison should not equal 0 - } + forAll { + ( + a: PublicKeyCredentialDescriptor, + b: PublicKeyCredentialDescriptor, + ) => + val comparison = a.compareTo(b) + + if (a == b) { + comparison should equal(0) + } else { + comparison should not equal 0 + } } } it("hashCode.") { - forAll { (a: PublicKeyCredentialDescriptor, b: PublicKeyCredentialDescriptor) => - if (a.compareTo(b) == 0) { - a.hashCode() should equal (b.hashCode()) - } - - if (a.hashCode() != b.hashCode()) { - a.compareTo(b) should not be 0 - } + forAll { + ( + a: PublicKeyCredentialDescriptor, + b: PublicKeyCredentialDescriptor, + ) => + if (a.compareTo(b) == 0) { + a.hashCode() should equal(b.hashCode()) + } + + if (a.hashCode() != b.hashCode()) { + a.compareTo(b) should not be 0 + } } } diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/extension/appid/Generators.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/extension/appid/Generators.scala index dcab17666..333cba989 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/extension/appid/Generators.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/extension/appid/Generators.scala @@ -28,13 +28,12 @@ import com.yubico.scalacheck.gen.JavaGenerators._ import org.scalacheck.Arbitrary import org.scalacheck.Gen - object Generators { implicit val arbitraryAppId: Arbitrary[AppId] = Arbitrary(for { url <- url( scheme = Gen.const("https"), - path = Gen.alphaNumStr suchThat (_ != "/") + path = Gen.alphaNumStr suchThat (_ != "/"), ) } yield new AppId(url.toExternalForm)) diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/test/Helpers.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/test/Helpers.scala index 708b118fa..994a8cf77 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/test/Helpers.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/test/Helpers.scala @@ -1,65 +1,111 @@ package com.yubico.webauthn.test -import java.util.Optional - import com.yubico.internal.util.scala.JavaConverters._ -import com.yubico.webauthn.data.ByteArray +import com.yubico.webauthn.CredentialRepository import com.yubico.webauthn.RegisteredCredential +import com.yubico.webauthn.RegistrationResult +import com.yubico.webauthn.data.ByteArray import com.yubico.webauthn.data.PublicKeyCredentialDescriptor import com.yubico.webauthn.data.UserIdentity -import com.yubico.webauthn.CredentialRepository -import com.yubico.webauthn.RegistrationResult +import java.util.Optional import scala.jdk.CollectionConverters._ - object Helpers { object CredentialRepository { val empty = new CredentialRepository { - override def getCredentialIdsForUsername(username: String): java.util.Set[PublicKeyCredentialDescriptor] = Set.empty.asJava - override def getUserHandleForUsername(username: String): Optional[ByteArray] = None.asJava - override def getUsernameForUserHandle(userHandle: ByteArray): Optional[String] = None.asJava - override def lookup(credentialId: ByteArray, userHandle: ByteArray): Optional[RegisteredCredential] = None.asJava - override def lookupAll(credentialId: ByteArray): java.util.Set[RegisteredCredential] = Set.empty.asJava + override def getCredentialIdsForUsername( + username: String + ): java.util.Set[PublicKeyCredentialDescriptor] = Set.empty.asJava + override def getUserHandleForUsername( + username: String + ): Optional[ByteArray] = None.asJava + override def getUsernameForUserHandle( + userHandle: ByteArray + ): Optional[String] = None.asJava + override def lookup( + credentialId: ByteArray, + userHandle: ByteArray, + ): Optional[RegisteredCredential] = None.asJava + override def lookupAll( + credentialId: ByteArray + ): java.util.Set[RegisteredCredential] = Set.empty.asJava } val unimplemented = new CredentialRepository { - 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): java.util.Set[RegisteredCredential] = ??? + 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 + ): java.util.Set[RegisteredCredential] = ??? } - def withUser(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 withUser( + 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 toRegisteredCredential(user: UserIdentity, result: RegistrationResult): RegisteredCredential = - RegisteredCredential.builder() - .credentialId(result.getKeyId.getId) - .userHandle(user.getId) - .publicKeyCose(result.getPublicKeyCose) - .build() + def toRegisteredCredential( + user: UserIdentity, + result: RegistrationResult, + ): RegisteredCredential = + RegisteredCredential + .builder() + .credentialId(result.getKeyId.getId) + .userHandle(user.getId) + .publicKeyCose(result.getPublicKeyCose) + .build() } diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/test/RealExamples.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/test/RealExamples.scala index 63ce87c19..e0c932d46 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/test/RealExamples.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/test/RealExamples.scala @@ -1,8 +1,7 @@ package com.yubico.webauthn.test -import java.nio.charset.StandardCharsets - import com.yubico.internal.util.JacksonCodecs +import com.yubico.webauthn.WebAuthnTestCodecs import com.yubico.webauthn.data.AttestationObject import com.yubico.webauthn.data.AuthenticatorAssertionResponse import com.yubico.webauthn.data.AuthenticatorAttestationResponse @@ -10,23 +9,40 @@ import com.yubico.webauthn.data.AuthenticatorData import com.yubico.webauthn.data.ByteArray import com.yubico.webauthn.data.ClientAssertionExtensionOutputs import com.yubico.webauthn.data.ClientRegistrationExtensionOutputs +import com.yubico.webauthn.data.CollectedClientData import com.yubico.webauthn.data.PublicKeyCredential import com.yubico.webauthn.data.RelyingPartyIdentity import com.yubico.webauthn.data.UserIdentity +import java.nio.charset.StandardCharsets sealed trait HasClientData { def clientData: String - def clientDataJSON: ByteArray = new ByteArray(clientData.getBytes(StandardCharsets.UTF_8)) - def challenge: ByteArray = ByteArray.fromBase64Url(JacksonCodecs.json().readTree(clientData).get("challenge").textValue()) + def clientDataJSON: ByteArray = + new ByteArray(clientData.getBytes(StandardCharsets.UTF_8)) + def clientDataJSONHash: ByteArray = WebAuthnTestCodecs.sha256(clientDataJSON) + def collectedClientData: CollectedClientData = + new CollectedClientData(clientDataJSON) + def challenge: ByteArray = + ByteArray.fromBase64Url( + JacksonCodecs.json().readTree(clientData).get("challenge").textValue() + ) } object RealExamples { - case class AttestationExample(clientData: String, attestationObjectBytes: ByteArray) extends HasClientData { - def attestationObject: AttestationObject = new AttestationObject(attestationObjectBytes) - def authenticatorData: AuthenticatorData = attestationObject.getAuthenticatorData - def credential: PublicKeyCredential[AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs] = + case class AttestationExample( + clientData: String, + attestationObjectBytes: ByteArray, + ) extends HasClientData { + def attestationObject: AttestationObject = + new AttestationObject(attestationObjectBytes) + def authenticatorData: AuthenticatorData = + attestationObject.getAuthenticatorData + def credential: PublicKeyCredential[ + AuthenticatorAttestationResponse, + ClientRegistrationExtensionOutputs, + ] = PublicKeyCredential.parseRegistrationResponseJson(s"""{ "type": "public-key", "id": "${authenticatorData.getAttestedCredentialData.get.getCredentialId.getBase64Url}", @@ -38,9 +54,19 @@ object RealExamples { }""") } - case class AssertionExample(id: ByteArray, `type`: String = "public-key", clientData: String, authDataBytes: ByteArray, sig: ByteArray) extends HasClientData { - def authenticatorData: AuthenticatorData = new AuthenticatorData(authDataBytes) - def credential: PublicKeyCredential[AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs] = + case class AssertionExample( + id: ByteArray, + `type`: String = "public-key", + clientData: String, + authDataBytes: ByteArray, + sig: ByteArray, + ) extends HasClientData { + def authenticatorData: AuthenticatorData = + new AuthenticatorData(authDataBytes) + def credential: PublicKeyCredential[ + AuthenticatorAssertionResponse, + ClientAssertionExtensionOutputs, + ] = PublicKeyCredential.parseAssertionResponseJson(s"""{ "type": "public-key", "id": "${id.getBase64Url}", @@ -54,178 +80,384 @@ object RealExamples { } case class Example( - rp: RelyingPartyIdentity, - user: UserIdentity, - attestation: AttestationExample, - assertion: AssertionExample + rp: RelyingPartyIdentity, + user: UserIdentity, + attestation: AttestationExample, + assertion: AssertionExample, ) { def attestationCert: ByteArray = - new ByteArray(attestation.attestationObject.getAttestationStatement.get("x5c").get(0).binaryValue()) + new ByteArray( + attestation.attestationObject.getAttestationStatement + .get("x5c") + .get(0) + .binaryValue() + ) } val YubiKeyNeo = Example( RelyingPartyIdentity.builder().id("example.com").name("Example RP").build(), - UserIdentity.builder().name("test@example.org").displayName("A. User").id(ByteArray.fromBase64Url("dXNlcl9pZA==")).build(), + UserIdentity + .builder() + .name("test@example.org") + .displayName("A. User") + .id(ByteArray.fromBase64Url("dXNlcl9pZA==")) + .build(), AttestationExample( """{"type": "webauthn.create", "clientExtensions": {}, "challenge": "Y2hhbGxlbmdl", "origin": "https://example.com"}""", - ByteArray.fromBase64Url("o2NmbXRoZmlkby11MmZnYXR0U3RtdKJjc2lnWEcwRQIgZAIktn1uQmeCpXkStM74_oaFdb0MH0-J0k4ZXmIXM18CIQDZgPvwVDPBsTfreHAqoWa6n7v5bRS3cn0rcthSLAmDaGN4NWOBWQJTMIICTzCCATegAwIBAgIEWzpHQjANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZdWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAwMDBaGA8yMDUwMDkwNDAwMDAwMFowMTEvMC0GA1UEAwwmWXViaWNvIFUyRiBFRSBTZXJpYWwgMjM5MjU3MzUzMjgyMDQ2MTAwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAR-OFbId0OQrrorm-5x_bGglxHhfuEK-vP9tO3JRNO_gvmSTfgvjtZPYIlQ4Cr6gRt_Hfn6WDjT-Xt0Q14pfb20ozswOTAiBgkrBgEEAYLECgIEFTEuMy42LjEuNC4xLjQxNDgyLjEuMjATBgsrBgEEAYLlHAIBAQQEAwIFIDANBgkqhkiG9w0BAQsFAAOCAQEAa27ZcXkZ-pa1ywtQqWf1MeHFNxSGUDwpKBAUC7zCfgd8pINbK_1VHhbp3Q6tDiH-PO0wZnqE-oDQGi4KxFUFYyDjJszYL4HoVwUSORHeu8zCpO_zVoqUOR8OSeuDwZgububzfPu3NlaWDBkgUhhYoZDyCtFdGPyqT2foxk3iDpjzlG8zfU-t2pYIIAF9Q_LJv-XEkYqNGsAEZMxKMeNoB3n6p5mWxULriqoJPiajFYInzu9Kk7OiznJJ01-xovAGvtVeiFOZZljUkLCVa5eX7INYzdaa9L3LAHW81IdsoE7yFL2OPNcPR0kOiqsMoiJ6sgYy4MRTuxIu2ke-lIH5UWhhdXRoRGF0YVjEo3mm9u6vuaVeN4wRgDTidR5oL6ufLTCrE9ISVYbOGUdBAAAAAAAAAAAAAAAAAAAAAAAAAAAAQBeSCzJvnCNSyal5T2DPO0ypt2760sLlwynV_0Id2WiXWiq-Rv0MY1BfwD4QFJpryrRUHDgKt-T8ztr-DQfIKT2lAQIDJiABIVggOclPu93GlKkl5vhlfGaRbP6EzQIi7fzygbIfXNw0eSMiWCDWsNICHP4XJKb-geNWjE_64zkpghajCvwYwLl18uKokQ==") + ByteArray.fromBase64Url("o2NmbXRoZmlkby11MmZnYXR0U3RtdKJjc2lnWEcwRQIgZAIktn1uQmeCpXkStM74_oaFdb0MH0-J0k4ZXmIXM18CIQDZgPvwVDPBsTfreHAqoWa6n7v5bRS3cn0rcthSLAmDaGN4NWOBWQJTMIICTzCCATegAwIBAgIEWzpHQjANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZdWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAwMDBaGA8yMDUwMDkwNDAwMDAwMFowMTEvMC0GA1UEAwwmWXViaWNvIFUyRiBFRSBTZXJpYWwgMjM5MjU3MzUzMjgyMDQ2MTAwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAR-OFbId0OQrrorm-5x_bGglxHhfuEK-vP9tO3JRNO_gvmSTfgvjtZPYIlQ4Cr6gRt_Hfn6WDjT-Xt0Q14pfb20ozswOTAiBgkrBgEEAYLECgIEFTEuMy42LjEuNC4xLjQxNDgyLjEuMjATBgsrBgEEAYLlHAIBAQQEAwIFIDANBgkqhkiG9w0BAQsFAAOCAQEAa27ZcXkZ-pa1ywtQqWf1MeHFNxSGUDwpKBAUC7zCfgd8pINbK_1VHhbp3Q6tDiH-PO0wZnqE-oDQGi4KxFUFYyDjJszYL4HoVwUSORHeu8zCpO_zVoqUOR8OSeuDwZgububzfPu3NlaWDBkgUhhYoZDyCtFdGPyqT2foxk3iDpjzlG8zfU-t2pYIIAF9Q_LJv-XEkYqNGsAEZMxKMeNoB3n6p5mWxULriqoJPiajFYInzu9Kk7OiznJJ01-xovAGvtVeiFOZZljUkLCVa5eX7INYzdaa9L3LAHW81IdsoE7yFL2OPNcPR0kOiqsMoiJ6sgYy4MRTuxIu2ke-lIH5UWhhdXRoRGF0YVjEo3mm9u6vuaVeN4wRgDTidR5oL6ufLTCrE9ISVYbOGUdBAAAAAAAAAAAAAAAAAAAAAAAAAAAAQBeSCzJvnCNSyal5T2DPO0ypt2760sLlwynV_0Id2WiXWiq-Rv0MY1BfwD4QFJpryrRUHDgKt-T8ztr-DQfIKT2lAQIDJiABIVggOclPu93GlKkl5vhlfGaRbP6EzQIi7fzygbIfXNw0eSMiWCDWsNICHP4XJKb-geNWjE_64zkpghajCvwYwLl18uKokQ=="), ), AssertionExample( - id = ByteArray.fromBase64Url("F5ILMm-cI1LJqXlPYM87TKm3bvrSwuXDKdX_Qh3ZaJdaKr5G_QxjUF_APhAUmmvKtFQcOAq35PzO2v4NB8gpPQ=="), + id = + ByteArray.fromBase64Url("F5ILMm-cI1LJqXlPYM87TKm3bvrSwuXDKdX_Qh3ZaJdaKr5G_QxjUF_APhAUmmvKtFQcOAq35PzO2v4NB8gpPQ=="), clientData = """{"type": "webauthn.get", "clientExtensions": {}, "challenge": "Q0hBTExFTkdF", "origin": "https://example.com"}""", - authDataBytes = ByteArray.fromBase64Url("o3mm9u6vuaVeN4wRgDTidR5oL6ufLTCrE9ISVYbOGUcBAAAAAQ=="), - sig = ByteArray.fromBase64Url("MEUCIFEYoFCb4DZmBWm_5ho_0RpLQfZIvS3sU-HQi5O85BiuAiEAmj7_8Kr--lGm7YhM6-4FvFEIGzKlzFt7F6SxHVhmNfo="), - ) + authDataBytes = ByteArray.fromBase64Url( + "o3mm9u6vuaVeN4wRgDTidR5oL6ufLTCrE9ISVYbOGUcBAAAAAQ==" + ), + sig = + ByteArray.fromBase64Url("MEUCIFEYoFCb4DZmBWm_5ho_0RpLQfZIvS3sU-HQi5O85BiuAiEAmj7_8Kr--lGm7YhM6-4FvFEIGzKlzFt7F6SxHVhmNfo="), + ), ) val YubiKey4 = Example( RelyingPartyIdentity.builder().id("example.com").name("Example RP").build(), - UserIdentity.builder().name("test@example.org").displayName("A. User").id(ByteArray.fromBase64Url("dXNlcl9pZA==")).build(), + UserIdentity + .builder() + .name("test@example.org") + .displayName("A. User") + .id(ByteArray.fromBase64Url("dXNlcl9pZA==")) + .build(), AttestationExample( """{"type": "webauthn.create", "clientExtensions": {}, "challenge": "Y2hhbGxlbmdl", "origin": "https://example.com"}""", - ByteArray.fromBase64Url("o2NmbXRoZmlkby11MmZnYXR0U3RtdKJjc2lnWEcwRQIgMMS3Sk-YpRfKh7lGev4vNApGpQD0Md6l2bwWGsQIXWUCIQD9Krgg7JQVL5jLZhqHE-n7auwBcDdQvqpQ4VddgKR9S2N4NWOBWQJTMIICTzCCATegAwIBAgIEPGgpTTANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZdWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAwMDBaGA8yMDUwMDkwNDAwMDAwMFowMTEvMC0GA1UEAwwmWXViaWNvIFUyRiBFRSBTZXJpYWwgMjM5MjU3MzQ4MTExMTc5MDEwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAS932eT23eUw1Axce0sTUVK2XNmdRpIuqXZ-bVqOiCBeWtO3yvNe5J6FJMQ-8RoR2_8V5KpfbYvoChrxqMgAg5jozswOTAiBgkrBgEEAYLECgIEFTEuMy42LjEuNC4xLjQxNDgyLjEuNTATBgsrBgEEAYLlHAIBAQQEAwIFIDANBgkqhkiG9w0BAQsFAAOCAQEAqsANUQl-7BWkhrN5vMSDQPhn05cuzmpn-6Rw42DGRFnwrThC0_8IHnHqiVOXGyP5JcCtAMJHMRhSBvCzqRkp-5G3ZrU_4TNSKoNYuNEgtKv7f-jvJHtk_8amIUrB2b5zNv3g86gYP5NLUhh19eP3iYCvlwpbHgQqOHbXS6i-7-kt0uNzzGRByJStfNmk9H2tPaT-r0eRmEdT41oInOTL49PINurQoqfOpWFa1-RIEIbDd7NmRNL7mWu84pshrbiV95OC7sVJPk7BM8IWfwdx9ZkxcxIP8o1T6IGol0DBMs88NGgsu89OXb3B4IAiH4dSmYFB3RSW1w86sD8sW8B_rWhhdXRoRGF0YVjEo3mm9u6vuaVeN4wRgDTidR5oL6ufLTCrE9ISVYbOGUdBAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEZ3ZpSx4x3b5ta9SnTMyCNtIAkzgZwfRff5n251aUcLfadvNqYhCylFC1FdfljBSHxcibx2oD45K2HSE3sCGfSlAQIDJiABIVggREuYlwMvN1mWVAPf8QrgB-cUNJYyS8vwZtr2tAWnoCQiWCAFm1_ct7jy-C_IQr73ChoiLZKkEAOnCJ_F5rf3wlOT5Q==") + ByteArray.fromBase64Url("o2NmbXRoZmlkby11MmZnYXR0U3RtdKJjc2lnWEcwRQIgMMS3Sk-YpRfKh7lGev4vNApGpQD0Md6l2bwWGsQIXWUCIQD9Krgg7JQVL5jLZhqHE-n7auwBcDdQvqpQ4VddgKR9S2N4NWOBWQJTMIICTzCCATegAwIBAgIEPGgpTTANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZdWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAwMDBaGA8yMDUwMDkwNDAwMDAwMFowMTEvMC0GA1UEAwwmWXViaWNvIFUyRiBFRSBTZXJpYWwgMjM5MjU3MzQ4MTExMTc5MDEwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAS932eT23eUw1Axce0sTUVK2XNmdRpIuqXZ-bVqOiCBeWtO3yvNe5J6FJMQ-8RoR2_8V5KpfbYvoChrxqMgAg5jozswOTAiBgkrBgEEAYLECgIEFTEuMy42LjEuNC4xLjQxNDgyLjEuNTATBgsrBgEEAYLlHAIBAQQEAwIFIDANBgkqhkiG9w0BAQsFAAOCAQEAqsANUQl-7BWkhrN5vMSDQPhn05cuzmpn-6Rw42DGRFnwrThC0_8IHnHqiVOXGyP5JcCtAMJHMRhSBvCzqRkp-5G3ZrU_4TNSKoNYuNEgtKv7f-jvJHtk_8amIUrB2b5zNv3g86gYP5NLUhh19eP3iYCvlwpbHgQqOHbXS6i-7-kt0uNzzGRByJStfNmk9H2tPaT-r0eRmEdT41oInOTL49PINurQoqfOpWFa1-RIEIbDd7NmRNL7mWu84pshrbiV95OC7sVJPk7BM8IWfwdx9ZkxcxIP8o1T6IGol0DBMs88NGgsu89OXb3B4IAiH4dSmYFB3RSW1w86sD8sW8B_rWhhdXRoRGF0YVjEo3mm9u6vuaVeN4wRgDTidR5oL6ufLTCrE9ISVYbOGUdBAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEZ3ZpSx4x3b5ta9SnTMyCNtIAkzgZwfRff5n251aUcLfadvNqYhCylFC1FdfljBSHxcibx2oD45K2HSE3sCGfSlAQIDJiABIVggREuYlwMvN1mWVAPf8QrgB-cUNJYyS8vwZtr2tAWnoCQiWCAFm1_ct7jy-C_IQr73ChoiLZKkEAOnCJ_F5rf3wlOT5Q=="), ), AssertionExample( - id = ByteArray.fromBase64Url("RndmlLHjHdvm1r1KdMzII20gCTOBnB9F9_mfbnVpRwt9p282piELKUULUV1-WMFIfFyJvHagPjkrYdITewIZ9A=="), + id = + ByteArray.fromBase64Url("RndmlLHjHdvm1r1KdMzII20gCTOBnB9F9_mfbnVpRwt9p282piELKUULUV1-WMFIfFyJvHagPjkrYdITewIZ9A=="), clientData = """{"type": "webauthn.get", "clientExtensions": {}, "challenge": "Q0hBTExFTkdF", "origin": "https://example.com"}""", - authDataBytes = ByteArray.fromBase64Url("o3mm9u6vuaVeN4wRgDTidR5oL6ufLTCrE9ISVYbOGUcBAAAAAA=="), - sig = ByteArray.fromBase64Url("MEQCIDniM0szLdfVU1CtXMjUmbYmAU3cL5F8umwXbIhqmTFfAiBHxk-ZOxTzXIMd0ghIFVpaJBWG-6lNJP6DOrkufJVx_Q=="), - ) + authDataBytes = ByteArray.fromBase64Url( + "o3mm9u6vuaVeN4wRgDTidR5oL6ufLTCrE9ISVYbOGUcBAAAAAA==" + ), + sig = + ByteArray.fromBase64Url("MEQCIDniM0szLdfVU1CtXMjUmbYmAU3cL5F8umwXbIhqmTFfAiBHxk-ZOxTzXIMd0ghIFVpaJBWG-6lNJP6DOrkufJVx_Q=="), + ), ) val YubiKey5 = Example( RelyingPartyIdentity.builder().id("example.com").name("Example RP").build(), - UserIdentity.builder().name("test@example.org").displayName("A. User").id(ByteArray.fromBase64Url("dXNlcl9pZA==")).build(), + UserIdentity + .builder() + .name("test@example.org") + .displayName("A. User") + .id(ByteArray.fromBase64Url("dXNlcl9pZA==")) + .build(), AttestationExample( """{"type": "webauthn.create", "clientExtensions": {}, "challenge": "Y2hhbGxlbmdl", "origin": "https://example.com"}""", - ByteArray.fromBase64Url("o2NmbXRmcGFja2VkZ2F0dFN0bXSjY2FsZyZjc2lnWEcwRQIgCrcg9FJhbV35puNRlN36gSO9_YNWweirVdB2n3Ojez0CIQDOvSCusMldIS57ittkKJ9cne9RYQS6a--ivsKFYWrAIWN4NWOBWQLAMIICvDCCAaSgAwIBAgIEA63wEjANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZdWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAwMDBaGA8yMDUwMDkwNDAwMDAwMFowbTELMAkGA1UEBhMCU0UxEjAQBgNVBAoMCVl1YmljbyBBQjEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjEmMCQGA1UEAwwdWXViaWNvIFUyRiBFRSBTZXJpYWwgNjE3MzA4MzQwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQZnoecFi233DnuSkKgRhalswn-ygkvdr4JSPltbpXK5MxlzVSgWc-9x8mzGysdbBhEecLAYfQYqpVLWWosHPoXo2wwajAiBgkrBgEEAYLECgIEFTEuMy42LjEuNC4xLjQxNDgyLjEuNzATBgsrBgEEAYLlHAIBAQQEAwIEMDAhBgsrBgEEAYLlHAEBBAQSBBD6K5ncnjlCV4-SSjDSPEEYMAwGA1UdEwEB_wQCMAAwDQYJKoZIhvcNAQELBQADggEBACjrs2f-0djw4onryp_22AdXxg6a5XyxcoybHDjKu72E2SN9qDGsIZSfDy38DDFr_bF1s25joiu7WA6tylKA0HmEDloeJXJiWjv7h2Az2_siqWnJOLic4XE1lAChJS2XAqkSk9VFGelg3SLOiifrBet-ebdQwAL-2QFrcR7JrXRQG9kUy76O2VcSgbdPROsHfOYeywarhalyVSZ-6OOYK_Q_DLIaOC0jXrnkzm2ymMQFQlBAIysrYeEM1wxiFbwDt-lAcbcOEtHEf5ZlWi75nUzlWn8bSx_5FO4TbZ5hIEcUiGRpiIBEMRZlOIm4ZIbZycn_vJOFRTVps0V0S4ygtDdoYXV0aERhdGFYxKN5pvbur7mlXjeMEYA04nUeaC-rny0wqxPSElWGzhlHQQAAAAv6K5ncnjlCV4-SSjDSPEEYAED94RxjDuKGTpu5usg0Vcee9gqqhDVGw1__eyvx3YUhH7gba6zYjbwI1e1CZa78jZq8167iUIHbM_kNbyXIHSNhpQECAyYgASFYIIIyZu4ct876xj7sKSV90mbX0PpGLuRIRGu6IxnWUhD9Ilggw2qT1jtmMhn-X9raOZxqjWkzfdF8aJqFpvp3QXI-vNY=") + ByteArray.fromBase64Url("o2NmbXRmcGFja2VkZ2F0dFN0bXSjY2FsZyZjc2lnWEcwRQIgCrcg9FJhbV35puNRlN36gSO9_YNWweirVdB2n3Ojez0CIQDOvSCusMldIS57ittkKJ9cne9RYQS6a--ivsKFYWrAIWN4NWOBWQLAMIICvDCCAaSgAwIBAgIEA63wEjANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZdWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAwMDBaGA8yMDUwMDkwNDAwMDAwMFowbTELMAkGA1UEBhMCU0UxEjAQBgNVBAoMCVl1YmljbyBBQjEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjEmMCQGA1UEAwwdWXViaWNvIFUyRiBFRSBTZXJpYWwgNjE3MzA4MzQwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQZnoecFi233DnuSkKgRhalswn-ygkvdr4JSPltbpXK5MxlzVSgWc-9x8mzGysdbBhEecLAYfQYqpVLWWosHPoXo2wwajAiBgkrBgEEAYLECgIEFTEuMy42LjEuNC4xLjQxNDgyLjEuNzATBgsrBgEEAYLlHAIBAQQEAwIEMDAhBgsrBgEEAYLlHAEBBAQSBBD6K5ncnjlCV4-SSjDSPEEYMAwGA1UdEwEB_wQCMAAwDQYJKoZIhvcNAQELBQADggEBACjrs2f-0djw4onryp_22AdXxg6a5XyxcoybHDjKu72E2SN9qDGsIZSfDy38DDFr_bF1s25joiu7WA6tylKA0HmEDloeJXJiWjv7h2Az2_siqWnJOLic4XE1lAChJS2XAqkSk9VFGelg3SLOiifrBet-ebdQwAL-2QFrcR7JrXRQG9kUy76O2VcSgbdPROsHfOYeywarhalyVSZ-6OOYK_Q_DLIaOC0jXrnkzm2ymMQFQlBAIysrYeEM1wxiFbwDt-lAcbcOEtHEf5ZlWi75nUzlWn8bSx_5FO4TbZ5hIEcUiGRpiIBEMRZlOIm4ZIbZycn_vJOFRTVps0V0S4ygtDdoYXV0aERhdGFYxKN5pvbur7mlXjeMEYA04nUeaC-rny0wqxPSElWGzhlHQQAAAAv6K5ncnjlCV4-SSjDSPEEYAED94RxjDuKGTpu5usg0Vcee9gqqhDVGw1__eyvx3YUhH7gba6zYjbwI1e1CZa78jZq8167iUIHbM_kNbyXIHSNhpQECAyYgASFYIIIyZu4ct876xj7sKSV90mbX0PpGLuRIRGu6IxnWUhD9Ilggw2qT1jtmMhn-X9raOZxqjWkzfdF8aJqFpvp3QXI-vNY="), ), AssertionExample( - id = ByteArray.fromBase64Url("_eEcYw7ihk6bubrINFXHnvYKqoQ1RsNf_3sr8d2FIR-4G2us2I28CNXtQmWu_I2avNeu4lCB2zP5DW8lyB0jYQ=="), + id = + ByteArray.fromBase64Url("_eEcYw7ihk6bubrINFXHnvYKqoQ1RsNf_3sr8d2FIR-4G2us2I28CNXtQmWu_I2avNeu4lCB2zP5DW8lyB0jYQ=="), clientData = """{"type": "webauthn.get", "clientExtensions": {}, "challenge": "Q0hBTExFTkdF", "origin": "https://example.com"}""", - authDataBytes = ByteArray.fromBase64Url("o3mm9u6vuaVeN4wRgDTidR5oL6ufLTCrE9ISVYbOGUcBAAAADA=="), - sig = ByteArray.fromBase64Url("MEQCIE5k9IsMKGNpn6l29eIuoXkkuyZmSTePbQRKrWUaF5IxAiA-3veAkhDgW06BA-L_TLNw8KZDzHzU5zaw6Guqk-_J5Q=="), - ) + authDataBytes = ByteArray.fromBase64Url( + "o3mm9u6vuaVeN4wRgDTidR5oL6ufLTCrE9ISVYbOGUcBAAAADA==" + ), + sig = + ByteArray.fromBase64Url("MEQCIE5k9IsMKGNpn6l29eIuoXkkuyZmSTePbQRKrWUaF5IxAiA-3veAkhDgW06BA-L_TLNw8KZDzHzU5zaw6Guqk-_J5Q=="), + ), ) val YubiKey5Nfc = Example( - RelyingPartyIdentity.builder().id("demo.yubico.com").name("YubicoDemo").build(), - UserIdentity.builder().name("dfgfdfgf").displayName("dfgfdfgf").id(ByteArray.fromBase64Url("FBUasomeAb_g7CUQf_Ub6PtpXNJ8843IOgsnE50JLP0")).build(), + RelyingPartyIdentity + .builder() + .id("demo.yubico.com") + .name("YubicoDemo") + .build(), + UserIdentity + .builder() + .name("dfgfdfgf") + .displayName("dfgfdfgf") + .id( + ByteArray.fromBase64Url("FBUasomeAb_g7CUQf_Ub6PtpXNJ8843IOgsnE50JLP0") + ) + .build(), AttestationExample( """{"type":"webauthn.create","challenge":"0b-5-z3_EvP6pqaBj6Fu7A4M5SdefgZ_jcAoFa6_miU","origin":"https://demo.yubico.com","crossOrigin":false}""", - ByteArray.fromBase64Url("o2NmbXRmcGFja2VkZ2F0dFN0bXSjY2FsZyZjc2lnWEcwRQIgZUEF7FMB8dEzallsJvUFVhHRU8xdWDkDwQQI-ZU8XRMCIQCqzI3-lWRlSEBLGk2XVqkp72q2QzbhdOzZyWOrke4jsmN4NWOBWQLAMIICvDCCAaSgAwIBAgIEA63wEjANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZdWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAwMDBaGA8yMDUwMDkwNDAwMDAwMFowbTELMAkGA1UEBhMCU0UxEjAQBgNVBAoMCVl1YmljbyBBQjEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjEmMCQGA1UEAwwdWXViaWNvIFUyRiBFRSBTZXJpYWwgNjE3MzA4MzQwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQZnoecFi233DnuSkKgRhalswn-ygkvdr4JSPltbpXK5MxlzVSgWc-9x8mzGysdbBhEecLAYfQYqpVLWWosHPoXo2wwajAiBgkrBgEEAYLECgIEFTEuMy42LjEuNC4xLjQxNDgyLjEuNzATBgsrBgEEAYLlHAIBAQQEAwIEMDAhBgsrBgEEAYLlHAEBBAQSBBD6K5ncnjlCV4-SSjDSPEEYMAwGA1UdEwEB_wQCMAAwDQYJKoZIhvcNAQELBQADggEBACjrs2f-0djw4onryp_22AdXxg6a5XyxcoybHDjKu72E2SN9qDGsIZSfDy38DDFr_bF1s25joiu7WA6tylKA0HmEDloeJXJiWjv7h2Az2_siqWnJOLic4XE1lAChJS2XAqkSk9VFGelg3SLOiifrBet-ebdQwAL-2QFrcR7JrXRQG9kUy76O2VcSgbdPROsHfOYeywarhalyVSZ-6OOYK_Q_DLIaOC0jXrnkzm2ymMQFQlBAIysrYeEM1wxiFbwDt-lAcbcOEtHEf5ZlWi75nUzlWn8bSx_5FO4TbZ5hIEcUiGRpiIBEMRZlOIm4ZIbZycn_vJOFRTVps0V0S4ygtDdoYXV0aERhdGFYxMRs74KtG1Rkd1kdAIsIdZ7D5tLstPOUdL_qaWmSXQO3QQAAADn6K5ncnjlCV4-SSjDSPEEYAECSDhJoaRjVyhU9DO24CFhDHIm8rwh5dHFRVONEpTj2eXiqpzRs5xNoNlEq5cotavl1nTbQ6DhXaOYm_ulT16RMpQECAyYgASFYIJbLqy9JV7ETZUEdPtNzlfl6fBTDZNgioYpDIxIVhRGOIlggS8YE-ZzHh63D4jN3vShnN3F7heKxyJuAApMeRvTJuc8") + ByteArray.fromBase64Url("o2NmbXRmcGFja2VkZ2F0dFN0bXSjY2FsZyZjc2lnWEcwRQIgZUEF7FMB8dEzallsJvUFVhHRU8xdWDkDwQQI-ZU8XRMCIQCqzI3-lWRlSEBLGk2XVqkp72q2QzbhdOzZyWOrke4jsmN4NWOBWQLAMIICvDCCAaSgAwIBAgIEA63wEjANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZdWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAwMDBaGA8yMDUwMDkwNDAwMDAwMFowbTELMAkGA1UEBhMCU0UxEjAQBgNVBAoMCVl1YmljbyBBQjEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjEmMCQGA1UEAwwdWXViaWNvIFUyRiBFRSBTZXJpYWwgNjE3MzA4MzQwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQZnoecFi233DnuSkKgRhalswn-ygkvdr4JSPltbpXK5MxlzVSgWc-9x8mzGysdbBhEecLAYfQYqpVLWWosHPoXo2wwajAiBgkrBgEEAYLECgIEFTEuMy42LjEuNC4xLjQxNDgyLjEuNzATBgsrBgEEAYLlHAIBAQQEAwIEMDAhBgsrBgEEAYLlHAEBBAQSBBD6K5ncnjlCV4-SSjDSPEEYMAwGA1UdEwEB_wQCMAAwDQYJKoZIhvcNAQELBQADggEBACjrs2f-0djw4onryp_22AdXxg6a5XyxcoybHDjKu72E2SN9qDGsIZSfDy38DDFr_bF1s25joiu7WA6tylKA0HmEDloeJXJiWjv7h2Az2_siqWnJOLic4XE1lAChJS2XAqkSk9VFGelg3SLOiifrBet-ebdQwAL-2QFrcR7JrXRQG9kUy76O2VcSgbdPROsHfOYeywarhalyVSZ-6OOYK_Q_DLIaOC0jXrnkzm2ymMQFQlBAIysrYeEM1wxiFbwDt-lAcbcOEtHEf5ZlWi75nUzlWn8bSx_5FO4TbZ5hIEcUiGRpiIBEMRZlOIm4ZIbZycn_vJOFRTVps0V0S4ygtDdoYXV0aERhdGFYxMRs74KtG1Rkd1kdAIsIdZ7D5tLstPOUdL_qaWmSXQO3QQAAADn6K5ncnjlCV4-SSjDSPEEYAECSDhJoaRjVyhU9DO24CFhDHIm8rwh5dHFRVONEpTj2eXiqpzRs5xNoNlEq5cotavl1nTbQ6DhXaOYm_ulT16RMpQECAyYgASFYIJbLqy9JV7ETZUEdPtNzlfl6fBTDZNgioYpDIxIVhRGOIlggS8YE-ZzHh63D4jN3vShnN3F7heKxyJuAApMeRvTJuc8"), ), AssertionExample( - id = ByteArray.fromBase64Url("kg4SaGkY1coVPQztuAhYQxyJvK8IeXRxUVTjRKU49nl4qqc0bOcTaDZRKuXKLWr5dZ020Og4V2jmJv7pU9ekTA"), + id = + ByteArray.fromBase64Url("kg4SaGkY1coVPQztuAhYQxyJvK8IeXRxUVTjRKU49nl4qqc0bOcTaDZRKuXKLWr5dZ020Og4V2jmJv7pU9ekTA"), clientData = """{"type":"webauthn.get","challenge":"AK6EVGBeT_DvQQk3hoUCocO8k3WVvnQnwL5Kd2oFWzM","origin":"https://demo.yubico.com","crossOrigin":false,"extra_keys_may_be_added_here":"do not compare clientDataJSON against a template. See https://goo.gl/yabPex"}""", - authDataBytes = ByteArray.fromBase64Url("xGzvgq0bVGR3WR0Aiwh1nsPm0uy085R0v-ppaZJdA7cBAAAAOg"), - sig = ByteArray.fromBase64Url("MEUCIDkZa6d7HwRxGCZdAldFuTo4qUZvaV8j7IYGjO74liKcAiEAj_PLArWm-VylAUsKgWoj50NQSpnn_qhZEgasgfWmG1Y"), - ) + authDataBytes = ByteArray.fromBase64Url( + "xGzvgq0bVGR3WR0Aiwh1nsPm0uy085R0v-ppaZJdA7cBAAAAOg" + ), + sig = + ByteArray.fromBase64Url("MEUCIDkZa6d7HwRxGCZdAldFuTo4qUZvaV8j7IYGjO74liKcAiEAj_PLArWm-VylAUsKgWoj50NQSpnn_qhZEgasgfWmG1Y"), + ), ) val YubiKey5NfcPost5cNfc = Example( - RelyingPartyIdentity.builder().id("demo.yubico.com").name("YubicoDemo").build(), - UserIdentity.builder().name("Yubico demo user").displayName("Yubico demo user").id(ByteArray.fromBase64Url("a9n4HpAeWRGIKzLWEkgia_yeBm_VGLgNj5uND9wyuOg")).build(), + RelyingPartyIdentity + .builder() + .id("demo.yubico.com") + .name("YubicoDemo") + .build(), + UserIdentity + .builder() + .name("Yubico demo user") + .displayName("Yubico demo user") + .id( + ByteArray.fromBase64Url("a9n4HpAeWRGIKzLWEkgia_yeBm_VGLgNj5uND9wyuOg") + ) + .build(), AttestationExample( """{"challenge":"q17naevQpc84vHK9Ge6hwCXnLt3LmlFqwVJ-YETQHwk","clientExtensions":{},"hashAlgorithm":"SHA-256","origin":"https://demo.yubico.com","type":"webauthn.create"}""", - ByteArray.fromBase64Url("o2NmbXRoZmlkby11MmZnYXR0U3RtdKJjc2lnWEcwRQIhALT8jIrN8OmV77OopLGKHXLupu_2yEHVEk9eaMmVlqGPAiBfgBugvPNvhED79Dbom5yBUxh47IqHZlIyiZGujZMb-GN4NWOBWQLBMIICvTCCAaWgAwIBAgIEHo-HNDANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZdWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAwMDBaGA8yMDUwMDkwNDAwMDAwMFowbjELMAkGA1UEBhMCU0UxEjAQBgNVBAoMCVl1YmljbyBBQjEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjEnMCUGA1UEAwweWXViaWNvIFUyRiBFRSBTZXJpYWwgNTEyNzIyNzQwMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEqHn4IzjtFJS6wHBLzH_GY9GycXFZdiQxAcdgURXXwVKeKBwcZzItOEtc1V3T6YGNX9hcIq8ybgxk_CCv4z8jZqNsMGowIgYJKwYBBAGCxAoCBBUxLjMuNi4xLjQuMS40MTQ4Mi4xLjcwEwYLKwYBBAGC5RwCAQEEBAMCBDAwIQYLKwYBBAGC5RwBAQQEEgQQL8BXn4ETR-qxFrtajbkgKjAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4IBAQCGk_9i3w1XedR0jX_I0QInMYqOWA5qOlfBCOlOA8OFaLNmiU_OViS-Sj79fzQRiz2ZN0P3kqGYkWDI_JrgsE49-e4V4-iMBPyCqNy_WBjhCNzCloV3rnn_ZiuUc0497EWXMF1z5uVe4r65zZZ4ygk15TPrY4-OJvq7gXzaRB--mDGDKuX24q2ZL56720xiI4uPjXq0gdbTJjvNv55KV1UDcJiK1YE0QPoDLK22cjyt2PjXuoCfdbQ8_6Clua3RQjLvnZ4UgSY4IzxMpKhzufismOMroZFnYG4VkJ_N20ot_72uRiAkn5pmRqyB5IMtERn-v6pzGogtolp3gn1G0ZAXaGF1dGhEYXRhWMTEbO-CrRtUZHdZHQCLCHWew-bS7LTzlHS_6mlpkl0Dt0EAAAAAAAAAAAAAAAAAAAAAAAAAAABAYiZiFWXtspQv_5_ZmKPIsSIV6yqQb1evWuRAfdipNhRgUWo2lUefvU8q7y6MtbjgYhoVA-5pGTIZv-r7oNi1YKUBAgMmIAEhWCDMHeuArInpowl_rB8S9AFGO-G_VmhM-0tM2ggV1SB7NSJYIPvfLUW8-Aoiqd4eQF649w1u274AFkg7fAvXya_G6dP9") + ByteArray.fromBase64Url("o2NmbXRoZmlkby11MmZnYXR0U3RtdKJjc2lnWEcwRQIhALT8jIrN8OmV77OopLGKHXLupu_2yEHVEk9eaMmVlqGPAiBfgBugvPNvhED79Dbom5yBUxh47IqHZlIyiZGujZMb-GN4NWOBWQLBMIICvTCCAaWgAwIBAgIEHo-HNDANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZdWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAwMDBaGA8yMDUwMDkwNDAwMDAwMFowbjELMAkGA1UEBhMCU0UxEjAQBgNVBAoMCVl1YmljbyBBQjEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjEnMCUGA1UEAwweWXViaWNvIFUyRiBFRSBTZXJpYWwgNTEyNzIyNzQwMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEqHn4IzjtFJS6wHBLzH_GY9GycXFZdiQxAcdgURXXwVKeKBwcZzItOEtc1V3T6YGNX9hcIq8ybgxk_CCv4z8jZqNsMGowIgYJKwYBBAGCxAoCBBUxLjMuNi4xLjQuMS40MTQ4Mi4xLjcwEwYLKwYBBAGC5RwCAQEEBAMCBDAwIQYLKwYBBAGC5RwBAQQEEgQQL8BXn4ETR-qxFrtajbkgKjAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4IBAQCGk_9i3w1XedR0jX_I0QInMYqOWA5qOlfBCOlOA8OFaLNmiU_OViS-Sj79fzQRiz2ZN0P3kqGYkWDI_JrgsE49-e4V4-iMBPyCqNy_WBjhCNzCloV3rnn_ZiuUc0497EWXMF1z5uVe4r65zZZ4ygk15TPrY4-OJvq7gXzaRB--mDGDKuX24q2ZL56720xiI4uPjXq0gdbTJjvNv55KV1UDcJiK1YE0QPoDLK22cjyt2PjXuoCfdbQ8_6Clua3RQjLvnZ4UgSY4IzxMpKhzufismOMroZFnYG4VkJ_N20ot_72uRiAkn5pmRqyB5IMtERn-v6pzGogtolp3gn1G0ZAXaGF1dGhEYXRhWMTEbO-CrRtUZHdZHQCLCHWew-bS7LTzlHS_6mlpkl0Dt0EAAAAAAAAAAAAAAAAAAAAAAAAAAABAYiZiFWXtspQv_5_ZmKPIsSIV6yqQb1evWuRAfdipNhRgUWo2lUefvU8q7y6MtbjgYhoVA-5pGTIZv-r7oNi1YKUBAgMmIAEhWCDMHeuArInpowl_rB8S9AFGO-G_VmhM-0tM2ggV1SB7NSJYIPvfLUW8-Aoiqd4eQF649w1u274AFkg7fAvXya_G6dP9"), ), AssertionExample( - id = ByteArray.fromBase64Url("YiZiFWXtspQv_5_ZmKPIsSIV6yqQb1evWuRAfdipNhRgUWo2lUefvU8q7y6MtbjgYhoVA-5pGTIZv-r7oNi1YA"), + id = + ByteArray.fromBase64Url("YiZiFWXtspQv_5_ZmKPIsSIV6yqQb1evWuRAfdipNhRgUWo2lUefvU8q7y6MtbjgYhoVA-5pGTIZv-r7oNi1YA"), clientData = """{"challenge":"YM-QmlCkDwETwz4XOfqgZTv6pG8NMFtIRkoNaDaY5jw","clientExtensions":{},"hashAlgorithm":"SHA-256","origin":"https://demo.yubico.com","type":"webauthn.get"}""", - authDataBytes = ByteArray.fromBase64Url("xGzvgq0bVGR3WR0Aiwh1nsPm0uy085R0v-ppaZJdA7cBAAAAAw"), - sig = ByteArray.fromBase64Url("MEYCIQCaLSBboXlSI5uff61mlXG_S9OXRRT5kx-0KuHBu8Fm0QIhAIeNMEJkH1wzaKi2NZy5u8aJm4lOj9vsFdSkNiMhcVjw"), - ) + authDataBytes = ByteArray.fromBase64Url( + "xGzvgq0bVGR3WR0Aiwh1nsPm0uy085R0v-ppaZJdA7cBAAAAAw" + ), + sig = + ByteArray.fromBase64Url("MEYCIQCaLSBboXlSI5uff61mlXG_S9OXRRT5kx-0KuHBu8Fm0QIhAIeNMEJkH1wzaKi2NZy5u8aJm4lOj9vsFdSkNiMhcVjw"), + ), ) val YubiKey5cNfc = Example( - RelyingPartyIdentity.builder().id("demo.yubico.com").name("YubicoDemo").build(), - UserIdentity.builder().name("Yubico demo user").displayName("Yubico demo user").id(ByteArray.fromBase64Url("a9n4HpAeWRGIKzLWEkgia_yeBm_VGLgNj5uND9wyuOg")).build(), + RelyingPartyIdentity + .builder() + .id("demo.yubico.com") + .name("YubicoDemo") + .build(), + UserIdentity + .builder() + .name("Yubico demo user") + .displayName("Yubico demo user") + .id( + ByteArray.fromBase64Url("a9n4HpAeWRGIKzLWEkgia_yeBm_VGLgNj5uND9wyuOg") + ) + .build(), AttestationExample( """{"challenge":"TYD4p7LaPJjcQRlvZmXaEryYznCbS8farrjvBTPIaMc","clientExtensions":{},"hashAlgorithm":"SHA-256","origin":"https://demo.yubico.com","type":"webauthn.create"}""", - ByteArray.fromBase64Url("o2NmbXRoZmlkby11MmZnYXR0U3RtdKJjc2lnWEcwRQIhALIMxk1lmndZxLUHPct8ggYZGAXKiYEzsj5SECYGa6WbAiBt4a_4vDP-lYjvm344LxoXfEAyjEiqPIBsYsSuzidPrGN4NWOBWQLBMIICvTCCAaWgAwIBAgIEHo-HNDANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZdWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAwMDBaGA8yMDUwMDkwNDAwMDAwMFowbjELMAkGA1UEBhMCU0UxEjAQBgNVBAoMCVl1YmljbyBBQjEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjEnMCUGA1UEAwweWXViaWNvIFUyRiBFRSBTZXJpYWwgNTEyNzIyNzQwMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEqHn4IzjtFJS6wHBLzH_GY9GycXFZdiQxAcdgURXXwVKeKBwcZzItOEtc1V3T6YGNX9hcIq8ybgxk_CCv4z8jZqNsMGowIgYJKwYBBAGCxAoCBBUxLjMuNi4xLjQuMS40MTQ4Mi4xLjcwEwYLKwYBBAGC5RwCAQEEBAMCBDAwIQYLKwYBBAGC5RwBAQQEEgQQL8BXn4ETR-qxFrtajbkgKjAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4IBAQCGk_9i3w1XedR0jX_I0QInMYqOWA5qOlfBCOlOA8OFaLNmiU_OViS-Sj79fzQRiz2ZN0P3kqGYkWDI_JrgsE49-e4V4-iMBPyCqNy_WBjhCNzCloV3rnn_ZiuUc0497EWXMF1z5uVe4r65zZZ4ygk15TPrY4-OJvq7gXzaRB--mDGDKuX24q2ZL56720xiI4uPjXq0gdbTJjvNv55KV1UDcJiK1YE0QPoDLK22cjyt2PjXuoCfdbQ8_6Clua3RQjLvnZ4UgSY4IzxMpKhzufismOMroZFnYG4VkJ_N20ot_72uRiAkn5pmRqyB5IMtERn-v6pzGogtolp3gn1G0ZAXaGF1dGhEYXRhWMTEbO-CrRtUZHdZHQCLCHWew-bS7LTzlHS_6mlpkl0Dt0EAAAAAAAAAAAAAAAAAAAAAAAAAAABAwNqAJZnNrdJI3M00vcUGRnsJ8jaIdmw6h0vN-otjgHKMcL4ymacqevxbk2Rb6gBAl7Zun9MwzYXBVrs5aZMPq6UBAgMmIAEhWCBJr3rb8dowo8mLlcq6vqIntuJG8KO7C4idTE1NzvUkgyJYIE7fArHgQIuZQt__H-5ujH6ZH515OqgQKSTZD9PfzXpp") + ByteArray.fromBase64Url("o2NmbXRoZmlkby11MmZnYXR0U3RtdKJjc2lnWEcwRQIhALIMxk1lmndZxLUHPct8ggYZGAXKiYEzsj5SECYGa6WbAiBt4a_4vDP-lYjvm344LxoXfEAyjEiqPIBsYsSuzidPrGN4NWOBWQLBMIICvTCCAaWgAwIBAgIEHo-HNDANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZdWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAwMDBaGA8yMDUwMDkwNDAwMDAwMFowbjELMAkGA1UEBhMCU0UxEjAQBgNVBAoMCVl1YmljbyBBQjEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjEnMCUGA1UEAwweWXViaWNvIFUyRiBFRSBTZXJpYWwgNTEyNzIyNzQwMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEqHn4IzjtFJS6wHBLzH_GY9GycXFZdiQxAcdgURXXwVKeKBwcZzItOEtc1V3T6YGNX9hcIq8ybgxk_CCv4z8jZqNsMGowIgYJKwYBBAGCxAoCBBUxLjMuNi4xLjQuMS40MTQ4Mi4xLjcwEwYLKwYBBAGC5RwCAQEEBAMCBDAwIQYLKwYBBAGC5RwBAQQEEgQQL8BXn4ETR-qxFrtajbkgKjAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4IBAQCGk_9i3w1XedR0jX_I0QInMYqOWA5qOlfBCOlOA8OFaLNmiU_OViS-Sj79fzQRiz2ZN0P3kqGYkWDI_JrgsE49-e4V4-iMBPyCqNy_WBjhCNzCloV3rnn_ZiuUc0497EWXMF1z5uVe4r65zZZ4ygk15TPrY4-OJvq7gXzaRB--mDGDKuX24q2ZL56720xiI4uPjXq0gdbTJjvNv55KV1UDcJiK1YE0QPoDLK22cjyt2PjXuoCfdbQ8_6Clua3RQjLvnZ4UgSY4IzxMpKhzufismOMroZFnYG4VkJ_N20ot_72uRiAkn5pmRqyB5IMtERn-v6pzGogtolp3gn1G0ZAXaGF1dGhEYXRhWMTEbO-CrRtUZHdZHQCLCHWew-bS7LTzlHS_6mlpkl0Dt0EAAAAAAAAAAAAAAAAAAAAAAAAAAABAwNqAJZnNrdJI3M00vcUGRnsJ8jaIdmw6h0vN-otjgHKMcL4ymacqevxbk2Rb6gBAl7Zun9MwzYXBVrs5aZMPq6UBAgMmIAEhWCBJr3rb8dowo8mLlcq6vqIntuJG8KO7C4idTE1NzvUkgyJYIE7fArHgQIuZQt__H-5ujH6ZH515OqgQKSTZD9PfzXpp"), ), AssertionExample( - id = ByteArray.fromBase64Url("wNqAJZnNrdJI3M00vcUGRnsJ8jaIdmw6h0vN-otjgHKMcL4ymacqevxbk2Rb6gBAl7Zun9MwzYXBVrs5aZMPqw"), + id = + ByteArray.fromBase64Url("wNqAJZnNrdJI3M00vcUGRnsJ8jaIdmw6h0vN-otjgHKMcL4ymacqevxbk2Rb6gBAl7Zun9MwzYXBVrs5aZMPqw"), clientData = """{"challenge":"uF0u0XJg7NyFuvBVHrtBPKYBC5h-1_P9Dn9lmerQCBQ","clientExtensions":{},"hashAlgorithm":"SHA-256","origin":"https://demo.yubico.com","type":"webauthn.get"}""", - authDataBytes = ByteArray.fromBase64Url("xGzvgq0bVGR3WR0Aiwh1nsPm0uy085R0v-ppaZJdA7cBAAAAAQ"), - sig = ByteArray.fromBase64Url("MEYCIQCVio9swx3DxzBUr4eexfpKP2wmoeEQR0nYp_QxB_rFowIhAOIRFy-7-CP41Q65l5eJIZH49wnj-rrdPklWlBkHcoHG"), - ) + authDataBytes = ByteArray.fromBase64Url( + "xGzvgq0bVGR3WR0Aiwh1nsPm0uy085R0v-ppaZJdA7cBAAAAAQ" + ), + sig = + ByteArray.fromBase64Url("MEYCIQCVio9swx3DxzBUr4eexfpKP2wmoeEQR0nYp_QxB_rFowIhAOIRFy-7-CP41Q65l5eJIZH49wnj-rrdPklWlBkHcoHG"), + ), ) val YubiKey5Nano = Example( RelyingPartyIdentity.builder().id("example.com").name("Example RP").build(), - UserIdentity.builder().name("test@example.org").displayName("A. User").id(ByteArray.fromBase64Url("dXNlcl9pZA==")).build(), + UserIdentity + .builder() + .name("test@example.org") + .displayName("A. User") + .id(ByteArray.fromBase64Url("dXNlcl9pZA==")) + .build(), AttestationExample( """{"type": "webauthn.create", "clientExtensions": {}, "challenge": "Y2hhbGxlbmdl", "origin": "https://example.com"}""", - ByteArray.fromBase64Url("o2NmbXRmcGFja2VkZ2F0dFN0bXSjY2FsZyZjc2lnWEgwRgIhAMw3heLnLh7oOq4gwxQRviDPT0_VDxys8Kq2MFOfTZBzAiEAtL3D6ZqtiupoAMqntqi07OrEl5RJGJkoZ7bLwepVJQBjeDVjgVkCwTCCAr0wggGloAMCAQICBBisRsAwDQYJKoZIhvcNAQELBQAwLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290IENBIFNlcmlhbCA0NTcyMDA2MzEwIBcNMTQwODAxMDAwMDAwWhgPMjA1MDA5MDQwMDAwMDBaMG4xCzAJBgNVBAYTAlNFMRIwEAYDVQQKDAlZdWJpY28gQUIxIjAgBgNVBAsMGUF1dGhlbnRpY2F0b3IgQXR0ZXN0YXRpb24xJzAlBgNVBAMMHll1YmljbyBVMkYgRUUgU2VyaWFsIDQxMzk0MzQ4ODBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABHnqOyx8SXAQYiMM0j_rYOUpMXHUg_EAvoWdaw-DlwMBtUbN1G7PyuPj8w-B6e1ivSaNTB69N7O8vpKowq7rTjqjbDBqMCIGCSsGAQQBgsQKAgQVMS4zLjYuMS40LjEuNDE0ODIuMS43MBMGCysGAQQBguUcAgEBBAQDAgUgMCEGCysGAQQBguUcAQEEBBIEEMtpSB6P90A5k-wKJymhVKgwDAYDVR0TAQH_BAIwADANBgkqhkiG9w0BAQsFAAOCAQEAl50Dl9hg-C7hXTEceW66-yL6p-CE2bq0xhu7V_PmtMGKSDe4XDxO2-SDQ_TWpdmxztqK4f7UkSkhcwWOXuHL3WvawHVXxqDo02gluhWef7WtjNr4BIaM-Q6PH4rqF8AWtVwqetSXyJT7cddT15uaSEtsN21yO5mNLh1DBr8QM7Wu-Myly7JWi2kkIm0io1irfYfkrF8uCRqnFXnzpWkJSX1y9U4GusHDtEE7ul6vlMO2TzT566Qay2rig3dtNkZTeEj-6IS93fWxuleYVM_9zrrDRAWVJ-Vt1Zj49WZxWr5DAd0ZETDmufDGQDkSU-IpgD867ydL7b_eP8u9QurWeWhhdXRoRGF0YVjEo3mm9u6vuaVeN4wRgDTidR5oL6ufLTCrE9ISVYbOGUdFAAAAyMtpSB6P90A5k-wKJymhVKgAQApDelLpYd9AP-NbX7v8lJelMv5xVvJq1u4va8qaLTf2e4Tf7QL7F4nkZZnfTVBv74xF0i8794sPbpK--e0N8-SlAQIDJiABIVggXaCve37FWbdyNEXiSmuDUdsc0K-UDHnYEQ-Sc3PHxcAiWCD7VMEBw6F_IOsfg7DISuN8aT70W14W1NQCX0xjQSUnsw==") + ByteArray.fromBase64Url("o2NmbXRmcGFja2VkZ2F0dFN0bXSjY2FsZyZjc2lnWEgwRgIhAMw3heLnLh7oOq4gwxQRviDPT0_VDxys8Kq2MFOfTZBzAiEAtL3D6ZqtiupoAMqntqi07OrEl5RJGJkoZ7bLwepVJQBjeDVjgVkCwTCCAr0wggGloAMCAQICBBisRsAwDQYJKoZIhvcNAQELBQAwLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290IENBIFNlcmlhbCA0NTcyMDA2MzEwIBcNMTQwODAxMDAwMDAwWhgPMjA1MDA5MDQwMDAwMDBaMG4xCzAJBgNVBAYTAlNFMRIwEAYDVQQKDAlZdWJpY28gQUIxIjAgBgNVBAsMGUF1dGhlbnRpY2F0b3IgQXR0ZXN0YXRpb24xJzAlBgNVBAMMHll1YmljbyBVMkYgRUUgU2VyaWFsIDQxMzk0MzQ4ODBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABHnqOyx8SXAQYiMM0j_rYOUpMXHUg_EAvoWdaw-DlwMBtUbN1G7PyuPj8w-B6e1ivSaNTB69N7O8vpKowq7rTjqjbDBqMCIGCSsGAQQBgsQKAgQVMS4zLjYuMS40LjEuNDE0ODIuMS43MBMGCysGAQQBguUcAgEBBAQDAgUgMCEGCysGAQQBguUcAQEEBBIEEMtpSB6P90A5k-wKJymhVKgwDAYDVR0TAQH_BAIwADANBgkqhkiG9w0BAQsFAAOCAQEAl50Dl9hg-C7hXTEceW66-yL6p-CE2bq0xhu7V_PmtMGKSDe4XDxO2-SDQ_TWpdmxztqK4f7UkSkhcwWOXuHL3WvawHVXxqDo02gluhWef7WtjNr4BIaM-Q6PH4rqF8AWtVwqetSXyJT7cddT15uaSEtsN21yO5mNLh1DBr8QM7Wu-Myly7JWi2kkIm0io1irfYfkrF8uCRqnFXnzpWkJSX1y9U4GusHDtEE7ul6vlMO2TzT566Qay2rig3dtNkZTeEj-6IS93fWxuleYVM_9zrrDRAWVJ-Vt1Zj49WZxWr5DAd0ZETDmufDGQDkSU-IpgD867ydL7b_eP8u9QurWeWhhdXRoRGF0YVjEo3mm9u6vuaVeN4wRgDTidR5oL6ufLTCrE9ISVYbOGUdFAAAAyMtpSB6P90A5k-wKJymhVKgAQApDelLpYd9AP-NbX7v8lJelMv5xVvJq1u4va8qaLTf2e4Tf7QL7F4nkZZnfTVBv74xF0i8794sPbpK--e0N8-SlAQIDJiABIVggXaCve37FWbdyNEXiSmuDUdsc0K-UDHnYEQ-Sc3PHxcAiWCD7VMEBw6F_IOsfg7DISuN8aT70W14W1NQCX0xjQSUnsw=="), ), AssertionExample( - id = ByteArray.fromBase64Url("CkN6Uulh30A_41tfu_yUl6Uy_nFW8mrW7i9rypotN_Z7hN_tAvsXieRlmd9NUG_vjEXSLzv3iw9ukr757Q3z5A=="), + id = + ByteArray.fromBase64Url("CkN6Uulh30A_41tfu_yUl6Uy_nFW8mrW7i9rypotN_Z7hN_tAvsXieRlmd9NUG_vjEXSLzv3iw9ukr757Q3z5A=="), clientData = """{"type": "webauthn.get", "clientExtensions": {}, "challenge": "Q0hBTExFTkdF", "origin": "https://example.com"}""", - authDataBytes = ByteArray.fromBase64Url("o3mm9u6vuaVeN4wRgDTidR5oL6ufLTCrE9ISVYbOGUcFAAAAyQ=="), - sig = ByteArray.fromBase64Url("MEYCIQCUeExQH6ZbZxoyiYEqFdmMyIeu-klCkyREiB1ekfBItgIhAKcsV2cK-PXubj96AYk5DWU_qE-M6ZmH8AQBYW9RF56P"), - ) + authDataBytes = ByteArray.fromBase64Url( + "o3mm9u6vuaVeN4wRgDTidR5oL6ufLTCrE9ISVYbOGUcFAAAAyQ==" + ), + sig = + ByteArray.fromBase64Url("MEYCIQCUeExQH6ZbZxoyiYEqFdmMyIeu-klCkyREiB1ekfBItgIhAKcsV2cK-PXubj96AYk5DWU_qE-M6ZmH8AQBYW9RF56P"), + ), ) val YubiKey5Ci = Example( RelyingPartyIdentity.builder().id("example.com").name("Example RP").build(), - UserIdentity.builder().name("test@example.org").displayName("A. User").id(ByteArray.fromBase64Url("dXNlcl9pZA==")).build(), + UserIdentity + .builder() + .name("test@example.org") + .displayName("A. User") + .id(ByteArray.fromBase64Url("dXNlcl9pZA==")) + .build(), AttestationExample( """{"type": "webauthn.create", "clientExtensions": {}, "challenge": "Y2hhbGxlbmdl", "origin": "https://example.com"}""", - ByteArray.fromBase64Url("o2NmbXRmcGFja2VkZ2F0dFN0bXSjY2FsZyZjc2lnWEYwRAIgXOZEuIaBrKT5VYJu9_D410HgJRm1SenwlKiXtcQxe0ICIG1_ycPCKHPjEsgRFVr4WdK5IY8K7aCyAc03c1-wnBJCY3g1Y4FZAsEwggK9MIIBpaADAgECAgQr8Xx4MA0GCSqGSIb3DQEBCwUAMC4xLDAqBgNVBAMTI1l1YmljbyBVMkYgUm9vdCBDQSBTZXJpYWwgNDU3MjAwNjMxMCAXDTE0MDgwMTAwMDAwMFoYDzIwNTAwOTA0MDAwMDAwWjBuMQswCQYDVQQGEwJTRTESMBAGA1UECgwJWXViaWNvIEFCMSIwIAYDVQQLDBlBdXRoZW50aWNhdG9yIEF0dGVzdGF0aW9uMScwJQYDVQQDDB5ZdWJpY28gVTJGIEVFIFNlcmlhbCA3MzcyNDYzMjgwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAR0wseEI8hxLptI8llYZvxwQK5M3wfXd9WFrwSTme36kjy-tJ-XFvn1WnhsNCUfyPNePehbVnBQOMcLoScZYHmLo2wwajAiBgkrBgEEAYLECgIEFTEuMy42LjEuNC4xLjQxNDgyLjEuNzATBgsrBgEEAYLlHAIBAQQEAwICJDAhBgsrBgEEAYLlHAEBBAQSBBDF71X_rZpLn7WAreuv4CbQMAwGA1UdEwEB_wQCMAAwDQYJKoZIhvcNAQELBQADggEBAItuk3adeE1u6dkA0nECf8J35Lgm5mw5udSIucstLQU9ZrTVNjwXugnxsT5oVriRN7o1BB-Lz7KJmtDw34kvh_uA11A9Ksf6veIV3hK-ugN7WNok7gn0t6IWOZF1xVr7lyo0XgbV88Kh-_D1biUqc5u49qSvTH-Jx1WrUxeFh1S1CTpmvmYGdzgWE32qLsNeoscPkbtkVSYbB8hwPb7SbV_WbBBLzJEPn79oMJ_e-63B12iLdyu2K_PKuibBsqSVHioe6cnvksZktkDykn-ZedRDpNOyBGo-89eBA9tLIYx_bP8Mg9tCoIP8GZzh2P2joujOF4F0O1xkICNI9MB3-6JoYXV0aERhdGFYxKN5pvbur7mlXjeMEYA04nUeaC-rny0wqxPSElWGzhlHQQAAAATF71X_rZpLn7WAreuv4CbQAEDDAvEvv-vY_dFxV_gwT7mhKUN9M6PatW8FqDSEjXAaJL4EjL5exyo-FIaoqgH4lfmw-19_6ao6j9zPlFGHBmUOpQECAyYgASFYILUgImoYph7H0FqX_aKS3A4Ph1Aki_Edg9YB6oxw7nrIIlgghBKeVu0Z4cV6-Cya1H2ZTeeWdisBlK6QWDM89ne6794=") + ByteArray.fromBase64Url("o2NmbXRmcGFja2VkZ2F0dFN0bXSjY2FsZyZjc2lnWEYwRAIgXOZEuIaBrKT5VYJu9_D410HgJRm1SenwlKiXtcQxe0ICIG1_ycPCKHPjEsgRFVr4WdK5IY8K7aCyAc03c1-wnBJCY3g1Y4FZAsEwggK9MIIBpaADAgECAgQr8Xx4MA0GCSqGSIb3DQEBCwUAMC4xLDAqBgNVBAMTI1l1YmljbyBVMkYgUm9vdCBDQSBTZXJpYWwgNDU3MjAwNjMxMCAXDTE0MDgwMTAwMDAwMFoYDzIwNTAwOTA0MDAwMDAwWjBuMQswCQYDVQQGEwJTRTESMBAGA1UECgwJWXViaWNvIEFCMSIwIAYDVQQLDBlBdXRoZW50aWNhdG9yIEF0dGVzdGF0aW9uMScwJQYDVQQDDB5ZdWJpY28gVTJGIEVFIFNlcmlhbCA3MzcyNDYzMjgwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAR0wseEI8hxLptI8llYZvxwQK5M3wfXd9WFrwSTme36kjy-tJ-XFvn1WnhsNCUfyPNePehbVnBQOMcLoScZYHmLo2wwajAiBgkrBgEEAYLECgIEFTEuMy42LjEuNC4xLjQxNDgyLjEuNzATBgsrBgEEAYLlHAIBAQQEAwICJDAhBgsrBgEEAYLlHAEBBAQSBBDF71X_rZpLn7WAreuv4CbQMAwGA1UdEwEB_wQCMAAwDQYJKoZIhvcNAQELBQADggEBAItuk3adeE1u6dkA0nECf8J35Lgm5mw5udSIucstLQU9ZrTVNjwXugnxsT5oVriRN7o1BB-Lz7KJmtDw34kvh_uA11A9Ksf6veIV3hK-ugN7WNok7gn0t6IWOZF1xVr7lyo0XgbV88Kh-_D1biUqc5u49qSvTH-Jx1WrUxeFh1S1CTpmvmYGdzgWE32qLsNeoscPkbtkVSYbB8hwPb7SbV_WbBBLzJEPn79oMJ_e-63B12iLdyu2K_PKuibBsqSVHioe6cnvksZktkDykn-ZedRDpNOyBGo-89eBA9tLIYx_bP8Mg9tCoIP8GZzh2P2joujOF4F0O1xkICNI9MB3-6JoYXV0aERhdGFYxKN5pvbur7mlXjeMEYA04nUeaC-rny0wqxPSElWGzhlHQQAAAATF71X_rZpLn7WAreuv4CbQAEDDAvEvv-vY_dFxV_gwT7mhKUN9M6PatW8FqDSEjXAaJL4EjL5exyo-FIaoqgH4lfmw-19_6ao6j9zPlFGHBmUOpQECAyYgASFYILUgImoYph7H0FqX_aKS3A4Ph1Aki_Edg9YB6oxw7nrIIlgghBKeVu0Z4cV6-Cya1H2ZTeeWdisBlK6QWDM89ne6794="), ), AssertionExample( - id = ByteArray.fromBase64Url("wwLxL7_r2P3RcVf4ME-5oSlDfTOj2rVvBag0hI1wGiS-BIy-XscqPhSGqKoB-JX5sPtff-mqOo_cz5RRhwZlDg=="), + id = + ByteArray.fromBase64Url("wwLxL7_r2P3RcVf4ME-5oSlDfTOj2rVvBag0hI1wGiS-BIy-XscqPhSGqKoB-JX5sPtff-mqOo_cz5RRhwZlDg=="), clientData = """{"type": "webauthn.get", "clientExtensions": {}, "challenge": "Q0hBTExFTkdF", "origin": "https://example.com"}""", - authDataBytes = ByteArray.fromBase64Url("o3mm9u6vuaVeN4wRgDTidR5oL6ufLTCrE9ISVYbOGUcBAAAABw=="), - sig = ByteArray.fromBase64Url("MEQCIHqWh09siRtXwUCVOnTrWUTfJfe9zv0_-WYd376qUcBqAiBMdsCPp-LpUEhgSbOz8y6hS1YTKFgpN-nIrpYDTxQhiA=="), - ) + authDataBytes = ByteArray.fromBase64Url( + "o3mm9u6vuaVeN4wRgDTidR5oL6ufLTCrE9ISVYbOGUcBAAAABw==" + ), + sig = + ByteArray.fromBase64Url("MEQCIHqWh09siRtXwUCVOnTrWUTfJfe9zv0_-WYd376qUcBqAiBMdsCPp-LpUEhgSbOz8y6hS1YTKFgpN-nIrpYDTxQhiA=="), + ), ) val SecurityKey = Example( RelyingPartyIdentity.builder().id("example.com").name("Example RP").build(), - UserIdentity.builder().name("test@example.org").displayName("A. User").id(ByteArray.fromBase64Url("dXNlcl9pZA==")).build(), + UserIdentity + .builder() + .name("test@example.org") + .displayName("A. User") + .id(ByteArray.fromBase64Url("dXNlcl9pZA==")) + .build(), AttestationExample( """{"type": "webauthn.create", "clientExtensions": {}, "challenge": "Y2hhbGxlbmdl", "origin": "https://example.com"}""", - ByteArray.fromBase64Url("o2NmbXRoZmlkby11MmZnYXR0U3RtdKJjc2lnWEcwRQIhAM_R8LLPIBxj07Cimg1QVoFD2Y3xqQvbEYEdkbJLsQgiAiAEVIoe5lvTKHK9vCJBHJXS1uWBxFNEFv7im0cs2CjhcWN4NWOBWQIgMIICHDCCAQagAwIBAgIEOGbfdTALBgkqhkiG9w0BAQswLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290IENBIFNlcmlhbCA0NTcyMDA2MzEwIBcNMTQwODAxMDAwMDAwWhgPMjA1MDA5MDQwMDAwMDBaMCsxKTAnBgNVBAMMIFl1YmljbyBVMkYgRUUgU2VyaWFsIDEzODMxMTY3ODYxMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEN438dAxzm5RyTtPVI7m4doEGVsE96G-vrs842Q9V4sgKG_4LMNxTs13n0EW5bcuPK_lPqOC5AxY8f27cLkh7caMSMBAwDgYKKwYBBAGCxAoBAQQAMAsGCSqGSIb3DQEBCwOCAQECGkdkygCJz5KtuH-oSFOOcsw-_bs0eSlDBHuCFqk5uvTBE1YqNFthR1l5aXlHvOZxqmp8Bnlu1OuxuP1gJxm3Hes89kLpjbHZZm_wHm23T0WveWfARtbm_0tOCaMUGDS2mvFkZczezzoKgJwKpJp7GUP1vU49rjvcz95qcTpJJp6s-z-c7eC6eca7-6deYRjiDw-VfqYe7VJogibKtC33kQN-l-2l4t9gKdK7f8Mn50Xn-fWGK-0psGjLlyo2yGUi3rLHGWUzM13frri2-g21AmrKhFQZBhqk0XwHDpj6L9Zx1KzQwpDkdKG0eD7CRuD4mpiHwKTXqFxmKRm6JOp7nGhhdXRoRGF0YVjEo3mm9u6vuaVeN4wRgDTidR5oL6ufLTCrE9ISVYbOGUdBAAAAAAAAAAAAAAAAAAAAAAAAAAAAQCdfKS1ueJ9oKJVudbjt1UiNbDssecI5S-KjmJiG0i5OGGd4oF9xDvrXC4wfLalyG8CZyOC0yWGRdxOHY2zlreylAQIDJiABIVggZ2eg0SmeEp6vayyOWFQIsY8WaYPde8QgyNVLRcHVWmoiWCBxXpYVrCowr7PGNQlz7iFTUWQ1z8R1cPxRfHlm6DvZRw==") + ByteArray.fromBase64Url("o2NmbXRoZmlkby11MmZnYXR0U3RtdKJjc2lnWEcwRQIhAM_R8LLPIBxj07Cimg1QVoFD2Y3xqQvbEYEdkbJLsQgiAiAEVIoe5lvTKHK9vCJBHJXS1uWBxFNEFv7im0cs2CjhcWN4NWOBWQIgMIICHDCCAQagAwIBAgIEOGbfdTALBgkqhkiG9w0BAQswLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290IENBIFNlcmlhbCA0NTcyMDA2MzEwIBcNMTQwODAxMDAwMDAwWhgPMjA1MDA5MDQwMDAwMDBaMCsxKTAnBgNVBAMMIFl1YmljbyBVMkYgRUUgU2VyaWFsIDEzODMxMTY3ODYxMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEN438dAxzm5RyTtPVI7m4doEGVsE96G-vrs842Q9V4sgKG_4LMNxTs13n0EW5bcuPK_lPqOC5AxY8f27cLkh7caMSMBAwDgYKKwYBBAGCxAoBAQQAMAsGCSqGSIb3DQEBCwOCAQECGkdkygCJz5KtuH-oSFOOcsw-_bs0eSlDBHuCFqk5uvTBE1YqNFthR1l5aXlHvOZxqmp8Bnlu1OuxuP1gJxm3Hes89kLpjbHZZm_wHm23T0WveWfARtbm_0tOCaMUGDS2mvFkZczezzoKgJwKpJp7GUP1vU49rjvcz95qcTpJJp6s-z-c7eC6eca7-6deYRjiDw-VfqYe7VJogibKtC33kQN-l-2l4t9gKdK7f8Mn50Xn-fWGK-0psGjLlyo2yGUi3rLHGWUzM13frri2-g21AmrKhFQZBhqk0XwHDpj6L9Zx1KzQwpDkdKG0eD7CRuD4mpiHwKTXqFxmKRm6JOp7nGhhdXRoRGF0YVjEo3mm9u6vuaVeN4wRgDTidR5oL6ufLTCrE9ISVYbOGUdBAAAAAAAAAAAAAAAAAAAAAAAAAAAAQCdfKS1ueJ9oKJVudbjt1UiNbDssecI5S-KjmJiG0i5OGGd4oF9xDvrXC4wfLalyG8CZyOC0yWGRdxOHY2zlreylAQIDJiABIVggZ2eg0SmeEp6vayyOWFQIsY8WaYPde8QgyNVLRcHVWmoiWCBxXpYVrCowr7PGNQlz7iFTUWQ1z8R1cPxRfHlm6DvZRw=="), ), AssertionExample( - id = ByteArray.fromBase64Url("J18pLW54n2golW51uO3VSI1sOyx5wjlL4qOYmIbSLk4YZ3igX3EO-tcLjB8tqXIbwJnI4LTJYZF3E4djbOWt7A=="), + id = + ByteArray.fromBase64Url("J18pLW54n2golW51uO3VSI1sOyx5wjlL4qOYmIbSLk4YZ3igX3EO-tcLjB8tqXIbwJnI4LTJYZF3E4djbOWt7A=="), clientData = """{"type": "webauthn.get", "clientExtensions": {}, "challenge": "Q0hBTExFTkdF", "origin": "https://example.com"}""", - authDataBytes = ByteArray.fromBase64Url("o3mm9u6vuaVeN4wRgDTidR5oL6ufLTCrE9ISVYbOGUcBAAAAtw=="), - sig = ByteArray.fromBase64Url("MEUCIHgLEzmn8hKOXC0qBXDFBZ7a2GLrwho8uqyd1ZqwV9YCAiEA-3Y8g4ifwTxT1ROtA4uBmVzzfzlh9o0ijY9eEhGJEkg="), - ) + authDataBytes = ByteArray.fromBase64Url( + "o3mm9u6vuaVeN4wRgDTidR5oL6ufLTCrE9ISVYbOGUcBAAAAtw==" + ), + sig = + ByteArray.fromBase64Url("MEUCIHgLEzmn8hKOXC0qBXDFBZ7a2GLrwho8uqyd1ZqwV9YCAiEA-3Y8g4ifwTxT1ROtA4uBmVzzfzlh9o0ijY9eEhGJEkg="), + ), ) val SecurityKey2 = Example( RelyingPartyIdentity.builder().id("example.com").name("Example RP").build(), - UserIdentity.builder().name("test@example.org").displayName("A. User").id(ByteArray.fromBase64Url("dXNlcl9pZA==")).build(), + UserIdentity + .builder() + .name("test@example.org") + .displayName("A. User") + .id(ByteArray.fromBase64Url("dXNlcl9pZA==")) + .build(), AttestationExample( """{"type": "webauthn.create", "clientExtensions": {}, "challenge": "Y2hhbGxlbmdl", "origin": "https://example.com"}""", - ByteArray.fromBase64Url("o2NmbXRmcGFja2VkZ2F0dFN0bXSjY2FsZyZjc2lnWEYwRAIgMPJLGsBqS-rEdPOtwv50McRd8TLeMUBdqCdN9BQlqjoCICh5colw68TfL2QTa9OXPkpobZrePGqlfOzv4bzY9fffY3g1Y4FZAsIwggK-MIIBpqADAgECAgR0hv3CMA0GCSqGSIb3DQEBCwUAMC4xLDAqBgNVBAMTI1l1YmljbyBVMkYgUm9vdCBDQSBTZXJpYWwgNDU3MjAwNjMxMCAXDTE0MDgwMTAwMDAwMFoYDzIwNTAwOTA0MDAwMDAwWjBvMQswCQYDVQQGEwJTRTESMBAGA1UECgwJWXViaWNvIEFCMSIwIAYDVQQLDBlBdXRoZW50aWNhdG9yIEF0dGVzdGF0aW9uMSgwJgYDVQQDDB9ZdWJpY28gVTJGIEVFIFNlcmlhbCAxOTU1MDAzODQyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAElV3zrfckfTF17_2cxPMaToeOuuGBCVZhUPs4iy5fZSe_V0CapYGlDQrFLxhEXAoTVIoTU8ik5ZpwTlI7wE3r7aNsMGowIgYJKwYBBAGCxAoCBBUxLjMuNi4xLjQuMS40MTQ4Mi4xLjEwEwYLKwYBBAGC5RwCAQEEBAMCBSAwIQYLKwYBBAGC5RwBAQQEEgQQ-KAR84wKTRWABhcRH57cfTAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4IBAQAxXEiA5ppSfjhmib1p_Qqob0nrnk6FRUFVb6rQCzoAih3cAflsdvZoNhqR4jLIEKecYwdMm256RusdtdhcREifhop2Q9IqXIYuwD8D5YSL44B9es1V-OGuHuITrHOrSyDj-9UmjLB7h4AnHR9L4OXdrHNNOliXvU1zun81fqIIyZ2KTSkC5gl6AFxNyQTcChgSDgr30Az8lpoohuWxsWHz7cvGd6Z41_tTA5zNoYa-NLpTMZUjQ51_2Upw8jBiG5PEzkJo0xdNlDvGrj_JN8LeQ9a0TiEVPfhQkl-VkGIuvEbg6xjGQfD-fm8qCamykHcZ9i5hNaGQMqITwJi3KDzuaGF1dGhEYXRhWMSjeab27q-5pV43jBGANOJ1Hmgvq58tMKsT0hJVhs4ZR0EAAAAA-KAR84wKTRWABhcRH57cfQBAc17o2YwQc1hkrTX_Plsl34A6_rK5Fa6pJGIgkkTgVx3lEF_fnOa-M13COp5hgPrVVuDIGv5HI9gJH9JbOoxJS6UBAgMmIAEhWCDNva3Ohd7wYRZlfmu6V0J8Iy8sdGOLTG_dAlDxvRdSjyJYILal-lroy3ltDP4McgzBN5hKd9OSVn6dMgRBVjDWBtsN") + ByteArray.fromBase64Url("o2NmbXRmcGFja2VkZ2F0dFN0bXSjY2FsZyZjc2lnWEYwRAIgMPJLGsBqS-rEdPOtwv50McRd8TLeMUBdqCdN9BQlqjoCICh5colw68TfL2QTa9OXPkpobZrePGqlfOzv4bzY9fffY3g1Y4FZAsIwggK-MIIBpqADAgECAgR0hv3CMA0GCSqGSIb3DQEBCwUAMC4xLDAqBgNVBAMTI1l1YmljbyBVMkYgUm9vdCBDQSBTZXJpYWwgNDU3MjAwNjMxMCAXDTE0MDgwMTAwMDAwMFoYDzIwNTAwOTA0MDAwMDAwWjBvMQswCQYDVQQGEwJTRTESMBAGA1UECgwJWXViaWNvIEFCMSIwIAYDVQQLDBlBdXRoZW50aWNhdG9yIEF0dGVzdGF0aW9uMSgwJgYDVQQDDB9ZdWJpY28gVTJGIEVFIFNlcmlhbCAxOTU1MDAzODQyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAElV3zrfckfTF17_2cxPMaToeOuuGBCVZhUPs4iy5fZSe_V0CapYGlDQrFLxhEXAoTVIoTU8ik5ZpwTlI7wE3r7aNsMGowIgYJKwYBBAGCxAoCBBUxLjMuNi4xLjQuMS40MTQ4Mi4xLjEwEwYLKwYBBAGC5RwCAQEEBAMCBSAwIQYLKwYBBAGC5RwBAQQEEgQQ-KAR84wKTRWABhcRH57cfTAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4IBAQAxXEiA5ppSfjhmib1p_Qqob0nrnk6FRUFVb6rQCzoAih3cAflsdvZoNhqR4jLIEKecYwdMm256RusdtdhcREifhop2Q9IqXIYuwD8D5YSL44B9es1V-OGuHuITrHOrSyDj-9UmjLB7h4AnHR9L4OXdrHNNOliXvU1zun81fqIIyZ2KTSkC5gl6AFxNyQTcChgSDgr30Az8lpoohuWxsWHz7cvGd6Z41_tTA5zNoYa-NLpTMZUjQ51_2Upw8jBiG5PEzkJo0xdNlDvGrj_JN8LeQ9a0TiEVPfhQkl-VkGIuvEbg6xjGQfD-fm8qCamykHcZ9i5hNaGQMqITwJi3KDzuaGF1dGhEYXRhWMSjeab27q-5pV43jBGANOJ1Hmgvq58tMKsT0hJVhs4ZR0EAAAAA-KAR84wKTRWABhcRH57cfQBAc17o2YwQc1hkrTX_Plsl34A6_rK5Fa6pJGIgkkTgVx3lEF_fnOa-M13COp5hgPrVVuDIGv5HI9gJH9JbOoxJS6UBAgMmIAEhWCDNva3Ohd7wYRZlfmu6V0J8Iy8sdGOLTG_dAlDxvRdSjyJYILal-lroy3ltDP4McgzBN5hKd9OSVn6dMgRBVjDWBtsN"), ), AssertionExample( - id = ByteArray.fromBase64Url("c17o2YwQc1hkrTX_Plsl34A6_rK5Fa6pJGIgkkTgVx3lEF_fnOa-M13COp5hgPrVVuDIGv5HI9gJH9JbOoxJSw=="), + id = + ByteArray.fromBase64Url("c17o2YwQc1hkrTX_Plsl34A6_rK5Fa6pJGIgkkTgVx3lEF_fnOa-M13COp5hgPrVVuDIGv5HI9gJH9JbOoxJSw=="), clientData = """{"type": "webauthn.get", "clientExtensions": {}, "challenge": "Q0hBTExFTkdF", "origin": "https://example.com"}""", - authDataBytes = ByteArray.fromBase64Url("o3mm9u6vuaVeN4wRgDTidR5oL6ufLTCrE9ISVYbOGUcBAAAAAQ=="), - sig = ByteArray.fromBase64Url("MEUCIQC68JtgAd_DEc6UZYjn3eskqVGpIu64yQlXKx25HDXniwIgDQH8uK-md90SHKWbjj8qvqmgdmc4M7ZanCFLmQZRTCI="), - ) + authDataBytes = ByteArray.fromBase64Url( + "o3mm9u6vuaVeN4wRgDTidR5oL6ufLTCrE9ISVYbOGUcBAAAAAQ==" + ), + sig = + ByteArray.fromBase64Url("MEUCIQC68JtgAd_DEc6UZYjn3eskqVGpIu64yQlXKx25HDXniwIgDQH8uK-md90SHKWbjj8qvqmgdmc4M7ZanCFLmQZRTCI="), + ), ) val SecurityKeyNfc = Example( RelyingPartyIdentity.builder().id("example.com").name("Example RP").build(), - UserIdentity.builder().name("test@example.org").displayName("A. User").id(ByteArray.fromBase64Url("dXNlcl9pZA==")).build(), + UserIdentity + .builder() + .name("test@example.org") + .displayName("A. User") + .id(ByteArray.fromBase64Url("dXNlcl9pZA==")) + .build(), AttestationExample( """{"type": "webauthn.create", "clientExtensions": {}, "challenge": "Y2hhbGxlbmdl", "origin": "https://example.com"}""", - ByteArray.fromBase64Url("o2NmbXRoZmlkby11MmZnYXR0U3RtdKJjc2lnWEcwRQIhAJKRPuYlfW8dZZlsJrJiwA-BvAyOvIe1TScv5qlek1SQAiAnglgs-nRjA7kpc61PewQ4VULjdlzLmReI7-MJT1TLrGN4NWOBWQLBMIICvTCCAaWgAwIBAgIEMAIspTANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZdWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAwMDBaGA8yMDUwMDkwNDAwMDAwMFowbjELMAkGA1UEBhMCU0UxEjAQBgNVBAoMCVl1YmljbyBBQjEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjEnMCUGA1UEAwweWXViaWNvIFUyRiBFRSBTZXJpYWwgODA1NDQ4ODY5MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE-66HSEytO3plXno3zPhH1k-zFwWxESIdrTbQp4HSEuzFum1Mwpy8itoOosBQksnIrefLHkTRNUtV8jIrFKAvbaNsMGowIgYJKwYBBAGCxAoCBBUxLjMuNi4xLjQuMS40MTQ4Mi4xLjEwEwYLKwYBBAGC5RwCAQEEBAMCBDAwIQYLKwYBBAGC5RwBAQQEEgQQbUS6m_bsLkm5MAyP6SDLczAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4IBAQBlZXnJy-X3fJfNdlIdIQlFpO5-A5uM41jJ2XgdRag_8rSxXCz98t_jyoWth5FQF9As96Ags3p-Lyaqb1bpEc9RfmkxiiqwDzDI56Sj4HKlANF2tddm-ew29H9yaNbpU5y6aleCeH2rR4t1cFgcBRAV84IndIH0cYASRnyrFbHjI80vlPNR0z4j-_W9vYEWBpLeS_wrdKPVW7C7wyuc4bobauCyhElBPZUwblR_Ll0iovmfazD17VLCBMA4p_SVVTwSXpKyZjMiCotj8mDhQ1ymhvCepkK82EwnrBMJIzCi_joxAXqxLPMs6yJrz_hFUkZaloa1ZS6f7aGAmAKhRNO2aGF1dGhEYXRhWMSjeab27q-5pV43jBGANOJ1Hmgvq58tMKsT0hJVhs4ZR0EAAAAAAAAAAAAAAAAAAAAAAAAAAABAJT086Ym5LhLsK6MRwYRSdjVn9jVYVtwiGwgq_bDPpVuI3aaOW7UQfqGWdos-kVwHnQccbDRnQDvQmCDqy6QdSaUBAgMmIAEhWCCRGd2Bo0vIj-suQxM-cOCXovv1Ag6azqHn8PE31Fcu4iJYIOiLha_PR9JwOhCw4SC2Xq7cOackGAMsq4UUJ_IRCCcq") + ByteArray.fromBase64Url("o2NmbXRoZmlkby11MmZnYXR0U3RtdKJjc2lnWEcwRQIhAJKRPuYlfW8dZZlsJrJiwA-BvAyOvIe1TScv5qlek1SQAiAnglgs-nRjA7kpc61PewQ4VULjdlzLmReI7-MJT1TLrGN4NWOBWQLBMIICvTCCAaWgAwIBAgIEMAIspTANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZdWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAwMDBaGA8yMDUwMDkwNDAwMDAwMFowbjELMAkGA1UEBhMCU0UxEjAQBgNVBAoMCVl1YmljbyBBQjEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjEnMCUGA1UEAwweWXViaWNvIFUyRiBFRSBTZXJpYWwgODA1NDQ4ODY5MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE-66HSEytO3plXno3zPhH1k-zFwWxESIdrTbQp4HSEuzFum1Mwpy8itoOosBQksnIrefLHkTRNUtV8jIrFKAvbaNsMGowIgYJKwYBBAGCxAoCBBUxLjMuNi4xLjQuMS40MTQ4Mi4xLjEwEwYLKwYBBAGC5RwCAQEEBAMCBDAwIQYLKwYBBAGC5RwBAQQEEgQQbUS6m_bsLkm5MAyP6SDLczAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4IBAQBlZXnJy-X3fJfNdlIdIQlFpO5-A5uM41jJ2XgdRag_8rSxXCz98t_jyoWth5FQF9As96Ags3p-Lyaqb1bpEc9RfmkxiiqwDzDI56Sj4HKlANF2tddm-ew29H9yaNbpU5y6aleCeH2rR4t1cFgcBRAV84IndIH0cYASRnyrFbHjI80vlPNR0z4j-_W9vYEWBpLeS_wrdKPVW7C7wyuc4bobauCyhElBPZUwblR_Ll0iovmfazD17VLCBMA4p_SVVTwSXpKyZjMiCotj8mDhQ1ymhvCepkK82EwnrBMJIzCi_joxAXqxLPMs6yJrz_hFUkZaloa1ZS6f7aGAmAKhRNO2aGF1dGhEYXRhWMSjeab27q-5pV43jBGANOJ1Hmgvq58tMKsT0hJVhs4ZR0EAAAAAAAAAAAAAAAAAAAAAAAAAAABAJT086Ym5LhLsK6MRwYRSdjVn9jVYVtwiGwgq_bDPpVuI3aaOW7UQfqGWdos-kVwHnQccbDRnQDvQmCDqy6QdSaUBAgMmIAEhWCCRGd2Bo0vIj-suQxM-cOCXovv1Ag6azqHn8PE31Fcu4iJYIOiLha_PR9JwOhCw4SC2Xq7cOackGAMsq4UUJ_IRCCcq"), ), AssertionExample( - id = ByteArray.fromBase64Url("JT086Ym5LhLsK6MRwYRSdjVn9jVYVtwiGwgq_bDPpVuI3aaOW7UQfqGWdos-kVwHnQccbDRnQDvQmCDqy6QdSQ=="), + id = + ByteArray.fromBase64Url("JT086Ym5LhLsK6MRwYRSdjVn9jVYVtwiGwgq_bDPpVuI3aaOW7UQfqGWdos-kVwHnQccbDRnQDvQmCDqy6QdSQ=="), clientData = """{"type": "webauthn.get", "clientExtensions": {}, "challenge": "Q0hBTExFTkdF", "origin": "https://example.com"}""", - authDataBytes = ByteArray.fromBase64Url("o3mm9u6vuaVeN4wRgDTidR5oL6ufLTCrE9ISVYbOGUcBAAAADA=="), - sig = ByteArray.fromBase64Url("MEYCIQD8tVtVU-esAvCSNVR4JLfW0MKf2C_Rb1Xn4UBBS4jbmwIhAM5AfKuhVrHcMfcNwVDYQ4q7qU_a6avSWgdydnunVaq7"), - ) + authDataBytes = ByteArray.fromBase64Url( + "o3mm9u6vuaVeN4wRgDTidR5oL6ufLTCrE9ISVYbOGUcBAAAADA==" + ), + sig = + ByteArray.fromBase64Url("MEYCIQD8tVtVU-esAvCSNVR4JLfW0MKf2C_Rb1Xn4UBBS4jbmwIhAM5AfKuhVrHcMfcNwVDYQ4q7qU_a6avSWgdydnunVaq7"), + ), + ) + + val AppleAttestationIos = Example( + RelyingPartyIdentity + .builder() + .id("demo.yubico.com") + .name("YubicoDemo") + .build(), + UserIdentity + .builder() + .name("Yubico demo user") + .displayName("Yubico demo user") + .id( + ByteArray.fromBase64Url("Fe0QmfU9xebikAVYRtOyGfI5ulgxbVVf7VNaON8edmU=") + ) + .build(), + AttestationExample( + new String( + ByteArray + .fromBase64( + "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiUUs2c25Jak40MGNNZG9oNlUtR3NEZnlFYzlQY3pKdEgtSTczM3daSDRIZyIsIm9yaWdpbiI6Imh0dHBzOi8vZGVtby55dWJpY28uY29tIn0=" + ) + .getBytes, + StandardCharsets.UTF_8, + ), + ByteArray.fromBase64("o2NmbXRlYXBwbGVnYXR0U3RtdKFjeDVjglkCRjCCAkIwggHJoAMCAQICBgF4xhYQszAKBggqhkjOPQQDAjBIMRwwGgYDVQQDDBNBcHBsZSBXZWJBdXRobiBDQSAxMRMwEQYDVQQKDApBcHBsZSBJbmMuMRMwEQYDVQQIDApDYWxpZm9ybmlhMB4XDTIxMDQxMTEyMzcxOFoXDTIxMDQxNDEyMzcxOFowgZExSTBHBgNVBAMMQDMxYzRlOTM2YzgwZjY1Y2VjNzcxZWZkOGNhNWMxNDdlZTgxZjY4ZjVhODE5YTUzNDFiMDU5NmJkYmU4YWI0OTExGjAYBgNVBAsMEUFBQSBDZXJ0aWZpY2F0aW9uMRMwEQYDVQQKDApBcHBsZSBJbmMuMRMwEQYDVQQIDApDYWxpZm9ybmlhMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEYc87v7q19IYjqS3vizLAet/NcW0NVpYRvzvZFfCT00nBR0rzITI4iuuBupVtSRFhZfHa3GhYUu/w3Mo2h3s/+qNVMFMwDAYDVR0TAQH/BAIwADAOBgNVHQ8BAf8EBAMCBPAwMwYJKoZIhvdjZAgCBCYwJKEiBCC+B6u5EUpszNBikhFRpOuBolX7jPReSqGkIvBr0orEZDAKBggqhkjOPQQDAgNnADBkAjAZpK9Vw3hR3uCca+kUAorfR4Sj/HkCcmydzm/KuewaYC5lmIwRTw9SKEVmAAITRlUCMEC9P/ksVc5DUHtKt+rQ9mXHeobdGymHSM7xZtYMNOfze8hPo5HLnwtWCB5qF8MQRVkCODCCAjQwggG6oAMCAQICEFYlU5XHp/tA6+Io2CYIU7YwCgYIKoZIzj0EAwMwSzEfMB0GA1UEAwwWQXBwbGUgV2ViQXV0aG4gUm9vdCBDQTETMBEGA1UECgwKQXBwbGUgSW5jLjETMBEGA1UECAwKQ2FsaWZvcm5pYTAeFw0yMDAzMTgxODM4MDFaFw0zMDAzMTMwMDAwMDBaMEgxHDAaBgNVBAMME0FwcGxlIFdlYkF1dGhuIENBIDExEzARBgNVBAoMCkFwcGxlIEluYy4xEzARBgNVBAgMCkNhbGlmb3JuaWEwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAASDLocvJhSRgQIlufX81rtjeLX1Xz/LBFvHNZk0df1UkETfm/4ZIRdlxpod2gULONRQg0AaQ0+yTREtVsPhz7/LmJH+wGlggb75bLx3yI3dr0alruHdUVta+quTvpwLJpGjZjBkMBIGA1UdEwEB/wQIMAYBAf8CAQAwHwYDVR0jBBgwFoAUJtdk2cV4wlpn0afeaxLQG2PxxtcwHQYDVR0OBBYEFOuugsT/oaxbUdTPJGEFAL5jvXeIMA4GA1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNoADBlAjEA3YsaNIGl+tnbtOdle4QeFEwnt1uHakGGwrFHV1Azcifv5VRFfvZIlQxjLlxIPnDBAjAsimBE3CAfz+Wbw00pMMFIeFHZYO1qdfHrSsq+OM0luJfQyAW+8Mf3iwelccboDgdoYXV0aERhdGFYmMRs74KtG1Rkd1kdAIsIdZ7D5tLstPOUdL/qaWmSXQO3RQAAAAAAAAAAAAAAAAAAAAAAAAAAABRK0rg7vzmd/BAatDNkXX6aBhPZSaUBAgMmIAEhWCBhzzu/urX0hiOpLe+LMsB6381xbQ1WlhG/O9kV8JPTSSJYIMFHSvMhMjiK64G6lW1JEWFl8drcaFhS7/DcyjaHez/6"), + ), + AssertionExample( + id = ByteArray.fromBase64Url("StK4O785nfwQGrQzZF1-mgYT2Uk"), + clientData = new String( + ByteArray + .fromBase64( + "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoid2V5TG9keXVzUl96SWtPWUg3bTVUYjBreGViQnEtV2QzYVJreUhMeHl0SSIsIm9yaWdpbiI6Imh0dHBzOi8vZGVtby55dWJpY28uY29tIn0=" + ) + .getBytes, + StandardCharsets.UTF_8, + ), + authDataBytes = ByteArray.fromBase64( + "xGzvgq0bVGR3WR0Aiwh1nsPm0uy085R0v+ppaZJdA7cFAAAAAA==" + ), + sig = + ByteArray.fromBase64("MEUCIQDv9Sye6lyu6nonnsI9bSjkBXyhPRmei4LGRhfuOGc0AwIgPEQFsGHZDMIeSVDmgB85otg1Ba0XNl7S/Bgj6diIIoo="), + ), + ) + + val AppleAttestationMacos = Example( + RelyingPartyIdentity + .builder() + .id("demo.yubico.com") + .name("YubicoDemo") + .build(), + UserIdentity + .builder() + .name("Yubico demo user") + .displayName("Yubico demo user") + .id(ByteArray.fromBase64("+8eKyPo9MGrhWx8Y7ZeoczjaS5mbRr2kqF7/zllIgZ8=")) + .build(), + AttestationExample( + new String( + ByteArray + .fromBase64( + "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoicWszNE1GRVA4dWxXaHVpOEpncmt0ZVE5RXhIV2NKYndJcjNDUm1lVGtqZyIsIm9yaWdpbiI6Imh0dHBzOi8vZGVtby55dWJpY28uY29tIn0=" + ) + .getBytes, + StandardCharsets.UTF_8, + ), + ByteArray.fromBase64("o2NmbXRlYXBwbGVnYXR0U3RtdKFjeDVjglkCRjCCAkIwggHJoAMCAQICBgF4xjGSqDAKBggqhkjOPQQDAjBIMRwwGgYDVQQDDBNBcHBsZSBXZWJBdXRobiBDQSAxMRMwEQYDVQQKDApBcHBsZSBJbmMuMRMwEQYDVQQIDApDYWxpZm9ybmlhMB4XDTIxMDQxMTEzMDcyMFoXDTIxMDQxNDEzMDcyMFowgZExSTBHBgNVBAMMQDYxYmQ5NzY4M2JlMTk0NTVjOGJjOWVhNDZhMjY4NzU0MzVjMmIwNmVlMTI4YzY4ZDFiMGE4NDczODkwNTgzMjYxGjAYBgNVBAsMEUFBQSBDZXJ0aWZpY2F0aW9uMRMwEQYDVQQKDApBcHBsZSBJbmMuMRMwEQYDVQQIDApDYWxpZm9ybmlhMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEdrvYDb+UbAjcbbommtRqw+2Lm1fvHG6ll1dOgeEM25H8ThQ0yj4R3hVbc/ean1I5eqc/RXDFm/jJI/Lmp1uEFqNVMFMwDAYDVR0TAQH/BAIwADAOBgNVHQ8BAf8EBAMCBPAwMwYJKoZIhvdjZAgCBCYwJKEiBCAQ6ifyo7KWlR86ueS0JMAuIi66gYkJsX+VxAcvbtEEcTAKBggqhkjOPQQDAgNnADBkAjAIu8Vx1tdGHSarO63RF7QaUo3/Iuk1CXA2Z0YIbDG4mLS15JQ/AUwctOpePcZoDngCMFMfnXi6jlhNBmppj5/8VQz2Kbz5eNxg+dqALz59ctCqXkdCVLMhUOpHWgMhhOadj1kCODCCAjQwggG6oAMCAQICEFYlU5XHp/tA6+Io2CYIU7YwCgYIKoZIzj0EAwMwSzEfMB0GA1UEAwwWQXBwbGUgV2ViQXV0aG4gUm9vdCBDQTETMBEGA1UECgwKQXBwbGUgSW5jLjETMBEGA1UECAwKQ2FsaWZvcm5pYTAeFw0yMDAzMTgxODM4MDFaFw0zMDAzMTMwMDAwMDBaMEgxHDAaBgNVBAMME0FwcGxlIFdlYkF1dGhuIENBIDExEzARBgNVBAoMCkFwcGxlIEluYy4xEzARBgNVBAgMCkNhbGlmb3JuaWEwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAASDLocvJhSRgQIlufX81rtjeLX1Xz/LBFvHNZk0df1UkETfm/4ZIRdlxpod2gULONRQg0AaQ0+yTREtVsPhz7/LmJH+wGlggb75bLx3yI3dr0alruHdUVta+quTvpwLJpGjZjBkMBIGA1UdEwEB/wQIMAYBAf8CAQAwHwYDVR0jBBgwFoAUJtdk2cV4wlpn0afeaxLQG2PxxtcwHQYDVR0OBBYEFOuugsT/oaxbUdTPJGEFAL5jvXeIMA4GA1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNoADBlAjEA3YsaNIGl+tnbtOdle4QeFEwnt1uHakGGwrFHV1Azcifv5VRFfvZIlQxjLlxIPnDBAjAsimBE3CAfz+Wbw00pMMFIeFHZYO1qdfHrSsq+OM0luJfQyAW+8Mf3iwelccboDgdoYXV0aERhdGFYmMRs74KtG1Rkd1kdAIsIdZ7D5tLstPOUdL/qaWmSXQO3RQAAAAAAAAAAAAAAAAAAAAAAAAAAABRhYCgh40b6Uj1WdjckwPAdCwd4fKUBAgMmIAEhWCB2u9gNv5RsCNxtuiaa1GrD7YubV+8cbqWXV06B4QzbkSJYIPxOFDTKPhHeFVtz95qfUjl6pz9FcMWb+Mkj8uanW4QW"), + ), + AssertionExample( + id = ByteArray.fromBase64Url("YWAoIeNG-lI9VnY3JMDwHQsHeHw"), + clientData = new String( + ByteArray + .fromBase64( + "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiVVdobmx5VTdlVzZBTEw1M1VPcENnU1N3ckEzNm92R3VpQUV6ZE91OFdTYyIsIm9yaWdpbiI6Imh0dHBzOi8vZGVtby55dWJpY28uY29tIn0=" + ) + .getBytes, + StandardCharsets.UTF_8, + ), + authDataBytes = ByteArray.fromBase64( + "xGzvgq0bVGR3WR0Aiwh1nsPm0uy085R0v+ppaZJdA7cFAAAAAA==" + ), + sig = + ByteArray.fromBase64("MEUCIQDkspL//pE98spvRtyTAZqPjmpd6/G+KmNsjMUfX7pKkAIgcld+Y3j0yt95CMqKmR99SKuoiitIL8SBElZw/qFEX5s="), + ), ) } diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/test/Test.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/test/Test.scala index b97fb1947..d47749ac0 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/test/Test.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/test/Test.scala @@ -24,13 +24,6 @@ package com.yubico.webauthn.test -import java.security.interfaces.ECPublicKey -import java.util.Base64 - -import com.yubico.webauthn.data.AttestationObject -import org.bouncycastle.asn1.ASN1InputStream -import org.bouncycastle.asn1.ASN1Primitive -import org.bouncycastle.asn1.util.ASN1Dump import COSE.OneKey import com.fasterxml.jackson.databind.JsonNode import com.upokecenter.cbor.CBORObject @@ -38,13 +31,18 @@ import com.yubico.internal.util.BinaryUtil import com.yubico.internal.util.CertificateParser import com.yubico.internal.util.JacksonCodecs import com.yubico.webauthn.RegistrationTestData +import com.yubico.webauthn.WebAuthnTestCodecs +import com.yubico.webauthn.data.AttestationObject import com.yubico.webauthn.data.AuthenticatorDataFlags import com.yubico.webauthn.data.ByteArray -import com.yubico.webauthn.WebAuthnTestCodecs +import org.bouncycastle.asn1.ASN1InputStream +import org.bouncycastle.asn1.ASN1Primitive +import org.bouncycastle.asn1.util.ASN1Dump +import java.security.interfaces.ECPublicKey +import java.util.Base64 import scala.jdk.CollectionConverters._ - object Test extends App { // val attestationObject: ByteArray = ByteArray.fromBase64Url("o2NmbXRmcGFja2VkaGF1dGhEYXRhWLhsce9f2O4QMCXrg1cu1lwknOxtXBryURzsRWDk8tD7pkEAAAAAAAAAAAAAAAAAAAAAAAAAAABAawIjwBjPgvsbJe-gqVwMFEQeZ0zTgj93jw3fFdOTLTshl3F2qwb6O35qI520Iw53fXcsNMoFWL767oiSpHB4ggQ0PVe2C-FLegMiA73oT8Tbd-R7wB7HOrYY5FOQdmCN2aGm5dT2RrmsRHq_EhEUF6L_X4aY2zIkXH7-UlI0MtQMZ2F0dFN0bXSjY2FsZ2VFUzI1NmNzaWdYRzBFAiBgPH9xOEVrf3XqFbYkn78oHbBu-c8-0z0g6sT00MzcJAIhAJdwAJuhzS_SqJm8q8R--yc_YXj4VvNLlCVWnFlycIIXY3g1Y4FZAlMwggJPMIIBN6ADAgECAgQSNtF_MA0GCSqGSIb3DQEBCwUAMC4xLDAqBgNVBAMTI1l1YmljbyBVMkYgUm9vdCBDQSBTZXJpYWwgNDU3MjAwNjMxMCAXDTE0MDgwMTAwMDAwMFoYDzIwNTAwOTA0MDAwMDAwWjAxMS8wLQYDVQQDDCZZdWJpY28gVTJGIEVFIFNlcmlhbCAyMzkyNTczNDEwMzI0MTA4NzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABNNlqR5emeDVtDnA2a-7h_QFjkfdErFE7bFNKzP401wVE-QNefD5maviNnGVk4HJ3CsHhYuCrGNHYgTM9zTWriGjOzA5MCIGCSsGAQQBgsQKAgQVMS4zLjYuMS40LjEuNDE0ODIuMS41MBMGCysGAQQBguUcAgEBBAQDAgUgMA0GCSqGSIb3DQEBCwUAA4IBAQAiG5uzsnIk8T6-oyLwNR6vRklmo29yaYV8jiP55QW1UnXdTkEiPn8mEQkUac-Sn6UmPmzHdoGySG2q9B-xz6voVQjxP2dQ9sgbKd5gG15yCLv6ZHblZKkdfWSrUkrQTrtaziGLFSbxcfh83vUjmOhDLFC5vxV4GXq2674yq9F2kzg4nCS4yXrO4_G8YWR2yvQvE2ffKSjQJlXGO5080Ktptplv5XN4i5lS-AKrT5QRVbEJ3B4g7G0lQhdYV-6r4ZtHil8mF4YNMZ0-RaYPxAaYNWkFYdzOZCaIdQbXRZefgGfbMUiAC2gwWN7fiPHV9eu82NYypGU32OijG9BjhGt_") @@ -72,9 +70,12 @@ object Test extends App { runWith(RegistrationTestData.AndroidSafetynet.RealExample.attestationObject) - val attestationObjectFirefox58 = ByteArray.fromBase64Url("o2hhdXRoRGF0YVjKlJKaCrOPLCdHaydNzfSylZWY-KV_PzorqW39Kb9p5mdBAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAixHvZmRdDp0DhMTCfB9dHQF9frTlVXNqXGJ3tGfVV0hR6mIk9ioAbK4AK9VkoxdIE04kjemwBHc5Yaz8BrZ9ujY2FsZ2VFUzI1NmF4WCD7ESIYejaHqAg9C9hTMy1hQafvKmy1KIuXW6Artariq2F5WCCpWfXbnYPAUpTL18oD9A_BUFR7z9IhodehhSYlN_Y2mWNmbXRoZmlkby11MmZnYXR0U3RtdKJjeDVjgVkCTjCCAkowggEyoAMCAQICBFcW98AwDQYJKoZIhvcNAQELBQAwLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290IENBIFNlcmlhbCA0NTcyMDA2MzEwIBcNMTQwODAxMDAwMDAwWhgPMjA1MDA5MDQwMDAwMDBaMCwxKjAoBgNVBAMMIVl1YmljbyBVMkYgRUUgU2VyaWFsIDI1MDU2OTIyNjE3NjBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABGTZHFTW2VeN07BgeExGNtVIo5g6rzUnzustxuMrZ54z_OnoYxStvPhXUhsvTEmflK9wVWW-IhlbKbZOTa0N0GKjOzA5MCIGCSsGAQQBgsQKAgQVMS4zLjYuMS40LjEuNDE0ODIuMS41MBMGCysGAQQBguUcAgEBBAQDAgUgMA0GCSqGSIb3DQEBCwUAA4IBAQB4mxjKm6TbdiDi-IuM_elRJm0qInfV4_vptoyaoOkaakjqdUzvC95ZhANtrn-Lo_3mS1F2BMOhGy5tdU1KNxnvqHaTD6Dg8wY_WkygvKAxT38Fo-pdTt2R00pmtV1cj6huAOe_X92AI36z24I-NOMSZ7NJqsecJKcSpZ6ASqqNa6klJqJ3p3HeMWpzvzxxH8VNImYn-teV5PROG3ADTwn8_ji33il4k_tZrIscM8_Fxr5djkzCf0ofRvb4RPh4wHKL3B37pnHaEIf1jOOOWWuj8p_QWUFdxQqjL4kUfNPbCAE31OZbvOsLv-VBiBiOzBQxGHlRkqs6c4eppXJ_EEiGY3NpZ1hHMEUCIEMdeuTafyyaQFAjrZv0ANFt6mHzqjABJBwtUPFfbU0BAiEAvHJuqQCMUoNErDJFR928WnJnykwmoEi5XxdvsjtbDIw") + val attestationObjectFirefox58 = + ByteArray.fromBase64Url("o2hhdXRoRGF0YVjKlJKaCrOPLCdHaydNzfSylZWY-KV_PzorqW39Kb9p5mdBAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAixHvZmRdDp0DhMTCfB9dHQF9frTlVXNqXGJ3tGfVV0hR6mIk9ioAbK4AK9VkoxdIE04kjemwBHc5Yaz8BrZ9ujY2FsZ2VFUzI1NmF4WCD7ESIYejaHqAg9C9hTMy1hQafvKmy1KIuXW6Artariq2F5WCCpWfXbnYPAUpTL18oD9A_BUFR7z9IhodehhSYlN_Y2mWNmbXRoZmlkby11MmZnYXR0U3RtdKJjeDVjgVkCTjCCAkowggEyoAMCAQICBFcW98AwDQYJKoZIhvcNAQELBQAwLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290IENBIFNlcmlhbCA0NTcyMDA2MzEwIBcNMTQwODAxMDAwMDAwWhgPMjA1MDA5MDQwMDAwMDBaMCwxKjAoBgNVBAMMIVl1YmljbyBVMkYgRUUgU2VyaWFsIDI1MDU2OTIyNjE3NjBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABGTZHFTW2VeN07BgeExGNtVIo5g6rzUnzustxuMrZ54z_OnoYxStvPhXUhsvTEmflK9wVWW-IhlbKbZOTa0N0GKjOzA5MCIGCSsGAQQBgsQKAgQVMS4zLjYuMS40LjEuNDE0ODIuMS41MBMGCysGAQQBguUcAgEBBAQDAgUgMA0GCSqGSIb3DQEBCwUAA4IBAQB4mxjKm6TbdiDi-IuM_elRJm0qInfV4_vptoyaoOkaakjqdUzvC95ZhANtrn-Lo_3mS1F2BMOhGy5tdU1KNxnvqHaTD6Dg8wY_WkygvKAxT38Fo-pdTt2R00pmtV1cj6huAOe_X92AI36z24I-NOMSZ7NJqsecJKcSpZ6ASqqNa6klJqJ3p3HeMWpzvzxxH8VNImYn-teV5PROG3ADTwn8_ji33il4k_tZrIscM8_Fxr5djkzCf0ofRvb4RPh4wHKL3B37pnHaEIf1jOOOWWuj8p_QWUFdxQqjL4kUfNPbCAE31OZbvOsLv-VBiBiOzBQxGHlRkqs6c4eppXJ_EEiGY3NpZ1hHMEUCIEMdeuTafyyaQFAjrZv0ANFt6mHzqjABJBwtUPFfbU0BAiEAvHJuqQCMUoNErDJFR928WnJnykwmoEi5XxdvsjtbDIw") // val attestationObjectFirefoxNightly = ByteArray.fromBase64Url("o2NmbXRoZmlkby11MmZnYXR0U3RtdKJjc2lnWEcwRQIhAMN34Lky1H00Yio4AJcCVh-cIbw__8fOgVPacfZqQMtSAiAHLWI6GKHAi7pmRMEljNuWBq_BHrKObzzzui9Duqmo7GN4NWOBWQJOMIICSjCCATKgAwIBAgIEVxb3wDANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZdWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAwMDBaGA8yMDUwMDkwNDAwMDAwMFowLDEqMCgGA1UEAwwhWXViaWNvIFUyRiBFRSBTZXJpYWwgMjUwNTY5MjI2MTc2MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEZNkcVNbZV43TsGB4TEY21UijmDqvNSfO6y3G4ytnnjP86ehjFK28-FdSGy9MSZ-Ur3BVZb4iGVsptk5NrQ3QYqM7MDkwIgYJKwYBBAGCxAoCBBUxLjMuNi4xLjQuMS40MTQ4Mi4xLjUwEwYLKwYBBAGC5RwCAQEEBAMCBSAwDQYJKoZIhvcNAQELBQADggEBAHibGMqbpNt2IOL4i4z96VEmbSoid9Xj--m2jJqg6RpqSOp1TO8L3lmEA22uf4uj_eZLUXYEw6EbLm11TUo3Ge-odpMPoODzBj9aTKC8oDFPfwWj6l1O3ZHTSma1XVyPqG4A579f3YAjfrPbgj404xJns0mqx5wkpxKlnoBKqo1rqSUmonencd4xanO_PHEfxU0iZif615Xk9E4bcANPCfz-OLfeKXiT-1msixwzz8XGvl2OTMJ_Sh9G9vhE-HjAcovcHfumcdoQh_WM445Za6Pyn9BZQV3FCqMviRR809sIATfU5lu86wu_5UGIGI7MFDEYeVGSqzpzh6mlcn8QSIZoYXV0aERhdGFYxJSSmgqzjywnR2snTc30spWVmPilfz86K6lt_Sm_aeZnQQAAAAAAAAAAAAAAAAAAAAAAAAAAAEBsMp3vlrdYz8qUyb6o0J9M9l7FLS6XI70p9Txx0LIDuG87doFwc-9Tu6pW0njfyIISSif4kXZkF87vrgCcDp3UpQECAyYgASFYIKjQ7ovDDFsXm-I3q1vX8WUtU2CQ5IwX0cPfgR1KxBZLIlggQR9CYSfpMsRLoL9Y1ADVV_rKHMStoipUywjOct0g7cA") - val attestationObjectFirefoxNightly = new ByteArray(Base64.getMimeDecoder.decode("o2NmbXRoZmlkby11MmZnYXR0U3RtdKJjc2lnWEcwRQIhAMN34Lky1H00Yio4AJcCVh+cIbw//8fOgVPacfZqQMtSAiAHLWI6GKHAi7pmRMEljNuWBq/BHrKObzzzui9Duqmo7GN4NWOBWQJOMIICSjCCATKgAwIBAgIEVxb3wDANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZdWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAwMDBaGA8yMDUwMDkwNDAwMDAwMFowLDEqMCgGA1UEAwwhWXViaWNvIFUyRiBFRSBTZXJpYWwgMjUwNTY5MjI2MTc2MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEZNkcVNbZV43TsGB4TEY21UijmDqvNSfO6y3G4ytnnjP86ehjFK28+FdSGy9MSZ+Ur3BVZb4iGVsptk5NrQ3QYqM7MDkwIgYJKwYBBAGCxAoCBBUxLjMuNi4xLjQuMS40MTQ4Mi4xLjUwEwYLKwYBBAGC5RwCAQEEBAMCBSAwDQYJKoZIhvcNAQELBQADggEBAHibGMqbpNt2IOL4i4z96VEmbSoid9Xj++m2jJqg6RpqSOp1TO8L3lmEA22uf4uj/eZLUXYEw6EbLm11TUo3Ge+odpMPoODzBj9aTKC8oDFPfwWj6l1O3ZHTSma1XVyPqG4A579f3YAjfrPbgj404xJns0mqx5wkpxKlnoBKqo1rqSUmonencd4xanO/PHEfxU0iZif615Xk9E4bcANPCfz+OLfeKXiT+1msixwzz8XGvl2OTMJ/Sh9G9vhE+HjAcovcHfumcdoQh/WM445Za6Pyn9BZQV3FCqMviRR809sIATfU5lu86wu/5UGIGI7MFDEYeVGSqzpzh6mlcn8QSIZoYXV0aERhdGFYxJSSmgqzjywnR2snTc30spWVmPilfz86K6lt/Sm/aeZnQQAAAAAAAAAAAAAAAAAAAAAAAAAAAEBsMp3vlrdYz8qUyb6o0J9M9l7FLS6XI70p9Txx0LIDuG87doFwc+9Tu6pW0njfyIISSif4kXZkF87vrgCcDp3UpQECAyYgASFYIKjQ7ovDDFsXm+I3q1vX8WUtU2CQ5IwX0cPfgR1KxBZLIlggQR9CYSfpMsRLoL9Y1ADVV/rKHMStoipUywjOct0g7cA=")) + val attestationObjectFirefoxNightly = new ByteArray( + Base64.getMimeDecoder.decode("o2NmbXRoZmlkby11MmZnYXR0U3RtdKJjc2lnWEcwRQIhAMN34Lky1H00Yio4AJcCVh+cIbw//8fOgVPacfZqQMtSAiAHLWI6GKHAi7pmRMEljNuWBq/BHrKObzzzui9Duqmo7GN4NWOBWQJOMIICSjCCATKgAwIBAgIEVxb3wDANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZdWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAwMDBaGA8yMDUwMDkwNDAwMDAwMFowLDEqMCgGA1UEAwwhWXViaWNvIFUyRiBFRSBTZXJpYWwgMjUwNTY5MjI2MTc2MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEZNkcVNbZV43TsGB4TEY21UijmDqvNSfO6y3G4ytnnjP86ehjFK28+FdSGy9MSZ+Ur3BVZb4iGVsptk5NrQ3QYqM7MDkwIgYJKwYBBAGCxAoCBBUxLjMuNi4xLjQuMS40MTQ4Mi4xLjUwEwYLKwYBBAGC5RwCAQEEBAMCBSAwDQYJKoZIhvcNAQELBQADggEBAHibGMqbpNt2IOL4i4z96VEmbSoid9Xj++m2jJqg6RpqSOp1TO8L3lmEA22uf4uj/eZLUXYEw6EbLm11TUo3Ge+odpMPoODzBj9aTKC8oDFPfwWj6l1O3ZHTSma1XVyPqG4A579f3YAjfrPbgj404xJns0mqx5wkpxKlnoBKqo1rqSUmonencd4xanO/PHEfxU0iZif615Xk9E4bcANPCfz+OLfeKXiT+1msixwzz8XGvl2OTMJ/Sh9G9vhE+HjAcovcHfumcdoQh/WM445Za6Pyn9BZQV3FCqMviRR809sIATfU5lu86wu/5UGIGI7MFDEYeVGSqzpzh6mlcn8QSIZoYXV0aERhdGFYxJSSmgqzjywnR2snTc30spWVmPilfz86K6lt/Sm/aeZnQQAAAAAAAAAAAAAAAAAAAAAAAAAAAEBsMp3vlrdYz8qUyb6o0J9M9l7FLS6XI70p9Txx0LIDuG87doFwc+9Tu6pW0njfyIISSif4kXZkF87vrgCcDp3UpQECAyYgASFYIKjQ7ovDDFsXm+I3q1vX8WUtU2CQ5IwX0cPfgR1KxBZLIlggQR9CYSfpMsRLoL9Y1ADVV/rKHMStoipUywjOct0g7cA=") + ) runWith(attestationObjectFirefoxNightly) @@ -85,19 +86,28 @@ object Test extends App { val parsedAttObj = new AttestationObject(attestationObject) println(parsedAttObj) println(parsedAttObj.getAuthenticatorData.getBytes.getHex) - println(WebAuthnTestCodecs.importCosePublicKey(parsedAttObj.getAuthenticatorData.getAttestedCredentialData.get.getCredentialPublicKey)) - - val attestationObjectCbor = JacksonCodecs.cbor.readTree(attestationObject.getBytes) + println( + WebAuthnTestCodecs.importCosePublicKey( + parsedAttObj.getAuthenticatorData.getAttestedCredentialData.get.getCredentialPublicKey + ) + ) + + val attestationObjectCbor = + JacksonCodecs.cbor.readTree(attestationObject.getBytes) println(attestationObjectCbor) println(attestationObjectCbor.get("authData")) - val authDataBytes: ByteArray = new ByteArray(attestationObjectCbor.get("authData").binaryValue) + val authDataBytes: ByteArray = new ByteArray( + attestationObjectCbor.get("authData").binaryValue + ) println(authDataBytes) doAuthData(authDataBytes) println("Manually extracted public key:") - val manuallyExtractedPubKeyBytes = new ByteArray(attestationObject.getBytes.drop(32 + 1 + 4 + 16 + 2 + 64)) + val manuallyExtractedPubKeyBytes = new ByteArray( + attestationObject.getBytes.drop(32 + 1 + 4 + 16 + 2 + 64) + ) println(manuallyExtractedPubKeyBytes) println(JacksonCodecs.cbor.readTree(manuallyExtractedPubKeyBytes.getBytes)) @@ -106,25 +116,52 @@ object Test extends App { if (parsedAttObj.getFormat == "android-safetynet") { println("Attestation statement \"response\" field:") - println(parsedAttObj.getAttestationStatement.get("response").binaryValue.toVector) - val safetynetJwsCompact = new String(parsedAttObj.getAttestationStatement.get("response").binaryValue, "UTF-8") + println( + parsedAttObj.getAttestationStatement + .get("response") + .binaryValue + .toVector + ) + val safetynetJwsCompact = new String( + parsedAttObj.getAttestationStatement.get("response").binaryValue, + "UTF-8", + ) println(safetynetJwsCompact) println(safetynetJwsCompact.split('.').toVector) - val Array(jwsHeaderBase64, jwsPayloadBase64, jwsSigBase64) = safetynetJwsCompact.split('.') + val Array(jwsHeaderBase64, jwsPayloadBase64, jwsSigBase64) = + safetynetJwsCompact.split('.') println(ByteArray.fromBase64Url(jwsHeaderBase64)) - println(prettifyJson(new String(ByteArray.fromBase64Url(jwsHeaderBase64).getBytes, "UTF-8"))) - for { x5cNode: JsonNode <- JacksonCodecs.json().readTree(ByteArray.fromBase64Url(jwsHeaderBase64).getBytes).get("x5c").elements().asScala } { + println( + prettifyJson( + new String(ByteArray.fromBase64Url(jwsHeaderBase64).getBytes, "UTF-8") + ) + ) + for { + x5cNode: JsonNode <- + JacksonCodecs + .json() + .readTree(ByteArray.fromBase64Url(jwsHeaderBase64).getBytes) + .get("x5c") + .elements() + .asScala + } { val x5cBytes = ByteArray.fromBase64(x5cNode.textValue()) val cert = CertificateParser.parseDer(x5cBytes.getBytes) println(cert) } - println(ByteArray.fromBase64Url(jwsPayloadBase64)) - println(prettifyJson(new String(ByteArray.fromBase64Url(jwsPayloadBase64).getBytes, "UTF-8"))) + println( + prettifyJson( + new String( + ByteArray.fromBase64Url(jwsPayloadBase64).getBytes, + "UTF-8", + ) + ) + ) println(ByteArray.fromBase64Url(jwsSigBase64)) } @@ -133,13 +170,17 @@ object Test extends App { } def prettifyJson(json: String): String = - JacksonCodecs.json().writerWithDefaultPrettyPrinter().writeValueAsString(JacksonCodecs.json().readTree(json)) + JacksonCodecs + .json() + .writerWithDefaultPrettyPrinter() + .writeValueAsString(JacksonCodecs.json().readTree(json)) def doAuthData(authDataBytes: ByteArray) = { val rpidBytes: Array[Byte] = authDataBytes.getBytes.slice(0, 32) val flagsByte: Byte = authDataBytes.getBytes()(32) val flags = new AuthenticatorDataFlags(flagsByte) - val counterBytes: Array[Byte] = authDataBytes.getBytes.slice(32 + 1, 32 + 1 + 4) + val counterBytes: Array[Byte] = + authDataBytes.getBytes.slice(32 + 1, 32 + 1 + 4) val attestedCredData: Array[Byte] = authDataBytes.getBytes.drop(32 + 1 + 4) println("Authenticator data:") @@ -153,17 +194,25 @@ object Test extends App { val L = BinaryUtil.getUint16(Lbytes) val credentialIdBytes = attestedCredData.slice(16 + 2, 16 + 2 + L) val credentialPublicKeyBytes = attestedCredData.drop(16 + 2 + L) - val credentialPublicKeyCbor = JacksonCodecs.cbor.readTree(credentialPublicKeyBytes) - val credentialPublicKeyCbor2 = CBORObject.DecodeFromBytes(credentialPublicKeyBytes) - val credentialPublicKeyDecoded: ECPublicKey = new OneKey(CBORObject.DecodeFromBytes(credentialPublicKeyBytes)).AsPublicKey().asInstanceOf[ECPublicKey] + val credentialPublicKeyCbor = + JacksonCodecs.cbor.readTree(credentialPublicKeyBytes) + val credentialPublicKeyCbor2 = + CBORObject.DecodeFromBytes(credentialPublicKeyBytes) + val credentialPublicKeyDecoded: ECPublicKey = new OneKey( + CBORObject.DecodeFromBytes(credentialPublicKeyBytes) + ).AsPublicKey().asInstanceOf[ECPublicKey] println("Attested credential data:") println(s"AAGUID: ${aaguid}") println(s"Lbytes: ${Lbytes}") println(s"L: ${L}") println(s"credentialId: ${BinaryUtil.toHex(credentialIdBytes)}") - println(s"credentialPublicKeyBytes: ${BinaryUtil.toHex(credentialPublicKeyBytes)}") - println(s"credentialPublicKeyBytes length: ${credentialPublicKeyBytes.length}") + println( + s"credentialPublicKeyBytes: ${BinaryUtil.toHex(credentialPublicKeyBytes)}" + ) + println( + s"credentialPublicKeyBytes length: ${credentialPublicKeyBytes.length}" + ) println(s"credentialPublicKeyCbor: ${credentialPublicKeyCbor}") println(s"credentialPublicKeyCbor2: ${credentialPublicKeyCbor2}") println(s"credentialPublicKeyDecoded: ${credentialPublicKeyDecoded}") @@ -185,7 +234,8 @@ object Test extends App { } // doAuthData(authDataBytes) - val cbormeAuthDataBytes = ByteArray.fromHex("94929A0AB38F2C27476B274DCDF4B2959598F8A57F3F3A2BA96DFD29BF69E66741000000000000000000000000000000000000000000406C329DEF96B758CFCA94C9BEA8D09F4CF65EC52D2E9723BD29F53C71D0B203B86F3B76817073EF53BBAA56D278DFC882124A27F891766417CEEFAE009C0E9DD4A5010203262001215820A8D0EE8BC30C5B179BE237AB5BD7F1652D536090E48C17D1C3DF811D4AC4164B225820411F426127E932C44BA0BF58D400D557FACA1CC4ADA22A54CB08CE72DD20EDC0") + val cbormeAuthDataBytes = + ByteArray.fromHex("94929A0AB38F2C27476B274DCDF4B2959598F8A57F3F3A2BA96DFD29BF69E66741000000000000000000000000000000000000000000406C329DEF96B758CFCA94C9BEA8D09F4CF65EC52D2E9723BD29F53C71D0B203B86F3B76817073EF53BBAA56D278DFC882124A27F891766417CEEFAE009C0E9DD4A5010203262001215820A8D0EE8BC30C5B179BE237AB5BD7F1652D536090E48C17D1C3DF811D4AC4164B225820411F426127E932C44BA0BF58D400D557FACA1CC4ADA22A54CB08CE72DD20EDC0") // val cbormeAuthDataBytes = ByteArray.fromHex("94929A0AB38F2C27476B274DCDF4B2959598F8A57F3F3A2BA96DFD29BF69E667410000000000000000000000000000000000000000004008B11EF66645D0E9D0384C4C27C1F5D1D017D7EB4E555736A5C6277B467D5574851EA6224F62A006CAE002BD564A31748134E248DE9B004773961ACFC06B67DBA363616C6765455332353661785820FB1122187A3687A8083D0BD853332D6141A7EF2A6CB5288B975BA02BB5AAE2AB61795820A959F5DB9D83C05294CBD7CA03F40FC150547BCFD221A1D7A185262537F63699") doAuthData(cbormeAuthDataBytes) diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/test/Util.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/test/Util.scala index 4543efdbd..2b4ff3f7e 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/test/Util.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/test/Util.scala @@ -24,16 +24,6 @@ package com.yubico.webauthn.test -import java.io.BufferedReader -import java.io.InputStream -import java.io.InputStreamReader -import java.security.GeneralSecurityException -import java.security.KeyFactory -import java.security.PublicKey -import java.security.cert.X509Certificate -import scala.language.reflectiveCalls -import scala.util.Try - import com.yubico.internal.util.CertificateParser import com.yubico.webauthn.data.ByteArray import org.bouncycastle.asn1.sec.SECNamedCurves @@ -43,6 +33,15 @@ import org.bouncycastle.jce.spec.ECParameterSpec import org.bouncycastle.jce.spec.ECPublicKeySpec import org.bouncycastle.openssl.PEMParser +import java.io.BufferedReader +import java.io.InputStream +import java.io.InputStreamReader +import java.security.GeneralSecurityException +import java.security.KeyFactory +import java.security.PublicKey +import java.security.cert.X509Certificate +import scala.language.reflectiveCalls +import scala.util.Try object Util { @@ -54,24 +53,43 @@ object Util { .getEncoded ) - def decodePublicKey(encodedPublicKey: ByteArray): PublicKey = try { - val curve = SECNamedCurves.getByName("secp256r1") - val point = curve.getCurve.decodePoint(encodedPublicKey.getBytes) + def decodePublicKey(encodedPublicKey: ByteArray): PublicKey = + try { + val curve = SECNamedCurves.getByName("secp256r1") + val point = curve.getCurve.decodePoint(encodedPublicKey.getBytes) - KeyFactory.getInstance("ECDSA", new BouncyCastleProvider) - .generatePublic(new ECPublicKeySpec(point, new ECParameterSpec(curve.getCurve, curve.getG, curve.getN, curve.getH))) - } catch { - case e: RuntimeException => - throw new IllegalArgumentException("Could not parse user public key: " + encodedPublicKey.getBase64Url, e) - case e: GeneralSecurityException => - //This should not happen - throw new RuntimeException("Failed to decode public key: " + encodedPublicKey.getBase64Url, e) - } + KeyFactory + .getInstance("ECDSA", new BouncyCastleProvider) + .generatePublic( + new ECPublicKeySpec( + point, + new ECParameterSpec( + curve.getCurve, + curve.getG, + curve.getN, + curve.getH, + ), + ) + ) + } catch { + case e: RuntimeException => + throw new IllegalArgumentException( + "Could not parse user public key: " + encodedPublicKey.getBase64Url, + e, + ) + case e: GeneralSecurityException => + //This should not happen + throw new RuntimeException( + "Failed to decode public key: " + encodedPublicKey.getBase64Url, + e, + ) + } type Stepish[A] = { def validate(): Unit; def next(): A } case class StepWithUtilities[A](a: Stepish[A]) { def validations: Try[Unit] = Try(a.validate()) def tryNext: Try[A] = Try(a.next()) } - implicit def toStepWithUtilities[A](a: Stepish[A]): StepWithUtilities[A] = StepWithUtilities(a) + implicit def toStepWithUtilities[A](a: Stepish[A]): StepWithUtilities[A] = + StepWithUtilities(a) } diff --git a/webauthn-server-demo/build.gradle b/webauthn-server-demo/build.gradle index 4456433f3..6cdca1146 100644 --- a/webauthn-server-demo/build.gradle +++ b/webauthn-server-demo/build.gradle @@ -3,6 +3,7 @@ plugins { id 'war' id 'application' id 'scala' + id 'io.github.cosmicsilence.scalafix' } description = 'WebAuthn demo' @@ -12,44 +13,45 @@ configurations { } dependencies { + implementation(platform(rootProject)) implementation( project(':webauthn-server-attestation'), project(':webauthn-server-core-minimal'), project(':yubico-util'), - 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'), + 'com.google.guava:guava', + 'com.fasterxml.jackson.core:jackson-databind', + 'com.upokecenter:cbor', + 'javax.ws.rs:javax.ws.rs-api', + 'org.eclipse.jetty:jetty-server', + 'org.eclipse.jetty:jetty-servlet', + 'org.glassfish.jersey.containers:jersey-container-servlet-core', + 'org.slf4j:slf4j-api', ) forJdk10( - addVersion('javax.activation:activation'), - addVersion('javax.xml.bind:jaxb-api'), + 'javax.activation:activation', + 'javax.xml.bind:jaxb-api', ) runtimeOnly( configurations.forJdk10, - addVersion('ch.qos.logback:logback-classic'), - addVersion('org.glassfish.jersey.containers:jersey-container-servlet'), - addVersion('org.glassfish.jersey.inject:jersey-hk2'), + 'ch.qos.logback:logback-classic', + 'org.glassfish.jersey.containers:jersey-container-servlet', + 'org.glassfish.jersey.inject:jersey-hk2', ) testImplementation( project(':webauthn-server-core-minimal').sourceSets.test.output, project(':yubico-util-scala'), - addVersion('junit:junit'), - addVersion('org.mockito:mockito-core'), - addVersion('org.scala-lang:scala-library'), - addVersion('org.scalacheck:scalacheck_2.13'), - addVersion('org.scalatest:scalatest_2.13'), + 'junit:junit', + 'org.mockito:mockito-core', + 'org.scala-lang:scala-library', + 'org.scalacheck:scalacheck_2.13', + 'org.scalatest:scalatest_2.13', ) modules { diff --git a/webauthn-server-demo/src/main/java/com/yubico/util/Either.java b/webauthn-server-demo/src/main/java/com/yubico/util/Either.java index 3dc7ed459..487c6a4c6 100644 --- a/webauthn-server-demo/src/main/java/com/yubico/util/Either.java +++ b/webauthn-server-demo/src/main/java/com/yubico/util/Either.java @@ -24,70 +24,68 @@ package com.yubico.util; - import java.util.Optional; import java.util.function.Function; public final class Either { - private final boolean isRight; - private final L leftValue; - private final R rightValue; - - private Either(R rightValue) { - this.isRight = true; - this.leftValue = null; - this.rightValue = rightValue; - } - - private Either(boolean dummy, L leftValue) { - this.isRight = false; - this.leftValue = leftValue; - this.rightValue = null; - } - - public final boolean isLeft() { - return !isRight(); - } - - public final boolean isRight() { - return isRight; - } - - public final Optional left() { - if (isLeft()) { - return Optional.of(leftValue); - } else { - throw new IllegalStateException("Cannot call left() on a right value."); - } + private final boolean isRight; + private final L leftValue; + private final R rightValue; + + private Either(R rightValue) { + this.isRight = true; + this.leftValue = null; + this.rightValue = rightValue; + } + + private Either(boolean dummy, L leftValue) { + this.isRight = false; + this.leftValue = leftValue; + this.rightValue = null; + } + + public final boolean isLeft() { + return !isRight(); + } + + public final boolean isRight() { + return isRight; + } + + public final Optional left() { + if (isLeft()) { + return Optional.of(leftValue); + } else { + throw new IllegalStateException("Cannot call left() on a right value."); } + } - public final Optional right() { - if (isRight()) { - return Optional.of(rightValue); - } else { - throw new IllegalStateException("Cannot call right() on a left value."); - } + public final Optional right() { + if (isRight()) { + return Optional.of(rightValue); + } else { + throw new IllegalStateException("Cannot call right() on a left value."); } + } - public final Either map(Function func) { - return flatMap(r -> Either.right(func.apply(r))); - } + public final Either map(Function func) { + return flatMap(r -> Either.right(func.apply(r))); + } - public final Either flatMap(Function> func) { - if (isRight()) { - return func.apply(rightValue); - } else { - return Either.left(leftValue); - } + public final Either flatMap(Function> func) { + if (isRight()) { + return func.apply(rightValue); + } else { + return Either.left(leftValue); } + } - public static Either left(L value) { - return new Either<>(false, value); - } - - public static Either right(R value) { - return new Either<>(value); - } + public static Either left(L value) { + return new Either<>(false, value); + } + public static Either right(R value) { + return new Either<>(value); + } } diff --git a/webauthn-server-demo/src/main/java/com/yubico/webauthn/U2fVerifier.java b/webauthn-server-demo/src/main/java/com/yubico/webauthn/U2fVerifier.java index 0400f9da9..8f922fadd 100644 --- a/webauthn-server-demo/src/main/java/com/yubico/webauthn/U2fVerifier.java +++ b/webauthn-server-demo/src/main/java/com/yubico/webauthn/U2fVerifier.java @@ -41,35 +41,41 @@ public class U2fVerifier { - public static boolean verify(AppId appId, RegistrationRequest request, U2fRegistrationResponse response) throws CertificateException, IOException, Base64UrlException { - final ByteArray appIdHash = Crypto.hash(appId.getId()); - final ByteArray clientDataHash = Crypto.hash(response.getCredential().getU2fResponse().getClientDataJSON()); + public static boolean verify( + AppId appId, RegistrationRequest request, U2fRegistrationResponse response) + throws CertificateException, IOException, Base64UrlException { + final ByteArray appIdHash = Crypto.sha256(appId.getId()); + final ByteArray clientDataHash = + Crypto.sha256(response.getCredential().getU2fResponse().getClientDataJSON()); - final JsonNode clientData = JacksonCodecs.json().readTree(response.getCredential().getU2fResponse().getClientDataJSON().getBytes()); - final String challengeBase64 = clientData.get("challenge").textValue(); + final JsonNode clientData = + JacksonCodecs.json() + .readTree(response.getCredential().getU2fResponse().getClientDataJSON().getBytes()); + final String challengeBase64 = clientData.get("challenge").textValue(); - ExceptionUtil.assure( - request.getPublicKeyCredentialCreationOptions().getChallenge().equals(ByteArray.fromBase64Url(challengeBase64)), - "Wrong challenge." - ); + ExceptionUtil.assure( + request + .getPublicKeyCredentialCreationOptions() + .getChallenge() + .equals(ByteArray.fromBase64Url(challengeBase64)), + "Wrong challenge."); - InputStream attestationCertAndSignatureStream = new ByteArrayInputStream(response.getCredential().getU2fResponse().getAttestationCertAndSignature().getBytes()); + InputStream attestationCertAndSignatureStream = + new ByteArrayInputStream( + response.getCredential().getU2fResponse().getAttestationCertAndSignature().getBytes()); - final X509Certificate attestationCert = CertificateParser.parseDer(attestationCertAndSignatureStream); + final X509Certificate attestationCert = + CertificateParser.parseDer(attestationCertAndSignatureStream); - byte[] signatureBytes = new byte[attestationCertAndSignatureStream.available()]; - attestationCertAndSignatureStream.read(signatureBytes); - final ByteArray signature = new ByteArray(signatureBytes); + byte[] signatureBytes = new byte[attestationCertAndSignatureStream.available()]; + attestationCertAndSignatureStream.read(signatureBytes); + final ByteArray signature = new ByteArray(signatureBytes); - return new U2fRawRegisterResponse( + return new U2fRawRegisterResponse( response.getCredential().getU2fResponse().getPublicKey(), response.getCredential().getU2fResponse().getKeyHandle(), attestationCert, - signature - ).verifySignature( - appIdHash, - clientDataHash - ); - } - + signature) + .verifySignature(appIdHash, clientDataHash); + } } diff --git a/webauthn-server-demo/src/main/java/com/yubico/webauthn/attestation/resolver/SimpleTrustResolverWithEquality.java b/webauthn-server-demo/src/main/java/com/yubico/webauthn/attestation/resolver/SimpleTrustResolverWithEquality.java index 8bb5d8d12..b243de4a9 100644 --- a/webauthn-server-demo/src/main/java/com/yubico/webauthn/attestation/resolver/SimpleTrustResolverWithEquality.java +++ b/webauthn-server-demo/src/main/java/com/yubico/webauthn/attestation/resolver/SimpleTrustResolverWithEquality.java @@ -33,37 +33,39 @@ import java.util.Optional; /** - * Resolves a metadata object whose associated certificate has signed the - * argument certificate, or is equal to the argument certificate. + * Resolves a metadata object whose associated certificate has signed the argument certificate, or + * is equal to the argument certificate. */ public class SimpleTrustResolverWithEquality implements TrustResolver { - private final SimpleTrustResolver subresolver; - private final Multimap trustedCerts = ArrayListMultimap.create(); + private final SimpleTrustResolver subresolver; + private final Multimap trustedCerts = ArrayListMultimap.create(); - public SimpleTrustResolverWithEquality(Collection trustedCertificates) { - subresolver = new SimpleTrustResolver(trustedCertificates); + public SimpleTrustResolverWithEquality(Collection trustedCertificates) { + subresolver = new SimpleTrustResolver(trustedCertificates); - for (X509Certificate cert : trustedCertificates) { - trustedCerts.put(cert.getSubjectDN().getName(), cert); - } + for (X509Certificate cert : trustedCertificates) { + trustedCerts.put(cert.getSubjectDN().getName(), cert); } + } - @Override - public Optional resolveTrustAnchor(X509Certificate attestationCertificate, List caCertificateChain) { - Optional subResult = subresolver.resolveTrustAnchor(attestationCertificate, caCertificateChain); - - if (subResult.isPresent()) { - return subResult; - } else { - for (X509Certificate cert : trustedCerts.get(attestationCertificate.getSubjectDN().getName())) { - if (cert.equals(attestationCertificate)) { - return Optional.of(cert); - } - } + @Override + public Optional resolveTrustAnchor( + X509Certificate attestationCertificate, List caCertificateChain) { + Optional subResult = + subresolver.resolveTrustAnchor(attestationCertificate, caCertificateChain); - return Optional.empty(); + if (subResult.isPresent()) { + return subResult; + } else { + for (X509Certificate cert : + trustedCerts.get(attestationCertificate.getSubjectDN().getName())) { + if (cert.equals(attestationCertificate)) { + return Optional.of(cert); } - } + } + return Optional.empty(); + } + } } diff --git a/webauthn-server-demo/src/main/java/demo/App.java b/webauthn-server-demo/src/main/java/demo/App.java index 8b3f4a8ae..a6109b227 100644 --- a/webauthn-server-demo/src/main/java/demo/App.java +++ b/webauthn-server-demo/src/main/java/demo/App.java @@ -24,33 +24,26 @@ package demo; -import javax.ws.rs.core.Application; - import com.yubico.webauthn.extension.appid.InvalidAppIdException; import demo.webauthn.WebAuthnRestResource; import java.security.cert.CertificateException; import java.util.Arrays; import java.util.HashSet; import java.util.Set; +import javax.ws.rs.core.Application; public class App extends Application { - @Override + @Override + public Set> getClasses() { + return new HashSet<>(Arrays.asList(CorsFilter.class)); + } - public Set> getClasses() { - return new HashSet<>(Arrays.asList( - CorsFilter.class - )); + @Override + public Set getSingletons() { + try { + return new HashSet<>(Arrays.asList(new WebAuthnRestResource())); + } catch (InvalidAppIdException | CertificateException e) { + throw new RuntimeException(e); } - - @Override - public Set getSingletons() { - try { - return new HashSet<>(Arrays.asList( - new WebAuthnRestResource() - )); - } catch (InvalidAppIdException | CertificateException e) { - throw new RuntimeException(e); - } - } - + } } diff --git a/webauthn-server-demo/src/main/java/demo/CorsFilter.java b/webauthn-server-demo/src/main/java/demo/CorsFilter.java index 9ea93b3a3..59b273782 100644 --- a/webauthn-server-demo/src/main/java/demo/CorsFilter.java +++ b/webauthn-server-demo/src/main/java/demo/CorsFilter.java @@ -24,23 +24,23 @@ package demo; +import demo.webauthn.Config; import javax.ws.rs.container.ContainerRequestContext; import javax.ws.rs.container.ContainerResponseContext; import javax.ws.rs.container.ContainerResponseFilter; -import demo.webauthn.Config; - public class CorsFilter implements ContainerResponseFilter { - @Override - public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) { - String origin = requestContext.getHeaderString("origin"); - Config.getOrigins().stream() - .filter(allowedOrigin -> allowedOrigin.equals(origin)) - .forEach(allowedOrigin -> { - responseContext.getHeaders().add("Access-Control-Allow-Origin", allowedOrigin); - responseContext.getHeaders().add("Access-Control-Allow-Methods", "GET,POST,DELETE"); + @Override + public void filter( + ContainerRequestContext requestContext, ContainerResponseContext responseContext) { + String origin = requestContext.getHeaderString("origin"); + Config.getOrigins().stream() + .filter(allowedOrigin -> allowedOrigin.equals(origin)) + .forEach( + allowedOrigin -> { + responseContext.getHeaders().add("Access-Control-Allow-Origin", allowedOrigin); + responseContext.getHeaders().add("Access-Control-Allow-Methods", "GET,POST,DELETE"); }); - } - + } } diff --git a/webauthn-server-demo/src/main/java/demo/webauthn/Config.java b/webauthn-server-demo/src/main/java/demo/webauthn/Config.java index af3d587c5..106f6c37e 100644 --- a/webauthn-server-demo/src/main/java/demo/webauthn/Config.java +++ b/webauthn-server-demo/src/main/java/demo/webauthn/Config.java @@ -40,132 +40,131 @@ public class Config { - private static final Logger logger = LoggerFactory.getLogger(Config.class); - - private static final String DEFAULT_ORIGIN = "https://localhost:8443"; - private static final int DEFAULT_PORT = 8443; - private static final RelyingPartyIdentity DEFAULT_RP_ID - = RelyingPartyIdentity.builder().id("localhost").name("Yubico WebAuthn demo").build(); - - private final Set origins; - private final int port; - private final RelyingPartyIdentity rpIdentity; - private final Optional appId; - - private Config(Set origins, int port, RelyingPartyIdentity rpIdentity, Optional appId) { - this.origins = CollectionUtil.immutableSet(origins); - this.port = port; - this.rpIdentity = rpIdentity; - this.appId = appId; + private static final Logger logger = LoggerFactory.getLogger(Config.class); + + private static final String DEFAULT_ORIGIN = "https://localhost:8443"; + private static final int DEFAULT_PORT = 8443; + private static final RelyingPartyIdentity DEFAULT_RP_ID = + RelyingPartyIdentity.builder().id("localhost").name("Yubico WebAuthn demo").build(); + + private final Set origins; + private final int port; + private final RelyingPartyIdentity rpIdentity; + private final Optional appId; + + private Config( + Set origins, int port, RelyingPartyIdentity rpIdentity, Optional appId) { + this.origins = CollectionUtil.immutableSet(origins); + this.port = port; + this.rpIdentity = rpIdentity; + this.appId = appId; + } + + private static Config instance; + + private static Config getInstance() { + if (instance == null) { + try { + instance = new Config(computeOrigins(), computePort(), computeRpIdentity(), computeAppId()); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } catch (InvalidAppIdException e) { + throw new RuntimeException(e); + } } + return instance; + } - private static Config instance; - private static Config getInstance() { - if (instance == null) { - try { - instance = new Config(computeOrigins(), computePort(), computeRpIdentity(), computeAppId()); - } catch (MalformedURLException e) { - throw new RuntimeException(e); - } catch (InvalidAppIdException e) { - throw new RuntimeException(e); - } - } - return instance; - } + public static Set getOrigins() { + return getInstance().origins; + } - public static Set getOrigins() { - return getInstance().origins; - } + public static int getPort() { + return getInstance().port; + } - public static int getPort() { - return getInstance().port; - } + public static RelyingPartyIdentity getRpIdentity() { + return getInstance().rpIdentity; + } - public static RelyingPartyIdentity getRpIdentity() { - return getInstance().rpIdentity; - } + public static Optional getAppId() { + return getInstance().appId; + } - public static Optional getAppId() { - return getInstance().appId; - } + private static Set computeOrigins() { + final String origins = System.getenv("YUBICO_WEBAUTHN_ALLOWED_ORIGINS"); + + logger.debug("YUBICO_WEBAUTHN_ALLOWED_ORIGINS: {}", origins); - private static Set computeOrigins() { - final String origins = System.getenv("YUBICO_WEBAUTHN_ALLOWED_ORIGINS"); + final Set result; - logger.debug("YUBICO_WEBAUTHN_ALLOWED_ORIGINS: {}", origins); + if (origins == null) { + result = Collections.singleton(DEFAULT_ORIGIN); + } else { + result = new HashSet<>(Arrays.asList(origins.split(","))); + } - final Set result; + logger.info("Origins: {}", result); - if (origins == null) { - result = Collections.singleton(DEFAULT_ORIGIN); - } else { - result = new HashSet<>(Arrays.asList(origins.split(","))); - } + return result; + } - logger.info("Origins: {}", result); + private static int computePort() { + final String port = System.getenv("YUBICO_WEBAUTHN_PORT"); - return result; + if (port == null) { + return DEFAULT_PORT; + } else { + return Integer.parseInt(port); } + } + + private static RelyingPartyIdentity computeRpIdentity() throws MalformedURLException { + final String name = System.getenv("YUBICO_WEBAUTHN_RP_NAME"); + final String id = System.getenv("YUBICO_WEBAUTHN_RP_ID"); + final String icon = System.getenv("YUBICO_WEBAUTHN_RP_ICON"); + + logger.debug("RP name: {}", name); + logger.debug("RP ID: {}", id); + logger.debug("RP icon: {}", icon); - private static int computePort() { - final String port = System.getenv("YUBICO_WEBAUTHN_PORT"); + RelyingPartyIdentity.RelyingPartyIdentityBuilder resultBuilder = DEFAULT_RP_ID.toBuilder(); - if (port == null) { - return DEFAULT_PORT; - } else { - return Integer.parseInt(port); - } + if (name == null) { + logger.debug("RP name not given - using default."); + } else { + resultBuilder.name(name); } - private static RelyingPartyIdentity computeRpIdentity() throws MalformedURLException { - final String name = System.getenv("YUBICO_WEBAUTHN_RP_NAME"); - final String id = System.getenv("YUBICO_WEBAUTHN_RP_ID"); - final String icon = System.getenv("YUBICO_WEBAUTHN_RP_ICON"); - - logger.debug("RP name: {}", name); - logger.debug("RP ID: {}", id); - logger.debug("RP icon: {}", icon); - - RelyingPartyIdentity.RelyingPartyIdentityBuilder resultBuilder = DEFAULT_RP_ID.toBuilder(); - - if (name == null) { - logger.debug("RP name not given - using default."); - } else { - resultBuilder.name(name); - } - - if (id == null) { - logger.debug("RP ID not given - using default."); - } else { - resultBuilder.id(id); - } - - if (icon == null) { - logger.debug("RP icon not given - using none."); - } else { - try { - resultBuilder.icon(Optional.of(new URL(icon))); - } catch (MalformedURLException e) { - logger.error("Invalid icon URL: {}", icon, e); - throw e; - } - } - - final RelyingPartyIdentity result = resultBuilder.build(); - logger.info("RP identity: {}", result); - return result; + if (id == null) { + logger.debug("RP ID not given - using default."); + } else { + resultBuilder.id(id); } - private static Optional computeAppId() throws InvalidAppIdException { - final String appId = System.getenv("YUBICO_WEBAUTHN_U2F_APPID"); - logger.debug("YUBICO_WEBAUTHN_U2F_APPID: {}", appId); + if (icon == null) { + logger.debug("RP icon not given - using none."); + } else { + try { + resultBuilder.icon(Optional.of(new URL(icon))); + } catch (MalformedURLException e) { + logger.error("Invalid icon URL: {}", icon, e); + throw e; + } + } - AppId result = appId == null - ? new AppId("https://localhost:8443") - : new AppId(appId); + final RelyingPartyIdentity result = resultBuilder.build(); + logger.info("RP identity: {}", result); + return result; + } - logger.debug("U2F AppId: {}", result.getId()); - return Optional.of(result); - } + private static Optional computeAppId() throws InvalidAppIdException { + final String appId = System.getenv("YUBICO_WEBAUTHN_U2F_APPID"); + logger.debug("YUBICO_WEBAUTHN_U2F_APPID: {}", appId); + + AppId result = appId == null ? new AppId("https://localhost:8443") : new AppId(appId); + logger.debug("U2F AppId: {}", result.getId()); + return Optional.of(result); + } } diff --git a/webauthn-server-demo/src/main/java/demo/webauthn/EmbeddedServer.java b/webauthn-server-demo/src/main/java/demo/webauthn/EmbeddedServer.java index 2a11edf69..77626b03f 100644 --- a/webauthn-server-demo/src/main/java/demo/webauthn/EmbeddedServer.java +++ b/webauthn-server-demo/src/main/java/demo/webauthn/EmbeddedServer.java @@ -41,47 +41,46 @@ import org.glassfish.jersey.servlet.ServletContainer; /** - * Standalone Java application launcher that runs the demo server with the API - * but no static resources (i.e., no web GUI) + * Standalone Java application launcher that runs the demo server with the API but no static + * resources (i.e., no web GUI) */ public class EmbeddedServer { - public static void main(String[] args) throws Exception { - final int port = Config.getPort(); + public static void main(String[] args) throws Exception { + final int port = Config.getPort(); - App app = new App(); + App app = new App(); - ResourceConfig config = new ResourceConfig(); - config.registerClasses(app.getClasses()); - config.registerInstances(app.getSingletons()); + ResourceConfig config = new ResourceConfig(); + config.registerClasses(app.getClasses()); + config.registerInstances(app.getSingletons()); - SslContextFactory ssl = new SslContextFactory("keystore.jks"); - ssl.setKeyStorePassword("p@ssw0rd"); + SslContextFactory ssl = new SslContextFactory("keystore.jks"); + ssl.setKeyStorePassword("p@ssw0rd"); - Server server = new Server(); - HttpConfiguration httpConfig = new HttpConfiguration(); - httpConfig.setSecureScheme("https"); - httpConfig.setSecurePort(port); - HttpConfiguration httpsConfig = new HttpConfiguration(httpConfig); - httpsConfig.addCustomizer(new SecureRequestCustomizer()); + Server server = new Server(); + HttpConfiguration httpConfig = new HttpConfiguration(); + httpConfig.setSecureScheme("https"); + httpConfig.setSecurePort(port); + HttpConfiguration httpsConfig = new HttpConfiguration(httpConfig); + httpsConfig.addCustomizer(new SecureRequestCustomizer()); - ServerConnector connector = new ServerConnector( + ServerConnector connector = + new ServerConnector( server, new SslConnectionFactory(ssl, HttpVersion.HTTP_1_1.asString()), - new HttpConnectionFactory(httpsConfig) - ); + new HttpConnectionFactory(httpsConfig)); - connector.setPort(port); - connector.setHost("127.0.0.1"); + connector.setPort(port); + connector.setHost("127.0.0.1"); - ServletHolder servlet = new ServletHolder(new ServletContainer(config)); - ServletContextHandler context = new ServletContextHandler(server, "/"); - context.addServlet(DefaultServlet.class, "/"); - context.setResourceBase("src/main/webapp"); - context.addServlet(servlet, "/api/*"); - - server.setConnectors(new Connector[] { connector }); - server.start(); - } + ServletHolder servlet = new ServletHolder(new ServletContainer(config)); + ServletContextHandler context = new ServletContextHandler(server, "/"); + context.addServlet(DefaultServlet.class, "/"); + context.setResourceBase("src/main/webapp"); + context.addServlet(servlet, "/api/*"); + server.setConnectors(new Connector[] {connector}); + server.start(); + } } diff --git a/webauthn-server-demo/src/main/java/demo/webauthn/InMemoryRegistrationStorage.java b/webauthn-server-demo/src/main/java/demo/webauthn/InMemoryRegistrationStorage.java index 2e6b283b1..5f97c8a12 100644 --- a/webauthn-server-demo/src/main/java/demo/webauthn/InMemoryRegistrationStorage.java +++ b/webauthn-server-demo/src/main/java/demo/webauthn/InMemoryRegistrationStorage.java @@ -48,141 +48,150 @@ @Slf4j public class InMemoryRegistrationStorage implements RegistrationStorage, CredentialRepository { - private final Cache> storage = CacheBuilder.newBuilder() - .maximumSize(1000) - .expireAfterAccess(1, TimeUnit.DAYS) - .build(); - - private Logger logger = LoggerFactory.getLogger(InMemoryRegistrationStorage.class); - - @Override - public boolean addRegistrationByUsername(String username, CredentialRegistration reg) { - try { - return storage.get(username, HashSet::new).add(reg); - } catch (ExecutionException e) { - logger.error("Failed to add registration", e); - throw new RuntimeException(e); - } + private final Cache> storage = + CacheBuilder.newBuilder().maximumSize(1000).expireAfterAccess(1, TimeUnit.DAYS).build(); + + private Logger logger = LoggerFactory.getLogger(InMemoryRegistrationStorage.class); + + @Override + public boolean addRegistrationByUsername(String username, CredentialRegistration reg) { + try { + return storage.get(username, HashSet::new).add(reg); + } catch (ExecutionException e) { + logger.error("Failed to add registration", e); + throw new RuntimeException(e); } - - @Override - public Set getCredentialIdsForUsername(String username) { - return getRegistrationsByUsername(username).stream() - .map(registration -> PublicKeyCredentialDescriptor.builder() - .id(registration.getCredential().getCredentialId()) - .build()) - .collect(Collectors.toSet()); - } - - @Override - public Collection getRegistrationsByUsername(String username) { - try { - return storage.get(username, HashSet::new); - } catch (ExecutionException e) { - logger.error("Registration lookup failed", e); - throw new RuntimeException(e); - } + } + + @Override + public Set getCredentialIdsForUsername(String username) { + return getRegistrationsByUsername(username).stream() + .map( + registration -> + PublicKeyCredentialDescriptor.builder() + .id(registration.getCredential().getCredentialId()) + .build()) + .collect(Collectors.toSet()); + } + + @Override + public Collection getRegistrationsByUsername(String username) { + try { + return storage.get(username, HashSet::new); + } catch (ExecutionException e) { + logger.error("Registration lookup failed", e); + throw new RuntimeException(e); } - - @Override - public Collection getRegistrationsByUserHandle(ByteArray userHandle) { - return storage.asMap().values().stream() - .flatMap(Collection::stream) - .filter(credentialRegistration -> - userHandle.equals(credentialRegistration.getUserIdentity().getId()) - ) - .collect(Collectors.toList()); + } + + @Override + public Collection getRegistrationsByUserHandle(ByteArray userHandle) { + return storage.asMap().values().stream() + .flatMap(Collection::stream) + .filter( + credentialRegistration -> + userHandle.equals(credentialRegistration.getUserIdentity().getId())) + .collect(Collectors.toList()); + } + + @Override + public Optional getUsernameForUserHandle(ByteArray userHandle) { + return getRegistrationsByUserHandle(userHandle).stream() + .findAny() + .map(CredentialRegistration::getUsername); + } + + @Override + public Optional getUserHandleForUsername(String username) { + return getRegistrationsByUsername(username).stream() + .findAny() + .map(reg -> reg.getUserIdentity().getId()); + } + + @Override + public void updateSignatureCount(AssertionResult result) { + CredentialRegistration registration = + getRegistrationByUsernameAndCredentialId(result.getUsername(), result.getCredentialId()) + .orElseThrow( + () -> + new NoSuchElementException( + String.format( + "Credential \"%s\" is not registered to user \"%s\"", + result.getCredentialId(), result.getUsername()))); + + Set regs = storage.getIfPresent(result.getUsername()); + regs.remove(registration); + regs.add(registration.withSignatureCount(result.getSignatureCount())); + } + + @Override + public Optional getRegistrationByUsernameAndCredentialId( + String username, ByteArray id) { + try { + return storage.get(username, HashSet::new).stream() + .filter(credReg -> id.equals(credReg.getCredential().getCredentialId())) + .findFirst(); + } catch (ExecutionException e) { + logger.error("Registration lookup failed", e); + throw new RuntimeException(e); } - - @Override - public Optional getUsernameForUserHandle(ByteArray userHandle) { - return getRegistrationsByUserHandle(userHandle).stream() - .findAny() - .map(CredentialRegistration::getUsername); + } + + @Override + public boolean removeRegistrationByUsername( + String username, CredentialRegistration credentialRegistration) { + try { + return storage.get(username, HashSet::new).remove(credentialRegistration); + } catch (ExecutionException e) { + logger.error("Failed to remove registration", e); + throw new RuntimeException(e); } - - @Override - public Optional getUserHandleForUsername(String username) { - return getRegistrationsByUsername(username).stream() - .findAny() - .map(reg -> reg.getUserIdentity().getId()); - } - - @Override - public void updateSignatureCount(AssertionResult result) { - CredentialRegistration registration = getRegistrationByUsernameAndCredentialId(result.getUsername(), result.getCredentialId()) - .orElseThrow(() -> new NoSuchElementException(String.format( - "Credential \"%s\" is not registered to user \"%s\"", - result.getCredentialId(), result.getUsername() - ))); - - Set regs = storage.getIfPresent(result.getUsername()); - regs.remove(registration); - regs.add(registration.withSignatureCount(result.getSignatureCount())); - } - - @Override - public Optional getRegistrationByUsernameAndCredentialId(String username, ByteArray id) { - try { - return storage.get(username, HashSet::new).stream() - .filter(credReg -> id.equals(credReg.getCredential().getCredentialId())) - .findFirst(); - } catch (ExecutionException e) { - logger.error("Registration lookup failed", e); - throw new RuntimeException(e); - } - } - - @Override - public boolean removeRegistrationByUsername(String username, CredentialRegistration credentialRegistration) { - try { - return storage.get(username, HashSet::new).remove(credentialRegistration); - } catch (ExecutionException e) { - logger.error("Failed to remove registration", e); - throw new RuntimeException(e); - } - } - - @Override - public boolean removeAllRegistrations(String username) { - storage.invalidate(username); - return true; - } - - @Override - public Optional lookup(ByteArray credentialId, ByteArray userHandle) { - Optional registrationMaybe = storage.asMap().values().stream() + } + + @Override + public boolean removeAllRegistrations(String username) { + storage.invalidate(username); + return true; + } + + @Override + public Optional lookup(ByteArray credentialId, ByteArray userHandle) { + Optional registrationMaybe = + storage.asMap().values().stream() .flatMap(Collection::stream) .filter(credReg -> credentialId.equals(credReg.getCredential().getCredentialId())) .findAny(); - logger.debug("lookup credential ID: {}, user handle: {}; result: {}", credentialId, userHandle, registrationMaybe); - return registrationMaybe.flatMap(registration -> + logger.debug( + "lookup credential ID: {}, user handle: {}; result: {}", + credentialId, + userHandle, + registrationMaybe); + return registrationMaybe.flatMap( + registration -> Optional.of( RegisteredCredential.builder() .credentialId(registration.getCredential().getCredentialId()) .userHandle(registration.getUserIdentity().getId()) .publicKeyCose(registration.getCredential().getPublicKeyCose()) .signatureCount(registration.getSignatureCount()) - .build() - ) - ); - } - - @Override - public Set lookupAll(ByteArray credentialId) { - return CollectionUtil.immutableSet( - storage.asMap().values().stream() - .flatMap(Collection::stream) - .filter(reg -> reg.getCredential().getCredentialId().equals(credentialId)) - .map(reg -> RegisteredCredential.builder() - .credentialId(reg.getCredential().getCredentialId()) - .userHandle(reg.getUserIdentity().getId()) - .publicKeyCose(reg.getCredential().getPublicKeyCose()) - .signatureCount(reg.getSignatureCount()) - .build() - ) - .collect(Collectors.toSet())); - } + .build())); + } + @Override + public Set lookupAll(ByteArray credentialId) { + return CollectionUtil.immutableSet( + storage.asMap().values().stream() + .flatMap(Collection::stream) + .filter(reg -> reg.getCredential().getCredentialId().equals(credentialId)) + .map( + reg -> + RegisteredCredential.builder() + .credentialId(reg.getCredential().getCredentialId()) + .userHandle(reg.getUserIdentity().getId()) + .publicKeyCose(reg.getCredential().getPublicKeyCose()) + .signatureCount(reg.getSignatureCount()) + .build()) + .collect(Collectors.toSet())); + } } 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 e0fb64718..d4b5bb91a 100644 --- a/webauthn-server-demo/src/main/java/demo/webauthn/RegistrationStorage.java +++ b/webauthn-server-demo/src/main/java/demo/webauthn/RegistrationStorage.java @@ -24,8 +24,8 @@ package demo.webauthn; -import com.yubico.webauthn.CredentialRepository; import com.yubico.webauthn.AssertionResult; +import com.yubico.webauthn.CredentialRepository; import com.yubico.webauthn.data.ByteArray; import demo.webauthn.data.CredentialRegistration; import java.util.Collection; @@ -33,19 +33,23 @@ public interface RegistrationStorage extends CredentialRepository { - boolean addRegistrationByUsername(String username, CredentialRegistration reg); + boolean addRegistrationByUsername(String username, CredentialRegistration reg); + + Collection getRegistrationsByUsername(String username); + + Optional getRegistrationByUsernameAndCredentialId( + String username, ByteArray credentialId); - Collection getRegistrationsByUsername(String username); - Optional getRegistrationByUsernameAndCredentialId(String username, ByteArray credentialId); - Collection getRegistrationsByUserHandle(ByteArray userHandle); + Collection getRegistrationsByUserHandle(ByteArray userHandle); - default boolean userExists(String username) { - return !getRegistrationsByUsername(username).isEmpty(); - } + default boolean userExists(String username) { + return !getRegistrationsByUsername(username).isEmpty(); + } - boolean removeRegistrationByUsername(String username, CredentialRegistration credentialRegistration); - boolean removeAllRegistrations(String username); + boolean removeRegistrationByUsername( + String username, CredentialRegistration credentialRegistration); - void updateSignatureCount(AssertionResult result); + boolean removeAllRegistrations(String username); + void updateSignatureCount(AssertionResult result); } diff --git a/webauthn-server-demo/src/main/java/demo/webauthn/SessionManager.java b/webauthn-server-demo/src/main/java/demo/webauthn/SessionManager.java index dccc7088a..515034802 100644 --- a/webauthn-server-demo/src/main/java/demo/webauthn/SessionManager.java +++ b/webauthn-server-demo/src/main/java/demo/webauthn/SessionManager.java @@ -11,46 +11,42 @@ public class SessionManager { - private final SecureRandom random = new SecureRandom(); - - private final Cache sessionIdsToUsers = newCache(); - private final Cache usersToSessionIds = newCache(); - - private static Cache newCache() { - return CacheBuilder.newBuilder() - .maximumSize(100) - .expireAfterAccess(5, TimeUnit.MINUTES) - .build(); - } - - /** - * @return Create a new session for the given user, or return the existing one. - */ - public ByteArray createSession(@NonNull ByteArray userHandle) throws ExecutionException { - ByteArray sessionId = usersToSessionIds.get(userHandle, () -> generateRandom(32)); - sessionIdsToUsers.put(sessionId, userHandle); - return sessionId; - } - - /** - * @return the user handle of the given session, if any. - */ - public Optional getSession(@NonNull ByteArray token) { - return Optional.ofNullable(sessionIdsToUsers.getIfPresent(token)); - } - - public boolean isSessionForUser(@NonNull ByteArray claimedUserHandle, @NonNull ByteArray token) { - return getSession(token).map(claimedUserHandle::equals).orElse(false); - } - - public boolean isSessionForUser(@NonNull ByteArray claimedUserHandle, @NonNull Optional token) { - return token.map(t -> isSessionForUser(claimedUserHandle, t)).orElse(false); - } - - private ByteArray generateRandom(int length) { - byte[] bytes = new byte[length]; - random.nextBytes(bytes); - return new ByteArray(bytes); - } - + private final SecureRandom random = new SecureRandom(); + + private final Cache sessionIdsToUsers = newCache(); + private final Cache usersToSessionIds = newCache(); + + private static Cache newCache() { + return CacheBuilder.newBuilder() + .maximumSize(100) + .expireAfterAccess(5, TimeUnit.MINUTES) + .build(); + } + + /** @return Create a new session for the given user, or return the existing one. */ + public ByteArray createSession(@NonNull ByteArray userHandle) throws ExecutionException { + ByteArray sessionId = usersToSessionIds.get(userHandle, () -> generateRandom(32)); + sessionIdsToUsers.put(sessionId, userHandle); + return sessionId; + } + + /** @return the user handle of the given session, if any. */ + public Optional getSession(@NonNull ByteArray token) { + return Optional.ofNullable(sessionIdsToUsers.getIfPresent(token)); + } + + public boolean isSessionForUser(@NonNull ByteArray claimedUserHandle, @NonNull ByteArray token) { + return getSession(token).map(claimedUserHandle::equals).orElse(false); + } + + public boolean isSessionForUser( + @NonNull ByteArray claimedUserHandle, @NonNull Optional token) { + return token.map(t -> isSessionForUser(claimedUserHandle, t)).orElse(false); + } + + private ByteArray generateRandom(int length) { + byte[] bytes = new byte[length]; + random.nextBytes(bytes); + return new ByteArray(bytes); + } } diff --git a/webauthn-server-demo/src/main/java/demo/webauthn/WebAuthnRestResource.java b/webauthn-server-demo/src/main/java/demo/webauthn/WebAuthnRestResource.java index f90ce6505..de59a9237 100644 --- a/webauthn-server-demo/src/main/java/demo/webauthn/WebAuthnRestResource.java +++ b/webauthn-server-demo/src/main/java/demo/webauthn/WebAuthnRestResource.java @@ -24,22 +24,6 @@ package demo.webauthn; -import javax.ws.rs.Consumes; -import javax.ws.rs.DELETE; -import javax.ws.rs.DefaultValue; -import javax.ws.rs.FormParam; -import javax.ws.rs.GET; -import javax.ws.rs.POST; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.Produces; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.Response.ResponseBuilder; -import javax.ws.rs.core.Response.Status; -import javax.ws.rs.core.UriInfo; - import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; @@ -53,7 +37,6 @@ import com.yubico.webauthn.meta.VersionInfo; import demo.webauthn.WebAuthnServer.DeregisterCredentialResult; import demo.webauthn.data.AssertionRequestWrapper; -import demo.webauthn.data.CredentialRegistration; import demo.webauthn.data.RegistrationRequest; import java.io.IOException; import java.net.MalformedURLException; @@ -64,6 +47,20 @@ import java.util.Optional; import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.DefaultValue; +import javax.ws.rs.FormParam; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.ResponseBuilder; +import javax.ws.rs.core.Response.Status; +import javax.ws.rs.core.UriInfo; import lombok.NonNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -71,314 +68,320 @@ @Path("/v1") @Produces(MediaType.APPLICATION_JSON) public class WebAuthnRestResource { - private static final Logger logger = LoggerFactory.getLogger(WebAuthnRestResource.class); + private static final Logger logger = LoggerFactory.getLogger(WebAuthnRestResource.class); - private final WebAuthnServer server; - private final ObjectMapper jsonMapper = JacksonCodecs.json(); - private final JsonNodeFactory jsonFactory = JsonNodeFactory.instance; + private final WebAuthnServer server; + private final ObjectMapper jsonMapper = JacksonCodecs.json(); + private final JsonNodeFactory jsonFactory = JsonNodeFactory.instance; - public WebAuthnRestResource() throws InvalidAppIdException, CertificateException { - this(new WebAuthnServer()); - } + public WebAuthnRestResource() throws InvalidAppIdException, CertificateException { + this(new WebAuthnServer()); + } - public WebAuthnRestResource(WebAuthnServer server) { - this.server = server; - } + public WebAuthnRestResource(WebAuthnServer server) { + this.server = server; + } - @Context - private UriInfo uriInfo; + @Context private UriInfo uriInfo; - private final class IndexResponse { - public final Index actions = new Index(); - public final Info info = new Info(); - private IndexResponse() throws MalformedURLException { - } - } - private final class Index { - public final URL authenticate; - public final URL deleteAccount; - public final URL deregister; - public final URL register; - - - public Index() throws MalformedURLException { - authenticate = uriInfo.getAbsolutePathBuilder().path("authenticate").build().toURL(); - deleteAccount = uriInfo.getAbsolutePathBuilder().path("delete-account").build().toURL(); - deregister = uriInfo.getAbsolutePathBuilder().path("action").path("deregister").build().toURL(); - register = uriInfo.getAbsolutePathBuilder().path("register").build().toURL(); - } - } - private final class Info { - public final URL version; - - public Info() throws MalformedURLException { - version = uriInfo.getAbsolutePathBuilder().path("version").build().toURL(); - } + private final class IndexResponse { + public final Index actions = new Index(); + public final Info info = new Info(); - } + private IndexResponse() throws MalformedURLException {} + } - @GET - public Response index() throws IOException { - return Response.ok(writeJson(new IndexResponse())).build(); - } + private final class Index { + public final URL authenticate; + public final URL deleteAccount; + public final URL deregister; + public final URL register; - private static final class VersionResponse { - public final VersionInfo version = VersionInfo.getInstance(); - } - @GET - @Path("version") - public Response version() throws JsonProcessingException { - return Response.ok(writeJson(new VersionResponse())).build(); + public Index() throws MalformedURLException { + authenticate = uriInfo.getAbsolutePathBuilder().path("authenticate").build().toURL(); + deleteAccount = uriInfo.getAbsolutePathBuilder().path("delete-account").build().toURL(); + deregister = + uriInfo.getAbsolutePathBuilder().path("action").path("deregister").build().toURL(); + register = uriInfo.getAbsolutePathBuilder().path("register").build().toURL(); } + } + private final class Info { + public final URL version; - private final class StartRegistrationResponse { - public final boolean success = true; - public final RegistrationRequest request; - public final StartRegistrationActions actions = new StartRegistrationActions(); - private StartRegistrationResponse(RegistrationRequest request) throws MalformedURLException { - this.request = request; - } + public Info() throws MalformedURLException { + version = uriInfo.getAbsolutePathBuilder().path("version").build().toURL(); } - private final class StartRegistrationActions { - public final URL finish = uriInfo.getAbsolutePathBuilder().path("finish").build().toURL(); - public final URL finishU2f = uriInfo.getAbsolutePathBuilder().path("finish-u2f").build().toURL(); - private StartRegistrationActions() throws MalformedURLException { - } + } + + @GET + public Response index() throws IOException { + return Response.ok(writeJson(new IndexResponse())).build(); + } + + private static final class VersionResponse { + public final VersionInfo version = VersionInfo.getInstance(); + } + + @GET + @Path("version") + public Response version() throws JsonProcessingException { + return Response.ok(writeJson(new VersionResponse())).build(); + } + + private final class StartRegistrationResponse { + public final boolean success = true; + public final RegistrationRequest request; + public final StartRegistrationActions actions = new StartRegistrationActions(); + + private StartRegistrationResponse(RegistrationRequest request) throws MalformedURLException { + this.request = request; } - - @Consumes("application/x-www-form-urlencoded") - @Path("register") - @POST - public Response startRegistration( - @NonNull @FormParam("username") String username, - @NonNull @FormParam("displayName") String displayName, - @FormParam("credentialNickname") String credentialNickname, - @FormParam("requireResidentKey") @DefaultValue("false") boolean requireResidentKey, - @FormParam("sessionToken") String sessionTokenBase64 - ) throws MalformedURLException, ExecutionException { - logger.trace("startRegistration username: {}, displayName: {}, credentialNickname: {}, requireResidentKey: {}", username, displayName, credentialNickname, requireResidentKey); - Either result = server.startRegistration( + } + + private final class StartRegistrationActions { + public final URL finish = uriInfo.getAbsolutePathBuilder().path("finish").build().toURL(); + public final URL finishU2f = + uriInfo.getAbsolutePathBuilder().path("finish-u2f").build().toURL(); + + private StartRegistrationActions() throws MalformedURLException {} + } + + @Consumes("application/x-www-form-urlencoded") + @Path("register") + @POST + public Response startRegistration( + @NonNull @FormParam("username") String username, + @NonNull @FormParam("displayName") String displayName, + @FormParam("credentialNickname") String credentialNickname, + @FormParam("requireResidentKey") @DefaultValue("false") boolean requireResidentKey, + @FormParam("sessionToken") String sessionTokenBase64) + throws MalformedURLException, ExecutionException { + logger.trace( + "startRegistration username: {}, displayName: {}, credentialNickname: {}, requireResidentKey: {}", + username, + displayName, + credentialNickname, + requireResidentKey); + Either result = + server.startRegistration( username, Optional.of(displayName), Optional.ofNullable(credentialNickname), requireResidentKey, - Optional.ofNullable(sessionTokenBase64).map(base64 -> { - try { - return ByteArray.fromBase64Url(base64); - } catch (Base64UrlException e) { - throw new RuntimeException(e); - } - }) - ); - - if (result.isRight()) { - return startResponse("startRegistration", new StartRegistrationResponse(result.right().get())); - } else { - return messagesJson( - Response.status(Status.BAD_REQUEST), - result.left().get() - ); - } - } - - @Path("register/finish") - @POST - public Response finishRegistration(@NonNull String responseJson) { - logger.trace("finishRegistration responseJson: {}", responseJson); - Either, WebAuthnServer.SuccessfulRegistrationResult> result = server.finishRegistration(responseJson); - return finishResponse( - result, - "Attestation verification failed; further error message(s) were unfortunately lost to an internal server error.", - "finishRegistration", - responseJson - ); - } - - @Path("register/finish-u2f") - @POST - public Response finishU2fRegistration(@NonNull String responseJson) throws ExecutionException { - logger.trace("finishRegistration responseJson: {}", responseJson); - Either, WebAuthnServer.SuccessfulU2fRegistrationResult> result = server.finishU2fRegistration(responseJson); - return finishResponse( - result, - "U2F registration failed; further error message(s) were unfortunately lost to an internal server error.", - "finishU2fRegistration", - responseJson - ); + Optional.ofNullable(sessionTokenBase64) + .map( + base64 -> { + try { + return ByteArray.fromBase64Url(base64); + } catch (Base64UrlException e) { + throw new RuntimeException(e); + } + })); + + if (result.isRight()) { + return startResponse( + "startRegistration", new StartRegistrationResponse(result.right().get())); + } else { + return messagesJson(Response.status(Status.BAD_REQUEST), result.left().get()); } - - private final class StartAuthenticationResponse { - public final boolean success = true; - public final AssertionRequestWrapper request; - public final StartAuthenticationActions actions = new StartAuthenticationActions(); - private StartAuthenticationResponse(AssertionRequestWrapper request) throws MalformedURLException { - this.request = request; - } - } - private final class StartAuthenticationActions { - public final URL finish = uriInfo.getAbsolutePathBuilder().path("finish").build().toURL(); - private StartAuthenticationActions() throws MalformedURLException { - } - } - - @Consumes("application/x-www-form-urlencoded") - @Path("authenticate") - @POST - public Response startAuthentication( - @FormParam("username") String username - ) throws MalformedURLException { - logger.trace("startAuthentication username: {}", username); - Either, AssertionRequestWrapper> request = server.startAuthentication(Optional.ofNullable(username)); - if (request.isRight()) { - return startResponse("startAuthentication", new StartAuthenticationResponse(request.right().get())); - } else { - return messagesJson(Response.status(Status.BAD_REQUEST), request.left().get()); - } + } + + @Path("register/finish") + @POST + public Response finishRegistration(@NonNull String responseJson) { + logger.trace("finishRegistration responseJson: {}", responseJson); + Either, WebAuthnServer.SuccessfulRegistrationResult> result = + server.finishRegistration(responseJson); + return finishResponse( + result, + "Attestation verification failed; further error message(s) were unfortunately lost to an internal server error.", + "finishRegistration", + responseJson); + } + + @Path("register/finish-u2f") + @POST + public Response finishU2fRegistration(@NonNull String responseJson) throws ExecutionException { + logger.trace("finishRegistration responseJson: {}", responseJson); + Either, WebAuthnServer.SuccessfulU2fRegistrationResult> result = + server.finishU2fRegistration(responseJson); + return finishResponse( + result, + "U2F registration failed; further error message(s) were unfortunately lost to an internal server error.", + "finishU2fRegistration", + responseJson); + } + + private final class StartAuthenticationResponse { + public final boolean success = true; + public final AssertionRequestWrapper request; + public final StartAuthenticationActions actions = new StartAuthenticationActions(); + + private StartAuthenticationResponse(AssertionRequestWrapper request) + throws MalformedURLException { + this.request = request; } - - @Path("authenticate/finish") - @POST - public Response finishAuthentication(@NonNull String responseJson) { - logger.trace("finishAuthentication responseJson: {}", responseJson); - - Either, WebAuthnServer.SuccessfulAuthenticationResult> result = server.finishAuthentication(responseJson); - - return finishResponse( - result, - "Authentication verification failed; further error message(s) were unfortunately lost to an internal server error.", - "finishAuthentication", - responseJson - ); + } + + private final class StartAuthenticationActions { + public final URL finish = uriInfo.getAbsolutePathBuilder().path("finish").build().toURL(); + + private StartAuthenticationActions() throws MalformedURLException {} + } + + @Consumes("application/x-www-form-urlencoded") + @Path("authenticate") + @POST + public Response startAuthentication(@FormParam("username") String username) + throws MalformedURLException { + logger.trace("startAuthentication username: {}", username); + Either, AssertionRequestWrapper> request = + server.startAuthentication(Optional.ofNullable(username)); + if (request.isRight()) { + return startResponse( + "startAuthentication", new StartAuthenticationResponse(request.right().get())); + } else { + return messagesJson(Response.status(Status.BAD_REQUEST), request.left().get()); } - - @Path("action/deregister") - @POST - public Response deregisterCredential( - @NonNull @FormParam("sessionToken") String sessionTokenBase64, - @NonNull @FormParam("credentialId") String credentialIdBase64 - ) throws MalformedURLException, Base64UrlException { - logger.trace("deregisterCredential sesion: {}, credentialId: {}", sessionTokenBase64, credentialIdBase64); - - final ByteArray credentialId; - try { - credentialId = ByteArray.fromBase64Url(credentialIdBase64); - } catch (Base64UrlException e) { - return messagesJson( - Response.status(Status.BAD_REQUEST), - "Credential ID is not valid Base64Url data: " + credentialIdBase64 - ); - } - - Either, DeregisterCredentialResult> result = server.deregisterCredential( - ByteArray.fromBase64Url(sessionTokenBase64), - credentialId - ); - - if (result.isRight()) { - return finishResponse( - result, - "Failed to deregister credential; further error message(s) were unfortunately lost to an internal server error.", - "deregisterCredential", - "" - ); - } else { - return messagesJson( - Response.status(Status.BAD_REQUEST), - result.left().get() - ); - } + } + + @Path("authenticate/finish") + @POST + public Response finishAuthentication(@NonNull String responseJson) { + logger.trace("finishAuthentication responseJson: {}", responseJson); + + Either, WebAuthnServer.SuccessfulAuthenticationResult> result = + server.finishAuthentication(responseJson); + + return finishResponse( + result, + "Authentication verification failed; further error message(s) were unfortunately lost to an internal server error.", + "finishAuthentication", + responseJson); + } + + @Path("action/deregister") + @POST + public Response deregisterCredential( + @NonNull @FormParam("sessionToken") String sessionTokenBase64, + @NonNull @FormParam("credentialId") String credentialIdBase64) + throws MalformedURLException, Base64UrlException { + logger.trace( + "deregisterCredential sesion: {}, credentialId: {}", + sessionTokenBase64, + credentialIdBase64); + + final ByteArray credentialId; + try { + credentialId = ByteArray.fromBase64Url(credentialIdBase64); + } catch (Base64UrlException e) { + return messagesJson( + Response.status(Status.BAD_REQUEST), + "Credential ID is not valid Base64Url data: " + credentialIdBase64); } - @Path("delete-account") - @DELETE - public Response deleteAccount( - @NonNull @FormParam("username") String username - ) { - logger.trace("deleteAccount username: {}", username); - - Either, JsonNode> result = server.deleteAccount(username, () -> - ((ObjectNode) jsonFactory.objectNode() - .set("success", jsonFactory.booleanNode(true))) - .set("deletedAccount", jsonFactory.textNode(username)) - ); - - if (result.isRight()) { - return Response.ok(result.right().get().toString()).build(); - } else { - return messagesJson( - Response.status(Status.BAD_REQUEST), - result.left().get() - ); - } + Either, DeregisterCredentialResult> result = + server.deregisterCredential(ByteArray.fromBase64Url(sessionTokenBase64), credentialId); + + if (result.isRight()) { + return finishResponse( + result, + "Failed to deregister credential; further error message(s) were unfortunately lost to an internal server error.", + "deregisterCredential", + ""); + } else { + return messagesJson(Response.status(Status.BAD_REQUEST), result.left().get()); } + } - private Response startResponse(String operationName, Object request) { - try { - String json = writeJson(request); - logger.debug("{} JSON response: {}", operationName, json); - return Response.ok(json).build(); - } catch (IOException e) { - logger.error("Failed to encode response as JSON: {}", request, e); - return jsonFail(); - } - } + @Path("delete-account") + @DELETE + public Response deleteAccount(@NonNull @FormParam("username") String username) { + logger.trace("deleteAccount username: {}", username); - private Response finishResponse(Either, ?> result, String jsonFailMessage, String methodName, String responseJson) { - if (result.isRight()) { - try { - return Response.ok( - writeJson(result.right().get()) - ).build(); - } catch (JsonProcessingException e) { - logger.error("Failed to encode response as JSON: {}", result.right().get(), e); - return messagesJson( - Response.ok(), - jsonFailMessage - ); - } - } else { - logger.debug("fail {} responseJson: {}", methodName, responseJson); - return messagesJson( - Response.status(Status.BAD_REQUEST), - result.left().get() - ); - } + Either, JsonNode> result = + server.deleteAccount( + username, + () -> + ((ObjectNode) + jsonFactory.objectNode().set("success", jsonFactory.booleanNode(true))) + .set("deletedAccount", jsonFactory.textNode(username))); + + if (result.isRight()) { + return Response.ok(result.right().get().toString()).build(); + } else { + return messagesJson(Response.status(Status.BAD_REQUEST), result.left().get()); } - - private Response jsonFail() { - return Response.status(Status.INTERNAL_SERVER_ERROR) - .entity("{\"messages\":[\"Failed to encode response as JSON\"]}") - .build(); + } + + private Response startResponse(String operationName, Object request) { + try { + String json = writeJson(request); + logger.debug("{} JSON response: {}", operationName, json); + return Response.ok(json).build(); + } catch (IOException e) { + logger.error("Failed to encode response as JSON: {}", request, e); + return jsonFail(); } - - private Response messagesJson(ResponseBuilder response, String message) { - return messagesJson(response, Arrays.asList(message)); + } + + private Response finishResponse( + Either, ?> result, + String jsonFailMessage, + String methodName, + String responseJson) { + if (result.isRight()) { + try { + return Response.ok(writeJson(result.right().get())).build(); + } catch (JsonProcessingException e) { + logger.error("Failed to encode response as JSON: {}", result.right().get(), e); + return messagesJson(Response.ok(), jsonFailMessage); + } + } else { + logger.debug("fail {} responseJson: {}", methodName, responseJson); + return messagesJson(Response.status(Status.BAD_REQUEST), result.left().get()); } - - private Response messagesJson(ResponseBuilder response, List messages) { - logger.debug("Encoding messages as JSON: {}", messages); - try { - return response.entity( - writeJson( - jsonFactory.objectNode() - .set("messages", jsonFactory.arrayNode() - .addAll(messages.stream().map(jsonFactory::textNode).collect(Collectors.toList())) - ) - ) - ).build(); - } catch (JsonProcessingException e) { - logger.error("Failed to encode messages as JSON: {}", messages, e); - return jsonFail(); - } + } + + private Response jsonFail() { + return Response.status(Status.INTERNAL_SERVER_ERROR) + .entity("{\"messages\":[\"Failed to encode response as JSON\"]}") + .build(); + } + + private Response messagesJson(ResponseBuilder response, String message) { + return messagesJson(response, Arrays.asList(message)); + } + + private Response messagesJson(ResponseBuilder response, List messages) { + logger.debug("Encoding messages as JSON: {}", messages); + try { + return response + .entity( + writeJson( + jsonFactory + .objectNode() + .set( + "messages", + jsonFactory + .arrayNode() + .addAll( + messages.stream() + .map(jsonFactory::textNode) + .collect(Collectors.toList()))))) + .build(); + } catch (JsonProcessingException e) { + logger.error("Failed to encode messages as JSON: {}", messages, e); + return jsonFail(); } + } - private String writeJson(Object o) throws JsonProcessingException { - if (uriInfo.getQueryParameters().keySet().contains("pretty")) { - return jsonMapper.writerWithDefaultPrettyPrinter().writeValueAsString(o); - } else { - return jsonMapper.writeValueAsString(o); - } + private String writeJson(Object o) throws JsonProcessingException { + if (uriInfo.getQueryParameters().keySet().contains("pretty")) { + return jsonMapper.writerWithDefaultPrettyPrinter().writeValueAsString(o); + } else { + return jsonMapper.writeValueAsString(o); } - + } } 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 050b880bc..24b59a88a 100644 --- a/webauthn-server-demo/src/main/java/demo/webauthn/WebAuthnServer.java +++ b/webauthn-server-demo/src/main/java/demo/webauthn/WebAuthnServer.java @@ -101,44 +101,57 @@ import org.slf4j.LoggerFactory; public class WebAuthnServer { - private static final Logger logger = LoggerFactory.getLogger(WebAuthnServer.class); - private static final SecureRandom random = new SecureRandom(); - - private static final String PREVIEW_METADATA_PATH = "/preview-metadata.json"; - - private final Cache assertRequestStorage; - private final Cache registerRequestStorage; - private final RegistrationStorage userStorage; - private final SessionManager sessions = new SessionManager(); - - - private final TrustResolver trustResolver = new CompositeTrustResolver(Arrays.asList( - StandardMetadataService.createDefaultTrustResolver(), - createExtraTrustResolver() - )); - - private final MetadataService metadataService = new StandardMetadataService( - new CompositeAttestationResolver(Arrays.asList( - StandardMetadataService.createDefaultAttestationResolver(trustResolver), - createExtraMetadataResolver(trustResolver) - )) - ); - - private final Clock clock = Clock.systemDefaultZone(); - private final ObjectMapper jsonMapper = JacksonCodecs.json(); - - private final RelyingParty rp; - - public WebAuthnServer() throws InvalidAppIdException, CertificateException { - this(new InMemoryRegistrationStorage(), newCache(), newCache(), Config.getRpIdentity(), Config.getOrigins(), Config.getAppId()); - } - - public WebAuthnServer(RegistrationStorage userStorage, Cache registerRequestStorage, Cache assertRequestStorage, RelyingPartyIdentity rpIdentity, Set origins, Optional appId) throws InvalidAppIdException, CertificateException { - this.userStorage = userStorage; - this.registerRequestStorage = registerRequestStorage; - this.assertRequestStorage = assertRequestStorage; - - rp = RelyingParty.builder() + private static final Logger logger = LoggerFactory.getLogger(WebAuthnServer.class); + private static final SecureRandom random = new SecureRandom(); + + private static final String PREVIEW_METADATA_PATH = "/preview-metadata.json"; + + private final Cache assertRequestStorage; + private final Cache registerRequestStorage; + private final RegistrationStorage userStorage; + private final SessionManager sessions = new SessionManager(); + + private final TrustResolver trustResolver = + new CompositeTrustResolver( + Arrays.asList( + StandardMetadataService.createDefaultTrustResolver(), createExtraTrustResolver())); + + private final MetadataService metadataService = + new StandardMetadataService( + new CompositeAttestationResolver( + Arrays.asList( + StandardMetadataService.createDefaultAttestationResolver(trustResolver), + createExtraMetadataResolver(trustResolver)))); + + private final Clock clock = Clock.systemDefaultZone(); + private final ObjectMapper jsonMapper = JacksonCodecs.json(); + + private final RelyingParty rp; + + public WebAuthnServer() throws InvalidAppIdException, CertificateException { + this( + new InMemoryRegistrationStorage(), + newCache(), + newCache(), + Config.getRpIdentity(), + Config.getOrigins(), + Config.getAppId()); + } + + public WebAuthnServer( + RegistrationStorage userStorage, + Cache registerRequestStorage, + Cache assertRequestStorage, + RelyingPartyIdentity rpIdentity, + Set origins, + Optional appId) + throws InvalidAppIdException, CertificateException { + this.userStorage = userStorage; + this.registerRequestStorage = registerRequestStorage; + this.assertRequestStorage = assertRequestStorage; + + rp = + RelyingParty.builder() .identity(rpIdentity) .credentialRepository(this.userStorage) .origins(origins) @@ -151,529 +164,589 @@ public WebAuthnServer(RegistrationStorage userStorage, Cache Cache newCache() { + return CacheBuilder.newBuilder() + .maximumSize(100) + .expireAfterAccess(10, TimeUnit.MINUTES) + .build(); + } + + public Either startRegistration( + @NonNull String username, + Optional displayName, + Optional credentialNickname, + boolean requireResidentKey, + Optional sessionToken) + throws ExecutionException { + logger.trace( + "startRegistration username: {}, credentialNickname: {}", username, credentialNickname); + + final Collection registrations = + userStorage.getRegistrationsByUsername(username); + final Optional existingUser = + registrations.stream().findAny().map(CredentialRegistration::getUserIdentity); + final boolean permissionGranted = + existingUser + .map(userIdentity -> sessions.isSessionForUser(userIdentity.getId(), sessionToken)) + .orElse(true); - /** - * Create a {@link TrustResolver} that accepts attestation certificates that are directly recognised as trust anchors. - */ - private static TrustResolver createExtraTrustResolver() { - try { - MetadataObject metadata = readPreviewMetadata(); - return new SimpleTrustResolverWithEquality(metadata.getParsedTrustedCertificates()); - } catch (CertificateException e) { - throw ExceptionUtil.wrapAndLog(logger, "Failed to read trusted certificate(s)", e); - } + if (permissionGranted) { + final UserIdentity registrationUserId = + existingUser.orElseGet( + () -> + UserIdentity.builder() + .name(username) + .displayName(displayName.get()) + .id(generateRandom(32)) + .build()); + + RegistrationRequest request = + new RegistrationRequest( + username, + credentialNickname, + generateRandom(32), + rp.startRegistration( + StartRegistrationOptions.builder() + .user(registrationUserId) + .authenticatorSelection( + AuthenticatorSelectionCriteria.builder() + .requireResidentKey(requireResidentKey) + .build()) + .build()), + Optional.of(sessions.createSession(registrationUserId.getId()))); + registerRequestStorage.put(request.getRequestId(), request); + return Either.right(request); + } else { + return Either.left("The username \"" + username + "\" is already registered."); } + } - /** - * Create a {@link AttestationResolver} with additional metadata for unreleased YubiKey Preview devices. - */ - private static AttestationResolver createExtraMetadataResolver(TrustResolver trustResolver) { - try { - MetadataObject metadata = readPreviewMetadata(); - return new SimpleAttestationResolver(Collections.singleton(metadata), trustResolver); - } catch (CertificateException e) { - throw ExceptionUtil.wrapAndLog(logger, "Failed to read trusted certificate(s)", e); - } - } + @Value + public static class SuccessfulRegistrationResult { + final boolean success = true; + RegistrationRequest request; + RegistrationResponse response; + CredentialRegistration registration; + boolean attestationTrusted; + Optional attestationCert; - private static Cache newCache() { - return CacheBuilder.newBuilder() - .maximumSize(100) - .expireAfterAccess(10, TimeUnit.MINUTES) - .build(); - } + @JsonSerialize(using = AuthDataSerializer.class) + AuthenticatorData authData; - public Either startRegistration( - @NonNull String username, - Optional displayName, - Optional credentialNickname, - boolean requireResidentKey, - Optional sessionToken - ) throws ExecutionException { - logger.trace("startRegistration username: {}, credentialNickname: {}", username, credentialNickname); - - final Collection registrations = userStorage.getRegistrationsByUsername(username); - final Optional existingUser = - registrations.stream().findAny().map(CredentialRegistration::getUserIdentity); - final boolean permissionGranted = existingUser - .map(userIdentity -> - sessions.isSessionForUser(userIdentity.getId(), sessionToken)) - .orElse(true); + String username; + ByteArray sessionToken; - if (permissionGranted) { - final UserIdentity registrationUserId = existingUser.orElseGet(() -> - UserIdentity.builder() - .name(username) - .displayName(displayName.get()) - .id(generateRandom(32)) - .build() - ); - - RegistrationRequest request = new RegistrationRequest( - username, - credentialNickname, - generateRandom(32), - rp.startRegistration( - StartRegistrationOptions.builder() - .user(registrationUserId) - .authenticatorSelection(AuthenticatorSelectionCriteria.builder() - .requireResidentKey(requireResidentKey) - .build() - ) - .build() - ), - Optional.of(sessions.createSession(registrationUserId.getId())) - ); - registerRequestStorage.put(request.getRequestId(), request); - return Either.right(request); - } else { - return Either.left("The username \"" + username + "\" is already registered."); - } + public SuccessfulRegistrationResult( + RegistrationRequest request, + RegistrationResponse response, + CredentialRegistration registration, + boolean attestationTrusted, + ByteArray sessionToken) { + this.request = request; + this.response = response; + this.registration = registration; + this.attestationTrusted = attestationTrusted; + attestationCert = + Optional.ofNullable( + response + .getCredential() + .getResponse() + .getAttestation() + .getAttestationStatement() + .get("x5c")) + .map(certs -> certs.get(0)) + .flatMap( + (JsonNode certDer) -> { + try { + return Optional.of(new ByteArray(certDer.binaryValue())); + } catch (IOException e) { + logger.error("Failed to get binary value from x5c element: {}", certDer, e); + return Optional.empty(); + } + }) + .map(AttestationCertInfo::new); + this.authData = response.getCredential().getResponse().getParsedAuthenticatorData(); + this.username = request.getUsername(); + this.sessionToken = sessionToken; + } + } + + @Value + public class SuccessfulU2fRegistrationResult { + final boolean success = true; + final RegistrationRequest request; + final U2fRegistrationResponse response; + final CredentialRegistration registration; + boolean attestationTrusted; + Optional attestationCert; + final String username; + final ByteArray sessionToken; + } + + @Value + public static class AttestationCertInfo { + final ByteArray der; + final String text; + + public AttestationCertInfo(ByteArray certDer) { + der = certDer; + X509Certificate cert = null; + try { + cert = CertificateParser.parseDer(certDer.getBytes()); + } catch (CertificateException e) { + logger.error("Failed to parse attestation certificate"); + } + if (cert == null) { + text = null; + } else { + text = cert.toString(); + } + } + } + + public Either, SuccessfulRegistrationResult> finishRegistration( + String responseJson) { + logger.trace("finishRegistration responseJson: {}", responseJson); + RegistrationResponse response = null; + try { + response = jsonMapper.readValue(responseJson, RegistrationResponse.class); + } catch (IOException e) { + logger.error("JSON error in finishRegistration; responseJson: {}", responseJson, e); + return Either.left( + Arrays.asList( + "Registration failed!", "Failed to decode response object.", e.getMessage())); } - @Value - public static class SuccessfulRegistrationResult { - final boolean success = true; - RegistrationRequest request; - RegistrationResponse response; - CredentialRegistration registration; - boolean attestationTrusted; - Optional attestationCert; - @JsonSerialize(using = AuthDataSerializer.class) - AuthenticatorData authData; - String username; - ByteArray sessionToken; - - public SuccessfulRegistrationResult(RegistrationRequest request, RegistrationResponse response, CredentialRegistration registration, boolean attestationTrusted, ByteArray sessionToken) { - this.request = request; - this.response = response; - this.registration = registration; - this.attestationTrusted = attestationTrusted; - attestationCert = Optional.ofNullable( - response.getCredential().getResponse().getAttestation().getAttestationStatement().get("x5c") - ).map(certs -> certs.get(0)) - .flatMap((JsonNode certDer) -> { - try { - return Optional.of(new ByteArray(certDer.binaryValue())); - } catch (IOException e) { - logger.error("Failed to get binary value from x5c element: {}", certDer, e); - return Optional.empty(); - } - }) - .map(AttestationCertInfo::new); - this.authData = response.getCredential().getResponse().getParsedAuthenticatorData(); - this.username = request.getUsername(); - this.sessionToken = sessionToken; + RegistrationRequest request = registerRequestStorage.getIfPresent(response.getRequestId()); + registerRequestStorage.invalidate(response.getRequestId()); + + if (request == null) { + logger.debug("fail finishRegistration responseJson: {}", responseJson); + return Either.left( + Arrays.asList("Registration failed!", "No such registration in progress.")); + } else { + try { + com.yubico.webauthn.RegistrationResult registration = + rp.finishRegistration( + FinishRegistrationOptions.builder() + .request(request.getPublicKeyCredentialCreationOptions()) + .response(response.getCredential()) + .build()); + + if (userStorage.userExists(request.getUsername())) { + boolean permissionGranted = false; + + final boolean isValidSession = + request + .getSessionToken() + .map( + token -> + sessions.isSessionForUser( + request.getPublicKeyCredentialCreationOptions().getUser().getId(), + token)) + .orElse(false); + + logger.debug("Session token: {}", request.getSessionToken()); + logger.debug("Valid session: {}", isValidSession); + + if (isValidSession) { + permissionGranted = true; + logger.info( + "Session token accepted for user {}", + request.getPublicKeyCredentialCreationOptions().getUser().getId()); + } + + logger.debug("permissionGranted: {}", permissionGranted); + + if (!permissionGranted) { + throw new RegistrationFailedException( + new IllegalArgumentException( + String.format("User %s already exists", request.getUsername()))); + } } + return Either.right( + new SuccessfulRegistrationResult( + request, + response, + addRegistration( + request.getPublicKeyCredentialCreationOptions().getUser(), + request.getCredentialNickname(), + response, + registration), + registration.isAttestationTrusted(), + sessions.createSession( + request.getPublicKeyCredentialCreationOptions().getUser().getId()))); + } catch (RegistrationFailedException e) { + logger.debug("fail finishRegistration responseJson: {}", responseJson, e); + return Either.left(Arrays.asList("Registration failed!", e.getMessage())); + } catch (Exception e) { + logger.error("fail finishRegistration responseJson: {}", responseJson, e); + return Either.left( + Arrays.asList( + "Registration failed unexpectedly; this is likely a bug.", e.getMessage())); + } } - - @Value - public class SuccessfulU2fRegistrationResult { - final boolean success = true; - final RegistrationRequest request; - final U2fRegistrationResponse response; - final CredentialRegistration registration; - boolean attestationTrusted; - Optional attestationCert; - final String username; - final ByteArray sessionToken; + } + + public Either, SuccessfulU2fRegistrationResult> finishU2fRegistration( + String responseJson) throws ExecutionException { + logger.trace("finishU2fRegistration responseJson: {}", responseJson); + U2fRegistrationResponse response = null; + try { + response = jsonMapper.readValue(responseJson, U2fRegistrationResponse.class); + } catch (IOException e) { + logger.error("JSON error in finishU2fRegistration; responseJson: {}", responseJson, e); + return Either.left( + Arrays.asList( + "Registration failed!", "Failed to decode response object.", e.getMessage())); } - @Value - public static class AttestationCertInfo { - final ByteArray der; - final String text; - public AttestationCertInfo(ByteArray certDer) { - der = certDer; - X509Certificate cert = null; - try { - cert = CertificateParser.parseDer(certDer.getBytes()); - } catch (CertificateException e) { - logger.error("Failed to parse attestation certificate"); - } - if (cert == null) { - text = null; - } else { - text = cert.toString(); - } + RegistrationRequest request = registerRequestStorage.getIfPresent(response.getRequestId()); + registerRequestStorage.invalidate(response.getRequestId()); + + if (request == null) { + logger.debug("fail finishU2fRegistration responseJson: {}", responseJson); + return Either.left( + Arrays.asList("Registration failed!", "No such registration in progress.")); + } else { + + try { + ExceptionUtil.assure( + U2fVerifier.verify(rp.getAppId().get(), request, response), + "Failed to verify signature."); + } catch (Exception e) { + logger.debug("Failed to verify U2F signature.", e); + return Either.left(Arrays.asList("Failed to verify signature.", e.getMessage())); + } + + X509Certificate attestationCert = null; + try { + attestationCert = + CertificateParser.parseDer( + response + .getCredential() + .getU2fResponse() + .getAttestationCertAndSignature() + .getBytes()); + } catch (CertificateException e) { + logger.error( + "Failed to parse attestation certificate: {}", + response.getCredential().getU2fResponse().getAttestationCertAndSignature(), + e); + } + + Optional attestation = Optional.empty(); + try { + if (attestationCert != null) { + attestation = + Optional.of( + metadataService.getAttestation(Collections.singletonList(attestationCert))); } + } catch (CertificateEncodingException e) { + logger.error("Failed to resolve attestation", e); + } + + final U2fRegistrationResult result = + U2fRegistrationResult.builder() + .keyId( + PublicKeyCredentialDescriptor.builder() + .id(response.getCredential().getU2fResponse().getKeyHandle()) + .build()) + .attestationTrusted(attestation.map(Attestation::isTrusted).orElse(false)) + .publicKeyCose( + rawEcdaKeyToCose(response.getCredential().getU2fResponse().getPublicKey())) + .attestationMetadata(attestation) + .build(); + + return Either.right( + new SuccessfulU2fRegistrationResult( + request, + response, + addRegistration( + request.getPublicKeyCredentialCreationOptions().getUser(), + request.getCredentialNickname(), + 0, + result), + result.isAttestationTrusted(), + Optional.of( + new AttestationCertInfo( + response.getCredential().getU2fResponse().getAttestationCertAndSignature())), + request.getUsername(), + sessions.createSession( + request.getPublicKeyCredentialCreationOptions().getUser().getId()))); } + } - public Either, SuccessfulRegistrationResult> finishRegistration(String responseJson) { - logger.trace("finishRegistration responseJson: {}", responseJson); - RegistrationResponse response = null; - try { - response = jsonMapper.readValue(responseJson, RegistrationResponse.class); - } catch (IOException e) { - logger.error("JSON error in finishRegistration; responseJson: {}", responseJson, e); - return Either.left(Arrays.asList("Registration failed!", "Failed to decode response object.", e.getMessage())); - } - - RegistrationRequest request = registerRequestStorage.getIfPresent(response.getRequestId()); - registerRequestStorage.invalidate(response.getRequestId()); + public Either, AssertionRequestWrapper> startAuthentication( + Optional username) { + logger.trace("startAuthentication username: {}", username); - if (request == null) { - logger.debug("fail finishRegistration responseJson: {}", responseJson); - return Either.left(Arrays.asList("Registration failed!", "No such registration in progress.")); - } else { - try { - com.yubico.webauthn.RegistrationResult registration = rp.finishRegistration( - FinishRegistrationOptions.builder() - .request(request.getPublicKeyCredentialCreationOptions()) - .response(response.getCredential()) - .build() - ); - - if (userStorage.userExists(request.getUsername())) { - boolean permissionGranted = false; - - final boolean isValidSession = request.getSessionToken().map(token -> - sessions.isSessionForUser(request.getPublicKeyCredentialCreationOptions().getUser().getId(), token) - ).orElse(false); - - logger.debug("Session token: {}", request.getSessionToken()); - logger.debug("Valid session: {}", isValidSession); - - if (isValidSession) { - permissionGranted = true; - logger.info("Session token accepted for user {}", request.getPublicKeyCredentialCreationOptions().getUser().getId()); - } + if (username.isPresent() && !userStorage.userExists(username.get())) { + return Either.left( + Collections.singletonList("The username \"" + username.get() + "\" is not registered.")); + } else { + AssertionRequestWrapper request = + new AssertionRequestWrapper( + generateRandom(32), + rp.startAssertion(StartAssertionOptions.builder().username(username).build())); - logger.debug("permissionGranted: {}", permissionGranted); + assertRequestStorage.put(request.getRequestId(), request); - if (!permissionGranted) { - throw new RegistrationFailedException(new IllegalArgumentException(String.format( - "User %s already exists", - request.getUsername() - ))); - } - } - - return Either.right( - new SuccessfulRegistrationResult( - request, - response, - addRegistration( - request.getPublicKeyCredentialCreationOptions().getUser(), - request.getCredentialNickname(), - response, - registration - ), - registration.isAttestationTrusted(), - sessions.createSession(request.getPublicKeyCredentialCreationOptions().getUser().getId()) - ) - ); - } catch (RegistrationFailedException e) { - logger.debug("fail finishRegistration responseJson: {}", responseJson, e); - return Either.left(Arrays.asList("Registration failed!", e.getMessage())); - } catch (Exception e) { - logger.error("fail finishRegistration responseJson: {}", responseJson, e); - return Either.left(Arrays.asList("Registration failed unexpectedly; this is likely a bug.", e.getMessage())); - } - } + return Either.right(request); } - - public Either, SuccessfulU2fRegistrationResult> finishU2fRegistration(String responseJson) throws ExecutionException { - logger.trace("finishU2fRegistration responseJson: {}", responseJson); - U2fRegistrationResponse response = null; - try { - response = jsonMapper.readValue(responseJson, U2fRegistrationResponse.class); - } catch (IOException e) { - logger.error("JSON error in finishU2fRegistration; responseJson: {}", responseJson, e); - return Either.left(Arrays.asList("Registration failed!", "Failed to decode response object.", e.getMessage())); - } - - RegistrationRequest request = registerRequestStorage.getIfPresent(response.getRequestId()); - registerRequestStorage.invalidate(response.getRequestId()); - - if (request == null) { - logger.debug("fail finishU2fRegistration responseJson: {}", responseJson); - return Either.left(Arrays.asList("Registration failed!", "No such registration in progress.")); - } else { - - try { - ExceptionUtil.assure( - U2fVerifier.verify(rp.getAppId().get(), request, response), - "Failed to verify signature." - ); - } catch (Exception e) { - logger.debug("Failed to verify U2F signature.", e); - return Either.left(Arrays.asList("Failed to verify signature.", e.getMessage())); - } - - X509Certificate attestationCert = null; - try { - attestationCert = CertificateParser.parseDer(response.getCredential().getU2fResponse().getAttestationCertAndSignature().getBytes()); - } catch (CertificateException e) { - logger.error("Failed to parse attestation certificate: {}", response.getCredential().getU2fResponse().getAttestationCertAndSignature(), e); - } - - Optional attestation = Optional.empty(); - try { - if (attestationCert != null) { - attestation = Optional.of(metadataService.getAttestation(Collections.singletonList(attestationCert))); - } - } catch (CertificateEncodingException e) { - logger.error("Failed to resolve attestation", e); - } - - final U2fRegistrationResult result = U2fRegistrationResult.builder() - .keyId(PublicKeyCredentialDescriptor.builder().id(response.getCredential().getU2fResponse().getKeyHandle()).build()) - .attestationTrusted(attestation.map(Attestation::isTrusted).orElse(false)) - .publicKeyCose(rawEcdaKeyToCose(response.getCredential().getU2fResponse().getPublicKey())) - .attestationMetadata(attestation) - .build(); - - return Either.right( - new SuccessfulU2fRegistrationResult( - request, - response, - addRegistration( - request.getPublicKeyCredentialCreationOptions().getUser(), - request.getCredentialNickname(), - 0, - result - ), - result.isAttestationTrusted(), - Optional.of(new AttestationCertInfo(response.getCredential().getU2fResponse().getAttestationCertAndSignature())), - request.getUsername(), - sessions.createSession(request.getPublicKeyCredentialCreationOptions().getUser().getId()) - ) - ); - } + } + + @Value + @AllArgsConstructor + public static final class SuccessfulAuthenticationResult { + private final boolean success = true; + private final AssertionRequestWrapper request; + private final AssertionResponse response; + private final Collection registrations; + + @JsonSerialize(using = AuthDataSerializer.class) + AuthenticatorData authData; + + private final String username; + private final ByteArray sessionToken; + private final List warnings; + + public SuccessfulAuthenticationResult( + AssertionRequestWrapper request, + AssertionResponse response, + Collection registrations, + String username, + ByteArray sessionToken, + List warnings) { + this( + request, + response, + registrations, + response.getCredential().getResponse().getParsedAuthenticatorData(), + username, + sessionToken, + warnings); } - - public Either, AssertionRequestWrapper> startAuthentication(Optional username) { - logger.trace("startAuthentication username: {}", username); - - if (username.isPresent() && !userStorage.userExists(username.get())) { - return Either.left(Collections.singletonList("The username \"" + username.get() + "\" is not registered.")); - } else { - AssertionRequestWrapper request = new AssertionRequestWrapper( - generateRandom(32), - rp.startAssertion( - StartAssertionOptions.builder() - .username(username) - .build() - ) - ); - - assertRequestStorage.put(request.getRequestId(), request); - - return Either.right(request); - } + } + + public Either, SuccessfulAuthenticationResult> finishAuthentication( + String responseJson) { + logger.trace("finishAuthentication responseJson: {}", responseJson); + + final AssertionResponse response; + try { + response = jsonMapper.readValue(responseJson, AssertionResponse.class); + } catch (IOException e) { + logger.debug("Failed to decode response object", e); + return Either.left( + Arrays.asList("Assertion failed!", "Failed to decode response object.", e.getMessage())); } - @Value - @AllArgsConstructor - public static final class SuccessfulAuthenticationResult { - private final boolean success = true; - private final AssertionRequestWrapper request; - private final AssertionResponse response; - private final Collection registrations; - @JsonSerialize(using = AuthDataSerializer.class) AuthenticatorData authData; - private final String username; - private final ByteArray sessionToken; - private final List warnings; - - public SuccessfulAuthenticationResult(AssertionRequestWrapper request, AssertionResponse response, Collection registrations, String username, ByteArray sessionToken, List warnings) { - this( - request, - response, - registrations, - response.getCredential().getResponse().getParsedAuthenticatorData(), - username, - sessionToken, - warnings - ); + AssertionRequestWrapper request = assertRequestStorage.getIfPresent(response.getRequestId()); + assertRequestStorage.invalidate(response.getRequestId()); + + if (request == null) { + return Either.left(Arrays.asList("Assertion failed!", "No such assertion in progress.")); + } else { + try { + AssertionResult result = + rp.finishAssertion( + FinishAssertionOptions.builder() + .request(request.getRequest()) + .response(response.getCredential()) + .build()); + + if (result.isSuccess()) { + try { + userStorage.updateSignatureCount(result); + } catch (Exception e) { + logger.error( + "Failed to update signature count for user \"{}\", credential \"{}\"", + result.getUsername(), + response.getCredential().getId(), + e); + } + + return Either.right( + new SuccessfulAuthenticationResult( + request, + response, + userStorage.getRegistrationsByUsername(result.getUsername()), + result.getUsername(), + sessions.createSession(result.getUserHandle()), + result.getWarnings())); + } else { + return Either.left(Collections.singletonList("Assertion failed: Invalid assertion.")); } + } catch (AssertionFailedException e) { + logger.debug("Assertion failed", e); + return Either.left(Arrays.asList("Assertion failed!", e.getMessage())); + } catch (Exception e) { + logger.error("Assertion failed", e); + return Either.left( + Arrays.asList("Assertion failed unexpectedly; this is likely a bug.", e.getMessage())); + } } + } - public Either, SuccessfulAuthenticationResult> finishAuthentication(String responseJson) { - logger.trace("finishAuthentication responseJson: {}", responseJson); - - final AssertionResponse response; - try { - response = jsonMapper.readValue(responseJson, AssertionResponse.class); - } catch (IOException e) { - logger.debug("Failed to decode response object", e); - return Either.left(Arrays.asList("Assertion failed!", "Failed to decode response object.", e.getMessage())); - } + @Value + public static final class DeregisterCredentialResult { + boolean success = true; + CredentialRegistration droppedRegistration; + boolean accountDeleted; + } - AssertionRequestWrapper request = assertRequestStorage.getIfPresent(response.getRequestId()); - assertRequestStorage.invalidate(response.getRequestId()); + public Either, DeregisterCredentialResult> deregisterCredential( + @NonNull ByteArray sessionToken, ByteArray credentialId) { + logger.trace("deregisterCredential session: {}, credentialId: {}", sessionToken, credentialId); - if (request == null) { - return Either.left(Arrays.asList("Assertion failed!", "No such assertion in progress.")); - } else { - try { - AssertionResult result = rp.finishAssertion( - FinishAssertionOptions.builder() - .request(request.getRequest()) - .response(response.getCredential()) - .build() - ); - - if (result.isSuccess()) { - try { - userStorage.updateSignatureCount(result); - } catch (Exception e) { - logger.error( - "Failed to update signature count for user \"{}\", credential \"{}\"", - result.getUsername(), - response.getCredential().getId(), - e - ); - } - - return Either.right( - new SuccessfulAuthenticationResult( - request, - response, - userStorage.getRegistrationsByUsername(result.getUsername()), - result.getUsername(), - sessions.createSession(result.getUserHandle()), - result.getWarnings() - ) - ); - } else { - return Either.left(Collections.singletonList("Assertion failed: Invalid assertion.")); - } - } catch (AssertionFailedException e) { - logger.debug("Assertion failed", e); - return Either.left(Arrays.asList("Assertion failed!", e.getMessage())); - } catch (Exception e) { - logger.error("Assertion failed", e); - return Either.left(Arrays.asList("Assertion failed unexpectedly; this is likely a bug.", e.getMessage())); - } - } + if (credentialId == null || credentialId.getBytes().length == 0) { + return Either.left(Collections.singletonList("Credential ID must not be empty.")); } - @Value - public static final class DeregisterCredentialResult { - boolean success = true; - CredentialRegistration droppedRegistration; - boolean accountDeleted; - } + Optional session = sessions.getSession(sessionToken); + if (session.isPresent()) { + ByteArray userHandle = session.get(); + Optional username = userStorage.getUsernameForUserHandle(userHandle); - public Either, DeregisterCredentialResult> deregisterCredential( - @NonNull ByteArray sessionToken, - ByteArray credentialId - ) { - logger.trace("deregisterCredential session: {}, credentialId: {}", sessionToken, credentialId); + if (username.isPresent()) { + Optional credReg = + userStorage.getRegistrationByUsernameAndCredentialId(username.get(), credentialId); + if (credReg.isPresent()) { + userStorage.removeRegistrationByUsername(username.get(), credReg.get()); - if (credentialId == null || credentialId.getBytes().length == 0) { - return Either.left(Collections.singletonList("Credential ID must not be empty.")); - } - - Optional session = sessions.getSession(sessionToken); - if (session.isPresent()) { - ByteArray userHandle = session.get(); - Optional username = userStorage.getUsernameForUserHandle(userHandle); - - if (username.isPresent()) { - Optional credReg = userStorage.getRegistrationByUsernameAndCredentialId(username.get(), credentialId); - if (credReg.isPresent()) { - userStorage.removeRegistrationByUsername(username.get(), credReg.get()); - - return Either.right(new DeregisterCredentialResult( - credReg.get(), - !userStorage.userExists(username.get()) - )); - } else { - return Either.left(Collections.singletonList("Credential ID not registered:" + credentialId)); - } - } else { - return Either.left(Collections.singletonList("Invalid user handle")); - } + return Either.right( + new DeregisterCredentialResult( + credReg.get(), !userStorage.userExists(username.get()))); } else { - return Either.left(Collections.singletonList("Invalid session")); + return Either.left( + Collections.singletonList("Credential ID not registered:" + credentialId)); } + } else { + return Either.left(Collections.singletonList("Invalid user handle")); + } + } else { + return Either.left(Collections.singletonList("Invalid session")); } + } - public Either, T> deleteAccount(String username, Supplier onSuccess) { - logger.trace("deleteAccount username: {}", username); - - if (username == null || username.isEmpty()) { - return Either.left(Collections.singletonList("Username must not be empty.")); - } - - boolean removed = userStorage.removeAllRegistrations(username); + public Either, T> deleteAccount(String username, Supplier onSuccess) { + logger.trace("deleteAccount username: {}", username); - if (removed) { - return Either.right(onSuccess.get()); - } else { - return Either.left(Collections.singletonList("Username not registered:" + username)); - } + if (username == null || username.isEmpty()) { + return Either.left(Collections.singletonList("Username must not be empty.")); } - private CredentialRegistration addRegistration( - UserIdentity userIdentity, - Optional nickname, - RegistrationResponse response, - RegistrationResult result - ) { - return addRegistration( - userIdentity, - nickname, - response.getCredential().getResponse().getAttestation().getAuthenticatorData().getSignatureCounter(), - RegisteredCredential.builder() - .credentialId(result.getKeyId().getId()) - .userHandle(userIdentity.getId()) - .publicKeyCose(result.getPublicKeyCose()) - .signatureCount(response.getCredential().getResponse().getParsedAuthenticatorData().getSignatureCounter()) - .build(), - result.getAttestationMetadata() - ); - } + boolean removed = userStorage.removeAllRegistrations(username); - private CredentialRegistration addRegistration( - UserIdentity userIdentity, - Optional nickname, - long signatureCount, - U2fRegistrationResult result - ) { - return addRegistration( - userIdentity, - nickname, - signatureCount, - RegisteredCredential.builder() - .credentialId(result.getKeyId().getId()) - .userHandle(userIdentity.getId()) - .publicKeyCose(result.getPublicKeyCose()) - .signatureCount(signatureCount) - .build(), - result.getAttestationMetadata() - ); + if (removed) { + return Either.right(onSuccess.get()); + } else { + return Either.left(Collections.singletonList("Username not registered:" + username)); } - - private CredentialRegistration addRegistration( - UserIdentity userIdentity, - Optional nickname, - long signatureCount, - RegisteredCredential credential, - Optional attestationMetadata - ) { - CredentialRegistration reg = CredentialRegistration.builder() + } + + private CredentialRegistration addRegistration( + UserIdentity userIdentity, + Optional nickname, + RegistrationResponse response, + RegistrationResult result) { + return addRegistration( + userIdentity, + nickname, + response + .getCredential() + .getResponse() + .getAttestation() + .getAuthenticatorData() + .getSignatureCounter(), + RegisteredCredential.builder() + .credentialId(result.getKeyId().getId()) + .userHandle(userIdentity.getId()) + .publicKeyCose(result.getPublicKeyCose()) + .signatureCount( + response + .getCredential() + .getResponse() + .getParsedAuthenticatorData() + .getSignatureCounter()) + .build(), + result.getAttestationMetadata()); + } + + private CredentialRegistration addRegistration( + UserIdentity userIdentity, + Optional nickname, + long signatureCount, + U2fRegistrationResult result) { + return addRegistration( + userIdentity, + nickname, + signatureCount, + RegisteredCredential.builder() + .credentialId(result.getKeyId().getId()) + .userHandle(userIdentity.getId()) + .publicKeyCose(result.getPublicKeyCose()) + .signatureCount(signatureCount) + .build(), + result.getAttestationMetadata()); + } + + private CredentialRegistration addRegistration( + UserIdentity userIdentity, + Optional nickname, + long signatureCount, + RegisteredCredential credential, + Optional attestationMetadata) { + CredentialRegistration reg = + CredentialRegistration.builder() .userIdentity(userIdentity) .credentialNickname(nickname) .registrationTime(clock.instant()) @@ -682,61 +755,63 @@ private CredentialRegistration addRegistration( .attestationMetadata(attestationMetadata) .build(); - logger.debug( - "Adding registration: user: {}, nickname: {}, credential: {}", - userIdentity, - nickname, - credential - ); - userStorage.addRegistrationByUsername(userIdentity.getName(), reg); - return reg; + logger.debug( + "Adding registration: user: {}, nickname: {}, credential: {}", + userIdentity, + nickname, + credential); + userStorage.addRegistrationByUsername(userIdentity.getName(), reg); + return reg; + } + + 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])); } - 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()); - } - - - private static class AuthDataSerializer extends JsonSerializer { - @Override public void serialize(AuthenticatorData value, JsonGenerator gen, SerializerProvider serializers) throws IOException { - gen.writeStartObject(); - gen.writeStringField("rpIdHash", value.getRpIdHash().getHex()); - gen.writeObjectField("flags", value.getFlags()); - gen.writeNumberField("signatureCounter", value.getSignatureCounter()); - value.getAttestedCredentialData().ifPresent(acd -> { + 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()); + } + + private static class AuthDataSerializer extends JsonSerializer { + @Override + public void serialize( + AuthenticatorData value, JsonGenerator gen, SerializerProvider serializers) + throws IOException { + gen.writeStartObject(); + gen.writeStringField("rpIdHash", value.getRpIdHash().getHex()); + gen.writeObjectField("flags", value.getFlags()); + gen.writeNumberField("signatureCounter", value.getSignatureCounter()); + value + .getAttestedCredentialData() + .ifPresent( + acd -> { try { - gen.writeObjectFieldStart("attestedCredentialData"); - gen.writeStringField("aaguid", acd.getAaguid().getHex()); - gen.writeStringField("credentialId", acd.getCredentialId().getHex()); - gen.writeStringField("publicKey", acd.getCredentialPublicKey().getHex()); - gen.writeEndObject(); + gen.writeObjectFieldStart("attestedCredentialData"); + gen.writeStringField("aaguid", acd.getAaguid().getHex()); + gen.writeStringField("credentialId", acd.getCredentialId().getHex()); + gen.writeStringField("publicKey", acd.getCredentialPublicKey().getHex()); + gen.writeEndObject(); } catch (IOException e) { - throw new RuntimeException(e); + throw new RuntimeException(e); } - }); - gen.writeObjectField("extensions", value.getExtensions()); - gen.writeEndObject(); - } + }); + gen.writeObjectField("extensions", value.getExtensions()); + gen.writeEndObject(); } - + } } diff --git a/webauthn-server-demo/src/main/java/demo/webauthn/data/AssertionRequestWrapper.java b/webauthn-server-demo/src/main/java/demo/webauthn/data/AssertionRequestWrapper.java index 3acfb5643..260cccd69 100644 --- a/webauthn-server-demo/src/main/java/demo/webauthn/data/AssertionRequestWrapper.java +++ b/webauthn-server-demo/src/main/java/demo/webauthn/data/AssertionRequestWrapper.java @@ -34,30 +34,19 @@ @Value public class AssertionRequestWrapper { - @NonNull - private final ByteArray requestId; + @NonNull private final ByteArray requestId; - @NonNull - private final PublicKeyCredentialRequestOptions publicKeyCredentialRequestOptions; + @NonNull private final PublicKeyCredentialRequestOptions publicKeyCredentialRequestOptions; - @NonNull - private final Optional username; + @NonNull private final Optional username; - @NonNull - @JsonIgnore - private final transient com.yubico.webauthn.AssertionRequest request; - - public AssertionRequestWrapper( - @NonNull - ByteArray requestId, - @NonNull - com.yubico.webauthn.AssertionRequest request - ) { - this.requestId = requestId; - this.publicKeyCredentialRequestOptions = request.getPublicKeyCredentialRequestOptions(); - this.username = request.getUsername(); - this.request = request; - - } + @NonNull @JsonIgnore private final transient com.yubico.webauthn.AssertionRequest request; + public AssertionRequestWrapper( + @NonNull ByteArray requestId, @NonNull com.yubico.webauthn.AssertionRequest request) { + this.requestId = requestId; + this.publicKeyCredentialRequestOptions = request.getPublicKeyCredentialRequestOptions(); + this.username = request.getUsername(); + this.request = request; + } } diff --git a/webauthn-server-demo/src/main/java/demo/webauthn/data/AssertionResponse.java b/webauthn-server-demo/src/main/java/demo/webauthn/data/AssertionResponse.java index e9b8abf9a..65faf9132 100644 --- a/webauthn-server-demo/src/main/java/demo/webauthn/data/AssertionResponse.java +++ b/webauthn-server-demo/src/main/java/demo/webauthn/data/AssertionResponse.java @@ -33,18 +33,19 @@ import lombok.Value; @Value -@JsonIgnoreProperties({ "sessionToken" }) +@JsonIgnoreProperties({"sessionToken"}) public class AssertionResponse { - private final ByteArray requestId; - private final PublicKeyCredential credential; - - public AssertionResponse( - @JsonProperty("requestId") ByteArray requestId, - @JsonProperty("credential") PublicKeyCredential credential - ) { - this.requestId = requestId; - this.credential = credential; - } + private final ByteArray requestId; + private final PublicKeyCredential + credential; + public AssertionResponse( + @JsonProperty("requestId") ByteArray requestId, + @JsonProperty("credential") + PublicKeyCredential + credential) { + this.requestId = requestId; + this.credential = credential; + } } diff --git a/webauthn-server-demo/src/main/java/demo/webauthn/data/CredentialRegistration.java b/webauthn-server-demo/src/main/java/demo/webauthn/data/CredentialRegistration.java index f93166afa..880ae9354 100644 --- a/webauthn-server-demo/src/main/java/demo/webauthn/data/CredentialRegistration.java +++ b/webauthn-server-demo/src/main/java/demo/webauthn/data/CredentialRegistration.java @@ -40,24 +40,22 @@ @Wither public class CredentialRegistration { - long signatureCount; + long signatureCount; - UserIdentity userIdentity; - Optional credentialNickname; + UserIdentity userIdentity; + Optional credentialNickname; - @JsonIgnore - Instant registrationTime; - RegisteredCredential credential; + @JsonIgnore Instant registrationTime; + RegisteredCredential credential; - Optional attestationMetadata; + Optional attestationMetadata; - @JsonProperty("registrationTime") - public String getRegistrationTimestamp() { - return registrationTime.toString(); - } - - public String getUsername() { - return userIdentity.getName(); - } + @JsonProperty("registrationTime") + public String getRegistrationTimestamp() { + return registrationTime.toString(); + } + public String getUsername() { + return userIdentity.getName(); + } } diff --git a/webauthn-server-demo/src/main/java/demo/webauthn/data/RegistrationRequest.java b/webauthn-server-demo/src/main/java/demo/webauthn/data/RegistrationRequest.java index ffd50958f..52228d17e 100644 --- a/webauthn-server-demo/src/main/java/demo/webauthn/data/RegistrationRequest.java +++ b/webauthn-server-demo/src/main/java/demo/webauthn/data/RegistrationRequest.java @@ -34,10 +34,9 @@ @EqualsAndHashCode(callSuper = false) public class RegistrationRequest { - String username; - Optional credentialNickname; - ByteArray requestId; - PublicKeyCredentialCreationOptions publicKeyCredentialCreationOptions; - Optional sessionToken; - + String username; + Optional credentialNickname; + ByteArray requestId; + PublicKeyCredentialCreationOptions publicKeyCredentialCreationOptions; + Optional sessionToken; } diff --git a/webauthn-server-demo/src/main/java/demo/webauthn/data/RegistrationResponse.java b/webauthn-server-demo/src/main/java/demo/webauthn/data/RegistrationResponse.java index fd2f8668f..39db71d26 100644 --- a/webauthn-server-demo/src/main/java/demo/webauthn/data/RegistrationResponse.java +++ b/webauthn-server-demo/src/main/java/demo/webauthn/data/RegistrationResponse.java @@ -36,21 +36,23 @@ @Value public class RegistrationResponse { - private final ByteArray requestId; - - private final PublicKeyCredential credential; - - private final Optional sessionToken; - - @JsonCreator - public RegistrationResponse( - @JsonProperty("requestId") ByteArray requestId, - @JsonProperty("credential") PublicKeyCredential credential, - @JsonProperty("sessionToken") Optional sessionToken - ) { - this.requestId = requestId; - this.credential = credential; - this.sessionToken = sessionToken; - } - + private final ByteArray requestId; + + private final PublicKeyCredential< + AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs> + credential; + + private final Optional sessionToken; + + @JsonCreator + public RegistrationResponse( + @JsonProperty("requestId") ByteArray requestId, + @JsonProperty("credential") + PublicKeyCredential + credential, + @JsonProperty("sessionToken") Optional sessionToken) { + this.requestId = requestId; + this.credential = credential; + this.sessionToken = sessionToken; + } } diff --git a/webauthn-server-demo/src/main/java/demo/webauthn/data/U2fCredential.java b/webauthn-server-demo/src/main/java/demo/webauthn/data/U2fCredential.java index 6cbc0293c..769b3666e 100644 --- a/webauthn-server-demo/src/main/java/demo/webauthn/data/U2fCredential.java +++ b/webauthn-server-demo/src/main/java/demo/webauthn/data/U2fCredential.java @@ -32,13 +32,10 @@ @Value public class U2fCredential { - private final U2fCredentialResponse u2fResponse; - - @JsonCreator - public U2fCredential( - @NonNull @JsonProperty("u2fResponse") U2fCredentialResponse u2fResponse - ) { - this.u2fResponse = u2fResponse; - } + private final U2fCredentialResponse u2fResponse; + @JsonCreator + public U2fCredential(@NonNull @JsonProperty("u2fResponse") U2fCredentialResponse u2fResponse) { + this.u2fResponse = u2fResponse; + } } diff --git a/webauthn-server-demo/src/main/java/demo/webauthn/data/U2fCredentialResponse.java b/webauthn-server-demo/src/main/java/demo/webauthn/data/U2fCredentialResponse.java index b8efec7c9..1ad3f5fc4 100644 --- a/webauthn-server-demo/src/main/java/demo/webauthn/data/U2fCredentialResponse.java +++ b/webauthn-server-demo/src/main/java/demo/webauthn/data/U2fCredentialResponse.java @@ -33,22 +33,20 @@ @Value public class U2fCredentialResponse { - private final ByteArray keyHandle; - private final ByteArray publicKey; - private final ByteArray attestationCertAndSignature; - private final ByteArray clientDataJSON; - - @JsonCreator - public U2fCredentialResponse( - @NonNull @JsonProperty("keyHandle") ByteArray keyHandle, - @NonNull@JsonProperty("publicKey") ByteArray publicKey, - @NonNull@JsonProperty("attestationCertAndSignature") ByteArray attestationCertAndSignature, - @NonNull@JsonProperty("clientDataJSON") ByteArray clientDataJSON - ) { - this.keyHandle = keyHandle; - this.publicKey = publicKey; - this.attestationCertAndSignature = attestationCertAndSignature; - this.clientDataJSON = clientDataJSON; - } + private final ByteArray keyHandle; + private final ByteArray publicKey; + private final ByteArray attestationCertAndSignature; + private final ByteArray clientDataJSON; + @JsonCreator + public U2fCredentialResponse( + @NonNull @JsonProperty("keyHandle") ByteArray keyHandle, + @NonNull @JsonProperty("publicKey") ByteArray publicKey, + @NonNull @JsonProperty("attestationCertAndSignature") ByteArray attestationCertAndSignature, + @NonNull @JsonProperty("clientDataJSON") ByteArray clientDataJSON) { + this.keyHandle = keyHandle; + this.publicKey = publicKey; + this.attestationCertAndSignature = attestationCertAndSignature; + this.clientDataJSON = clientDataJSON; + } } diff --git a/webauthn-server-demo/src/main/java/demo/webauthn/data/U2fRegistrationResponse.java b/webauthn-server-demo/src/main/java/demo/webauthn/data/U2fRegistrationResponse.java index 36fe7e1ab..ef0d612a0 100644 --- a/webauthn-server-demo/src/main/java/demo/webauthn/data/U2fRegistrationResponse.java +++ b/webauthn-server-demo/src/main/java/demo/webauthn/data/U2fRegistrationResponse.java @@ -34,19 +34,17 @@ @Value public class U2fRegistrationResponse { - private final ByteArray requestId; - private final U2fCredential credential; - private final Optional sessionToken; - - @JsonCreator - public U2fRegistrationResponse( - @NonNull @JsonProperty("requestId") ByteArray requestId, - @NonNull @JsonProperty("credential") U2fCredential credential, - @NonNull @JsonProperty("sessionToken") Optional sessionToken - ) { - this.requestId = requestId; - this.credential = credential; - this.sessionToken = sessionToken; - } + private final ByteArray requestId; + private final U2fCredential credential; + private final Optional sessionToken; + @JsonCreator + public U2fRegistrationResponse( + @NonNull @JsonProperty("requestId") ByteArray requestId, + @NonNull @JsonProperty("credential") U2fCredential credential, + @NonNull @JsonProperty("sessionToken") Optional sessionToken) { + this.requestId = requestId; + this.credential = credential; + this.sessionToken = sessionToken; + } } diff --git a/webauthn-server-demo/src/main/java/demo/webauthn/data/U2fRegistrationResult.java b/webauthn-server-demo/src/main/java/demo/webauthn/data/U2fRegistrationResult.java index 3e148950b..aaaf0c94d 100644 --- a/webauthn-server-demo/src/main/java/demo/webauthn/data/U2fRegistrationResult.java +++ b/webauthn-server-demo/src/main/java/demo/webauthn/data/U2fRegistrationResult.java @@ -14,19 +14,14 @@ @Builder public class U2fRegistrationResult { - @NonNull - private final PublicKeyCredentialDescriptor keyId; + @NonNull private final PublicKeyCredentialDescriptor keyId; - private final boolean attestationTrusted; + private final boolean attestationTrusted; - @NonNull - private final ByteArray publicKeyCose; + @NonNull private final ByteArray publicKeyCose; - @NonNull - @Builder.Default - private final List warnings = Collections.emptyList(); + @NonNull @Builder.Default private final List warnings = Collections.emptyList(); - @NonNull - @Builder.Default - private final Optional attestationMetadata = Optional.empty(); + @NonNull @Builder.Default + private final Optional attestationMetadata = Optional.empty(); } diff --git a/webauthn-server-demo/src/test/scala/demo/webauthn/JsonSerializationSpec.scala b/webauthn-server-demo/src/test/scala/demo/webauthn/JsonSerializationSpec.scala index d51ce503a..c12311326 100644 --- a/webauthn-server-demo/src/test/scala/demo/webauthn/JsonSerializationSpec.scala +++ b/webauthn-server-demo/src/test/scala/demo/webauthn/JsonSerializationSpec.scala @@ -27,7 +27,6 @@ package demo.webauthn import com.yubico.internal.util.JacksonCodecs import com.yubico.webauthn.RegistrationTestData import com.yubico.webauthn.data.AuthenticatorAttestationResponse -import com.yubico.webauthn.WebAuthnCodecs import demo.webauthn.data.RegistrationResponse import org.junit.runner.RunWith import org.scalatest.FunSpec @@ -40,17 +39,26 @@ class JsonSerializationSpec extends FunSpec with Matchers { private val jsonMapper = JacksonCodecs.json() val testData = RegistrationTestData.FidoU2f.BasicAttestation - val authenticationAttestationResponseJson = s"""{"attestationObject":"${testData.response.getResponse.getAttestationObject.getBase64Url}","clientDataJSON":"${testData.response.getResponse.getClientDataJSON.getBase64Url}"}""" - val publicKeyCredentialJson = s"""{"id":"${testData.response.getId.getBase64Url}","response":${authenticationAttestationResponseJson},"clientExtensionResults":{},"type":"public-key"}""" - val registrationResponseJson = s"""{"requestId":"request1","credential":${publicKeyCredentialJson}}""" + val authenticationAttestationResponseJson = + s"""{"attestationObject":"${testData.response.getResponse.getAttestationObject.getBase64Url}","clientDataJSON":"${testData.response.getResponse.getClientDataJSON.getBase64Url}"}""" + val publicKeyCredentialJson = + s"""{"id":"${testData.response.getId.getBase64Url}","response":${authenticationAttestationResponseJson},"clientExtensionResults":{},"type":"public-key"}""" + val registrationResponseJson = + s"""{"requestId":"request1","credential":${publicKeyCredentialJson}}""" it("RegistrationResponse can be deserialized from JSON.") { - val parsed = jsonMapper.readValue(registrationResponseJson, classOf[RegistrationResponse]) - parsed.getCredential should equal (testData.response) + val parsed = jsonMapper.readValue( + registrationResponseJson, + classOf[RegistrationResponse], + ) + parsed.getCredential should equal(testData.response) } it("AuthenticatorAttestationResponse can be deserialized from JSON.") { - val parsed = jsonMapper.readValue(authenticationAttestationResponseJson, classOf[AuthenticatorAttestationResponse]) + val parsed = jsonMapper.readValue( + authenticationAttestationResponseJson, + classOf[AuthenticatorAttestationResponse], + ) parsed should not be null } diff --git a/webauthn-server-demo/src/test/scala/demo/webauthn/WebAuthnServerSpec.scala b/webauthn-server-demo/src/test/scala/demo/webauthn/WebAuthnServerSpec.scala index 778a998c3..46f8b28e1 100644 --- a/webauthn-server-demo/src/test/scala/demo/webauthn/WebAuthnServerSpec.scala +++ b/webauthn-server-demo/src/test/scala/demo/webauthn/WebAuthnServerSpec.scala @@ -24,28 +24,21 @@ package demo.webauthn -import java.security.KeyPair -import java.security.interfaces.ECPublicKey -import java.time.Instant -import java.util -import java.util.Optional -import java.util.concurrent.TimeUnit - import com.google.common.cache.Cache import com.google.common.cache.CacheBuilder -import com.yubico.internal.util.scala.JavaConverters._ import com.yubico.internal.util.JacksonCodecs +import com.yubico.internal.util.scala.JavaConverters._ +import com.yubico.webauthn.AssertionResult import com.yubico.webauthn.RegisteredCredential import com.yubico.webauthn.RegistrationTestData import com.yubico.webauthn.TestAuthenticator +import com.yubico.webauthn.WebAuthnTestCodecs import com.yubico.webauthn.data.ByteArray import com.yubico.webauthn.data.CollectedClientData import com.yubico.webauthn.data.PublicKeyCredentialDescriptor import com.yubico.webauthn.data.PublicKeyCredentialRequestOptions import com.yubico.webauthn.data.RelyingPartyIdentity import com.yubico.webauthn.extension.appid.AppId -import com.yubico.webauthn.AssertionResult -import com.yubico.webauthn.WebAuthnTestCodecs import demo.webauthn.data.AssertionRequestWrapper import demo.webauthn.data.CredentialRegistration import demo.webauthn.data.RegistrationRequest @@ -55,9 +48,14 @@ import org.scalatest.FunSpec import org.scalatest.Matchers import org.scalatestplus.junit.JUnitRunner +import java.security.KeyPair +import java.security.interfaces.ECPublicKey +import java.time.Instant +import java.util +import java.util.Optional +import java.util.concurrent.TimeUnit import scala.jdk.CollectionConverters._ - @RunWith(classOf[JUnitRunner]) class WebAuthnServerSpec extends FunSpec with Matchers { @@ -67,7 +65,8 @@ class WebAuthnServerSpec extends FunSpec with Matchers { private val credentialNickname = Some("My Lovely Credential").asJava private val requireResidentKey = false private val requestId = ByteArray.fromBase64Url("request1") - private val rpId = RelyingPartyIdentity.builder().id("localhost").name("Test party").build() + private val rpId = + RelyingPartyIdentity.builder().id("localhost").name("Test party").build() private val origins = Set("localhost").asJava private val appId = Optional.empty[AppId] @@ -77,7 +76,13 @@ class WebAuthnServerSpec extends FunSpec with Matchers { it("has a start method whose output can be serialized to JSON.") { val server = newServer - val request = server.startRegistration(username, Optional.of(displayName), credentialNickname, requireResidentKey, Optional.empty()) + val request = server.startRegistration( + username, + Optional.of(displayName), + credentialNickname, + requireResidentKey, + Optional.empty(), + ) val json = jsonMapper.writeValueAsString(request.right.get) json should not be null @@ -86,17 +91,22 @@ class WebAuthnServerSpec extends FunSpec with Matchers { it("has a finish method which accepts and outputs JSON.") { val requestId = ByteArray.fromBase64Url("request1") - val server = newServerWithRegistrationRequest(RegistrationTestData.FidoU2f.BasicAttestation) + val server = newServerWithRegistrationRequest( + RegistrationTestData.FidoU2f.BasicAttestation + ) val response = new RegistrationResponse( requestId, RegistrationTestData.FidoU2f.BasicAttestation.response, - Optional.empty() + Optional.empty(), ) - val authenticationAttestationResponseJson = """{"attestationObject":"v2hhdXRoRGF0YVikSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NBAAAFOQABAgMEBQYHCAkKCwwNDg8AIIjjhj6nH3qL2QF3tkUogilFykuaXjJTw35O4m-0NSX0pSJYIA5Nt8eYkLco-NQfKPXaA6dD9UfX_SHaYo-L-YQb78HsAyYBAiFYIOuzRl1o1Hem2jVRYhjkbSeIydhqLln9iltAgsDYjXRTIAFjZm10aGZpZG8tdTJmZ2F0dFN0bXS_Y3g1Y59ZAekwggHlMIIBjKADAgECAgIFOTAKBggqhkjOPQQDAjBqMSYwJAYDVQQDDB1ZdWJpY28gV2ViQXV0aG4gdW5pdCB0ZXN0cyBDQTEPMA0GA1UECgwGWXViaWNvMSIwIAYDVQQLDBlBdXRoZW50aWNhdG9yIEF0dGVzdGF0aW9uMQswCQYDVQQGEwJTRTAeFw0xODA5MDYxNzQyMDBaFw0xODA5MDYxNzQyMDBaMGcxIzAhBgNVBAMMGll1YmljbyBXZWJBdXRobiB1bml0IHRlc3RzMQ8wDQYDVQQKDAZZdWJpY28xIjAgBgNVBAsMGUF1dGhlbnRpY2F0b3IgQXR0ZXN0YXRpb24xCzAJBgNVBAYTAlNFMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEJ-8bFED9TnFhaArujgB0foNaV4gQIulP1mC5DO1wvSByw4eOyXujpPHkTw9y5e5J2J3N9coSReZJgBRpvFzYD6MlMCMwIQYLKwYBBAGC5RwBAQQEEgQQAAECAwQFBgcICQoLDA0ODzAKBggqhkjOPQQDAgNHADBEAiB4bL25EH06vPBOVnReObXrS910ARVOLJPPnKNoZbe64gIgX1Rg5oydH45zEMEVDjNPStwv6Z3nE_isMeY-szlQhv3_Y3NpZ1hHMEUCIQDBs1nbSuuKQ6yoHMQoRp8eCT_HZvR45F_aVP6qFX_wKgIgMCL58bv-crkLwTwiEL9ibCV4nDYM-DZuW5_BFCJbcxn__w","clientDataJSON":"eyJjaGFsbGVuZ2UiOiJBQUVCQWdNRkNBMFZJamRaRUdsNVlscyIsIm9yaWdpbiI6ImxvY2FsaG9zdCIsInR5cGUiOiJ3ZWJhdXRobi5jcmVhdGUiLCJ0b2tlbkJpbmRpbmciOnsic3RhdHVzIjoic3VwcG9ydGVkIn19"}""" - val publicKeyCredentialJson = s"""{"id":"iOOGPqcfeovZAXe2RSiCKUXKS5peMlPDfk7ib7Q1JfQ","response":${authenticationAttestationResponseJson},"clientExtensionResults":{},"type":"public-key"}""" - val responseJson = s"""{"requestId":"${requestId.getBase64Url}","credential":${publicKeyCredentialJson}}""" + val authenticationAttestationResponseJson = + """{"attestationObject":"v2hhdXRoRGF0YVikSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NBAAAFOQABAgMEBQYHCAkKCwwNDg8AIIjjhj6nH3qL2QF3tkUogilFykuaXjJTw35O4m-0NSX0pSJYIA5Nt8eYkLco-NQfKPXaA6dD9UfX_SHaYo-L-YQb78HsAyYBAiFYIOuzRl1o1Hem2jVRYhjkbSeIydhqLln9iltAgsDYjXRTIAFjZm10aGZpZG8tdTJmZ2F0dFN0bXS_Y3g1Y59ZAekwggHlMIIBjKADAgECAgIFOTAKBggqhkjOPQQDAjBqMSYwJAYDVQQDDB1ZdWJpY28gV2ViQXV0aG4gdW5pdCB0ZXN0cyBDQTEPMA0GA1UECgwGWXViaWNvMSIwIAYDVQQLDBlBdXRoZW50aWNhdG9yIEF0dGVzdGF0aW9uMQswCQYDVQQGEwJTRTAeFw0xODA5MDYxNzQyMDBaFw0xODA5MDYxNzQyMDBaMGcxIzAhBgNVBAMMGll1YmljbyBXZWJBdXRobiB1bml0IHRlc3RzMQ8wDQYDVQQKDAZZdWJpY28xIjAgBgNVBAsMGUF1dGhlbnRpY2F0b3IgQXR0ZXN0YXRpb24xCzAJBgNVBAYTAlNFMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEJ-8bFED9TnFhaArujgB0foNaV4gQIulP1mC5DO1wvSByw4eOyXujpPHkTw9y5e5J2J3N9coSReZJgBRpvFzYD6MlMCMwIQYLKwYBBAGC5RwBAQQEEgQQAAECAwQFBgcICQoLDA0ODzAKBggqhkjOPQQDAgNHADBEAiB4bL25EH06vPBOVnReObXrS910ARVOLJPPnKNoZbe64gIgX1Rg5oydH45zEMEVDjNPStwv6Z3nE_isMeY-szlQhv3_Y3NpZ1hHMEUCIQDBs1nbSuuKQ6yoHMQoRp8eCT_HZvR45F_aVP6qFX_wKgIgMCL58bv-crkLwTwiEL9ibCV4nDYM-DZuW5_BFCJbcxn__w","clientDataJSON":"eyJjaGFsbGVuZ2UiOiJBQUVCQWdNRkNBMFZJamRaRUdsNVlscyIsIm9yaWdpbiI6ImxvY2FsaG9zdCIsInR5cGUiOiJ3ZWJhdXRobi5jcmVhdGUiLCJ0b2tlbkJpbmRpbmciOnsic3RhdHVzIjoic3VwcG9ydGVkIn19"}""" + val publicKeyCredentialJson = + s"""{"id":"iOOGPqcfeovZAXe2RSiCKUXKS5peMlPDfk7ib7Q1JfQ","response":${authenticationAttestationResponseJson},"clientExtensionResults":{},"type":"public-key"}""" + val responseJson = + s"""{"requestId":"${requestId.getBase64Url}","credential":${publicKeyCredentialJson}}""" val request = server.finishRegistration(responseJson) val json = jsonMapper.writeValueAsString(request.right.get) @@ -109,33 +119,50 @@ class WebAuthnServerSpec extends FunSpec with Matchers { describe("authentication") { // These values were generated using TestAuthenticator.makeCredentialExample(TestAuthenticator.createCredential()) - 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 = RegistrationTestData.FidoU2f.BasicAttestation.response.getId - val signature: ByteArray = ByteArray.fromHex("30450221008d478e4c24894d261c7fd3790363ba9687facf4dd1d59610933a2c292cffc3d902205069264c167833d239d6af4c7bf7326c4883fb8c3517a2c86318aa3060d8b441") + 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 = + RegistrationTestData.FidoU2f.BasicAttestation.response.getId + val signature: ByteArray = + ByteArray.fromHex("30450221008d478e4c24894d261c7fd3790363ba9687facf4dd1d59610933a2c292cffc3d902205069264c167833d239d6af4c7bf7326c4883fb8c3517a2c86318aa3060d8b441") // These values are defined by the attestationObject and clientDataJson above - val clientDataJsonBytes: ByteArray = new ByteArray(clientDataJson.getBytes("UTF-8")) + val clientDataJsonBytes: ByteArray = + new ByteArray(clientDataJson.getBytes("UTF-8")) val clientData = new CollectedClientData(clientDataJsonBytes) val challenge: ByteArray = clientData.getChallenge val credentialKey: KeyPair = TestAuthenticator.importEcKeypair( - privateBytes = ByteArray.fromHex("308193020100301306072a8648ce3d020106082a8648ce3d0301070479307702010104206a88f478910df685bc0cfcc2077e64fb3a8ba770fb23fbbcd1f6572ce35cf360a00a06082a8648ce3d030107a14403420004d8020a2ec718c2c595bb890fcdaf9b81cc742118efdbb8812ac4a9dd5ace2990ec22a48faf1544df0fe5fe0e2e7a69720e63a83d7f46aa022f1323eaf7967762"), - publicBytes = ByteArray.fromHex("3059301306072a8648ce3d020106082a8648ce3d03010703420004d8020a2ec718c2c595bb890fcdaf9b81cc742118efdbb8812ac4a9dd5ace2990ec22a48faf1544df0fe5fe0e2e7a69720e63a83d7f46aa022f1323eaf7967762") + privateBytes = + ByteArray.fromHex("308193020100301306072a8648ce3d020106082a8648ce3d0301070479307702010104206a88f478910df685bc0cfcc2077e64fb3a8ba770fb23fbbcd1f6572ce35cf360a00a06082a8648ce3d030107a14403420004d8020a2ec718c2c595bb890fcdaf9b81cc742118efdbb8812ac4a9dd5ace2990ec22a48faf1544df0fe5fe0e2e7a69720e63a83d7f46aa022f1323eaf7967762"), + publicBytes = + ByteArray.fromHex("3059301306072a8648ce3d020106082a8648ce3d03010703420004d8020a2ec718c2c595bb890fcdaf9b81cc742118efdbb8812ac4a9dd5ace2990ec22a48faf1544df0fe5fe0e2e7a69720e63a83d7f46aa022f1323eaf7967762"), ) it("has a start method whose output can be serialized to JSON.") { - val server = newServerWithUser(RegistrationTestData.FidoU2f.BasicAttestation) - val request = server.startAuthentication(Optional.of(RegistrationTestData.FidoU2f.BasicAttestation.userId.getName)) + val server = + newServerWithUser(RegistrationTestData.FidoU2f.BasicAttestation) + val request = server.startAuthentication( + Optional.of( + RegistrationTestData.FidoU2f.BasicAttestation.userId.getName + ) + ) val json = jsonMapper.writeValueAsString(request.right.get) json should not be null } it("has a finish method which accepts and outputs JSON.") { - val server = newServerWithAuthenticationRequest(RegistrationTestData.FidoU2f.BasicAttestation) - val authenticatorAssertionResponseJson = s"""{"authenticatorData":"${authenticatorData.getBase64Url}","signature":"${signature.getBase64Url}","clientDataJSON":"${clientDataJsonBytes.getBase64Url}"}""" - val publicKeyCredentialJson = s"""{"id":"${credentialId.getBase64Url}","response":${authenticatorAssertionResponseJson},"clientExtensionResults":{},"type":"public-key"}""" - val responseJson = s"""{"requestId":"${requestId.getBase64Url}","credential":${publicKeyCredentialJson}}""" + val server = newServerWithAuthenticationRequest( + RegistrationTestData.FidoU2f.BasicAttestation + ) + val authenticatorAssertionResponseJson = + s"""{"authenticatorData":"${authenticatorData.getBase64Url}","signature":"${signature.getBase64Url}","clientDataJSON":"${clientDataJsonBytes.getBase64Url}"}""" + val publicKeyCredentialJson = + s"""{"id":"${credentialId.getBase64Url}","response":${authenticatorAssertionResponseJson},"clientExtensionResults":{},"type":"public-key"}""" + val responseJson = + s"""{"requestId":"${requestId.getBase64Url}","credential":${publicKeyCredentialJson}}""" val request = server.finishAuthentication(responseJson) val json = jsonMapper.writeValueAsString(request.right.get) @@ -143,23 +170,43 @@ class WebAuthnServerSpec extends FunSpec with Matchers { } def newServerWithAuthenticationRequest(testData: RegistrationTestData) = { - val assertionRequests: Cache[ByteArray, AssertionRequestWrapper] = newCache() + val assertionRequests: Cache[ByteArray, AssertionRequestWrapper] = + newCache() - assertionRequests.put(requestId, new AssertionRequestWrapper( + assertionRequests.put( + requestId, + new AssertionRequestWrapper( requestId, - com.yubico.webauthn.AssertionRequest.builder() + com.yubico.webauthn.AssertionRequest + .builder() .publicKeyCredentialRequestOptions( - PublicKeyCredentialRequestOptions.builder() + PublicKeyCredentialRequestOptions + .builder() .challenge(challenge) .rpId(rpId.getId) .build() ) .username(Some(testData.userId.getName).asJava) - .build() - )) + .build(), + ), + ) - val userStorage = makeUserStorage(testData, credentialPubkey = Some(WebAuthnTestCodecs.ecPublicKeyToCose(credentialKey.getPublic.asInstanceOf[ECPublicKey]))) - new WebAuthnServer(userStorage, newCache(), assertionRequests, rpId, origins, appId) + val userStorage = makeUserStorage( + testData, + credentialPubkey = Some( + WebAuthnTestCodecs.ecPublicKeyToCose( + credentialKey.getPublic.asInstanceOf[ECPublicKey] + ) + ), + ) + new WebAuthnServer( + userStorage, + newCache(), + assertionRequests, + rpId, + origins, + appId, + ) } } @@ -170,68 +217,133 @@ class WebAuthnServerSpec extends FunSpec with Matchers { private def newServerWithUser(testData: RegistrationTestData) = { val userStorage: RegistrationStorage = makeUserStorage(testData) - new WebAuthnServer(userStorage, newCache(), newCache(), rpId, origins, appId) + new WebAuthnServer( + userStorage, + newCache(), + newCache(), + rpId, + origins, + appId, + ) } private def makeUserStorage( - testData: RegistrationTestData, - credentialPubkey: Option[ByteArray] = None + testData: RegistrationTestData, + credentialPubkey: Option[ByteArray] = None, ) = { - val registrations = util.Arrays.asList(CredentialRegistration.builder() - .signatureCount(testData.response.getResponse.getAttestation.getAuthenticatorData.getSignatureCounter) - .userIdentity(testData.request.getUser) - .credentialNickname(credentialNickname) - .registrationTime(Instant.parse("2018-07-06T15:07:15Z")) - .credential(RegisteredCredential.builder() - .credentialId(testData.response.getId) - .userHandle(testData.request.getUser.getId) - .publicKeyCose(testData.response.getResponse.getParsedAuthenticatorData.getAttestedCredentialData.get.getCredentialPublicKey) + val registrations = util.Arrays.asList( + CredentialRegistration + .builder() + .signatureCount( + testData.response.getResponse.getAttestation.getAuthenticatorData.getSignatureCounter + ) + .userIdentity(testData.request.getUser) + .credentialNickname(credentialNickname) + .registrationTime(Instant.parse("2018-07-06T15:07:15Z")) + .credential( + RegisteredCredential + .builder() + .credentialId(testData.response.getId) + .userHandle(testData.request.getUser.getId) + .publicKeyCose( + testData.response.getResponse.getParsedAuthenticatorData.getAttestedCredentialData.get.getCredentialPublicKey + ) + .build() + ) .build() - ) - .build()) + ) new RegistrationStorage { - override def addRegistrationByUsername(username: String, reg: CredentialRegistration): Boolean = ??? - override def getRegistrationsByUsername(username: String): java.util.Collection[CredentialRegistration] = + override def addRegistrationByUsername( + username: String, + reg: CredentialRegistration, + ): Boolean = ??? + override def getRegistrationsByUsername( + username: String + ): java.util.Collection[CredentialRegistration] = if (username == testData.userId.getName) registrations else Nil.asJava - override def getRegistrationByUsernameAndCredentialId(username: String, credentialId: ByteArray): Optional[CredentialRegistration] = ??? - override def getRegistrationsByUserHandle(userHandle: ByteArray): java.util.Collection[CredentialRegistration] = ??? - override def removeRegistrationByUsername(username: String, credentialRegistration: CredentialRegistration): Boolean = ??? + override def getRegistrationByUsernameAndCredentialId( + username: String, + credentialId: ByteArray, + ): Optional[CredentialRegistration] = ??? + override def getRegistrationsByUserHandle( + userHandle: ByteArray + ): java.util.Collection[CredentialRegistration] = ??? + override def removeRegistrationByUsername( + username: String, + credentialRegistration: CredentialRegistration, + ): Boolean = ??? override def removeAllRegistrations(username: String): Boolean = ??? override def updateSignatureCount(result: AssertionResult): Unit = {} - override def getCredentialIdsForUsername(username: String): java.util.Set[PublicKeyCredentialDescriptor] = Set.empty.asJava - override def getUserHandleForUsername(username: String): Optional[ByteArray] = - if (username == testData.userId.getName) Optional.of(testData.userId.getId) else Optional.empty() - override def getUsernameForUserHandle(userHandle: ByteArray): Optional[String] = ??? - override def lookup(credentialId: ByteArray, userHandle: ByteArray): Optional[RegisteredCredential] = - if ((credentialId, userHandle) == (testData.response.getId, testData.userId.getId)) - Optional.of(RegisteredCredential.builder() - .credentialId(testData.response.getId) - .userHandle(testData.userId.getId) - .publicKeyCose(credentialPubkey getOrElse testData.response.getResponse.getAttestation.getAuthenticatorData.getAttestedCredentialData.get.getCredentialPublicKey) - .signatureCount(0) - .build()) + override def getCredentialIdsForUsername( + username: String + ): java.util.Set[PublicKeyCredentialDescriptor] = Set.empty.asJava + override def getUserHandleForUsername( + username: String + ): Optional[ByteArray] = + if (username == testData.userId.getName) + Optional.of(testData.userId.getId) + else Optional.empty() + override def getUsernameForUserHandle( + userHandle: ByteArray + ): Optional[String] = ??? + override def lookup( + credentialId: ByteArray, + userHandle: ByteArray, + ): Optional[RegisteredCredential] = + if ( + ( + credentialId, + userHandle, + ) == (testData.response.getId, testData.userId.getId) + ) + Optional.of( + RegisteredCredential + .builder() + .credentialId(testData.response.getId) + .userHandle(testData.userId.getId) + .publicKeyCose( + credentialPubkey getOrElse testData.response.getResponse.getAttestation.getAuthenticatorData.getAttestedCredentialData.get.getCredentialPublicKey + ) + .signatureCount(0) + .build() + ) else Optional.empty() - override def lookupAll(credentialId: ByteArray): java.util.Set[RegisteredCredential] = ??? + override def lookupAll( + credentialId: ByteArray + ): java.util.Set[RegisteredCredential] = ??? } } - private def newServerWithRegistrationRequest(testData: RegistrationTestData) = { + private def newServerWithRegistrationRequest( + testData: RegistrationTestData + ) = { val registrationRequests: Cache[ByteArray, RegistrationRequest] = newCache() - registrationRequests.put(requestId, new RegistrationRequest( - testData.userId.getName, - credentialNickname, + registrationRequests.put( requestId, - testData.request, - Optional.empty() - )) - - new WebAuthnServer(new InMemoryRegistrationStorage, registrationRequests, newCache(), rpId, origins, appId) + new RegistrationRequest( + testData.userId.getName, + credentialNickname, + requestId, + testData.request, + Optional.empty(), + ), + ) + + new WebAuthnServer( + new InMemoryRegistrationStorage, + registrationRequests, + newCache(), + rpId, + origins, + appId, + ) } private def newCache[K <: Object, V <: Object](): Cache[K, V] = - CacheBuilder.newBuilder() + CacheBuilder + .newBuilder() .maximumSize(100) .expireAfterAccess(10, TimeUnit.MINUTES) .build() diff --git a/yubico-util-scala/build.gradle b/yubico-util-scala/build.gradle index 1c4e48c83..5ff4568f4 100644 --- a/yubico-util-scala/build.gradle +++ b/yubico-util-scala/build.gradle @@ -1,18 +1,22 @@ plugins { id 'scala' + id 'io.github.cosmicsilence.scalafix' } description = 'Yubico internal Scala utilities' +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + dependencies { + implementation(platform(rootProject)) implementation( - addVersion('org.scala-lang:scala-library'), - addVersion('org.scalacheck:scalacheck_2.13'), + 'org.scala-lang:scala-library', + 'org.scalacheck:scalacheck_2.13', ) testImplementation( - addVersion('org.scalatest:scalatest_2.13'), + 'org.scalatest:scalatest_2.13', ) } - diff --git a/yubico-util-scala/src/main/scala/com/yubico/internal/util/scala/JavaConverters.scala b/yubico-util-scala/src/main/scala/com/yubico/internal/util/scala/JavaConverters.scala index d2b0ec392..626156e58 100644 --- a/yubico-util-scala/src/main/scala/com/yubico/internal/util/scala/JavaConverters.scala +++ b/yubico-util-scala/src/main/scala/com/yubico/internal/util/scala/JavaConverters.scala @@ -26,30 +26,33 @@ package com.yubico.internal.util.scala import java.util.Optional import java.util.function.Supplier - import scala.language.implicitConversions - case class AsJavaOptional[A](a: Option[A]) { - def asJava[B >: A]: Optional[B] = a match { - case Some(value) => Optional.of(value) - case None => Optional.empty() - } + def asJava[B >: A]: Optional[B] = + a match { + case Some(value) => Optional.of(value) + case None => Optional.empty() + } } case class AsScalaOption[A](a: Optional[A]) { def asScala: Option[A] = if (a.isPresent) Some(a.get()) else None } case class AsJavaSupplier[A](a: () => A) { - def asJava[B >: A]: Supplier[B] = new Supplier[B] { - override def get(): B = a() - } + def asJava[B >: A]: Supplier[B] = + new Supplier[B] { + override def get(): B = a() + } } object JavaConverters { - implicit def asJavaOptionalConverter[A](a: Option[A]): AsJavaOptional[A] = AsJavaOptional(a) - implicit def asJavaSupplierConverter[A](a: () => A): AsJavaSupplier[A] = AsJavaSupplier(a) - implicit def asScalaOptionConverter[A](a: Optional[A]): AsScalaOption[A] = AsScalaOption(a) + implicit def asJavaOptionalConverter[A](a: Option[A]): AsJavaOptional[A] = + AsJavaOptional(a) + implicit def asJavaSupplierConverter[A](a: () => A): AsJavaSupplier[A] = + AsJavaSupplier(a) + implicit def asScalaOptionConverter[A](a: Optional[A]): AsScalaOption[A] = + AsScalaOption(a) } diff --git a/yubico-util-scala/src/main/scala/com/yubico/scalacheck/gen/JavaGenerators.scala b/yubico-util-scala/src/main/scala/com/yubico/scalacheck/gen/JavaGenerators.scala index 98e768220..9dc840291 100644 --- a/yubico-util-scala/src/main/scala/com/yubico/scalacheck/gen/JavaGenerators.scala +++ b/yubico-util-scala/src/main/scala/com/yubico/scalacheck/gen/JavaGenerators.scala @@ -1,43 +1,54 @@ package com.yubico.scalacheck.gen -import java.net.URL -import java.util.Optional - import com.yubico.internal.util.scala.JavaConverters._ import org.scalacheck.Arbitrary -import org.scalacheck.Gen import org.scalacheck.Arbitrary.arbitrary +import org.scalacheck.Gen +import java.net.URL +import java.util.Optional import scala.jdk.CollectionConverters._ - object JavaGenerators { - implicit def arbitraryOptional[A](implicit a: Arbitrary[A]): Arbitrary[Optional[A]] = + implicit def arbitraryOptional[A](implicit + a: Arbitrary[A] + ): Arbitrary[Optional[A]] = Arbitrary(Gen.option(a.arbitrary).map(_.asJava)) - implicit def arbitraryList[A](implicit a: Arbitrary[List[A]]): Arbitrary[java.util.List[A]] = + implicit def arbitraryList[A](implicit + a: Arbitrary[List[A]] + ): Arbitrary[java.util.List[A]] = Arbitrary(a.arbitrary map (l => new java.util.ArrayList[A](l.asJava))) - implicit def arbitraryMap[A, B](implicit a: Arbitrary[Map[A, B]]): Arbitrary[java.util.Map[A, B]] = + implicit def arbitraryMap[A, B](implicit + a: Arbitrary[Map[A, B]] + ): Arbitrary[java.util.Map[A, B]] = Arbitrary(a.arbitrary map (m => new java.util.HashMap[A, B](m.asJava))) - implicit def arbitrarySet[A](implicit a: Arbitrary[Set[A]]): Arbitrary[java.util.Set[A]] = + implicit def arbitrarySet[A](implicit + a: Arbitrary[Set[A]] + ): Arbitrary[java.util.Set[A]] = Arbitrary(a.arbitrary map (s => new java.util.HashSet[A](s.asJava))) - implicit def arbitrarySortedSet[A](implicit a: Arbitrary[Set[A]]): Arbitrary[java.util.SortedSet[A]] = + implicit def arbitrarySortedSet[A](implicit + a: Arbitrary[Set[A]] + ): Arbitrary[java.util.SortedSet[A]] = Arbitrary(a.arbitrary map (s => new java.util.TreeSet[A](s.asJava))) implicit val arbitraryUrl: Arbitrary[URL] = Arbitrary(url()) def url( - scheme: Gen[String] = Gen.oneOf("http", "https"), - host: Gen[String] = Gen.alphaNumStr, - path: Gen[String] = Gen.alphaNumStr - ): Gen[URL] = for { - scheme <- scheme - host <- host - path <- path - } yield new URL(s"${scheme}://${host}${if (path.isEmpty) "" else "/"}${path}") + scheme: Gen[String] = Gen.oneOf("http", "https"), + host: Gen[String] = Gen.alphaNumStr, + path: Gen[String] = Gen.alphaNumStr, + ): Gen[URL] = + for { + scheme <- scheme + host <- host + path <- path + } yield new URL( + s"${scheme}://${host}${if (path.isEmpty) "" else "/"}${path}" + ) implicit val arbitraryBoolean: Arbitrary[java.lang.Boolean] = Arbitrary(arbitrary[Boolean].map((a: Boolean) => a)) diff --git a/yubico-util/build.gradle b/yubico-util/build.gradle index 89807f7a1..3ca9f7264 100644 --- a/yubico-util/build.gradle +++ b/yubico-util/build.gradle @@ -1,36 +1,45 @@ plugins { id 'java-library' id 'scala' + id 'io.github.cosmicsilence.scalafix' } description = 'Yubico internal utilities' project.ext.publishMe = true +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + dependencies { + api(platform(rootProject)) api( - addVersion('com.fasterxml.jackson.core:jackson-databind'), + 'com.fasterxml.jackson.core:jackson-databind', ) implementation( - addVersion('com.fasterxml.jackson.dataformat:jackson-dataformat-cbor'), - addVersion('com.fasterxml.jackson.datatype:jackson-datatype-jdk8'), - addVersion('com.google.guava:guava'), - addVersion('com.upokecenter:cbor'), - addVersion('org.slf4j:slf4j-api'), + 'com.fasterxml.jackson.dataformat:jackson-dataformat-cbor', + 'com.fasterxml.jackson.datatype:jackson-datatype-jdk8', + 'com.google.guava:guava', + 'com.upokecenter:cbor', + 'org.slf4j:slf4j-api', ) testImplementation( project(':yubico-util-scala'), - addVersion('junit:junit'), - addVersion('org.scala-lang:scala-library'), - addVersion('org.scalacheck:scalacheck_2.13'), - addVersion('org.scalatest:scalatest_2.13'), + 'junit:junit', + 'org.scala-lang:scala-library', + 'org.scalacheck:scalacheck_2.13', + 'org.scalatest:scalatest_2.13', + ) + + testRuntimeOnly( + 'ch.qos.logback:logback-classic', ) testRuntimeOnly( - addVersion('ch.qos.logback:logback-classic'), + 'ch.qos.logback:logback-classic', ) } diff --git a/yubico-util/src/main/java/com/yubico/internal/util/BinaryUtil.java b/yubico-util/src/main/java/com/yubico/internal/util/BinaryUtil.java index 74eabdd65..a251634aa 100644 --- a/yubico-util/src/main/java/com/yubico/internal/util/BinaryUtil.java +++ b/yubico-util/src/main/java/com/yubico/internal/util/BinaryUtil.java @@ -25,106 +25,93 @@ package com.yubico.internal.util; import com.google.common.io.BaseEncoding; - import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.Arrays; - public class BinaryUtil { - public static byte[] copy(byte[] bytes) { - return Arrays.copyOf(bytes, bytes.length); - } - - /** - * @param bytes - * Bytes to encode - */ - public static String toHex(byte[] bytes) { - return BaseEncoding.base16().encode(bytes).toLowerCase(); - } - - /** - * @param hex - * String of hexadecimal digits to decode as bytes. - */ - public static byte[] fromHex(String hex) { - return BaseEncoding.base16().decode(hex.toUpperCase()); + public static byte[] copy(byte[] bytes) { + return Arrays.copyOf(bytes, bytes.length); + } + + /** @param bytes Bytes to encode */ + public static String toHex(byte[] bytes) { + return BaseEncoding.base16().encode(bytes).toLowerCase(); + } + + /** @param hex String of hexadecimal digits to decode as bytes. */ + public static byte[] fromHex(String hex) { + return BaseEncoding.base16().decode(hex.toUpperCase()); + } + + /** + * Parse a single byte from two hexadecimal characters. + * + * @param hex String of hexadecimal digits to decode as bytes. + */ + public static byte singleFromHex(String hex) { + ExceptionUtil.assure( + hex.length() == 2, "Argument must be exactly 2 hexadecimal characters, was: %s", hex); + return fromHex(hex)[0]; + } + + /** + * Read one byte as an unsigned 8-bit integer. + * + *

Result is of type Short because Java don't have unsigned types. + * + * @return A value between 0 and 255, inclusive. + */ + public static short getUint8(byte b) { + // Prepend a zero so we can parse it as a signed int16 instead of a signed int8 + return ByteBuffer.wrap(new byte[] {0, b}).order(ByteOrder.BIG_ENDIAN).getShort(); + } + + /** + * Read 2 bytes as a big endian unsigned 16-bit integer. + * + *

Result is of type Int because Java don't have unsigned types. + * + * @return A value between 0 and 2^16- 1, inclusive. + */ + public static int getUint16(byte[] bytes) { + if (bytes.length == 2) { + // Prepend zeroes so we can parse it as a signed int32 instead of a signed int16 + return ByteBuffer.wrap(new byte[] {0, 0, bytes[0], bytes[1]}) + .order(ByteOrder.BIG_ENDIAN) + .getInt(); + } else { + throw new IllegalArgumentException("Argument must be 2 bytes, was: " + bytes.length); } - - /** - * Parse a single byte from two hexadecimal characters. - * - * @param hex - * String of hexadecimal digits to decode as bytes. - */ - public static byte singleFromHex(String hex) { - ExceptionUtil.assure(hex.length() == 2, "Argument must be exactly 2 hexadecimal characters, was: %s", hex); - return fromHex(hex)[0]; + } + + /** + * Read 4 bytes as a big endian unsigned 32-bit integer. + * + *

Result is of type Long because Java don't have unsigned types. + * + * @return A value between 0 and 2^32 - 1, inclusive. + */ + public static long getUint32(byte[] bytes) { + if (bytes.length == 4) { + // Prepend zeroes so we can parse it as a signed int32 instead of a signed int16 + return ByteBuffer.wrap(new byte[] {0, 0, 0, 0, bytes[0], bytes[1], bytes[2], bytes[3]}) + .order(ByteOrder.BIG_ENDIAN) + .getLong(); + } else { + throw new IllegalArgumentException("Argument must be 4 bytes, was: " + bytes.length); } - - /** - * Read one byte as an unsigned 8-bit integer. - *

- * Result is of type Short because Java don't have unsigned types. - * - * @return A value between 0 and 255, inclusive. - */ - public static short getUint8(byte b) { - // Prepend a zero so we can parse it as a signed int16 instead of a signed int8 - return ByteBuffer.wrap(new byte[]{ 0, b }) - .order(ByteOrder.BIG_ENDIAN) - .getShort(); - } - - - /** - * Read 2 bytes as a big endian unsigned 16-bit integer. - *

- * Result is of type Int because Java don't have unsigned types. - * - * @return A value between 0 and 2^16- 1, inclusive. - */ - public static int getUint16(byte[] bytes) { - if (bytes.length == 2) { - // Prepend zeroes so we can parse it as a signed int32 instead of a signed int16 - return ByteBuffer.wrap(new byte[] { 0, 0, bytes[0], bytes[1] }) - .order(ByteOrder.BIG_ENDIAN) - .getInt(); - } else { - throw new IllegalArgumentException("Argument must be 2 bytes, was: " + bytes.length); - } - } - - - /** - * Read 4 bytes as a big endian unsigned 32-bit integer. - *

- * Result is of type Long because Java don't have unsigned types. - * - * @return A value between 0 and 2^32 - 1, inclusive. - */ - public static long getUint32(byte[] bytes) { - if (bytes.length == 4) { - // Prepend zeroes so we can parse it as a signed int32 instead of a signed int16 - return ByteBuffer.wrap(new byte[] { 0, 0, 0, 0, bytes[0], bytes[1], bytes[2], bytes[3] }) - .order(ByteOrder.BIG_ENDIAN) - .getLong(); - } else { - throw new IllegalArgumentException("Argument must be 4 bytes, was: " + bytes.length); - } - } - - public static byte[] encodeUint16(int value) { - ExceptionUtil.assure(value >= 0, "Argument must be non-negative, was: %d", value); - ExceptionUtil.assure(value < 65536, "Argument must be smaller than 2^15=65536, was: %d", value); - - ByteBuffer b = ByteBuffer.allocate(4); - b.order(ByteOrder.BIG_ENDIAN); - b.putInt(value); - b.rewind(); - return Arrays.copyOfRange(b.array(), 2, 4); - } - + } + + public static byte[] encodeUint16(int value) { + ExceptionUtil.assure(value >= 0, "Argument must be non-negative, was: %d", value); + ExceptionUtil.assure(value < 65536, "Argument must be smaller than 2^15=65536, was: %d", value); + + ByteBuffer b = ByteBuffer.allocate(4); + b.order(ByteOrder.BIG_ENDIAN); + b.putInt(value); + b.rewind(); + return Arrays.copyOfRange(b.array(), 2, 4); + } } diff --git a/yubico-util/src/main/java/com/yubico/internal/util/ByteInputStream.java b/yubico-util/src/main/java/com/yubico/internal/util/ByteInputStream.java index 3dcf489d1..894ba9a2b 100644 --- a/yubico-util/src/main/java/com/yubico/internal/util/ByteInputStream.java +++ b/yubico-util/src/main/java/com/yubico/internal/util/ByteInputStream.java @@ -28,36 +28,34 @@ import java.io.DataInputStream; import java.io.IOException; -/** - * Provides an easy way to read a byte array in chunks. - */ +/** Provides an easy way to read a byte array in chunks. */ public class ByteInputStream extends DataInputStream { - public ByteInputStream(byte[] data) { - super(new ByteArrayInputStream(data)); - } - - public byte[] read(int numberOfBytes) throws IOException { - byte[] readBytes = new byte[numberOfBytes]; - readFully(readBytes); - return readBytes; - } - - public byte[] readAll() throws IOException { - byte[] readBytes = new byte[available()]; - readFully(readBytes); - return readBytes; - } - - public int readInteger() throws IOException { - return readInt(); - } - - public byte readSigned() throws IOException { - return readByte(); - } - - public int readUnsigned() throws IOException { - return readUnsignedByte(); - } + public ByteInputStream(byte[] data) { + super(new ByteArrayInputStream(data)); + } + + public byte[] read(int numberOfBytes) throws IOException { + byte[] readBytes = new byte[numberOfBytes]; + readFully(readBytes); + return readBytes; + } + + public byte[] readAll() throws IOException { + byte[] readBytes = new byte[available()]; + readFully(readBytes); + return readBytes; + } + + public int readInteger() throws IOException { + return readInt(); + } + + public byte readSigned() throws IOException { + return readByte(); + } + + public int readUnsigned() throws IOException { + return readUnsignedByte(); + } } diff --git a/yubico-util/src/main/java/com/yubico/internal/util/CertificateParser.java b/yubico-util/src/main/java/com/yubico/internal/util/CertificateParser.java index 89849a1e6..63553fa51 100755 --- a/yubico-util/src/main/java/com/yubico/internal/util/CertificateParser.java +++ b/yubico-util/src/main/java/com/yubico/internal/util/CertificateParser.java @@ -34,51 +34,59 @@ import java.util.List; public class CertificateParser { -// private static final Provider BC_PROVIDER = new BouncyCastleProvider(); - private static final Base64.Decoder BASE64_DECODER = Base64.getDecoder(); + // private static final Provider BC_PROVIDER = new BouncyCastleProvider(); + private static final Base64.Decoder BASE64_DECODER = Base64.getDecoder(); - private final static List FIXSIG = Arrays.asList( - "CN=Yubico U2F EE Serial 776137165", - "CN=Yubico U2F EE Serial 1086591525", - "CN=Yubico U2F EE Serial 1973679733", - "CN=Yubico U2F EE Serial 13503277888", - "CN=Yubico U2F EE Serial 13831167861", - "CN=Yubico U2F EE Serial 14803321578" - ); + private static final List FIXSIG = + Arrays.asList( + "CN=Yubico U2F EE Serial 776137165", + "CN=Yubico U2F EE Serial 1086591525", + "CN=Yubico U2F EE Serial 1973679733", + "CN=Yubico U2F EE Serial 13503277888", + "CN=Yubico U2F EE Serial 13831167861", + "CN=Yubico U2F EE Serial 14803321578"); - private static final int UNUSED_BITS_BYTE_INDEX_FROM_END = 257; + private static final int UNUSED_BITS_BYTE_INDEX_FROM_END = 257; - public static X509Certificate parsePem(String pemEncodedCert) throws CertificateException { - return parseDer(pemEncodedCert.replaceAll("-----BEGIN CERTIFICATE-----", "").replaceAll("-----END CERTIFICATE-----", "").replaceAll("\n", "")); - } + public static X509Certificate parsePem(String pemEncodedCert) throws CertificateException { + return parseDer( + pemEncodedCert + .replaceAll("-----BEGIN CERTIFICATE-----", "") + .replaceAll("-----END CERTIFICATE-----", "") + .replaceAll("\n", "")); + } - public static X509Certificate parseDer(String base64DerEncodedCert) throws CertificateException { - return parseDer(BASE64_DECODER.decode(base64DerEncodedCert)); - } + public static X509Certificate parseDer(String base64DerEncodedCert) throws CertificateException { + return parseDer(BASE64_DECODER.decode(base64DerEncodedCert)); + } - public static X509Certificate parseDer(byte[] derEncodedCert) throws CertificateException { - return parseDer(new ByteArrayInputStream(derEncodedCert)); - } + public static X509Certificate parseDer(byte[] derEncodedCert) throws CertificateException { + return parseDer(new ByteArrayInputStream(derEncodedCert)); + } - public static X509Certificate parseDer(InputStream is) throws CertificateException { - X509Certificate cert = (X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(is); - //Some known certs have an incorrect "unused bits" value, which causes problems on newer versions of BouncyCastle. - if(FIXSIG.contains(cert.getSubjectDN().getName())) { - byte[] encoded = cert.getEncoded(); + public static X509Certificate parseDer(InputStream is) throws CertificateException { + X509Certificate cert = + (X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(is); + // Some known certs have an incorrect "unused bits" value, which causes problems on newer + // versions of BouncyCastle. + if (FIXSIG.contains(cert.getSubjectDN().getName())) { + byte[] encoded = cert.getEncoded(); - if (encoded.length >= UNUSED_BITS_BYTE_INDEX_FROM_END) { - encoded[encoded.length - UNUSED_BITS_BYTE_INDEX_FROM_END] = 0; // Fix the "unused bits" field (should always be 0). - } else { - throw new IllegalArgumentException(String.format( - "Expected DER encoded cert to be at least %d bytes, was %d: %s", - UNUSED_BITS_BYTE_INDEX_FROM_END, - encoded.length, - cert - )); - } + if (encoded.length >= UNUSED_BITS_BYTE_INDEX_FROM_END) { + encoded[encoded.length - UNUSED_BITS_BYTE_INDEX_FROM_END] = + 0; // Fix the "unused bits" field (should always be 0). + } else { + throw new IllegalArgumentException( + String.format( + "Expected DER encoded cert to be at least %d bytes, was %d: %s", + UNUSED_BITS_BYTE_INDEX_FROM_END, encoded.length, cert)); + } - cert = (X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(new ByteArrayInputStream(encoded)); - } - return cert; + cert = + (X509Certificate) + CertificateFactory.getInstance("X.509") + .generateCertificate(new ByteArrayInputStream(encoded)); } + return cert; + } } diff --git a/yubico-util/src/main/java/com/yubico/internal/util/CollectionUtil.java b/yubico-util/src/main/java/com/yubico/internal/util/CollectionUtil.java index 8db9491db..90608333c 100644 --- a/yubico-util/src/main/java/com/yubico/internal/util/CollectionUtil.java +++ b/yubico-util/src/main/java/com/yubico/internal/util/CollectionUtil.java @@ -12,40 +12,39 @@ public class CollectionUtil { - /** - * Make an unmodifiable shallow copy of the argument. - * - * @return A shallow copy of m which cannot be modified - */ - public static Map immutableMap(Map m) { - return Collections.unmodifiableMap(new HashMap<>(m)); - } + /** + * Make an unmodifiable shallow copy of the argument. + * + * @return A shallow copy of m which cannot be modified + */ + public static Map immutableMap(Map m) { + return Collections.unmodifiableMap(new HashMap<>(m)); + } - /** - * Make an unmodifiable shallow copy of the argument. - * - * @return A shallow copy of l which cannot be modified - */ - public static List immutableList(List l) { - return Collections.unmodifiableList(new ArrayList<>(l)); - } + /** + * Make an unmodifiable shallow copy of the argument. + * + * @return A shallow copy of l which cannot be modified + */ + public static List immutableList(List l) { + return Collections.unmodifiableList(new ArrayList<>(l)); + } - /** - * Make an unmodifiable shallow copy of the argument. - * - * @return A shallow copy of s which cannot be modified - */ - public static Set immutableSet(Set s) { - return Collections.unmodifiableSet(new HashSet<>(s)); - } - - /** - * Make an unmodifiable shallow copy of the argument. - * - * @return A shallow copy of s which cannot be modified - */ - public static SortedSet immutableSortedSet(SortedSet s) { - return Collections.unmodifiableSortedSet(new TreeSet<>(s)); - } + /** + * Make an unmodifiable shallow copy of the argument. + * + * @return A shallow copy of s which cannot be modified + */ + public static Set immutableSet(Set s) { + return Collections.unmodifiableSet(new HashSet<>(s)); + } + /** + * Make an unmodifiable shallow copy of the argument. + * + * @return A shallow copy of s which cannot be modified + */ + public static SortedSet immutableSortedSet(SortedSet s) { + return Collections.unmodifiableSortedSet(new TreeSet<>(s)); + } } diff --git a/yubico-util/src/main/java/com/yubico/internal/util/ComparableUtil.java b/yubico-util/src/main/java/com/yubico/internal/util/ComparableUtil.java index 13c140f5f..465f14cc2 100644 --- a/yubico-util/src/main/java/com/yubico/internal/util/ComparableUtil.java +++ b/yubico-util/src/main/java/com/yubico/internal/util/ComparableUtil.java @@ -29,28 +29,28 @@ public class ComparableUtil { - public static > int compareComparableSets(SortedSet a, SortedSet b) { - if (a.size() == b.size()) { - final Iterator as = a.iterator(); - final Iterator bs = b.iterator(); + 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 (bs.hasNext()) { - return -1; - } else { - return 0; - } - } else { - return a.size() - b.size(); + 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 (bs.hasNext()) { + return -1; + } else { + return 0; + } + } else { + return a.size() - b.size(); + } + } } diff --git a/yubico-util/src/main/java/com/yubico/internal/util/ExceptionUtil.java b/yubico-util/src/main/java/com/yubico/internal/util/ExceptionUtil.java index 7240a44cf..dc61c11c2 100644 --- a/yubico-util/src/main/java/com/yubico/internal/util/ExceptionUtil.java +++ b/yubico-util/src/main/java/com/yubico/internal/util/ExceptionUtil.java @@ -30,16 +30,16 @@ @UtilityClass public class ExceptionUtil { - public static RuntimeException wrapAndLog(Logger log, String message, Throwable t) { - RuntimeException err = new RuntimeException(message, t); - log.error(err.getMessage(), err); - return err; - } + public static RuntimeException wrapAndLog(Logger log, String message, Throwable t) { + RuntimeException err = new RuntimeException(message, t); + log.error(err.getMessage(), err); + return err; + } - public static void assure(boolean condition, String failureMessageTemplate, Object... failureMessageArgs) { - if (!condition) { - throw new IllegalArgumentException(String.format(failureMessageTemplate, failureMessageArgs)); - } + public static void assure( + boolean condition, String failureMessageTemplate, Object... failureMessageArgs) { + if (!condition) { + throw new IllegalArgumentException(String.format(failureMessageTemplate, failureMessageArgs)); } - + } } diff --git a/yubico-util/src/main/java/com/yubico/internal/util/JacksonCodecs.java b/yubico-util/src/main/java/com/yubico/internal/util/JacksonCodecs.java index daf0d1ff3..c8879cf24 100644 --- a/yubico-util/src/main/java/com/yubico/internal/util/JacksonCodecs.java +++ b/yubico-util/src/main/java/com/yubico/internal/util/JacksonCodecs.java @@ -13,30 +13,28 @@ public class JacksonCodecs { - public static ObjectMapper cbor() { - return new ObjectMapper(new CBORFactory()).setBase64Variant(Base64Variants.MODIFIED_FOR_URL); - } + public static ObjectMapper cbor() { + return new ObjectMapper(new CBORFactory()).setBase64Variant(Base64Variants.MODIFIED_FOR_URL); + } - public static ObjectMapper json() { - return new ObjectMapper() - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true) - .configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false) - .setSerializationInclusion(Include.NON_ABSENT) - .setBase64Variant(Base64Variants.MODIFIED_FOR_URL) - .registerModule(new Jdk8Module()) - ; - } + public static ObjectMapper json() { + return new ObjectMapper() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true) + .configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false) + .setSerializationInclusion(Include.NON_ABSENT) + .setBase64Variant(Base64Variants.MODIFIED_FOR_URL) + .registerModule(new Jdk8Module()); + } - public static CBORObject deepCopy(CBORObject a) { - return CBORObject.DecodeFromBytes(a.EncodeToBytes()); - } + public static CBORObject deepCopy(CBORObject a) { + return CBORObject.DecodeFromBytes(a.EncodeToBytes()); + } - public static ObjectNode deepCopy(ObjectNode a) { - try { - return (ObjectNode) json().readTree(json().writeValueAsString(a)); - } catch (IOException e) { - throw new RuntimeException(e); - } + public static ObjectNode deepCopy(ObjectNode a) { + try { + return (ObjectNode) json().readTree(json().writeValueAsString(a)); + } catch (IOException e) { + throw new RuntimeException(e); } - + } } diff --git a/yubico-util/src/main/java/com/yubico/internal/util/OptionalUtil.java b/yubico-util/src/main/java/com/yubico/internal/util/OptionalUtil.java new file mode 100644 index 000000000..d59bd643d --- /dev/null +++ b/yubico-util/src/main/java/com/yubico/internal/util/OptionalUtil.java @@ -0,0 +1,27 @@ +package com.yubico.internal.util; + +import java.util.Optional; +import java.util.function.BinaryOperator; +import lombok.experimental.UtilityClass; + +/** Utilities for working with {@link Optional} values. */ +@UtilityClass +public class OptionalUtil { + + /** + * If both a and b are present, return f(a, b). + * + *

If only a is present, return a. + * + *

Otherwise, return b. + */ + public static Optional zipWith(Optional a, Optional b, BinaryOperator f) { + if (a.isPresent() && b.isPresent()) { + return Optional.of(f.apply(a.get(), b.get())); + } else if (a.isPresent()) { + return a; + } else { + return b; + } + } +} diff --git a/yubico-util/src/main/java/com/yubico/internal/util/StreamUtil.java b/yubico-util/src/main/java/com/yubico/internal/util/StreamUtil.java index edbb67161..dc7ed1ce8 100644 --- a/yubico-util/src/main/java/com/yubico/internal/util/StreamUtil.java +++ b/yubico-util/src/main/java/com/yubico/internal/util/StreamUtil.java @@ -24,24 +24,22 @@ package com.yubico.internal.util; -import lombok.experimental.UtilityClass; - import java.util.Iterator; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; import java.util.stream.StreamSupport; +import lombok.experimental.UtilityClass; @UtilityClass public class StreamUtil { - public static Stream toStream(Iterator it) { - Iterable iterable = () -> it; - return StreamSupport.stream(iterable.spliterator(), false); - } - - public static Set toSet(Iterator it) { - return CollectionUtil.immutableSet(toStream(it).collect(Collectors.toSet())); - } + public static Stream toStream(Iterator it) { + Iterable iterable = () -> it; + return StreamSupport.stream(iterable.spliterator(), false); + } + public static Set toSet(Iterator it) { + return CollectionUtil.immutableSet(toStream(it).collect(Collectors.toSet())); + } } diff --git a/yubico-util/src/main/java/com/yubico/internal/util/json/JsonLongSerializable.java b/yubico-util/src/main/java/com/yubico/internal/util/json/JsonLongSerializable.java index 0a6d3edcc..a2d5ae40b 100644 --- a/yubico-util/src/main/java/com/yubico/internal/util/json/JsonLongSerializable.java +++ b/yubico-util/src/main/java/com/yubico/internal/util/json/JsonLongSerializable.java @@ -26,6 +26,5 @@ public interface JsonLongSerializable { - long toJsonNumber(); - + long toJsonNumber(); } diff --git a/yubico-util/src/main/java/com/yubico/internal/util/json/JsonLongSerializer.java b/yubico-util/src/main/java/com/yubico/internal/util/json/JsonLongSerializer.java index 4887fbb4a..f6588da7a 100644 --- a/yubico-util/src/main/java/com/yubico/internal/util/json/JsonLongSerializer.java +++ b/yubico-util/src/main/java/com/yubico/internal/util/json/JsonLongSerializer.java @@ -31,10 +31,9 @@ public class JsonLongSerializer extends JsonSerializer { - @Override - public void serialize(T t, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { - jsonGenerator.writeNumber(t.toJsonNumber()); - } - + @Override + public void serialize(T t, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) + throws IOException { + jsonGenerator.writeNumber(t.toJsonNumber()); + } } - diff --git a/yubico-util/src/main/java/com/yubico/internal/util/json/JsonStringSerializable.java b/yubico-util/src/main/java/com/yubico/internal/util/json/JsonStringSerializable.java index d3b52aedd..06e43e8ec 100644 --- a/yubico-util/src/main/java/com/yubico/internal/util/json/JsonStringSerializable.java +++ b/yubico-util/src/main/java/com/yubico/internal/util/json/JsonStringSerializable.java @@ -26,6 +26,5 @@ public interface JsonStringSerializable { - String toJsonString(); - + String toJsonString(); } diff --git a/yubico-util/src/main/java/com/yubico/internal/util/json/JsonStringSerializer.java b/yubico-util/src/main/java/com/yubico/internal/util/json/JsonStringSerializer.java index 0012c3bb5..a2a728d67 100644 --- a/yubico-util/src/main/java/com/yubico/internal/util/json/JsonStringSerializer.java +++ b/yubico-util/src/main/java/com/yubico/internal/util/json/JsonStringSerializer.java @@ -31,9 +31,9 @@ public class JsonStringSerializer extends JsonSerializer { - @Override - public void serialize(T t, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { - jsonGenerator.writeString(t.toJsonString()); - } - + @Override + public void serialize(T t, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) + throws IOException { + jsonGenerator.writeString(t.toJsonString()); + } } diff --git a/yubico-util/src/main/java/com/yubico/internal/util/json/LocalDateJsonSerializer.java b/yubico-util/src/main/java/com/yubico/internal/util/json/LocalDateJsonSerializer.java index c658425b9..a3f73a0e7 100644 --- a/yubico-util/src/main/java/com/yubico/internal/util/json/LocalDateJsonSerializer.java +++ b/yubico-util/src/main/java/com/yubico/internal/util/json/LocalDateJsonSerializer.java @@ -30,12 +30,12 @@ import java.io.IOException; import java.time.LocalDate; - public class LocalDateJsonSerializer extends JsonSerializer { - @Override - public void serialize(LocalDate t, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { - jsonGenerator.writeString(t.toString()); - } - + @Override + public void serialize( + LocalDate t, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) + throws IOException { + jsonGenerator.writeString(t.toString()); + } } diff --git a/yubico-util/src/test/java/com/yubico/internal/util/CertificateParserTest.java b/yubico-util/src/test/java/com/yubico/internal/util/CertificateParserTest.java index 663320365..2ba576074 100644 --- a/yubico-util/src/test/java/com/yubico/internal/util/CertificateParserTest.java +++ b/yubico-util/src/test/java/com/yubico/internal/util/CertificateParserTest.java @@ -24,19 +24,20 @@ package com.yubico.internal.util; +import static org.junit.Assert.assertNotNull; + import java.security.cert.CertificateException; import org.junit.Test; -import static org.junit.Assert.assertNotNull; - public class CertificateParserTest { - private static final String ATTESTATION_CERT = "MIICGzCCAQWgAwIBAgIEdaP2dTALBgkqhkiG9w0BAQswLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290IENBIFNlcmlhbCA0NTcyMDA2MzEwIBcNMTQwODAxMDAwMDAwWhgPMjA1MDA5MDQwMDAwMDBaMCoxKDAmBgNVBAMMH1l1YmljbyBVMkYgRUUgU2VyaWFsIDE5NzM2Nzk3MzMwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQZo35Damtpl81YdmcbhEuXKAr7xDcQzAy5n3ftAAhtBbu8EeGU4ynfSgLonckqX6J2uXLBppTNE3v2bt+Yf8MLoxIwEDAOBgorBgEEAYLECgECBAAwCwYJKoZIhvcNAQELA4IBAQG9LbiNPgs0sQYOHAJcg+lMk+HCsiWRlYVnbT4I/5lnqU907vY17XYAORd432bU3Nnhsbkvjz76kQJGXeNAF4DPANGGlz8JU+LNEVE2PWPGgEM0GXgB7mZN5Sinfy1AoOdO+3c3bfdJQuXlUxHbo+nDpxxKpzq9gr++RbokF1+0JBkMbaA/qLYL4WdhY5NvaOyMvYpO3sBxlzn6FcP67hlotGH1wU7qhCeh+uur7zDeAWVh7c4QtJOXHkLJQfV3Z7ZMvhkIA6jZJAX99hisABU/SSa5DtgX7AfsHwa04h69AAAWDUzSk3HgOXbUd1FaSOPdlVFkG2N2JllFHykyO3zO"; - private static final String PEM_ATTESTATION_CERT = "-----BEGIN CERTIFICATE-----\n" + ATTESTATION_CERT + "\n-----END CERTIFICATE-----\n"; - - @Test - public void parsePemDoesNotReturnNull() throws CertificateException { - assertNotNull(CertificateParser.parsePem(PEM_ATTESTATION_CERT)); - } + private static final String ATTESTATION_CERT = + "MIICGzCCAQWgAwIBAgIEdaP2dTALBgkqhkiG9w0BAQswLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290IENBIFNlcmlhbCA0NTcyMDA2MzEwIBcNMTQwODAxMDAwMDAwWhgPMjA1MDA5MDQwMDAwMDBaMCoxKDAmBgNVBAMMH1l1YmljbyBVMkYgRUUgU2VyaWFsIDE5NzM2Nzk3MzMwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQZo35Damtpl81YdmcbhEuXKAr7xDcQzAy5n3ftAAhtBbu8EeGU4ynfSgLonckqX6J2uXLBppTNE3v2bt+Yf8MLoxIwEDAOBgorBgEEAYLECgECBAAwCwYJKoZIhvcNAQELA4IBAQG9LbiNPgs0sQYOHAJcg+lMk+HCsiWRlYVnbT4I/5lnqU907vY17XYAORd432bU3Nnhsbkvjz76kQJGXeNAF4DPANGGlz8JU+LNEVE2PWPGgEM0GXgB7mZN5Sinfy1AoOdO+3c3bfdJQuXlUxHbo+nDpxxKpzq9gr++RbokF1+0JBkMbaA/qLYL4WdhY5NvaOyMvYpO3sBxlzn6FcP67hlotGH1wU7qhCeh+uur7zDeAWVh7c4QtJOXHkLJQfV3Z7ZMvhkIA6jZJAX99hisABU/SSa5DtgX7AfsHwa04h69AAAWDUzSk3HgOXbUd1FaSOPdlVFkG2N2JllFHykyO3zO"; + private static final String PEM_ATTESTATION_CERT = + "-----BEGIN CERTIFICATE-----\n" + ATTESTATION_CERT + "\n-----END CERTIFICATE-----\n"; + @Test + public void parsePemDoesNotReturnNull() throws CertificateException { + assertNotNull(CertificateParser.parsePem(PEM_ATTESTATION_CERT)); + } } diff --git a/yubico-util/src/test/scala/com/yubico/internal/util/BinaryUtilSpec.scala b/yubico-util/src/test/scala/com/yubico/internal/util/BinaryUtilSpec.scala index df88ae90b..34070c13d 100644 --- a/yubico-util/src/test/scala/com/yubico/internal/util/BinaryUtilSpec.scala +++ b/yubico-util/src/test/scala/com/yubico/internal/util/BinaryUtilSpec.scala @@ -31,47 +31,53 @@ import org.scalatest.Matchers import org.scalatestplus.junit.JUnitRunner import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks - @RunWith(classOf[JUnitRunner]) -class BinaryUtilSpec extends FunSpec with Matchers with ScalaCheckDrivenPropertyChecks { +class BinaryUtilSpec + extends FunSpec + with Matchers + with ScalaCheckDrivenPropertyChecks { describe("BinaryUtil.fromHex") { it("decodes 00 to [0].") { - BinaryUtil.fromHex("00").toVector should equal (Array[Byte](0)) + BinaryUtil.fromHex("00").toVector should equal(Array[Byte](0)) } it("decodes 2a to [42].") { - BinaryUtil.fromHex("2a").toVector should equal (Array[Byte](42)) + BinaryUtil.fromHex("2a").toVector should equal(Array[Byte](42)) } it("decodes 000101020305080d15 to [0, 1, 1, 2, 3, 5, 8, 13, 21].") { - BinaryUtil.fromHex("000101020305080d15").toVector should equal (Array[Byte](0, 1, 1, 2, 3, 5, 8, 13, 21)) + BinaryUtil.fromHex("000101020305080d15").toVector should equal( + Array[Byte](0, 1, 1, 2, 3, 5, 8, 13, 21) + ) } } describe("BinaryUtil.toHex") { it("encodes [0, 1, 1, 2, 3, 5, 8, 13, 21] to 000101020305080d15.") { - BinaryUtil.toHex(Array[Byte](0, 1, 1, 2, 3, 5, 8, 13, 21)) should equal ("000101020305080d15") + BinaryUtil.toHex(Array[Byte](0, 1, 1, 2, 3, 5, 8, 13, 21)) should equal( + "000101020305080d15" + ) } } describe("BinaryUtil.getUint8") { it("returns 0 for 0x00.") { - BinaryUtil.getUint8(BinaryUtil.fromHex("00").head) should equal (0) + BinaryUtil.getUint8(BinaryUtil.fromHex("00").head) should equal(0) } it("returns 127 for 0x7f.") { - BinaryUtil.getUint8(BinaryUtil.fromHex("7f").head) should equal (127) + BinaryUtil.getUint8(BinaryUtil.fromHex("7f").head) should equal(127) } it("returns 128 for 0x80.") { - BinaryUtil.getUint8(BinaryUtil.fromHex("80").head) should equal (128) + BinaryUtil.getUint8(BinaryUtil.fromHex("80").head) should equal(128) } it("returns 255 for 0xff.") { - BinaryUtil.getUint8(BinaryUtil.fromHex("ff").head) should equal (255) + BinaryUtil.getUint8(BinaryUtil.fromHex("ff").head) should equal(255) } } @@ -79,15 +85,15 @@ class BinaryUtilSpec extends FunSpec with Matchers with ScalaCheckDrivenProperty describe("BinaryUtil.getUint16") { it("returns 0 for 0x0000.") { - BinaryUtil.getUint16(BinaryUtil.fromHex("0000")) should equal (0) + BinaryUtil.getUint16(BinaryUtil.fromHex("0000")) should equal(0) } it("returns 256 for 0x0100.") { - BinaryUtil.getUint16(BinaryUtil.fromHex("0100")) should equal (256) + BinaryUtil.getUint16(BinaryUtil.fromHex("0100")) should equal(256) } it("returns 65535 for 0xffff.") { - BinaryUtil.getUint16(BinaryUtil.fromHex("ffff")) should equal (65535) + BinaryUtil.getUint16(BinaryUtil.fromHex("ffff")) should equal(65535) } } @@ -95,15 +101,17 @@ class BinaryUtilSpec extends FunSpec with Matchers with ScalaCheckDrivenProperty describe("BinaryUtil.getUint32") { it("returns 0 for 0x0000.") { - BinaryUtil.getUint32(BinaryUtil.fromHex("00000000")) should equal (0) + BinaryUtil.getUint32(BinaryUtil.fromHex("00000000")) should equal(0) } it("returns 65536 for 0x00010000.") { - BinaryUtil.getUint32(BinaryUtil.fromHex("00010000")) should equal (65536) + BinaryUtil.getUint32(BinaryUtil.fromHex("00010000")) should equal(65536) } it("returns 4294967295 for 0xffffffff.") { - BinaryUtil.getUint32(BinaryUtil.fromHex("ffffffff")) should equal (4294967295L) + BinaryUtil.getUint32(BinaryUtil.fromHex("ffffffff")) should equal( + 4294967295L + ) } } @@ -111,11 +119,11 @@ class BinaryUtilSpec extends FunSpec with Matchers with ScalaCheckDrivenProperty describe("BinaryUtil.encodeUint16") { it("returns 0x0000 for 0.") { - BinaryUtil.encodeUint16(0) should equal (Array(0, 0)) + BinaryUtil.encodeUint16(0) should equal(Array(0, 0)) } it("returns 0xEFFF for 32767.") { - BinaryUtil.getUint32(BinaryUtil.fromHex("00010000")) should equal (65536) + BinaryUtil.getUint32(BinaryUtil.fromHex("00010000")) should equal(65536) } it("returns a value that getUint16 can reverse.") { @@ -126,13 +134,17 @@ class BinaryUtilSpec extends FunSpec with Matchers with ScalaCheckDrivenProperty it("rejects negative inputs.") { forAll(Gen.choose(Int.MinValue, -1)) { i => - an [IllegalArgumentException] shouldBe thrownBy (BinaryUtil.encodeUint16(i)) + an[IllegalArgumentException] shouldBe thrownBy( + BinaryUtil.encodeUint16(i) + ) } } it("rejects too large inputs.") { forAll(Gen.choose(65536, Int.MaxValue)) { i => - an [IllegalArgumentException] shouldBe thrownBy (BinaryUtil.encodeUint16(i)) + an[IllegalArgumentException] shouldBe thrownBy( + BinaryUtil.encodeUint16(i) + ) } } } diff --git a/yubico-util/src/test/scala/com/yubico/internal/util/CollectionUtilSpec.scala b/yubico-util/src/test/scala/com/yubico/internal/util/CollectionUtilSpec.scala index e5a28f326..fcd173b40 100644 --- a/yubico-util/src/test/scala/com/yubico/internal/util/CollectionUtilSpec.scala +++ b/yubico-util/src/test/scala/com/yubico/internal/util/CollectionUtilSpec.scala @@ -8,29 +8,40 @@ import org.scalatestplus.junit.JUnitRunner import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks @RunWith(classOf[JUnitRunner]) -class CollectionUtilSpec extends FunSpec with Matchers with ScalaCheckDrivenPropertyChecks { +class CollectionUtilSpec + extends FunSpec + with Matchers + with ScalaCheckDrivenPropertyChecks { describe("immutableMap") { - it("returns a Map instance which throws exceptions on modification attempts.") { + it( + "returns a Map instance which throws exceptions on modification attempts." + ) { forAll { m: java.util.Map[Int, Int] => val immutable = CollectionUtil.immutableMap(m) - an [UnsupportedOperationException] should be thrownBy { immutable.put(0, 0) } + an[UnsupportedOperationException] should be thrownBy { + immutable.put(0, 0) + } } forAll(minSize(1)) { m: java.util.Map[Int, Int] => val immutable = CollectionUtil.immutableMap(m) - an [UnsupportedOperationException] should be thrownBy { immutable.remove(0) } + an[UnsupportedOperationException] should be thrownBy { + immutable.remove(0) + } } } - it("prevents mutations to the argument from propagating to the return value.") { + it( + "prevents mutations to the argument from propagating to the return value." + ) { forAll { m: java.util.Map[Int, Int] => val immutable = CollectionUtil.immutableMap(m) - immutable should equal (m) + immutable should equal(m) m.put(0.to(10000).find(i => !m.containsKey(i)).get, 0) immutable should not equal m - immutable.size should equal (m.size - 1) + immutable.size should equal(m.size - 1) } } } @@ -39,48 +50,62 @@ class CollectionUtilSpec extends FunSpec with Matchers with ScalaCheckDrivenProp it("returns a List instance which throws exceptions on modification attempts.") { forAll { l: java.util.List[Int] => val immutable = CollectionUtil.immutableList(l) - an [UnsupportedOperationException] should be thrownBy { immutable.add(0) } + an[UnsupportedOperationException] should be thrownBy { + immutable.add(0) + } } forAll(minSize(1)) { l: java.util.List[Int] => val immutable = CollectionUtil.immutableList(l) - an [UnsupportedOperationException] should be thrownBy { immutable.remove(0) } + an[UnsupportedOperationException] should be thrownBy { + immutable.remove(0) + } } } - it("prevents mutations to the argument from propagating to the return value.") { + it( + "prevents mutations to the argument from propagating to the return value." + ) { forAll { l: java.util.List[Int] => val immutable = CollectionUtil.immutableList(l) - immutable should equal (l) + immutable should equal(l) l.add(0) immutable should not equal l - immutable.size should equal (l.size - 1) + immutable.size should equal(l.size - 1) } } } describe("immutableSet") { - it("returns a Set instance which throws exceptions on modification attempts.") { + it( + "returns a Set instance which throws exceptions on modification attempts." + ) { forAll { s: java.util.Set[Int] => val immutable = CollectionUtil.immutableSet(s) - an [UnsupportedOperationException] should be thrownBy { immutable.add(0) } + an[UnsupportedOperationException] should be thrownBy { + immutable.add(0) + } } forAll(minSize(1)) { s: java.util.Set[Int] => val immutable = CollectionUtil.immutableSet(s) - an [UnsupportedOperationException] should be thrownBy { immutable.remove(0) } + an[UnsupportedOperationException] should be thrownBy { + immutable.remove(0) + } } } - it("prevents mutations to the argument from propagating to the return value.") { + it( + "prevents mutations to the argument from propagating to the return value." + ) { forAll { s: java.util.Set[Int] => val immutable = CollectionUtil.immutableSet(s) - immutable should equal (s) + immutable should equal(s) s.add(0.to(10000).find(i => !s.contains(i)).get) immutable should not equal s - immutable.size should equal (s.size - 1) + immutable.size should equal(s.size - 1) } } } @@ -89,23 +114,29 @@ class CollectionUtilSpec extends FunSpec with Matchers with ScalaCheckDrivenProp it("returns a SortedSet instance which throws exceptions on modification attempts.") { forAll { s: java.util.SortedSet[Int] => val immutable = CollectionUtil.immutableSortedSet(s) - an [UnsupportedOperationException] should be thrownBy { immutable.add(0) } + an[UnsupportedOperationException] should be thrownBy { + immutable.add(0) + } } forAll(minSize(1)) { s: java.util.SortedSet[Int] => val immutable = CollectionUtil.immutableSortedSet(s) - an [UnsupportedOperationException] should be thrownBy { immutable.remove(0) } + an[UnsupportedOperationException] should be thrownBy { + immutable.remove(0) + } } } - it("prevents mutations to the argument from propagating to the return value.") { + it( + "prevents mutations to the argument from propagating to the return value." + ) { forAll { s: java.util.SortedSet[Int] => val immutable = CollectionUtil.immutableSortedSet(s) - immutable should equal (s) + immutable should equal(s) s.add(0.to(10000).find(i => !s.contains(i)).get) immutable should not equal s - immutable.size should equal (s.size - 1) + immutable.size should equal(s.size - 1) } } } 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 index 5c515f94b..1592f8bd7 100644 --- a/yubico-util/src/test/scala/com/yubico/internal/util/ComparableUtilSpec.scala +++ b/yubico-util/src/test/scala/com/yubico/internal/util/ComparableUtilSpec.scala @@ -1,27 +1,31 @@ package com.yubico.internal.util +import _root_.scala.jdk.CollectionConverters._ import org.junit.runner.RunWith import org.scalacheck.Arbitrary.arbitrary import org.scalacheck.Gen -import org.scalatestplus.junit.JUnitRunner import org.scalatest.FunSpec import org.scalatest.Matchers +import org.scalatestplus.junit.JUnitRunner import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks -import _root_.scala.jdk.CollectionConverters._ - - @RunWith(classOf[JUnitRunner]) -class ComparableUtilSpec extends FunSpec with Matchers with ScalaCheckDrivenPropertyChecks { - - 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) +class ComparableUtilSpec + extends FunSpec + with Matchers + with ScalaCheckDrivenPropertyChecks { + + 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(_.asInstanceOf[java.lang.Integer]).asJava) + new java.util.TreeSet[Integer]( + s.map(_.asInstanceOf[java.lang.Integer]).asJava + ) describe("compareComparableSets") { it("sorts differently-sized sets in order of cardinality.") { @@ -38,18 +42,20 @@ class ComparableUtilSpec extends FunSpec with Matchers with ScalaCheckDrivenProp } 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) + 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) + } } - } } } }