From cd6613bf5dadb3efb12a89d63f7bc044148c934f Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Thu, 4 May 2023 16:53:43 +0200 Subject: [PATCH 01/42] Update syntax of Gradle dependency declaration snippets --- README | 2 +- webauthn-server-attestation/README.adoc | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README b/README index 66ec489e6..469aa2a14 100644 --- a/README +++ b/README @@ -72,7 +72,7 @@ Maven: Gradle: ---------- -compile 'com.yubico:webauthn-server-core:2.4.1' +implementation("com.yubico:webauthn-server-core:2.4.1") ---------- NOTE: You may need additional dependencies with JCA providers to support some signature algorithms. diff --git a/webauthn-server-attestation/README.adoc b/webauthn-server-attestation/README.adoc index 11eb29a4c..c9cc6e6ef 100644 --- a/webauthn-server-attestation/README.adoc +++ b/webauthn-server-attestation/README.adoc @@ -102,7 +102,7 @@ Maven: Gradle: ---------- -compile 'com.yubico:webauthn-server-attestation:2.4.1' +implementation("com.yubico:webauthn-server-attestation:2.4.1") ---------- From fa2b55669b760a45026567e8966dfad50991124f Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 9 May 2023 17:59:16 +0200 Subject: [PATCH 02/42] Set release or target/sourceCompatibility conditionally --- .../src/main/kotlin/project-convention-java.gradle.kts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/buildSrc/src/main/kotlin/project-convention-java.gradle.kts b/buildSrc/src/main/kotlin/project-convention-java.gradle.kts index 100a52a62..944c8d6ae 100644 --- a/buildSrc/src/main/kotlin/project-convention-java.gradle.kts +++ b/buildSrc/src/main/kotlin/project-convention-java.gradle.kts @@ -13,7 +13,13 @@ tasks.withType(JavaCompile::class) { options.compilerArgs.add("-Xlint:deprecation") options.compilerArgs.add("-Xlint:unchecked") options.encoding = "UTF-8" - options.release.set(8) + + if (JavaVersion.current().isJava9Compatible) { + options.release.set(8) + } else { + targetCompatibility = "1.8" + sourceCompatibility = "1.8" + } } tasks.withType(Test::class) { From 094f746002cca2b7ccc75faaf3976050ee3aa5c5 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 9 May 2023 19:38:55 +0200 Subject: [PATCH 03/42] Pin OpenJDK version in reproducible binary workflow on release --- doc/releasing.md | 80 +++++++++++++++++++++++++++++++++++++----------- 1 file changed, 62 insertions(+), 18 deletions(-) diff --git a/doc/releasing.md b/doc/releasing.md index ce198f8b4..88083b50c 100644 --- a/doc/releasing.md +++ b/doc/releasing.md @@ -12,7 +12,29 @@ Release candidate versions $ ./gradlew clean check ``` - 3. Tag the head commit with an `X.Y.Z-RCN` tag: + 3. Update the Java version in the [`release-verify-signatures` + workflow](https://github.com/Yubico/java-webauthn-server/blob/main/.github/workflows/release-verify-signatures.yml#L42). + + See the `openjdk version` line of output from `java -version`: + + ``` + $ java -version # (example output below) + openjdk version "17.0.7" 2023-04-18 + OpenJDK Runtime Environment (build 17.0.7+7) + OpenJDK 64-Bit Server VM (build 17.0.7+7, mixed mode) + ``` + + Given the above output as an example, update the workflow like so: + + ```yaml + strategy: + matrix: + java: ["17.0.7"] + ``` + + Commit this change, if any. + + 4. Tag the head commit with an `X.Y.Z-RCN` tag: ``` $ git tag -a -s 1.4.0-RC1 -m "Pre-release 1.4.0-RC1" @@ -20,13 +42,13 @@ Release candidate versions No tag body needed. - 4. Publish to Sonatype Nexus: + 5. Publish to Sonatype Nexus: ``` $ ./gradlew publishToSonatype closeAndReleaseSonatypeStagingRepository ``` - 5. Push to GitHub. + 6. Push to GitHub. If the pre-release makes significant changes to the project README, such that the README does not accurately reflect the latest non-pre-release @@ -44,16 +66,17 @@ Release candidate versions $ git push origin main 1.4.0-RC1 ``` - 6. Make GitHub release. + 7. Make GitHub release. - - Use the new tag as the release tag - - Check the pre-release checkbox + - Use the new tag as the release tag. + - Check the pre-release checkbox. - Copy the release notes from `NEWS` into the GitHub release notes; reformat from ASCIIdoc to Markdown and remove line wraps. Include only changes/additions since the previous release or pre-release. - - Note which JDK version was used to build the artifacts. + - Note the JDK version shown by `java -version` in step 3. + For example: `openjdk version "17.0.7" 2023-04-18`. - 7. Check that the ["Reproducible binary" + 8. Check that the ["Reproducible binary" workflow](https://github.com/Yubico/java-webauthn-server/actions/workflows/release-verify-signatures.yml) runs and succeeds. @@ -91,20 +114,40 @@ Release versions 5. Update the version in JavaDoc links in the READMEs. - 6. Amend these changes into the merge commit: + 6. Update the Java version in the [`release-verify-signatures` + workflow](https://github.com/Yubico/java-webauthn-server/blob/main/.github/workflows/release-verify-signatures.yml#L42). + + See the `openjdk version` line of output from `java -version`: + + ``` + $ java -version # (example output below) + openjdk version "17.0.7" 2023-04-18 + OpenJDK Runtime Environment (build 17.0.7+7) + OpenJDK 64-Bit Server VM (build 17.0.7+7, mixed mode) + ``` + + Given the above output as an example, update the workflow like so: + + ```yaml + strategy: + matrix: + java: ["17.0.7"] + ``` + + 7. Amend these changes into the merge commit: ``` - $ git add NEWS + $ git add NEWS README */README .github/workflows/release-verify-signatures.yml $ git commit --amend --reset-author ``` - 7. Run the tests one more time: + 8. Run the tests one more time: ``` $ ./gradlew clean check ``` - 8. Tag the merge commit with an `X.Y.Z` tag: + 9. Tag the merge commit with an `X.Y.Z` tag: ``` $ git tag -a -s 1.4.0 -m "Release 1.4.0" @@ -112,26 +155,27 @@ Release versions No tag body needed since that's included in the commit. - 9. Publish to Sonatype Nexus: +10. Publish to Sonatype Nexus: ``` $ ./gradlew publishToSonatype closeAndReleaseSonatypeStagingRepository ``` -10. Push to GitHub: +11. Push to GitHub: ``` $ git push origin main 1.4.0 ``` -11. Make GitHub release. +12. Make GitHub release. - - Use the new tag as the release tag + - Use the new tag as the release tag. - Copy the release notes from `NEWS` into the GitHub release notes; reformat from ASCIIdoc to Markdown and remove line wraps. Include all changes since the previous release (not just changes since the previous pre-release). - - Note which JDK version was used to build the artifacts. + - Note the JDK version shown by `java -version` in step 6. + For example: `openjdk version "17.0.7" 2023-04-18`. -12. Check that the ["Reproducible binary" +13. Check that the ["Reproducible binary" workflow](https://github.com/Yubico/java-webauthn-server/actions/workflows/release-verify-signatures.yml) runs and succeeds. From 373678b863e94cee155eb09058d12bc75fcb659e Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 9 May 2023 19:58:49 +0200 Subject: [PATCH 04/42] Don't prescribe which Java toolchain to use --- .github/workflows/build.yml | 17 ++++++++++++++--- .../kotlin/project-convention-java.gradle.kts | 13 ------------- .../kotlin/project-convention-scala.gradle.kts | 3 --- 3 files changed, 14 insertions(+), 19 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4a70e0e8c..782041a8a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -44,9 +44,6 @@ jobs: - name: Compile libraries and tests run: ./gradlew clean testClasses - - name: Build archives - run: ./gradlew assemble - - name: Set up JDK ${{ matrix.java }} uses: actions/setup-java@v3 with: @@ -56,6 +53,19 @@ jobs: - name: Run tests against JDK17-compiled code run: ./gradlew test --exclude-task compileJava + - name: Archive HTML test report on failure + if: ${{ failure() }} + uses: actions/upload-artifact@v3 + with: + name: test-reports-java17-java${{ matrix.java }}-${{ matrix.distribution }}-html + path: "*/build/reports/**" + + - name: Build and test with JDK ${{ matrix.java }} + run: ./gradlew clean test + + - name: Build archives + run: ./gradlew assemble + - name: Archive HTML test report if: ${{ always() }} uses: actions/upload-artifact@v3 @@ -71,6 +81,7 @@ jobs: path: "*/build/test-results/**/*.xml" - name: Check binary reproducibility + if: ${{ matrix.java != 8 }} # JDK 8 does not produce reproducible binaries run: | ./gradlew clean primaryPublishJar find . -name '*.jar' | xargs sha256sum | tee checksums.sha256sum diff --git a/buildSrc/src/main/kotlin/project-convention-java.gradle.kts b/buildSrc/src/main/kotlin/project-convention-java.gradle.kts index 944c8d6ae..bf19301ad 100644 --- a/buildSrc/src/main/kotlin/project-convention-java.gradle.kts +++ b/buildSrc/src/main/kotlin/project-convention-java.gradle.kts @@ -2,13 +2,6 @@ plugins { java } -java { - toolchain { - // Java 8 binaries are not reproducible - languageVersion.set(JavaLanguageVersion.of(11)) - } -} - tasks.withType(JavaCompile::class) { options.compilerArgs.add("-Xlint:deprecation") options.compilerArgs.add("-Xlint:unchecked") @@ -21,9 +14,3 @@ tasks.withType(JavaCompile::class) { sourceCompatibility = "1.8" } } - -tasks.withType(Test::class) { - javaLauncher.set(javaToolchains.launcherFor { - languageVersion.set(JavaLanguageVersion.of(8)) - }) -} diff --git a/buildSrc/src/main/kotlin/project-convention-scala.gradle.kts b/buildSrc/src/main/kotlin/project-convention-scala.gradle.kts index 16f2f34b8..83a20b599 100644 --- a/buildSrc/src/main/kotlin/project-convention-scala.gradle.kts +++ b/buildSrc/src/main/kotlin/project-convention-scala.gradle.kts @@ -8,7 +8,4 @@ tasks.withType(ScalaCompile::class) { // See: https://github.com/gradle/gradle/pull/23198 // See: https://github.com/gradle/gradle/pull/23751 scalaCompileOptions.additionalParameters = mutableListOf("-Wunused") - javaLauncher.set(javaToolchains.launcherFor { - languageVersion.set(JavaLanguageVersion.of(8)) - }) } From 793053db279c8586daa289a94e72bcc66a6ca65d Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Wed, 10 May 2023 15:53:39 +0200 Subject: [PATCH 05/42] Delete unused code in demo --- .../java/demo/webauthn/WebAuthnServer.java | 49 +------------------ 1 file changed, 2 insertions(+), 47 deletions(-) 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 217c64a4c..5ed9fdbb3 100644 --- a/webauthn-server-demo/src/main/java/demo/webauthn/WebAuthnServer.java +++ b/webauthn-server-demo/src/main/java/demo/webauthn/WebAuthnServer.java @@ -32,9 +32,6 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; -import com.upokecenter.cbor.CBORObject; -import com.yubico.fido.metadata.FidoMetadataDownloaderException; -import com.yubico.fido.metadata.UnexpectedLegalHeader; import com.yubico.internal.util.CertificateParser; import com.yubico.internal.util.JacksonCodecs; import com.yubico.util.Either; @@ -53,36 +50,25 @@ import com.yubico.webauthn.data.AuthenticatorSelectionCriteria; import com.yubico.webauthn.data.AuthenticatorTransport; import com.yubico.webauthn.data.ByteArray; -import com.yubico.webauthn.data.COSEAlgorithmIdentifier; import com.yubico.webauthn.data.RelyingPartyIdentity; import com.yubico.webauthn.data.ResidentKeyRequirement; import com.yubico.webauthn.data.UserIdentity; -import com.yubico.webauthn.data.exception.Base64UrlException; import com.yubico.webauthn.exception.AssertionFailedException; import com.yubico.webauthn.exception.RegistrationFailedException; -import com.yubico.webauthn.extension.appid.InvalidAppIdException; import demo.webauthn.data.AssertionRequestWrapper; import demo.webauthn.data.AssertionResponse; import demo.webauthn.data.CredentialRegistration; import demo.webauthn.data.RegistrationRequest; import demo.webauthn.data.RegistrationResponse; import java.io.IOException; -import java.security.DigestException; -import java.security.InvalidAlgorithmParameterException; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; -import java.security.SignatureException; -import java.security.cert.CertPathValidatorException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.time.Clock; import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.SortedSet; @@ -112,11 +98,7 @@ public class WebAuthnServer { private final RelyingParty rp; - public WebAuthnServer() - throws InvalidAppIdException, CertificateException, CertPathValidatorException, - InvalidAlgorithmParameterException, Base64UrlException, DigestException, - FidoMetadataDownloaderException, UnexpectedLegalHeader, IOException, - NoSuchAlgorithmException, SignatureException, InvalidKeyException { + public WebAuthnServer() { this( new InMemoryRegistrationStorage(), newCache(), @@ -130,11 +112,7 @@ public WebAuthnServer( Cache registerRequestStorage, Cache assertRequestStorage, RelyingPartyIdentity rpIdentity, - Set origins) - throws InvalidAppIdException, CertificateException, CertPathValidatorException, - InvalidAlgorithmParameterException, Base64UrlException, DigestException, - FidoMetadataDownloaderException, UnexpectedLegalHeader, IOException, - NoSuchAlgorithmException, SignatureException, InvalidKeyException { + Set origins) { this.userStorage = userStorage; this.registerRequestStorage = registerRequestStorage; this.assertRequestStorage = assertRequestStorage; @@ -579,29 +557,6 @@ private CredentialRegistration addRegistration( 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])); - } - - 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( From 6ac533320634f428f34e388e567e192fb306daaf Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Wed, 10 May 2023 16:30:21 +0200 Subject: [PATCH 06/42] Enable using FidoMetadataService in demo --- webauthn-server-demo/.gitignore | 3 + webauthn-server-demo/README | 14 ++++ webauthn-server-demo/build.gradle.kts | 3 + .../YubicoJsonMetadataService.java | 80 +++++++++++-------- .../webauthn/CompositeMetadataService.java | 46 +++++++++++ .../src/main/java/demo/webauthn/Config.java | 4 + .../webauthn/FidoMetadataServiceAdapter.java | 30 +++++++ .../java/demo/webauthn/MetadataService.java | 10 +++ .../java/demo/webauthn/WebAuthnServer.java | 64 ++++++++++++--- .../webauthn/data/CredentialRegistration.java | 3 +- .../src/main/webapp/index.html | 45 ++++++++--- 11 files changed, 246 insertions(+), 56 deletions(-) create mode 100644 webauthn-server-demo/.gitignore create mode 100644 webauthn-server-demo/src/main/java/demo/webauthn/CompositeMetadataService.java create mode 100644 webauthn-server-demo/src/main/java/demo/webauthn/FidoMetadataServiceAdapter.java create mode 100644 webauthn-server-demo/src/main/java/demo/webauthn/MetadataService.java diff --git a/webauthn-server-demo/.gitignore b/webauthn-server-demo/.gitignore new file mode 100644 index 000000000..31192d386 --- /dev/null +++ b/webauthn-server-demo/.gitignore @@ -0,0 +1,3 @@ +# FidoMetadataDownloader cache files +webauthn-server-demo-fido-mds-blob-cache.bin +webauthn-server-demo-fido-mds-trust-root-cache.bin diff --git a/webauthn-server-demo/README b/webauthn-server-demo/README index f1e50c32f..5f549cd57 100644 --- a/webauthn-server-demo/README +++ b/webauthn-server-demo/README @@ -18,6 +18,12 @@ class which provides the REST API on top of it. $ ./gradlew run $ $BROWSER https://localhost:8443/ +Or to run with the https://fidoalliance.org/metadata/[FIDO Metadata Service] as +a source of attestation metadata: + + $ YUBICO_WEBAUTHN_USE_FIDO_MDS=true ./gradlew run + $ $BROWSER https://localhost:8443/ + == Architecture @@ -150,3 +156,11 @@ correct environment. https://www.w3.org/TR/webauthn/#dom-publickeycredentialentity-name[RP name] the server will report. Example: `YUBICO_WEBAUTHN_RP_ID='Yubico Web Authentication demo'` + + - `YUBICO_WEBAUTHN_USE_FIDO_MDS`: If set to `true` (case-insensitive), use + https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.4.1/com/yubico/fido/metadata/FidoMetadataService.html[`FidoMetadataService`] + from the link:../webauthn-server-attestation[`webauthn-server-attestation`] + module as a source of attestation data in addition to the static JSON file + bundled with the demo. This will write cache files to the + `webauthn-server-demo` project root directory; see the patterns in the + `.gitignore` file. diff --git a/webauthn-server-demo/build.gradle.kts b/webauthn-server-demo/build.gradle.kts index 7a77b2347..82830c46e 100644 --- a/webauthn-server-demo/build.gradle.kts +++ b/webauthn-server-demo/build.gradle.kts @@ -55,6 +55,9 @@ dependencies { application { mainClass.set("demo.webauthn.EmbeddedServer") + + // Required for processing CRL distribution points extension + applicationDefaultJvmArgs = listOf("-Dcom.sun.security.enableCRLDP=true") } for (task in listOf(tasks.installDist, tasks.distZip, tasks.distTar)) { diff --git a/webauthn-server-demo/src/main/java/com/yubico/webauthn/attestation/YubicoJsonMetadataService.java b/webauthn-server-demo/src/main/java/com/yubico/webauthn/attestation/YubicoJsonMetadataService.java index baf21282e..d704fe12d 100644 --- a/webauthn-server-demo/src/main/java/com/yubico/webauthn/attestation/YubicoJsonMetadataService.java +++ b/webauthn-server-demo/src/main/java/com/yubico/webauthn/attestation/YubicoJsonMetadataService.java @@ -31,8 +31,10 @@ import com.yubico.internal.util.CertificateParser; import com.yubico.internal.util.CollectionUtil; import com.yubico.internal.util.OptionalUtil; +import com.yubico.webauthn.RegistrationResult; import com.yubico.webauthn.attestation.matcher.ExtensionMatcher; import com.yubico.webauthn.data.ByteArray; +import demo.webauthn.MetadataService; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.Collection; @@ -48,7 +50,7 @@ import lombok.extern.slf4j.Slf4j; @Slf4j -public final class YubicoJsonMetadataService implements AttestationTrustSource { +public final class YubicoJsonMetadataService implements AttestationTrustSource, MetadataService { private static final String SELECTORS = "selectors"; private static final String SELECTOR_TYPE = "type"; @@ -90,43 +92,53 @@ public YubicoJsonMetadataService() { DEFAULT_DEVICE_MATCHERS); } - public Optional findMetadata(X509Certificate attestationCertificate) { - return metadataObjects.stream() + @Override + public Set findEntries(@NonNull RegistrationResult registrationResult) { + return registrationResult + .getAttestationTrustPath() .map( - metadata -> { - Map vendorProperties; - Map deviceProperties = null; - String identifier; + certs -> { + X509Certificate attestationCertificate = certs.get(0); + return metadataObjects.stream() + .map( + metadata -> { + Map vendorProperties; + Map deviceProperties = null; + String identifier; - identifier = metadata.getIdentifier(); - vendorProperties = Maps.filterValues(metadata.getVendorInfo(), Objects::nonNull); - for (JsonNode device : metadata.getDevices()) { - if (deviceMatches(device.get(SELECTORS), attestationCertificate)) { - 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; - } - } + identifier = metadata.getIdentifier(); + vendorProperties = + Maps.filterValues(metadata.getVendorInfo(), Objects::nonNull); + for (JsonNode device : metadata.getDevices()) { + if (deviceMatches(device.get(SELECTORS), attestationCertificate)) { + 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; + } + } - return Optional.ofNullable(deviceProperties) - .map( - deviceProps -> - Attestation.builder() - .metadataIdentifier(Optional.ofNullable(identifier)) - .vendorProperties(Optional.of(vendorProperties)) - .deviceProperties(deviceProps) - .build()); + return Optional.ofNullable(deviceProperties) + .map( + deviceProps -> + Attestation.builder() + .metadataIdentifier(Optional.ofNullable(identifier)) + .vendorProperties(Optional.of(vendorProperties)) + .deviceProperties(deviceProps) + .build()); + }) + .flatMap(OptionalUtil::stream) + .map(attestation -> (Object) attestation) + .collect(Collectors.toSet()); }) - .flatMap(OptionalUtil::stream) - .findAny(); + .orElseGet(Collections::emptySet); } private boolean deviceMatches( diff --git a/webauthn-server-demo/src/main/java/demo/webauthn/CompositeMetadataService.java b/webauthn-server-demo/src/main/java/demo/webauthn/CompositeMetadataService.java new file mode 100644 index 000000000..d8d4d0e1d --- /dev/null +++ b/webauthn-server-demo/src/main/java/demo/webauthn/CompositeMetadataService.java @@ -0,0 +1,46 @@ +package demo.webauthn; + +import com.yubico.webauthn.RegistrationResult; +import com.yubico.webauthn.attestation.AttestationTrustSource; +import com.yubico.webauthn.data.ByteArray; +import java.security.cert.X509Certificate; +import java.util.*; +import lombok.NonNull; + +/** + * Combines several attestation metadata sources into one, which delegates to each sub-service in + * order until one returns a non-empty result. + */ +public class CompositeMetadataService implements AttestationTrustSource, MetadataService { + + private final List delegates; + + public CompositeMetadataService(MetadataService... delegates) { + this.delegates = Collections.unmodifiableList(Arrays.asList(delegates)); + } + + @Override + public TrustRootsResult findTrustRoots( + List attestationCertificateChain, Optional aaguid) { + for (MetadataService delegate : delegates) { + TrustRootsResult res = delegate.findTrustRoots(attestationCertificateChain, aaguid); + if (!res.getTrustRoots().isEmpty()) { + return res; + } + } + + return TrustRootsResult.builder().trustRoots(Collections.emptySet()).build(); + } + + @Override + public Set findEntries(@NonNull RegistrationResult registrationResult) { + for (MetadataService delegate : delegates) { + Set res = delegate.findEntries(registrationResult); + if (!res.isEmpty()) { + return res; + } + } + + return Collections.emptySet(); + } +} 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 bcb884be2..bf4dcebd4 100644 --- a/webauthn-server-demo/src/main/java/demo/webauthn/Config.java +++ b/webauthn-server-demo/src/main/java/demo/webauthn/Config.java @@ -78,6 +78,10 @@ public static RelyingPartyIdentity getRpIdentity() { return getInstance().rpIdentity; } + public static boolean useFidoMds() { + return "true".equalsIgnoreCase(System.getenv("YUBICO_WEBAUTHN_USE_FIDO_MDS")); + } + private static Set computeOrigins() { final String origins = System.getenv("YUBICO_WEBAUTHN_ALLOWED_ORIGINS"); diff --git a/webauthn-server-demo/src/main/java/demo/webauthn/FidoMetadataServiceAdapter.java b/webauthn-server-demo/src/main/java/demo/webauthn/FidoMetadataServiceAdapter.java new file mode 100644 index 000000000..e5b41074e --- /dev/null +++ b/webauthn-server-demo/src/main/java/demo/webauthn/FidoMetadataServiceAdapter.java @@ -0,0 +1,30 @@ +package demo.webauthn; + +import com.yubico.fido.metadata.FidoMetadataService; +import com.yubico.webauthn.RegistrationResult; +import com.yubico.webauthn.data.ByteArray; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import lombok.AllArgsConstructor; +import lombok.NonNull; + +@AllArgsConstructor +public class FidoMetadataServiceAdapter implements MetadataService { + private final FidoMetadataService fido; + + @Override + public TrustRootsResult findTrustRoots( + List attestationCertificateChain, Optional aaguid) { + return fido.findTrustRoots(attestationCertificateChain, aaguid); + } + + @Override + public Set findEntries(@NonNull RegistrationResult registrationResult) { + return fido.findEntries(registrationResult).stream() + .map(metadataBLOBPayloadEntry -> (Object) metadataBLOBPayloadEntry) + .collect(Collectors.toSet()); + } +} diff --git a/webauthn-server-demo/src/main/java/demo/webauthn/MetadataService.java b/webauthn-server-demo/src/main/java/demo/webauthn/MetadataService.java new file mode 100644 index 000000000..9b8838861 --- /dev/null +++ b/webauthn-server-demo/src/main/java/demo/webauthn/MetadataService.java @@ -0,0 +1,10 @@ +package demo.webauthn; + +import com.yubico.webauthn.RegistrationResult; +import com.yubico.webauthn.attestation.AttestationTrustSource; +import java.util.Set; +import lombok.NonNull; + +public interface MetadataService extends AttestationTrustSource { + Set findEntries(@NonNull RegistrationResult registrationResult); +} 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 5ed9fdbb3..a087c61a2 100644 --- a/webauthn-server-demo/src/main/java/demo/webauthn/WebAuthnServer.java +++ b/webauthn-server-demo/src/main/java/demo/webauthn/WebAuthnServer.java @@ -32,6 +32,10 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; +import com.yubico.fido.metadata.FidoMetadataDownloader; +import com.yubico.fido.metadata.FidoMetadataDownloaderException; +import com.yubico.fido.metadata.FidoMetadataService; +import com.yubico.fido.metadata.UnexpectedLegalHeader; import com.yubico.internal.util.CertificateParser; import com.yubico.internal.util.JacksonCodecs; import com.yubico.util.Either; @@ -43,7 +47,6 @@ import com.yubico.webauthn.RelyingParty; import com.yubico.webauthn.StartAssertionOptions; import com.yubico.webauthn.StartRegistrationOptions; -import com.yubico.webauthn.attestation.Attestation; import com.yubico.webauthn.attestation.YubicoJsonMetadataService; import com.yubico.webauthn.data.AttestationConveyancePreference; import com.yubico.webauthn.data.AuthenticatorData; @@ -53,6 +56,7 @@ import com.yubico.webauthn.data.RelyingPartyIdentity; import com.yubico.webauthn.data.ResidentKeyRequirement; import com.yubico.webauthn.data.UserIdentity; +import com.yubico.webauthn.data.exception.Base64UrlException; import com.yubico.webauthn.exception.AssertionFailedException; import com.yubico.webauthn.exception.RegistrationFailedException; import demo.webauthn.data.AssertionRequestWrapper; @@ -60,8 +64,15 @@ import demo.webauthn.data.CredentialRegistration; import demo.webauthn.data.RegistrationRequest; import demo.webauthn.data.RegistrationResponse; +import java.io.File; import java.io.IOException; +import java.security.DigestException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; +import java.security.SignatureException; +import java.security.cert.CertPathValidatorException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.time.Clock; @@ -91,14 +102,48 @@ public class WebAuthnServer { private final InMemoryRegistrationStorage userStorage; private final SessionManager sessions = new SessionManager(); - private final YubicoJsonMetadataService metadataService = new YubicoJsonMetadataService(); + private final MetadataService metadataService = getMetadataService(); + + private static MetadataService getMetadataService() + throws CertPathValidatorException, InvalidAlgorithmParameterException, Base64UrlException, + DigestException, FidoMetadataDownloaderException, CertificateException, + UnexpectedLegalHeader, IOException, NoSuchAlgorithmException, SignatureException, + InvalidKeyException { + if (Config.useFidoMds()) { + logger.info("Using combination of Yubico JSON file and FIDO MDS for attestation metadata."); + return new CompositeMetadataService( + new YubicoJsonMetadataService(), + new FidoMetadataServiceAdapter( + FidoMetadataService.builder() + .useBlob( + FidoMetadataDownloader.builder() + .expectLegalHeader( + "Retrieval and use of this BLOB indicates acceptance of the appropriate agreement located at https://fidoalliance.org/metadata/metadata-legal-terms/") + .useDefaultTrustRoot() + .useTrustRootCacheFile( + new File("webauthn-server-demo-fido-mds-trust-root-cache.bin")) + .useDefaultBlob() + .useBlobCacheFile( + new File("webauthn-server-demo-fido-mds-blob-cache.bin")) + .build() + .loadCachedBlob()) + .build())); + } else { + logger.info("Using only Yubico JSON file for attestation metadata."); + return new YubicoJsonMetadataService(); + } + } private final Clock clock = Clock.systemDefaultZone(); private final ObjectMapper jsonMapper = JacksonCodecs.json(); private final RelyingParty rp; - public WebAuthnServer() { + public WebAuthnServer() + throws CertificateException, CertPathValidatorException, InvalidAlgorithmParameterException, + Base64UrlException, DigestException, FidoMetadataDownloaderException, + UnexpectedLegalHeader, IOException, NoSuchAlgorithmException, SignatureException, + InvalidKeyException { this( new InMemoryRegistrationStorage(), newCache(), @@ -112,7 +157,11 @@ public WebAuthnServer( Cache registerRequestStorage, Cache assertRequestStorage, RelyingPartyIdentity rpIdentity, - Set origins) { + Set origins) + throws CertificateException, CertPathValidatorException, InvalidAlgorithmParameterException, + Base64UrlException, DigestException, FidoMetadataDownloaderException, + UnexpectedLegalHeader, IOException, NoSuchAlgorithmException, SignatureException, + InvalidKeyException { this.userStorage = userStorage; this.registerRequestStorage = registerRequestStorage; this.assertRequestStorage = assertRequestStorage; @@ -526,10 +575,7 @@ private CredentialRegistration addRegistration( .signatureCount(result.getSignatureCount()) .build(), result.getKeyId().getTransports().orElseGet(TreeSet::new), - result - .getAttestationTrustPath() - .flatMap(x5c -> x5c.stream().findFirst()) - .flatMap(metadataService::findMetadata)); + metadataService.findEntries(result).stream().findAny()); } private CredentialRegistration addRegistration( @@ -537,7 +583,7 @@ private CredentialRegistration addRegistration( Optional nickname, RegisteredCredential credential, SortedSet transports, - Optional attestationMetadata) { + Optional attestationMetadata) { CredentialRegistration reg = CredentialRegistration.builder() .userIdentity(userIdentity) 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 e88aba83d..ef5878821 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 @@ -27,7 +27,6 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import com.yubico.webauthn.RegisteredCredential; -import com.yubico.webauthn.attestation.Attestation; import com.yubico.webauthn.data.AuthenticatorTransport; import com.yubico.webauthn.data.UserIdentity; import java.time.Instant; @@ -49,7 +48,7 @@ public class CredentialRegistration { @JsonIgnore Instant registrationTime; RegisteredCredential credential; - Optional attestationMetadata; + Optional attestationMetadata; @JsonProperty("registrationTime") public String getRegistrationTimestamp() { diff --git a/webauthn-server-demo/src/main/webapp/index.html b/webauthn-server-demo/src/main/webapp/index.html index df79e27d9..2c27d1f11 100644 --- a/webauthn-server-demo/src/main/webapp/index.html +++ b/webauthn-server-demo/src/main/webapp/index.html @@ -158,25 +158,48 @@ function hideDeviceInfo() { document.getElementById("device-info").style = "display: none"; } -function showDeviceInfo(params) { - document.getElementById("device-info").style = undefined; - if (params.displayName) { - document.getElementById("device-name-row").style = undefined; - document.getElementById("device-name").textContent = params.displayName; - } else { - document.getElementById("device-name-row").style = "display: none"; - } +function showDeviceInfo(params) { + document.getElementById("device-icon").style = "display: none"; + document.getElementById("device-name-row").style = "display: none"; if (params.nickname) { document.getElementById("device-nickname-row").style = undefined; document.getElementById("device-nickname").textContent = params.nickname; } else { - document.getElementById("device-nickname-row").style = "display: none"; + document.getElementById("device-nickname-row").style = "display: none"; + } + + if (params?.metadataStatement) { + showDeviceInfoFidoMds(params.metadataStatement); + } else { + showDeviceInfoYubico(params); } - if (params.imageUrl) { + document.getElementById("device-info").style = undefined; +} + +function showDeviceInfoYubico(params) { + if (params?.displayName) { + document.getElementById("device-name-row").style = undefined; + document.getElementById("device-name").textContent = params.displayName; + } + + if (params?.imageUrl) { document.getElementById("device-icon").src = params.imageUrl; + document.getElementById("device-icon").style = undefined; + } +} + +function showDeviceInfoFidoMds(metadataStatement) { + if (metadataStatement?.description) { + document.getElementById("device-name-row").style = undefined; + document.getElementById("device-name").textContent = metadataStatement?.description; + } + + if (metadataStatement?.icon) { + document.getElementById("device-icon").src = metadataStatement?.icon; + document.getElementById("device-icon").style = undefined; } } @@ -329,7 +352,7 @@ if (data.registration && data.registration.attestationMetadata) { showDeviceInfo(extend( - data.registration.attestationMetadata.deviceProperties, + data.registration.attestationMetadata?.deviceProperties || data.registration.attestationMetadata, nicknameInfo )); } else { From e952e68f51e86b3b5b7cd648c58ccb59de3c7ce5 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Fri, 12 May 2023 12:33:06 +0200 Subject: [PATCH 07/42] Use ASCIIDoc cross-link for cross-link to reproducible builds --- README | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README b/README index 469aa2a14..d57eecb71 100644 --- a/README +++ b/README @@ -44,7 +44,7 @@ toc::[] - Optionally integrates with an "attestation trust source" to verify https://www.w3.org/TR/webauthn/#sctn-attestation[authenticator attestations] - Reproducible builds: release signatures match fresh builds from source. See - link:#reproducible-builds[Reproducible builds] below. + <> below. === Non-features @@ -689,6 +689,7 @@ $ ./gradlew pitest ---------- +[#reproducible-builds] === Reproducible builds Starting in version `1.4.0-RC2`, artifacts are built reproducibly. Fresh builds from tagged commits should therefore be verifiable by signatures from Maven Central From d21061cabf456cf26d8b8794ce0dfd4e89d2e90c Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Fri, 12 May 2023 13:49:55 +0200 Subject: [PATCH 08/42] Update README and JavaDoc for passkeys --- NEWS | 6 + README | 135 ++++++++++++++---- .../com/yubico/webauthn/AssertionRequest.java | 48 ++++--- .../yubico/webauthn/RegistrationResult.java | 10 +- .../webauthn/StartAssertionOptions.java | 42 +++--- .../data/AuthenticatorSelectionCriteria.java | 28 +++- .../com/yubico/webauthn/data/Extensions.java | 6 +- .../webauthn/data/ResidentKeyRequirement.java | 17 ++- 8 files changed, 214 insertions(+), 78 deletions(-) diff --git a/NEWS b/NEWS index 581fc2c80..7d7f49d63 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,9 @@ +== Version 2.4.1 (unreleased) == + +* Updated README and JavaDoc to use the "passkey" term and provide more guidance + around passkey use cases. + + == Version 2.4.1 == Changes: diff --git a/README b/README index d57eecb71..813ebe475 100644 --- a/README +++ b/README @@ -11,8 +11,7 @@ image:https://github.com/Yubico/java-webauthn-server/actions/workflows/release-v Server-side https://www.w3.org/TR/webauthn/[Web Authentication] library for Java. Provides implementations of the https://www.w3.org/TR/webauthn/#sctn-rp-operations[Relying Party operations] required -for a server to support Web Authentication. This includes registering -authenticators and authenticating registered authenticators. +for a server to support Web Authentication, including https://passkeys.dev/[passkey authentication]. [WARNING] @@ -30,6 +29,7 @@ If you are, we urge you to upgrade your Java deployment to a version that is saf *Table of contents* +:toclevels: 3 toc::[] @@ -133,7 +133,7 @@ The server side involves: and link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/RelyingParty.html#finishAssertion(com.yubico.webauthn.FinishAssertionOptions)[`RelyingParty.finishAssertion(...)`] methods to perform authentication ceremonies. - 5. Use the outputs of `finishRegistration` and `finishAssertion` to update your database, initiate sessions, etc. + 5. Optionally use additional features: passkeys, passwordless multi-factor authentication, credential backup state. The client side involves: @@ -275,18 +275,21 @@ Here is an example of things you will likely want to store: [source,java] ---------- -storeCredential( // Some database access method of your own design - "alice", // Username or other appropriate user identifier - result.getKeyId(), // Credential ID and transports for allowCredentials - result.getPublicKeyCose(), // Public key for verifying authentication signatures - result.isDiscoverable(), // Can this key be used for username-less auth? - result.signatureCount(), // Initial signature counter value +storeCredential( // Some database access method of your own design + "alice", // Username or other appropriate user identifier + result.getKeyId(), // Credential ID and transports for allowCredentials + result.getPublicKeyCose(), // Public key for verifying authentication signatures + result.getSignatureCount(), // Initial signature counter value + result.isDiscoverable(), // Is this a passkey? + result.isBackupEligible(), // Can this credential be backed up (synced)? + result.isBackedUp(), // Is this credential currently backed up? pkc.getResponse().getAttestationObject(), // Store attestation object for future reference pkc.getResponse().getClientDataJSON() // Store client data for re-verifying signature if needed ); ---------- +[#getting-started-authentication] === 4. Authentication Like registration ceremonies, an authentication ceremony consists of 5 main steps: @@ -367,14 +370,15 @@ throw new RuntimeException("Authentication failed"); Finally, if the previous step was successful, update your database using the link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/AssertionResult.html[`AssertionResult`]. -Most importantly, you should update the signature counter. That might look something like this: +Most importantly you should update the signature counter, and the backup state flag if you use it. That might look something like this: [source,java] ---------- updateCredential( // Some database access method of your own design "alice", // Query by username or other appropriate user identifier result.getCredentialId(), // Query by credential ID of the credential used - result.signatureCount(), // Set new signature counter value + result.getSignatureCount(), // Set new signature counter value + result.isBackedUp(), // Set new backup state flag Clock.systemUTC().instant() // Set time of last use (now) ); ---------- @@ -382,16 +386,40 @@ updateCredential( // Some database access method of your own design Then do whatever else you need - for example, initiate a user session. -=== 5. Passwordless, username-less authentication +=== 5. Optional features: passkeys, multi-factor, backup state -WebAuthn supports passwordless multi-factor authentication via on-authenticator -https://www.w3.org/TR/webauthn-2/#user-verification[user verification], -and username-less authentication via -https://www.w3.org/TR/webauthn-2/#discoverable-credential[discoverable credentials] -(sometimes the term "passwordless" is used to mean the combination of both, but -here the two are treated separately). +WebAuthn supports a number of additional features beyond the basics: -Discoverable credentials must be enabled at registration time by setting the +- <>: passwordless, username-less authentication. + link:https://passkey.org[Try it on passkey.org!] +- <>: passwordless, streamlined multi-factor authentication. +- <>: Unintrusive passkey integration in traditional login forms. +- <>: hints on how vulnerable the user is to authenticator loss. + + +[#passkeys] +==== Passkeys: passwordless, username-less authentication + +A https://passkeys.dev/[passkey] is a WebAuthn credential that can simultaneously both _identify_ and _authenticate_ the user. +This is also called a link:https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#discoverable-credential[discoverable credential]. +By default, credentials are created non-discoverable, which means the server +must list them in the +https://www.w3.org/TR/webauthn/#dom-publickeycredentialrequestoptions-allowcredentials[`allowCredentials`] +parameter before the user can use them to authenticate. +This is typically because the credential private key is not stored within the authenticator, +but instead encoded into one of the credential IDs in `allowCredentials`. +This way even a small hardware authenticator can have an unlimited credential capacity, +but with the drawback that the user must first identify themself to the server +so the server can retrieve the correct `allowCredentials` list. + +Passkeys are instead stored within the authenticator, and also include the user's +link:https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#user-handle[user handle] +in addition to the credential ID. +This way the user can be both identified and authenticated simultaneously. +Many passkey-capable authenticators also offer a credential sync mechanism +to allow one passkey to be used on multiple devices. + +Passkeys can be created by setting the link:https://www.w3.org/TR/webauthn-2/#dom-publickeycredentialcreationoptions-authenticatorselection[`authenticatorSelection`].link:https://www.w3.org/TR/webauthn-2/#dom-authenticatorselectioncriteria-residentkey[`residentKey`] option: @@ -413,13 +441,22 @@ The username can then be omitted when starting an authentication ceremony: AssertionRequest request = rp.startAssertion(StartAssertionOptions.builder().build()); ---------- -Some authenticators might enable this feature even if not required, and setting -the `residentKey` option to `ResidentKeyRequirement.PREFERRED` will enable it if the -authenticator supports it. The -https://www.w3.org/TR/webauthn-2/#sctn-authenticator-credential-properties-extension[`credProps` extension] -can be used to determine whether the created credential is discoverable, and is enabled by default. +Some authenticators might create passkeys even if not required, and setting +the `residentKey` option to `ResidentKeyRequirement.PREFERRED` will create a passkey +if the authenticator supports it. +The +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/RegistrationResult.html#isDiscoverable()[`RegistrationResult.isDiscoverable()`] +method can be used to determine whether the created credential is a passkey. +This requires the +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/data/RegistrationExtensionInputs.RegistrationExtensionInputsBuilder.html#credProps()[`credProps` extension], +to be enabled, which it is by default. -User verification can be enforced independently per authentication ceremony: + +[#user-verification] +==== User verification: passwordless multi-factor authentication + +link:https://passkeys.dev/docs/reference/terms/#user-verification-uv[User verification] +can be enforced independently per authentication ceremony: [source,java] ---------- @@ -448,6 +485,54 @@ PublicKeyCredentialCreationOptions request = rp.startRegistration( .build()); ---------- +User verification can be used with both discoverable credentials (passkeys) and non-discoverable credentials. + + +[#autofill-ui] +==== Using passkeys with autofill UI + +Passkeys on platform authenticators may also support the WebAuthn +link:https://passkeys.dev/docs/reference/terms/#autofill-ui[autofill UI], also known as "conditional mediation". +This can help onboard users who are unfamiliar with a fully username-less login flow, +allowing a familiar username input field to opportunistically offer a shortcut using a passkey +if the user has one on their device. + +This library is compatible with the autofill UI but provides no server-side options for it, +because the steps to enable it are taken on the front-end side. +Using autofill UI does not affect the response verification procedure. + +See the link:https://passkeys.dev/docs/use-cases/bootstrapping/[guide on passkeys.dev] +for complete instructions on how to enable the autofill UI. +In particular you need to: + +- Add the credential request option `mediation: "conditional"` +alongside the `publicKey` option generated by +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/RelyingParty.html#startAssertion(com.yubico.webauthn.StartAssertionOptions)[`RelyingParty.startAssertion(...)`], +- Add `autocomplete="username webauthn"` to a username input field on the page, and +- Call `navigator.credentials.get()` in the background. + +If the Promise resolves, handle it like any other assertion response as described in +<> above. + +Because of technical limitations, autofill UI is as of May 2023 only supported for platform credentials, +i.e., passkeys stored on the user's computing devices. +Autofill UI might support passkeys on external security keys in the future. + + +[#credential-backup-state] +==== Credential backup state + +Some authenticators may allow credentials to be backed up and/or synced between devices. +This capability and its current state is signaled via the +link:https://w3c.github.io/webauthn/#sctn-credential-backup[Credential Backup State] flags, +which are available via the `isBackedUp()` and `isBackupEligible()` methods of +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/RegistrationResult.html[`RegistrationResult`] +and +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/AssertionResult.html[`AssertionResult`]. +These can be used as a hint about how vulnerable a user is to authenticator loss. +In particular, a user with only one credential which is not backed up +may risk getting locked out if they lose their authenticator. + == Migrating from version `1.x` 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 333e4ae76..a35a11f58 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 @@ -59,8 +59,10 @@ public class AssertionRequest { *

If both this and {@link #getUserHandle() userHandle} are empty, this indicates that this is * a request for an assertion by a client-side-discoverable - * credential, and identification of the user has been deferred until the response is - * received. + * credential (passkey), and identification of the user has been deferred until the response + * is received. + * + * @see Passkey */ private final String username; @@ -74,8 +76,10 @@ public class AssertionRequest { *

If both this and {@link #getUsername() username} are empty, this indicates that this is a * request for an assertion by a client-side-discoverable - * credential, and identification of the user has been deferred until the response is - * received. + * credential (passkey), and identification of the user has been deferred until the response + * is received. + * + * @see Passkey */ private final ByteArray userHandle; @@ -105,8 +109,10 @@ private AssertionRequest( *

If both this and {@link #getUserHandle()} are empty, this indicates that this is a request * for an assertion by a client-side-discoverable - * credential, and identification of the user has been deferred until the response is - * received. + * credential (passkey), and identification of the user has been deferred until the response + * is received. + * + * @see Passkey */ public Optional getUsername() { return Optional.ofNullable(username); @@ -121,8 +127,10 @@ public Optional getUsername() { *

If both this and {@link #getUsername()} are empty, this indicates that this is a request for * an assertion by a client-side-discoverable - * credential, and identification of the user has been deferred until the response is - * received. + * credential (passkey), and identification of the user has been deferred until the response + * is received. + * + * @see Passkey */ public Optional getUserHandle() { return Optional.ofNullable(userHandle); @@ -215,8 +223,10 @@ public AssertionRequestBuilder publicKeyCredentialRequestOptions( * *

If this is empty, this indicates that this is a request for an assertion by a client-side-discoverable - * credential, and identification of the user has been deferred until the response is - * received. + * credential (passkey), and identification of the user has been deferred until the response + * is received. + * + * @see Passkey */ public AssertionRequestBuilder username(@NonNull Optional username) { return this.username(username.orElse(null)); @@ -230,8 +240,10 @@ public AssertionRequestBuilder username(@NonNull Optional username) { * *

If this is empty, this indicates that this is a request for an assertion by a client-side-discoverable - * credential, and identification of the user has been deferred until the response is - * received. + * credential (passkey), and identification of the user has been deferred until the response + * is received. + * + * @see Passkey */ public AssertionRequestBuilder username(String username) { this.username = username; @@ -250,8 +262,10 @@ public AssertionRequestBuilder username(String username) { *

If both this and {@link #username(String)} are empty, this indicates that this is a * request for an assertion by a client-side-discoverable - * credential, and identification of the user has been deferred until the response is - * received. + * credential (passkey), and identification of the user has been deferred until the response + * is received. + * + * @see Passkey */ public AssertionRequestBuilder userHandle(@NonNull Optional userHandle) { return this.userHandle(userHandle.orElse(null)); @@ -266,8 +280,10 @@ public AssertionRequestBuilder userHandle(@NonNull Optional userHandl *

If both this and {@link #username(String)} are empty, this indicates that this is a * request for an assertion by a client-side-discoverable - * credential, and identification of the user has been deferred until the response is - * received. + * credential (passkey), and identification of the user has been deferred until the response + * is received. + * + * @see Passkey */ public AssertionRequestBuilder userHandle(ByteArray userHandle) { if (userHandle != null) { 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 73c8d6e14..12e0bad74 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 @@ -304,19 +304,21 @@ public Optional getAuthenticatorExten /** * Try to determine whether the created credential is a discoverable - * credential, using the output from the , also called a passkey, using the output from the * credProps extension. * - * @return A present true if the created credential is discoverable. A present - * false if the created credential is not discoverable. An empty value if it is not - * known whether the created credential is discoverable. + * @return A present true if the created credential is a passkey (discoverable). A + * present + * false if the created credential is not a passkey. An empty value if it is not known + * whether the created credential is a passkey. * @see §10.4. * Credential Properties Extension (credProps), "rk" output * @see Discoverable * Credential + * @see Passkey */ @JsonIgnore public Optional isDiscoverable() { 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 465d1a0d8..be9864d87 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 @@ -89,15 +89,16 @@ public class StartAssertionOptions { * PublicKeyCredentialRequestOptions#getAllowCredentials()} to the list of that user's * credentials. * - *

If this and {@link #getUserHandle()} are both absent, that implies a first-factor - * authentication operation - meaning identification of the user is deferred until after receiving - * the response from the client. + *

If this and {@link #getUserHandle()} are both absent, that implies authentication with a + * discoverable credential (passkey) - meaning identification of the user is deferred until after + * receiving the response from the client. * *

The default is empty (absent). * * @see Client-side-discoverable * credential + * @see Passkey */ public Optional getUsername() { return Optional.ofNullable(username); @@ -113,9 +114,9 @@ public Optional getUsername() { * PublicKeyCredentialRequestOptions#getAllowCredentials()} to the list of that user's * credentials. * - *

If this and {@link #getUsername()} are both absent, that implies a first-factor - * authentication operation - meaning identification of the user is deferred until after receiving - * the response from the client. + *

If this and {@link #getUsername()} are both absent, that implies authentication with a + * discoverable credential (passkey) - meaning identification of the user is deferred until after + * receiving the response from the client. * *

The default is empty (absent). * @@ -124,6 +125,7 @@ public Optional getUsername() { * @see Client-side-discoverable * credential + * @see Passkey */ public Optional getUserHandle() { return Optional.ofNullable(userHandle); @@ -175,9 +177,9 @@ public static class StartAssertionOptionsBuilder { * PublicKeyCredentialRequestOptions#getAllowCredentials()} to the list of that user's * credentials. * - *

If this and {@link #getUserHandle()} are both absent, that implies a first-factor - * authentication operation - meaning identification of the user is deferred until after - * receiving the response from the client. + *

If this and {@link #getUserHandle()} are both absent, that implies authentication with a + * discoverable credential (passkey) - meaning identification of the user is deferred until + * after receiving the response from the client. * *

The default is empty (absent). * @@ -187,6 +189,7 @@ public static class StartAssertionOptionsBuilder { * @see Client-side-discoverable * credential + * @see Passkey */ public StartAssertionOptionsBuilder username(@NonNull Optional username) { this.username = username.orElse(null); @@ -207,9 +210,9 @@ public StartAssertionOptionsBuilder username(@NonNull Optional username) * PublicKeyCredentialRequestOptions#getAllowCredentials()} to the list of that user's * credentials. * - *

If this and {@link #getUserHandle()} are both absent, that implies a first-factor - * authentication operation - meaning identification of the user is deferred until after - * receiving the response from the client. + *

If this and {@link #getUserHandle()} are both absent, that implies authentication with a + * discoverable credential (passkey) - meaning identification of the user is deferred until + * after receiving the response from the client. * *

The default is empty (absent). * @@ -219,6 +222,7 @@ public StartAssertionOptionsBuilder username(@NonNull Optional username) * @see Client-side-discoverable * credential + * @see Passkey */ public StartAssertionOptionsBuilder username(String username) { return this.username(Optional.ofNullable(username)); @@ -235,9 +239,9 @@ public StartAssertionOptionsBuilder username(String username) { * PublicKeyCredentialRequestOptions#getAllowCredentials()} to the list of that user's * credentials. * - *

If this and {@link #getUsername()} are both absent, that implies a first-factor - * authentication operation - meaning identification of the user is deferred until after - * receiving the response from the client. + *

If this and {@link #getUsername()} are both absent, that implies authentication with a + * discoverable credential (passkey) - meaning identification of the user is deferred until + * after receiving the response from the client. * *

The default is empty (absent). * @@ -249,6 +253,7 @@ public StartAssertionOptionsBuilder username(String username) { * @see Client-side-discoverable * credential + * @see Passkey */ public StartAssertionOptionsBuilder userHandle(@NonNull Optional userHandle) { this.userHandle = userHandle.orElse(null); @@ -269,9 +274,9 @@ public StartAssertionOptionsBuilder userHandle(@NonNull Optional user * PublicKeyCredentialRequestOptions#getAllowCredentials()} to the list of that user's * credentials. * - *

If this and {@link #getUsername()} are both absent, that implies a first-factor - * authentication operation - meaning identification of the user is deferred until after - * receiving the response from the client. + *

If this and {@link #getUsername()} are both absent, that implies authentication with a + * discoverable credential (passkey) - meaning identification of the user is deferred until + * after receiving the response from the client. * *

The default is empty (absent). * @@ -281,6 +286,7 @@ public StartAssertionOptionsBuilder userHandle(@NonNull Optional user * @see Client-side-discoverable * credential + * @see Passkey */ public StartAssertionOptionsBuilder userHandle(ByteArray userHandle) { return this.userHandle(Optional.ofNullable(userHandle)); 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 1a19d4dda..7b1d6aed0 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 @@ -51,10 +51,20 @@ public class AuthenticatorSelectionCriteria { /** * Specifies the extent to which the Relying Party desires to create a client-side discoverable - * credential. For historical reasons the naming retains the deprecated “resident” terminology. + * credential (passkey). For historical reasons the naming retains the deprecated “resident” + * terminology. * - *

By default, this is not set. When not set, the default in the browser is {@link - * ResidentKeyRequirement#DISCOURAGED}. + *

When this is set, {@link PublicKeyCredentialCreationOptions#toCredentialsCreateJson()} will + * also emit a + * requireResidentKey member for backwards compatibility with WebAuthn Level 1. + * It will be set to true if this is set to {@link ResidentKeyRequirement#REQUIRED + * REQUIRED} and false if this is set to anything else. When this is not set, a + * requireResidentKey member will not be emitted. + * + *

When not set, the default in the browser is {@link ResidentKeyRequirement#DISCOURAGED}. + * + *

By default, this is not set. * * @see ResidentKeyRequirement * @see Client-side * discoverable Credential + * @see Passkey */ private final ResidentKeyRequirement residentKey; @@ -95,10 +106,8 @@ public Optional getAuthenticatorAttachment() { /** * Specifies the extent to which the Relying Party desires to create a client-side discoverable - * credential. For historical reasons the naming retains the deprecated “resident” terminology. - * - *

By default, this is not set. When not set, the default in the browser is {@link - * ResidentKeyRequirement#DISCOURAGED}. + * credential (passkey). For historical reasons the naming retains the deprecated “resident” + * terminology. * *

When this is set, {@link PublicKeyCredentialCreationOptions#toCredentialsCreateJson()} will * also emit a getAuthenticatorAttachment() { * REQUIRED} and false if this is set to anything else. When this is not set, a * requireResidentKey member will not be emitted. * + *

When not set, the default in the browser is {@link ResidentKeyRequirement#DISCOURAGED}. + * + *

By default, this is not set. + * * @see ResidentKeyRequirement * @see §5.4.6. @@ -115,6 +128,7 @@ public Optional getAuthenticatorAttachment() { * @see Client-side * discoverable Credential + * @see Passkey */ public Optional getResidentKey() { return Optional.ofNullable(residentKey); diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/Extensions.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/Extensions.java index db831a05c..b2b50c283 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/Extensions.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/Extensions.java @@ -76,9 +76,10 @@ public static class CredentialPropertiesOutput { * This OPTIONAL property, known abstractly as the resident key credential property * (i.e., client-side discoverable credential property), is a Boolean value indicating * whether the {@link PublicKeyCredential} returned as a result of a registration ceremony is - * a client-side discoverable credential. + * a client-side discoverable credential (passkey). * - *

If this is true, the credential is a discoverable credential. + *

If this is true, the credential is a discoverable credential + * (passkey). * *

If this is false, the credential is a server-side credential. * @@ -94,6 +95,7 @@ public static class CredentialPropertiesOutput { * @see Server-side * Credential + * @see Passkey */ public Optional getRk() { return Optional.ofNullable(rk); diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/ResidentKeyRequirement.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/ResidentKeyRequirement.java index 0af26831d..a79c97b37 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/ResidentKeyRequirement.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/ResidentKeyRequirement.java @@ -34,7 +34,8 @@ /** * This enumeration's values describe the Relying Party's requirements for client-side discoverable - * credentials (formerly known as resident credentials or resident keys): + * credentials, also known as passkeys (formerly known as resident credentials or resident + * keys). * * @see §5.4.6. @@ -42,13 +43,14 @@ * @see Client-side * discoverable Credential + * @see Passkey */ @AllArgsConstructor public enum ResidentKeyRequirement { /** * The client and authenticator will try to create a server-side credential if possible, and a - * discoverable credential otherwise. + * discoverable credential (passkey) otherwise. * * @see §5.4.6. @@ -59,12 +61,13 @@ public enum ResidentKeyRequirement { * @see Server-side * Credential + * @see Passkey */ DISCOURAGED("discouraged"), /** - * The client and authenticator will try to create a discoverable credential if possible, and a - * server-side credential otherwise. + * The client and authenticator will try to create a discoverable credential (passkey) if + * possible, and a server-side credential otherwise. * * @see §5.4.6. @@ -75,12 +78,13 @@ public enum ResidentKeyRequirement { * @see Server-side * Credential + * @see Passkey */ PREFERRED("preferred"), /** - * The client and authenticator will try to create a discoverable credential, and fail the - * registration if that is not possible. + * The client and authenticator will try to create a discoverable credential (passkey), and fail + * the registration if that is not possible. * * @see §5.4.6. @@ -91,6 +95,7 @@ public enum ResidentKeyRequirement { * @see Server-side * Credential + * @see Passkey */ REQUIRED("required"); From 753375dc60a4d471e1544a259bf8664999e27fde Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 15 May 2023 16:56:07 +0200 Subject: [PATCH 09/42] Elaborate reference to "passkey" in passkeys.dev reference --- .../com/yubico/webauthn/AssertionRequest.java | 24 ++++++++++++------- .../yubico/webauthn/RegistrationResult.java | 3 ++- .../webauthn/StartAssertionOptions.java | 18 +++++++++----- .../data/AuthenticatorSelectionCriteria.java | 6 +++-- .../com/yubico/webauthn/data/Extensions.java | 3 ++- .../webauthn/data/ResidentKeyRequirement.java | 12 ++++++---- 6 files changed, 44 insertions(+), 22 deletions(-) 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 a35a11f58..5cbb90db5 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 @@ -62,7 +62,8 @@ public class AssertionRequest { * credential (passkey), and identification of the user has been deferred until the response * is received. * - * @see Passkey + * @see Passkey in passkeys.dev reference */ private final String username; @@ -79,7 +80,8 @@ public class AssertionRequest { * credential (passkey), and identification of the user has been deferred until the response * is received. * - * @see Passkey + * @see Passkey in passkeys.dev reference */ private final ByteArray userHandle; @@ -112,7 +114,8 @@ private AssertionRequest( * credential (passkey), and identification of the user has been deferred until the response * is received. * - * @see Passkey + * @see Passkey in passkeys.dev reference */ public Optional getUsername() { return Optional.ofNullable(username); @@ -130,7 +133,8 @@ public Optional getUsername() { * credential (passkey), and identification of the user has been deferred until the response * is received. * - * @see Passkey + * @see Passkey in passkeys.dev reference */ public Optional getUserHandle() { return Optional.ofNullable(userHandle); @@ -226,7 +230,8 @@ public AssertionRequestBuilder publicKeyCredentialRequestOptions( * credential (passkey), and identification of the user has been deferred until the response * is received. * - * @see Passkey + * @see Passkey in passkeys.dev reference */ public AssertionRequestBuilder username(@NonNull Optional username) { return this.username(username.orElse(null)); @@ -243,7 +248,8 @@ public AssertionRequestBuilder username(@NonNull Optional username) { * credential (passkey), and identification of the user has been deferred until the response * is received. * - * @see Passkey + * @see Passkey in passkeys.dev reference */ public AssertionRequestBuilder username(String username) { this.username = username; @@ -265,7 +271,8 @@ public AssertionRequestBuilder username(String username) { * credential (passkey), and identification of the user has been deferred until the response * is received. * - * @see Passkey + * @see Passkey in passkeys.dev reference */ public AssertionRequestBuilder userHandle(@NonNull Optional userHandle) { return this.userHandle(userHandle.orElse(null)); @@ -283,7 +290,8 @@ public AssertionRequestBuilder userHandle(@NonNull Optional userHandl * credential (passkey), and identification of the user has been deferred until the response * is received. * - * @see Passkey + * @see Passkey in passkeys.dev reference */ public AssertionRequestBuilder userHandle(ByteArray userHandle) { if (userHandle != null) { 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 12e0bad74..2189ee81c 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 @@ -318,7 +318,8 @@ public Optional getAuthenticatorExten * @see Discoverable * Credential - * @see Passkey + * @see Passkey in passkeys.dev reference */ @JsonIgnore public Optional isDiscoverable() { 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 be9864d87..461f31228 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 @@ -98,7 +98,8 @@ public class StartAssertionOptions { * @see Client-side-discoverable * credential - * @see Passkey + * @see Passkey in passkeys.dev reference */ public Optional getUsername() { return Optional.ofNullable(username); @@ -125,7 +126,8 @@ public Optional getUsername() { * @see Client-side-discoverable * credential - * @see Passkey + * @see Passkey in passkeys.dev reference */ public Optional getUserHandle() { return Optional.ofNullable(userHandle); @@ -189,7 +191,8 @@ public static class StartAssertionOptionsBuilder { * @see Client-side-discoverable * credential - * @see Passkey + * @see Passkey in passkeys.dev reference */ public StartAssertionOptionsBuilder username(@NonNull Optional username) { this.username = username.orElse(null); @@ -222,7 +225,8 @@ public StartAssertionOptionsBuilder username(@NonNull Optional username) * @see Client-side-discoverable * credential - * @see Passkey + * @see Passkey in passkeys.dev reference */ public StartAssertionOptionsBuilder username(String username) { return this.username(Optional.ofNullable(username)); @@ -253,7 +257,8 @@ public StartAssertionOptionsBuilder username(String username) { * @see Client-side-discoverable * credential - * @see Passkey + * @see Passkey in passkeys.dev reference */ public StartAssertionOptionsBuilder userHandle(@NonNull Optional userHandle) { this.userHandle = userHandle.orElse(null); @@ -286,7 +291,8 @@ public StartAssertionOptionsBuilder userHandle(@NonNull Optional user * @see Client-side-discoverable * credential - * @see Passkey + * @see Passkey in passkeys.dev reference */ public StartAssertionOptionsBuilder userHandle(ByteArray userHandle) { return this.userHandle(Optional.ofNullable(userHandle)); 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 7b1d6aed0..788ca210a 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 @@ -73,7 +73,8 @@ public class AuthenticatorSelectionCriteria { * @see Client-side * discoverable Credential - * @see Passkey + * @see Passkey in passkeys.dev reference */ private final ResidentKeyRequirement residentKey; @@ -128,7 +129,8 @@ public Optional getAuthenticatorAttachment() { * @see Client-side * discoverable Credential - * @see Passkey + * @see Passkey in passkeys.dev reference */ public Optional getResidentKey() { return Optional.ofNullable(residentKey); diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/Extensions.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/Extensions.java index b2b50c283..f9b02cdd5 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/Extensions.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/Extensions.java @@ -95,7 +95,8 @@ public static class CredentialPropertiesOutput { * @see Server-side * Credential - * @see Passkey + * @see Passkey in passkeys.dev reference */ public Optional getRk() { return Optional.ofNullable(rk); diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/ResidentKeyRequirement.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/ResidentKeyRequirement.java index a79c97b37..b27912d25 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/ResidentKeyRequirement.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/ResidentKeyRequirement.java @@ -43,7 +43,8 @@ * @see Client-side * discoverable Credential - * @see Passkey + * @see Passkey in passkeys.dev reference */ @AllArgsConstructor public enum ResidentKeyRequirement { @@ -61,7 +62,8 @@ public enum ResidentKeyRequirement { * @see Server-side * Credential - * @see Passkey + * @see Passkey in passkeys.dev reference */ DISCOURAGED("discouraged"), @@ -78,7 +80,8 @@ public enum ResidentKeyRequirement { * @see Server-side * Credential - * @see Passkey + * @see Passkey in passkeys.dev reference */ PREFERRED("preferred"), @@ -95,7 +98,8 @@ public enum ResidentKeyRequirement { * @see Server-side * Credential - * @see Passkey + * @see Passkey in passkeys.dev reference */ REQUIRED("required"); From 2e0a3acfc4b35fb4c5ecbe9d34cfa04f37dd3981 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 15 May 2023 17:05:13 +0200 Subject: [PATCH 10/42] Address review comments --- README | 2 +- .../com/yubico/webauthn/AssertionRequest.java | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/README b/README index 813ebe475..8bee3c34e 100644 --- a/README +++ b/README @@ -370,7 +370,7 @@ throw new RuntimeException("Authentication failed"); Finally, if the previous step was successful, update your database using the link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/AssertionResult.html[`AssertionResult`]. -Most importantly you should update the signature counter, and the backup state flag if you use it. That might look something like this: +Most importantly, you should update the signature counter. That might look something like this: [source,java] ---------- 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 5cbb90db5..b0a119a72 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 @@ -59,7 +59,7 @@ public class AssertionRequest { *

If both this and {@link #getUserHandle() userHandle} are empty, this indicates that this is * a request for an assertion by a client-side-discoverable - * credential (passkey), and identification of the user has been deferred until the response + * credential (passkey). Identification of the user is therefore deferred until the response * is received. * * @see Passkey in If both this and {@link #getUsername() username} are empty, this indicates that this is a * request for an assertion by a client-side-discoverable - * credential (passkey), and identification of the user has been deferred until the response + * credential (passkey). Identification of the user is therefore deferred until the response * is received. * * @see Passkey in If both this and {@link #getUserHandle()} are empty, this indicates that this is a request * for an assertion by a client-side-discoverable - * credential (passkey), and identification of the user has been deferred until the response + * credential (passkey). Identification of the user is therefore deferred until the response * is received. * * @see Passkey in getUsername() { *

If both this and {@link #getUsername()} are empty, this indicates that this is a request for * an assertion by a client-side-discoverable - * credential (passkey), and identification of the user has been deferred until the response + * credential (passkey). Identification of the user is therefore deferred until the response * is received. * * @see Passkey in If this is empty, this indicates that this is a request for an assertion by a client-side-discoverable - * credential (passkey), and identification of the user has been deferred until the response + * credential (passkey). Identification of the user is therefore deferred until the response * is received. * * @see Passkey in username) { * *

If this is empty, this indicates that this is a request for an assertion by a client-side-discoverable - * credential (passkey), and identification of the user has been deferred until the response + * credential (passkey). Identification of the user is therefore deferred until the response * is received. * * @see Passkey in If both this and {@link #username(String)} are empty, this indicates that this is a * request for an assertion by a client-side-discoverable - * credential (passkey), and identification of the user has been deferred until the response + * credential (passkey). Identification of the user is therefore deferred until the response * is received. * * @see Passkey in userHandl *

If both this and {@link #username(String)} are empty, this indicates that this is a * request for an assertion by a client-side-discoverable - * credential (passkey), and identification of the user has been deferred until the response + * credential (passkey). Identification of the user is therefore deferred until the response * is received. * * @see Passkey in Date: Mon, 15 May 2023 17:17:46 +0200 Subject: [PATCH 11/42] Fix next version number in NEWS --- NEWS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NEWS b/NEWS index 7d7f49d63..b33a0d5fc 100644 --- a/NEWS +++ b/NEWS @@ -1,4 +1,4 @@ -== Version 2.4.1 (unreleased) == +== Version 2.4.2 (unreleased) == * Updated README and JavaDoc to use the "passkey" term and provide more guidance around passkey use cases. From 60ce6d92a7380bf1552cef4352804a4ed7bd852e Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 15 May 2023 17:26:36 +0200 Subject: [PATCH 12/42] Link residentKey option to JavaDoc --- README | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/README b/README index 8bee3c34e..27e4a01a9 100644 --- a/README +++ b/README @@ -420,8 +420,9 @@ Many passkey-capable authenticators also offer a credential sync mechanism to allow one passkey to be used on multiple devices. Passkeys can be created by setting the -link:https://www.w3.org/TR/webauthn-2/#dom-publickeycredentialcreationoptions-authenticatorselection[`authenticatorSelection`].link:https://www.w3.org/TR/webauthn-2/#dom-authenticatorselectioncriteria-residentkey[`residentKey`] -option: +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/StartRegistrationOptions.StartRegistrationOptionsBuilder.html#authenticatorSelection(com.yubico.webauthn.data.AuthenticatorSelectionCriteria)[`authenticatorSelection`].link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/data/AuthenticatorSelectionCriteria.AuthenticatorSelectionCriteriaBuilder.html#residentKey(com.yubico.webauthn.data.ResidentKeyRequirement)[`residentKey`] +option to +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/data/ResidentKeyRequirement.html#REQUIRED[`REQUIRED`]: [source,java] ---------- @@ -442,8 +443,11 @@ AssertionRequest request = rp.startAssertion(StartAssertionOptions.builder().bui ---------- Some authenticators might create passkeys even if not required, and setting -the `residentKey` option to `ResidentKeyRequirement.PREFERRED` will create a passkey -if the authenticator supports it. +the +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/data/AuthenticatorSelectionCriteria.AuthenticatorSelectionCriteriaBuilder.html#residentKey(com.yubico.webauthn.data.ResidentKeyRequirement)[`residentKey`] +option to +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/data/ResidentKeyRequirement.html#PREFERRED[`PREFERRED`] +will create a passkey if the authenticator supports it. The link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/RegistrationResult.html#isDiscoverable()[`RegistrationResult.isDiscoverable()`] method can be used to determine whether the created credential is a passkey. From 0d6d4123f157e65e54f87a46e163ff223d3da5d2 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 15 May 2023 17:29:48 +0200 Subject: [PATCH 13/42] Remove extraneous comma --- README | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README b/README index 27e4a01a9..e40a6f5a3 100644 --- a/README +++ b/README @@ -452,7 +452,7 @@ The link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/RegistrationResult.html#isDiscoverable()[`RegistrationResult.isDiscoverable()`] method can be used to determine whether the created credential is a passkey. This requires the -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/data/RegistrationExtensionInputs.RegistrationExtensionInputsBuilder.html#credProps()[`credProps` extension], +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/data/RegistrationExtensionInputs.RegistrationExtensionInputsBuilder.html#credProps()[`credProps` extension] to be enabled, which it is by default. From 5c06fc0b4f176028b444c3fb5673a9f817e53bf9 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Fri, 12 May 2023 13:45:21 +0200 Subject: [PATCH 14/42] Add .isUserVerified() to RegistrationResult and AssertionResult --- NEWS | 6 ++- README | 44 +++++++++++++++++++ .../com/yubico/webauthn/AssertionResult.java | 21 +++++++++ .../yubico/webauthn/RegistrationResult.java | 22 ++++++++++ .../webauthn/RelyingPartyAssertionSpec.scala | 37 ++++++++++++++++ .../RelyingPartyRegistrationSpec.scala | 31 +++++++++++++ 6 files changed, 160 insertions(+), 1 deletion(-) diff --git a/NEWS b/NEWS index b33a0d5fc..43ce9ef0a 100644 --- a/NEWS +++ b/NEWS @@ -1,5 +1,9 @@ -== Version 2.4.2 (unreleased) == +== Version 2.5.0 (unreleased) == +New features: + +* Added method `.isUserVerified()` to `RegistrationResult` and `AssertionResult` + as a shortcut for accessing the UV flag in authenticator data. * Updated README and JavaDoc to use the "passkey" term and provide more guidance around passkey use cases. diff --git a/README b/README index e40a6f5a3..3f2e9a8cb 100644 --- a/README +++ b/README @@ -489,6 +489,50 @@ PublicKeyCredentialCreationOptions request = rp.startRegistration( .build()); ---------- +You can also request that user verification be used if possible, but is not required: + +[source,java] +---------- +PublicKeyCredentialCreationOptions request = rp.startRegistration( + StartRegistrationOptions.builder() + .user(/* ... */) + .authenticatorSelection(AuthenticatorSelectionCriteria.builder() + .userVerification(UserVerificationRequirement.PREFERRED) + .build()) + .build()); + +AssertionRequest request = rp.startAssertion(StartAssertionOptions.builder() + .username("alice") + .userVerification(UserVerificationRequirement.PREFERRED) + .build()); +---------- + +In this case +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/RelyingParty.html#finishRegistration(com.yubico.webauthn.FinishRegistrationOptions)[`RelyingParty.finishRegistration(...)`] +and +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/RelyingParty.html#finishAssertion(com.yubico.webauthn.FinishAssertionOptions)[`RelyingParty.finishAssertion(...)`] +will NOT enforce user verification, +but instead the `isUserVerified()` method of +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.5.0/com/yubico/webauthn/RegistrationResult.html[`RegistrationResult`] +and +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.5.0/com/yubico/webauthn/AssertionResult.html[`AssertionResult`] +will tell whether user verification was used. + +For example, you could prompt for a password as the second factor if `isUserVerified()` returns `false`: + +[source,java] +---------- +AssertionResult result = rp.finishAssertion(/* ... */); + +if (result.isSuccess()) { + if (result.isUserVerified()) { + return successfulLogin(result.getUsername()); + } else { + return passwordRequired(result.getUsername()); + } +} +---------- + User verification can be used with both discoverable credentials (passkeys) and non-discoverable credentials. 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 f426943f1..5763af7af 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 @@ -31,6 +31,8 @@ import com.yubico.webauthn.data.AuthenticatorAssertionResponse; import com.yubico.webauthn.data.AuthenticatorAttachment; import com.yubico.webauthn.data.AuthenticatorData; +import com.yubico.webauthn.data.AuthenticatorDataFlags; +import com.yubico.webauthn.data.AuthenticatorResponse; import com.yubico.webauthn.data.ByteArray; import com.yubico.webauthn.data.ClientAssertionExtensionOutputs; import com.yubico.webauthn.data.PublicKeyCredential; @@ -144,6 +146,25 @@ public ByteArray getUserHandle() { return credential.getUserHandle(); } + /** + * Check whether the user + * verification as performed during the authentication ceremony. + * + *

This flag is also available via + * {@link PublicKeyCredential}.{@link PublicKeyCredential#getResponse() getResponse()}.{@link AuthenticatorResponse#getParsedAuthenticatorData() getParsedAuthenticatorData()}.{@link AuthenticatorData#getFlags() getFlags()}.{@link AuthenticatorDataFlags#UV UV} + * . + * + * @return true if and only if the authenticator claims to have performed user + * verification during the authentication ceremony. + * @see User Verification + * @see UV flag in §6.1. Authenticator + * Data + */ + @JsonIgnore + public boolean isUserVerified() { + return credentialResponse.getResponse().getParsedAuthenticatorData().getFlags().UV; + } + /** * Check whether the asserted credential is backup eligible, using the user + * verification as performed during the registration ceremony. + * + *

This flag is also available via + * {@link PublicKeyCredential}.{@link PublicKeyCredential#getResponse() getResponse()}.{@link AuthenticatorResponse#getParsedAuthenticatorData() getParsedAuthenticatorData()}.{@link AuthenticatorData#getFlags() getFlags()}.{@link AuthenticatorDataFlags#UV UV} + * . + * + * @return true if and only if the authenticator claims to have performed user + * verification during the registration ceremony. + * @see User Verification + * @see UV flag in §6.1. Authenticator + * Data + */ + @JsonIgnore + public boolean isUserVerified() { + return credential.getResponse().getParsedAuthenticatorData().getFlags().UV; + } + /** * Check whether the created credential is backup eligible, using the Date: Wed, 17 May 2023 12:08:23 +0000 Subject: [PATCH 15/42] Bump com.diffplug.spotless:spotless-plugin-gradle from 6.13.0 to 6.18.0 Bumps [com.diffplug.spotless:spotless-plugin-gradle](https://github.com/diffplug/spotless) from 6.13.0 to 6.18.0. - [Changelog](https://github.com/diffplug/spotless/blob/main/CHANGES.md) - [Commits](https://github.com/diffplug/spotless/compare/plugin-gradle/6.13.0...gradle/6.18.0) --- updated-dependencies: - dependency-name: com.diffplug.spotless:spotless-plugin-gradle dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- buildSrc/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index e6e5dc5e5..df370ebb9 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -10,7 +10,7 @@ repositories { } dependencies { - implementation("com.diffplug.spotless:spotless-plugin-gradle:6.13.0") + implementation("com.diffplug.spotless:spotless-plugin-gradle:6.18.0") implementation("info.solidsoft.gradle.pitest:gradle-pitest-plugin:1.9.11") implementation("io.franzbecker:gradle-lombok:5.0.0") implementation("io.github.cosmicsilence:gradle-scalafix:0.1.14") From 699de9a972eea920b242bc246b40ebb48c793dd2 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Wed, 17 May 2023 15:27:41 +0200 Subject: [PATCH 16/42] gradle spotlessApply --- .../fido/metadata/FidoMetadataDownloader.java | 72 ++++++++++++++----- .../fido/metadata/FidoMetadataService.java | 13 +++- .../demo/webauthn/WebAuthnRestResource.java | 16 +++-- .../java/demo/webauthn/WebAuthnServer.java | 39 +++++++--- 4 files changed, 107 insertions(+), 33 deletions(-) diff --git a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/FidoMetadataDownloader.java b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/FidoMetadataDownloader.java index 28b027aa0..bee6831be 100644 --- a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/FidoMetadataDownloader.java +++ b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/FidoMetadataDownloader.java @@ -677,9 +677,16 @@ public FidoMetadataDownloaderBuilder trustHttpsCerts(@NonNull X509Certificate... * written to cache in this case. */ public MetadataBLOB loadCachedBlob() - throws CertPathValidatorException, InvalidAlgorithmParameterException, Base64UrlException, - CertificateException, IOException, NoSuchAlgorithmException, SignatureException, - InvalidKeyException, UnexpectedLegalHeader, DigestException, + throws CertPathValidatorException, + InvalidAlgorithmParameterException, + Base64UrlException, + CertificateException, + IOException, + NoSuchAlgorithmException, + SignatureException, + InvalidKeyException, + UnexpectedLegalHeader, + DigestException, FidoMetadataDownloaderException { final X509Certificate trustRoot = retrieveTrustRootCert(); @@ -773,9 +780,16 @@ public MetadataBLOB loadCachedBlob() * written to cache in this case. */ public MetadataBLOB refreshBlob() - throws CertPathValidatorException, InvalidAlgorithmParameterException, Base64UrlException, - CertificateException, IOException, NoSuchAlgorithmException, SignatureException, - InvalidKeyException, UnexpectedLegalHeader, DigestException, + throws CertPathValidatorException, + InvalidAlgorithmParameterException, + Base64UrlException, + CertificateException, + IOException, + NoSuchAlgorithmException, + SignatureException, + InvalidKeyException, + UnexpectedLegalHeader, + DigestException, FidoMetadataDownloaderException { final X509Certificate trustRoot = retrieveTrustRootCert(); @@ -797,9 +811,16 @@ public MetadataBLOB refreshBlob() private Optional refreshBlobInternal( @NonNull X509Certificate trustRoot, @NonNull Optional cached) - throws CertPathValidatorException, InvalidAlgorithmParameterException, Base64UrlException, - CertificateException, IOException, NoSuchAlgorithmException, SignatureException, - InvalidKeyException, UnexpectedLegalHeader, FidoMetadataDownloaderException { + throws CertPathValidatorException, + InvalidAlgorithmParameterException, + Base64UrlException, + CertificateException, + IOException, + NoSuchAlgorithmException, + SignatureException, + InvalidKeyException, + UnexpectedLegalHeader, + FidoMetadataDownloaderException { try { log.debug("Attempting to download new BLOB..."); @@ -928,9 +949,15 @@ private X509Certificate retrieveTrustRootCert() * signature. */ private Optional loadExplicitBlobOnly(X509Certificate trustRootCertificate) - throws Base64UrlException, CertPathValidatorException, CertificateException, IOException, - InvalidAlgorithmParameterException, InvalidKeyException, NoSuchAlgorithmException, - SignatureException, FidoMetadataDownloaderException { + throws Base64UrlException, + CertPathValidatorException, + CertificateException, + IOException, + InvalidAlgorithmParameterException, + InvalidKeyException, + NoSuchAlgorithmException, + SignatureException, + FidoMetadataDownloaderException { if (blobJwt != null) { return Optional.of( parseAndVerifyBlob( @@ -1008,9 +1035,15 @@ private ByteArray download(URL url) throws IOException { } private MetadataBLOB parseAndVerifyBlob(ByteArray jwt, X509Certificate trustRootCertificate) - throws CertPathValidatorException, InvalidAlgorithmParameterException, CertificateException, - IOException, NoSuchAlgorithmException, SignatureException, InvalidKeyException, - Base64UrlException, FidoMetadataDownloaderException { + throws CertPathValidatorException, + InvalidAlgorithmParameterException, + CertificateException, + IOException, + NoSuchAlgorithmException, + SignatureException, + InvalidKeyException, + Base64UrlException, + FidoMetadataDownloaderException { Scanner s = new Scanner(new ByteArrayInputStream(jwt.getBytes())).useDelimiter("\\."); final ByteArray header = ByteArray.fromBase64Url(s.next()); final ByteArray payload = ByteArray.fromBase64Url(s.next()); @@ -1023,8 +1056,13 @@ private MetadataBLOB verifyBlob( ByteArray jwtPayload, ByteArray jwtSignature, X509Certificate trustRootCertificate) - throws IOException, CertificateException, NoSuchAlgorithmException, InvalidKeyException, - SignatureException, CertPathValidatorException, InvalidAlgorithmParameterException, + throws IOException, + CertificateException, + NoSuchAlgorithmException, + InvalidKeyException, + SignatureException, + CertPathValidatorException, + InvalidAlgorithmParameterException, FidoMetadataDownloaderException { final ObjectMapper headerJsonMapper = com.yubico.internal.util.JacksonCodecs.json() diff --git a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/FidoMetadataService.java b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/FidoMetadataService.java index ae404db3c..fdf4b2c95 100644 --- a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/FidoMetadataService.java +++ b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/FidoMetadataService.java @@ -325,9 +325,16 @@ public FidoMetadataServiceBuilder certStore(@NonNull CertStore certStore) { } public FidoMetadataService build() - throws CertPathValidatorException, InvalidAlgorithmParameterException, Base64UrlException, - DigestException, FidoMetadataDownloaderException, CertificateException, - UnexpectedLegalHeader, IOException, NoSuchAlgorithmException, SignatureException, + throws CertPathValidatorException, + InvalidAlgorithmParameterException, + Base64UrlException, + DigestException, + FidoMetadataDownloaderException, + CertificateException, + UnexpectedLegalHeader, + IOException, + NoSuchAlgorithmException, + SignatureException, InvalidKeyException { return new FidoMetadataService(blob, prefilter, filter, certStore); } 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 3c1e9983c..6d451ee9f 100644 --- a/webauthn-server-demo/src/main/java/demo/webauthn/WebAuthnRestResource.java +++ b/webauthn-server-demo/src/main/java/demo/webauthn/WebAuthnRestResource.java @@ -84,10 +84,18 @@ public class WebAuthnRestResource { private final JsonNodeFactory jsonFactory = JsonNodeFactory.instance; public WebAuthnRestResource() - throws InvalidAppIdException, CertificateException, CertPathValidatorException, - InvalidAlgorithmParameterException, Base64UrlException, DigestException, - FidoMetadataDownloaderException, UnexpectedLegalHeader, IOException, - NoSuchAlgorithmException, SignatureException, InvalidKeyException { + throws InvalidAppIdException, + CertificateException, + CertPathValidatorException, + InvalidAlgorithmParameterException, + Base64UrlException, + DigestException, + FidoMetadataDownloaderException, + UnexpectedLegalHeader, + IOException, + NoSuchAlgorithmException, + SignatureException, + InvalidKeyException { this(new WebAuthnServer()); } 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 a087c61a2..bf7f661ce 100644 --- a/webauthn-server-demo/src/main/java/demo/webauthn/WebAuthnServer.java +++ b/webauthn-server-demo/src/main/java/demo/webauthn/WebAuthnServer.java @@ -105,9 +105,16 @@ public class WebAuthnServer { private final MetadataService metadataService = getMetadataService(); private static MetadataService getMetadataService() - throws CertPathValidatorException, InvalidAlgorithmParameterException, Base64UrlException, - DigestException, FidoMetadataDownloaderException, CertificateException, - UnexpectedLegalHeader, IOException, NoSuchAlgorithmException, SignatureException, + throws CertPathValidatorException, + InvalidAlgorithmParameterException, + Base64UrlException, + DigestException, + FidoMetadataDownloaderException, + CertificateException, + UnexpectedLegalHeader, + IOException, + NoSuchAlgorithmException, + SignatureException, InvalidKeyException { if (Config.useFidoMds()) { logger.info("Using combination of Yubico JSON file and FIDO MDS for attestation metadata."); @@ -140,9 +147,16 @@ private static MetadataService getMetadataService() private final RelyingParty rp; public WebAuthnServer() - throws CertificateException, CertPathValidatorException, InvalidAlgorithmParameterException, - Base64UrlException, DigestException, FidoMetadataDownloaderException, - UnexpectedLegalHeader, IOException, NoSuchAlgorithmException, SignatureException, + throws CertificateException, + CertPathValidatorException, + InvalidAlgorithmParameterException, + Base64UrlException, + DigestException, + FidoMetadataDownloaderException, + UnexpectedLegalHeader, + IOException, + NoSuchAlgorithmException, + SignatureException, InvalidKeyException { this( new InMemoryRegistrationStorage(), @@ -158,9 +172,16 @@ public WebAuthnServer( Cache assertRequestStorage, RelyingPartyIdentity rpIdentity, Set origins) - throws CertificateException, CertPathValidatorException, InvalidAlgorithmParameterException, - Base64UrlException, DigestException, FidoMetadataDownloaderException, - UnexpectedLegalHeader, IOException, NoSuchAlgorithmException, SignatureException, + throws CertificateException, + CertPathValidatorException, + InvalidAlgorithmParameterException, + Base64UrlException, + DigestException, + FidoMetadataDownloaderException, + UnexpectedLegalHeader, + IOException, + NoSuchAlgorithmException, + SignatureException, InvalidKeyException { this.userStorage = userStorage; this.registerRequestStorage = registerRequestStorage; From 332765acded94c32b28cd35afd616979df83f3ec Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Wed, 17 May 2023 15:31:48 +0200 Subject: [PATCH 17/42] Apply Spotless only in JDK 11+ --- buildSrc/build.gradle.kts | 8 +++-- ...convention-code-formatting-internal.gradle | 33 ++++++++++++++++++ ...ject-convention-code-formatting.gradle.kts | 34 ++----------------- 3 files changed, 42 insertions(+), 33 deletions(-) create mode 100644 buildSrc/src/main/groovy/project-convention-code-formatting-internal.gradle diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index df370ebb9..2e586cab7 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -10,8 +10,12 @@ repositories { } dependencies { - implementation("com.diffplug.spotless:spotless-plugin-gradle:6.18.0") implementation("info.solidsoft.gradle.pitest:gradle-pitest-plugin:1.9.11") implementation("io.franzbecker:gradle-lombok:5.0.0") - implementation("io.github.cosmicsilence:gradle-scalafix:0.1.14") + + // Spotless dropped Java 8 support in version 2.33.0 + if (JavaVersion.current().isJava11Compatible) { + implementation("com.diffplug.spotless:spotless-plugin-gradle:6.18.0") + implementation("io.github.cosmicsilence:gradle-scalafix:0.1.14") + } } diff --git a/buildSrc/src/main/groovy/project-convention-code-formatting-internal.gradle b/buildSrc/src/main/groovy/project-convention-code-formatting-internal.gradle new file mode 100644 index 000000000..29e7800f9 --- /dev/null +++ b/buildSrc/src/main/groovy/project-convention-code-formatting-internal.gradle @@ -0,0 +1,33 @@ +project.apply(plugin: "com.diffplug.spotless") +project.apply(plugin: "io.github.cosmicsilence.scalafix") + +spotless { + java { + googleJavaFormat() + } + scala { + scalafmt("2.6.3").configFile(project.rootProject.file("scalafmt.conf")) + } +} + +scalafix { + configFile.set(project.rootProject.file("scalafix.conf")) + + // Work around dependency resolution issues in April 2022 + semanticdb.autoConfigure.set(true) + semanticdb.version.set("4.5.5") +} + +project.dependencies.scalafix("com.github.liancheng:organize-imports_2.13:0.6.0") + + +project.afterEvaluate { + // These need to be in afterEvaluate due to this plugin + // being conditionally applied for Java 11+ only + project.tasks.spotlessApply.configure { dependsOn(project.tasks.scalafix) } + project.tasks.spotlessCheck.configure { dependsOn(project.tasks.checkScalafix) } + + // Scalafix adds tasks in afterEvaluate, so their configuration must be deferred + project.tasks.scalafix.finalizedBy(project.tasks.spotlessApply) + project.tasks.checkScalafix.finalizedBy(project.tasks.spotlessCheck) +} diff --git a/buildSrc/src/main/kotlin/project-convention-code-formatting.gradle.kts b/buildSrc/src/main/kotlin/project-convention-code-formatting.gradle.kts index 2147b795f..e64cf274d 100644 --- a/buildSrc/src/main/kotlin/project-convention-code-formatting.gradle.kts +++ b/buildSrc/src/main/kotlin/project-convention-code-formatting.gradle.kts @@ -1,32 +1,4 @@ -plugins { - id("com.diffplug.spotless") - id("io.github.cosmicsilence.scalafix") -} - -spotless { - java { - googleJavaFormat() - } - scala { - scalafmt("2.6.3").configFile(project.rootProject.file("scalafmt.conf")) - } -} - -scalafix { - configFile.set(project.rootProject.file("scalafix.conf")) - - // Work around dependency resolution issues in April 2022 - semanticdb.autoConfigure.set(true) - semanticdb.version.set("4.5.5") -} - -project.dependencies.scalafix("com.github.liancheng:organize-imports_2.13:0.6.0") - -project.tasks.spotlessApply.configure { dependsOn(project.tasks["scalafix"]) } -project.tasks.spotlessCheck.configure { dependsOn(project.tasks["checkScalafix"]) } - -// Scalafix adds tasks in afterEvaluate, so their configuration must be deferred -project.afterEvaluate { - project.tasks["scalafix"].finalizedBy(project.tasks.spotlessApply) - project.tasks["checkScalafix"].finalizedBy(project.tasks.spotlessCheck) +// Spotless dropped Java 8 support in version 2.33.0 +if (JavaVersion.current().isJava11Compatible) { + apply(plugin = "project-convention-code-formatting-internal") } From 8b9d6f20caf4e2cf051b596dc79518d918f47d55 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Fri, 19 May 2023 12:57:20 +0200 Subject: [PATCH 18/42] Revert "Add .isUserVerified() to RegistrationResult and AssertionResult" This reverts commit 5c06fc0b4f176028b444c3fb5673a9f817e53bf9. This commit makes changes to the README that will not be accurate until the new version is released, so these changes have to move to a release branch instead of main. --- NEWS | 6 +-- README | 44 ------------------- .../com/yubico/webauthn/AssertionResult.java | 21 --------- .../yubico/webauthn/RegistrationResult.java | 22 ---------- .../webauthn/RelyingPartyAssertionSpec.scala | 37 ---------------- .../RelyingPartyRegistrationSpec.scala | 31 ------------- 6 files changed, 1 insertion(+), 160 deletions(-) diff --git a/NEWS b/NEWS index 43ce9ef0a..b33a0d5fc 100644 --- a/NEWS +++ b/NEWS @@ -1,9 +1,5 @@ -== Version 2.5.0 (unreleased) == +== Version 2.4.2 (unreleased) == -New features: - -* Added method `.isUserVerified()` to `RegistrationResult` and `AssertionResult` - as a shortcut for accessing the UV flag in authenticator data. * Updated README and JavaDoc to use the "passkey" term and provide more guidance around passkey use cases. diff --git a/README b/README index 3f2e9a8cb..e40a6f5a3 100644 --- a/README +++ b/README @@ -489,50 +489,6 @@ PublicKeyCredentialCreationOptions request = rp.startRegistration( .build()); ---------- -You can also request that user verification be used if possible, but is not required: - -[source,java] ----------- -PublicKeyCredentialCreationOptions request = rp.startRegistration( - StartRegistrationOptions.builder() - .user(/* ... */) - .authenticatorSelection(AuthenticatorSelectionCriteria.builder() - .userVerification(UserVerificationRequirement.PREFERRED) - .build()) - .build()); - -AssertionRequest request = rp.startAssertion(StartAssertionOptions.builder() - .username("alice") - .userVerification(UserVerificationRequirement.PREFERRED) - .build()); ----------- - -In this case -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/RelyingParty.html#finishRegistration(com.yubico.webauthn.FinishRegistrationOptions)[`RelyingParty.finishRegistration(...)`] -and -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/RelyingParty.html#finishAssertion(com.yubico.webauthn.FinishAssertionOptions)[`RelyingParty.finishAssertion(...)`] -will NOT enforce user verification, -but instead the `isUserVerified()` method of -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.5.0/com/yubico/webauthn/RegistrationResult.html[`RegistrationResult`] -and -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.5.0/com/yubico/webauthn/AssertionResult.html[`AssertionResult`] -will tell whether user verification was used. - -For example, you could prompt for a password as the second factor if `isUserVerified()` returns `false`: - -[source,java] ----------- -AssertionResult result = rp.finishAssertion(/* ... */); - -if (result.isSuccess()) { - if (result.isUserVerified()) { - return successfulLogin(result.getUsername()); - } else { - return passwordRequired(result.getUsername()); - } -} ----------- - User verification can be used with both discoverable credentials (passkeys) and non-discoverable credentials. 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 5763af7af..f426943f1 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 @@ -31,8 +31,6 @@ import com.yubico.webauthn.data.AuthenticatorAssertionResponse; import com.yubico.webauthn.data.AuthenticatorAttachment; import com.yubico.webauthn.data.AuthenticatorData; -import com.yubico.webauthn.data.AuthenticatorDataFlags; -import com.yubico.webauthn.data.AuthenticatorResponse; import com.yubico.webauthn.data.ByteArray; import com.yubico.webauthn.data.ClientAssertionExtensionOutputs; import com.yubico.webauthn.data.PublicKeyCredential; @@ -146,25 +144,6 @@ public ByteArray getUserHandle() { return credential.getUserHandle(); } - /** - * Check whether the user - * verification as performed during the authentication ceremony. - * - *

This flag is also available via - * {@link PublicKeyCredential}.{@link PublicKeyCredential#getResponse() getResponse()}.{@link AuthenticatorResponse#getParsedAuthenticatorData() getParsedAuthenticatorData()}.{@link AuthenticatorData#getFlags() getFlags()}.{@link AuthenticatorDataFlags#UV UV} - * . - * - * @return true if and only if the authenticator claims to have performed user - * verification during the authentication ceremony. - * @see User Verification - * @see UV flag in §6.1. Authenticator - * Data - */ - @JsonIgnore - public boolean isUserVerified() { - return credentialResponse.getResponse().getParsedAuthenticatorData().getFlags().UV; - } - /** * Check whether the asserted credential is backup eligible, using the user - * verification as performed during the registration ceremony. - * - *

This flag is also available via - * {@link PublicKeyCredential}.{@link PublicKeyCredential#getResponse() getResponse()}.{@link AuthenticatorResponse#getParsedAuthenticatorData() getParsedAuthenticatorData()}.{@link AuthenticatorData#getFlags() getFlags()}.{@link AuthenticatorDataFlags#UV UV} - * . - * - * @return true if and only if the authenticator claims to have performed user - * verification during the registration ceremony. - * @see User Verification - * @see UV flag in §6.1. Authenticator - * Data - */ - @JsonIgnore - public boolean isUserVerified() { - return credential.getResponse().getParsedAuthenticatorData().getFlags().UV; - } - /** * Check whether the created credential is backup eligible, using the Date: Fri, 19 May 2023 12:58:25 +0200 Subject: [PATCH 19/42] Revert "Revert "Add .isUserVerified() to RegistrationResult and AssertionResult"" This reverts commit 8b9d6f20caf4e2cf051b596dc79518d918f47d55. --- NEWS | 6 ++- README | 44 +++++++++++++++++++ .../com/yubico/webauthn/AssertionResult.java | 21 +++++++++ .../yubico/webauthn/RegistrationResult.java | 22 ++++++++++ .../webauthn/RelyingPartyAssertionSpec.scala | 37 ++++++++++++++++ .../RelyingPartyRegistrationSpec.scala | 31 +++++++++++++ 6 files changed, 160 insertions(+), 1 deletion(-) diff --git a/NEWS b/NEWS index b33a0d5fc..43ce9ef0a 100644 --- a/NEWS +++ b/NEWS @@ -1,5 +1,9 @@ -== Version 2.4.2 (unreleased) == +== Version 2.5.0 (unreleased) == +New features: + +* Added method `.isUserVerified()` to `RegistrationResult` and `AssertionResult` + as a shortcut for accessing the UV flag in authenticator data. * Updated README and JavaDoc to use the "passkey" term and provide more guidance around passkey use cases. diff --git a/README b/README index e40a6f5a3..3f2e9a8cb 100644 --- a/README +++ b/README @@ -489,6 +489,50 @@ PublicKeyCredentialCreationOptions request = rp.startRegistration( .build()); ---------- +You can also request that user verification be used if possible, but is not required: + +[source,java] +---------- +PublicKeyCredentialCreationOptions request = rp.startRegistration( + StartRegistrationOptions.builder() + .user(/* ... */) + .authenticatorSelection(AuthenticatorSelectionCriteria.builder() + .userVerification(UserVerificationRequirement.PREFERRED) + .build()) + .build()); + +AssertionRequest request = rp.startAssertion(StartAssertionOptions.builder() + .username("alice") + .userVerification(UserVerificationRequirement.PREFERRED) + .build()); +---------- + +In this case +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/RelyingParty.html#finishRegistration(com.yubico.webauthn.FinishRegistrationOptions)[`RelyingParty.finishRegistration(...)`] +and +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/RelyingParty.html#finishAssertion(com.yubico.webauthn.FinishAssertionOptions)[`RelyingParty.finishAssertion(...)`] +will NOT enforce user verification, +but instead the `isUserVerified()` method of +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.5.0/com/yubico/webauthn/RegistrationResult.html[`RegistrationResult`] +and +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.5.0/com/yubico/webauthn/AssertionResult.html[`AssertionResult`] +will tell whether user verification was used. + +For example, you could prompt for a password as the second factor if `isUserVerified()` returns `false`: + +[source,java] +---------- +AssertionResult result = rp.finishAssertion(/* ... */); + +if (result.isSuccess()) { + if (result.isUserVerified()) { + return successfulLogin(result.getUsername()); + } else { + return passwordRequired(result.getUsername()); + } +} +---------- + User verification can be used with both discoverable credentials (passkeys) and non-discoverable credentials. 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 f426943f1..5763af7af 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 @@ -31,6 +31,8 @@ import com.yubico.webauthn.data.AuthenticatorAssertionResponse; import com.yubico.webauthn.data.AuthenticatorAttachment; import com.yubico.webauthn.data.AuthenticatorData; +import com.yubico.webauthn.data.AuthenticatorDataFlags; +import com.yubico.webauthn.data.AuthenticatorResponse; import com.yubico.webauthn.data.ByteArray; import com.yubico.webauthn.data.ClientAssertionExtensionOutputs; import com.yubico.webauthn.data.PublicKeyCredential; @@ -144,6 +146,25 @@ public ByteArray getUserHandle() { return credential.getUserHandle(); } + /** + * Check whether the user + * verification as performed during the authentication ceremony. + * + *

This flag is also available via + * {@link PublicKeyCredential}.{@link PublicKeyCredential#getResponse() getResponse()}.{@link AuthenticatorResponse#getParsedAuthenticatorData() getParsedAuthenticatorData()}.{@link AuthenticatorData#getFlags() getFlags()}.{@link AuthenticatorDataFlags#UV UV} + * . + * + * @return true if and only if the authenticator claims to have performed user + * verification during the authentication ceremony. + * @see User Verification + * @see UV flag in §6.1. Authenticator + * Data + */ + @JsonIgnore + public boolean isUserVerified() { + return credentialResponse.getResponse().getParsedAuthenticatorData().getFlags().UV; + } + /** * Check whether the asserted credential is backup eligible, using the user + * verification as performed during the registration ceremony. + * + *

This flag is also available via + * {@link PublicKeyCredential}.{@link PublicKeyCredential#getResponse() getResponse()}.{@link AuthenticatorResponse#getParsedAuthenticatorData() getParsedAuthenticatorData()}.{@link AuthenticatorData#getFlags() getFlags()}.{@link AuthenticatorDataFlags#UV UV} + * . + * + * @return true if and only if the authenticator claims to have performed user + * verification during the registration ceremony. + * @see User Verification + * @see UV flag in §6.1. Authenticator + * Data + */ + @JsonIgnore + public boolean isUserVerified() { + return credential.getResponse().getParsedAuthenticatorData().getFlags().UV; + } + /** * Check whether the created credential is backup eligible, using the Date: Wed, 24 May 2023 13:56:45 +0000 Subject: [PATCH 20/42] Bump com.diffplug.spotless:spotless-plugin-gradle from 6.18.0 to 6.19.0 Bumps [com.diffplug.spotless:spotless-plugin-gradle](https://github.com/diffplug/spotless) from 6.18.0 to 6.19.0. - [Changelog](https://github.com/diffplug/spotless/blob/main/CHANGES.md) - [Commits](https://github.com/diffplug/spotless/compare/gradle/6.18.0...gradle/6.19.0) --- updated-dependencies: - dependency-name: com.diffplug.spotless:spotless-plugin-gradle dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- buildSrc/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 2e586cab7..8e2ebbf9b 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -15,7 +15,7 @@ dependencies { // Spotless dropped Java 8 support in version 2.33.0 if (JavaVersion.current().isJava11Compatible) { - implementation("com.diffplug.spotless:spotless-plugin-gradle:6.18.0") + implementation("com.diffplug.spotless:spotless-plugin-gradle:6.19.0") implementation("io.github.cosmicsilence:gradle-scalafix:0.1.14") } } From 8eb6f59b776ff359c7562f262d026ca3dc1192ef Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Wed, 24 May 2023 17:01:36 +0200 Subject: [PATCH 21/42] ./gradlew spotlessApply --- .../com/yubico/webauthn/data/RegistrationExtensionInputs.java | 1 + 1 file changed, 1 insertion(+) 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 0e18cbe05..7d4d2c009 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 @@ -287,6 +287,7 @@ public RegistrationExtensionInputsBuilder uvm() { this.uvm = true; return this; } + /** For compatibility with {@link Builder}(toBuilder = true) */ private RegistrationExtensionInputsBuilder uvm(Boolean uvm) { this.uvm = uvm; From f207b279bdf740abfc0618398c92bc2cea2557e6 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 30 May 2023 15:30:44 +0200 Subject: [PATCH 22/42] Simplify parsing of RSA COSE public keys --- .../com/yubico/webauthn/WebAuthnCodecs.java | 22 ++++--------------- 1 file changed, 4 insertions(+), 18 deletions(-) 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 60aacab0d..ee3797468 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 @@ -31,13 +31,11 @@ import com.yubico.webauthn.data.ByteArray; import com.yubico.webauthn.data.COSEAlgorithmIdentifier; import java.io.IOException; -import java.math.BigInteger; import java.security.KeyFactory; import java.security.NoSuchAlgorithmException; import java.security.PublicKey; import java.security.interfaces.ECPublicKey; import java.security.spec.InvalidKeySpecException; -import java.security.spec.RSAPublicKeySpec; import java.security.spec.X509EncodedKeySpec; import java.util.Arrays; import java.util.HashMap; @@ -125,29 +123,17 @@ static PublicKey importCosePublicKey(ByteArray key) final int kty = cose.get(CBORObject.FromObject(1)).AsInt32(); switch (kty) { case 1: + // COSE-JAVA is hardcoded to ed25519-java provider ("EdDSA") which would require an + // additional dependency to parse EdDSA keys via the OneKey constructor return importCoseEdDsaPublicKey(cose); - case 2: - return importCoseP256PublicKey(cose); + case 2: // Fall through case 3: - return importCoseRsaPublicKey(cose); + return new OneKey(cose).AsPublicKey(); default: throw new IllegalArgumentException("Unsupported key type: " + kty); } } - 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 KeyFactory.getInstance("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(); From 91db292074af52730fc2eadc8953da1a6a97e4af Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 5 Jun 2023 13:18:03 +0200 Subject: [PATCH 23/42] Apply Nexus publishing plugin conditionally --- build.gradle | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/build.gradle b/build.gradle index 96d7be6eb..d61a8e0bf 100644 --- a/build.gradle +++ b/build.gradle @@ -4,11 +4,14 @@ buildscript { } dependencies { classpath 'com.cinnober.gradle:semver-git:2.5.0' + + if (project.findProperty('yubicoPublish') == 'true') { + classpath 'io.github.gradle-nexus:publish-plugin:1.3.0' + } } } plugins { id 'java-platform' - id 'io.github.gradle-nexus.publish-plugin' version '1.3.0' // 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 @@ -21,10 +24,7 @@ import com.yubico.gradle.GitUtils rootProject.description = "Metadata root for the com.yubico:webauthn-server-* module family" project.ext.isCiBuild = System.env.CI == 'true' - -project.ext.publishEnabled = !isCiBuild && - project.hasProperty('yubicoPublish') && project.yubicoPublish && - project.hasProperty('ossrhUsername') && project.hasProperty('ossrhPassword') +project.ext.publishEnabled = !isCiBuild && project.findProperty('yubicoPublish') == 'true' wrapper { gradleVersion = '8.1.1' @@ -65,6 +65,8 @@ allprojects { } if (publishEnabled) { + apply plugin: 'io.github.gradle-nexus.publish-plugin' + nexusPublishing { repositories { sonatype { From 4f3e17e0398e94bddbddf4a40f5f3e1fc6b30254 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 5 Jun 2023 13:19:19 +0200 Subject: [PATCH 24/42] Add instructions for enabling publishing to Sonatype Nexus --- doc/development.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/doc/development.md b/doc/development.md index 9969f3ad8..0acb00b5d 100644 --- a/doc/development.md +++ b/doc/development.md @@ -2,6 +2,20 @@ Developer docs === +Setup for publishing +--- + +To enable publishing to Maven Central via Sonatype Nexus, set +`yubicoPublish=true` in `$HOME/.gradle/gradle.properties` and add your Sonatype +username and password. Example: + +```properties +yubicoPublish=true +ossrhUsername=8pnmjKQP +ossrhPassword=bmjuyWSIik8P3Nq/ZM2G0Xs0sHEKBg+4q4zTZ8JDDRCr +``` + + Code formatting --- From 6d9129a9cb01513933f02df8a84d3236c7275083 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Wed, 7 Jun 2023 15:02:34 +0200 Subject: [PATCH 25/42] Revert "Simplify parsing of RSA COSE public keys" This reverts commit f207b279bdf740abfc0618398c92bc2cea2557e6. --- .../com/yubico/webauthn/WebAuthnCodecs.java | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) 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 ee3797468..60aacab0d 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 @@ -31,11 +31,13 @@ import com.yubico.webauthn.data.ByteArray; import com.yubico.webauthn.data.COSEAlgorithmIdentifier; import java.io.IOException; +import java.math.BigInteger; import java.security.KeyFactory; import java.security.NoSuchAlgorithmException; import java.security.PublicKey; import java.security.interfaces.ECPublicKey; import java.security.spec.InvalidKeySpecException; +import java.security.spec.RSAPublicKeySpec; import java.security.spec.X509EncodedKeySpec; import java.util.Arrays; import java.util.HashMap; @@ -123,17 +125,29 @@ static PublicKey importCosePublicKey(ByteArray key) final int kty = cose.get(CBORObject.FromObject(1)).AsInt32(); switch (kty) { case 1: - // COSE-JAVA is hardcoded to ed25519-java provider ("EdDSA") which would require an - // additional dependency to parse EdDSA keys via the OneKey constructor return importCoseEdDsaPublicKey(cose); - case 2: // Fall through + case 2: + return importCoseP256PublicKey(cose); case 3: - return new OneKey(cose).AsPublicKey(); + return importCoseRsaPublicKey(cose); default: throw new IllegalArgumentException("Unsupported key type: " + kty); } } + 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 KeyFactory.getInstance("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(); From 1e852bd13a29f9e9558581b6c3231d35a4040ff9 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Wed, 7 Jun 2023 15:04:18 +0200 Subject: [PATCH 26/42] Add comments explaining why WebAuthnCodecs needs some custom key parsing logic --- .../src/main/java/com/yubico/webauthn/WebAuthnCodecs.java | 3 +++ 1 file changed, 3 insertions(+) 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 60aacab0d..94ba42ea1 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 @@ -125,10 +125,13 @@ static PublicKey importCosePublicKey(ByteArray key) final int kty = cose.get(CBORObject.FromObject(1)).AsInt32(); switch (kty) { case 1: + // COSE-JAVA is hardcoded to ed25519-java provider ("EdDSA") which would require an + // additional dependency to parse EdDSA keys via the OneKey constructor return importCoseEdDsaPublicKey(cose); case 2: return importCoseP256PublicKey(cose); case 3: + // COSE-JAVA supports RSA in v1.1.0 but not in v1.0.0 return importCoseRsaPublicKey(cose); default: throw new IllegalArgumentException("Unsupported key type: " + kty); From 61f93003b5fcbbdbbe11017ab0bedd3766e8fe84 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Wed, 14 Jun 2023 14:58:29 +0200 Subject: [PATCH 27/42] Add option to ignore MDS BLOB signature when loading from cache or explicit config --- NEWS | 11 ++ webauthn-server-attestation/README.adoc | 2 + .../fido/metadata/FidoMetadataDownloader.java | 104 +++++++--- .../metadata/FidoMetadataDownloaderSpec.scala | 183 +++++++++++++++++- 4 files changed, 270 insertions(+), 30 deletions(-) diff --git a/NEWS b/NEWS index 43ce9ef0a..a49094521 100644 --- a/NEWS +++ b/NEWS @@ -1,5 +1,7 @@ == Version 2.5.0 (unreleased) == +`webauthn-server-core`: + New features: * Added method `.isUserVerified()` to `RegistrationResult` and `AssertionResult` @@ -7,6 +9,15 @@ New features: * Updated README and JavaDoc to use the "passkey" term and provide more guidance around passkey use cases. +`webauthn-server-attestation`: + +New features: + +* Added option `verifyDownloadsOnly(boolean)` to `FidoMetadataDownloader`. When + set to `true`, the BLOB signature will not be verified when loading a BLOB + from cache or when explicitly given. Default setting is `false`, which + preserves the previous behaviour. + == Version 2.4.1 == diff --git a/webauthn-server-attestation/README.adoc b/webauthn-server-attestation/README.adoc index c9cc6e6ef..1a557b359 100644 --- a/webauthn-server-attestation/README.adoc +++ b/webauthn-server-attestation/README.adoc @@ -154,6 +154,8 @@ FidoMetadataDownloader downloader = FidoMetadataDownloader.builder() .useTrustRootCacheFile(new File("/var/cache/webauthn-server/fido-mds-trust-root.bin")) .useDefaultBlob() .useBlobCacheFile(new File("/var/cache/webauthn-server/fido-mds-blob.bin")) + .verifyDownloadsOnly(true) // Recommended, otherwise cache may expire if BLOB certificate expires + // See: https://github.com/Yubico/java-webauthn-server/issues/294 .build(); FidoMetadataService mds = FidoMetadataService.builder() diff --git a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/FidoMetadataDownloader.java b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/FidoMetadataDownloader.java index bee6831be..15fc7ae08 100644 --- a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/FidoMetadataDownloader.java +++ b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/FidoMetadataDownloader.java @@ -87,6 +87,7 @@ import lombok.AllArgsConstructor; import lombok.NonNull; import lombok.RequiredArgsConstructor; +import lombok.Value; import lombok.extern.slf4j.Slf4j; /** @@ -119,6 +120,7 @@ public final class FidoMetadataDownloader { private final CertStore certStore; @NonNull private final Clock clock; private final KeyStore httpsTrustStore; + private final boolean verifyDownloadsOnly; /** * Begin configuring a {@link FidoMetadataDownloader} instance. See the {@link @@ -148,6 +150,7 @@ public static class FidoMetadataDownloaderBuilder { private CertStore certStore = null; @NonNull private Clock clock = Clock.systemUTC(); private KeyStore httpsTrustStore = null; + private boolean verifyDownloadsOnly = false; public FidoMetadataDownloader build() { return new FidoMetadataDownloader( @@ -165,7 +168,8 @@ public FidoMetadataDownloader build() { blobCacheConsumer, certStore, clock, - httpsTrustStore); + httpsTrustStore, + verifyDownloadsOnly); } /** @@ -611,6 +615,26 @@ public FidoMetadataDownloaderBuilder trustHttpsCerts(@NonNull X509Certificate... return this; } + + /** + * If set to true, the BLOB signature will not be verified when loading the BLOB + * from cache or when explicitly set via {@link Step4#useBlob(String)}. This means that if a + * BLOB was successfully verified once and written to cache, that cached value will be + * implicitly trusted when loaded in the future. + * + *

If set to false, the BLOB signature will always be verified no matter where + * the BLOB came from. This means that a cached BLOB may become invalid if the BLOB certificate + * expires, even if the BLOB was successfully verified at the time it was downloaded. + * + *

The default setting is false. + * + * @param verifyDownloadsOnly true if the BLOB signature should be ignored when + * loading the BLOB from cache or when explicitly set via {@link Step4#useBlob(String)}. + */ + public FidoMetadataDownloaderBuilder verifyDownloadsOnly(final boolean verifyDownloadsOnly) { + this.verifyDownloadsOnly = verifyDownloadsOnly; + return this; + } } /** @@ -960,7 +984,7 @@ private Optional loadExplicitBlobOnly(X509Certificate trustRootCer FidoMetadataDownloaderException { if (blobJwt != null) { return Optional.of( - parseAndVerifyBlob( + parseAndMaybeVerifyBlob( new ByteArray(blobJwt.getBytes(StandardCharsets.UTF_8)), trustRootCertificate)); } else { @@ -987,7 +1011,7 @@ private Optional loadCachedBlobOnly(X509Certificate trustRootCerti return cachedContents.map( cached -> { try { - return parseAndVerifyBlob(cached, trustRootCertificate); + return parseAndMaybeVerifyBlob(cached, trustRootCertificate); } catch (Exception e) { log.warn("Failed to read or parse cached BLOB.", e); return null; @@ -1044,18 +1068,27 @@ private MetadataBLOB parseAndVerifyBlob(ByteArray jwt, X509Certificate trustRoot InvalidKeyException, Base64UrlException, FidoMetadataDownloaderException { - Scanner s = new Scanner(new ByteArrayInputStream(jwt.getBytes())).useDelimiter("\\."); - final ByteArray header = ByteArray.fromBase64Url(s.next()); - final ByteArray payload = ByteArray.fromBase64Url(s.next()); - final ByteArray signature = ByteArray.fromBase64Url(s.next()); - return verifyBlob(header, payload, signature, trustRootCertificate); + return verifyBlob(parseBlob(jwt), trustRootCertificate); } - private MetadataBLOB verifyBlob( - ByteArray jwtHeader, - ByteArray jwtPayload, - ByteArray jwtSignature, - X509Certificate trustRootCertificate) + private MetadataBLOB parseAndMaybeVerifyBlob(ByteArray jwt, X509Certificate trustRootCertificate) + throws CertPathValidatorException, + InvalidAlgorithmParameterException, + CertificateException, + IOException, + NoSuchAlgorithmException, + SignatureException, + InvalidKeyException, + Base64UrlException, + FidoMetadataDownloaderException { + if (verifyDownloadsOnly) { + return parseBlob(jwt).blob; + } else { + return verifyBlob(parseBlob(jwt), trustRootCertificate); + } + } + + private MetadataBLOB verifyBlob(ParseResult parseResult, X509Certificate trustRootCertificate) throws IOException, CertificateException, NoSuchAlgorithmException, @@ -1064,12 +1097,7 @@ private MetadataBLOB verifyBlob( CertPathValidatorException, InvalidAlgorithmParameterException, FidoMetadataDownloaderException { - final ObjectMapper headerJsonMapper = - com.yubico.internal.util.JacksonCodecs.json() - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true) - .setBase64Variant(Base64Variants.MIME_NO_LINEFEEDS); - final MetadataBLOBHeader header = - headerJsonMapper.readValue(jwtHeader.getBytes(), MetadataBLOBHeader.class); + final MetadataBLOBHeader header = parseResult.blob.getHeader(); final List certChain; if (header.getX5u().isPresent()) { @@ -1117,9 +1145,9 @@ private MetadataBLOB verifyBlob( signature.initVerify(leafCert.getPublicKey()); signature.update( - (jwtHeader.getBase64Url() + "." + jwtPayload.getBase64Url()) + (parseResult.jwtHeader.getBase64Url() + "." + parseResult.jwtPayload.getBase64Url()) .getBytes(StandardCharsets.UTF_8)); - if (!signature.verify(jwtSignature.getBytes())) { + if (!signature.verify(parseResult.jwtSignature.getBytes())) { throw new FidoMetadataDownloaderException(Reason.BAD_SIGNATURE); } @@ -1134,10 +1162,28 @@ private MetadataBLOB verifyBlob( pathParams.setDate(Date.from(clock.instant())); cpv.validate(blobCertPath, pathParams); - return new MetadataBLOB( - header, - JacksonCodecs.jsonWithDefaultEnums() - .readValue(jwtPayload.getBytes(), MetadataBLOBPayload.class)); + return parseResult.blob; + } + + private static ParseResult parseBlob(ByteArray jwt) throws IOException, Base64UrlException { + Scanner s = new Scanner(new ByteArrayInputStream(jwt.getBytes())).useDelimiter("\\."); + final ByteArray jwtHeader = ByteArray.fromBase64Url(s.next()); + final ByteArray jwtPayload = ByteArray.fromBase64Url(s.next()); + final ByteArray jwtSignature = ByteArray.fromBase64Url(s.next()); + + final ObjectMapper headerJsonMapper = + com.yubico.internal.util.JacksonCodecs.json() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true) + .setBase64Variant(Base64Variants.MIME_NO_LINEFEEDS); + + return new ParseResult( + new MetadataBLOB( + headerJsonMapper.readValue(jwtHeader.getBytes(), MetadataBLOBHeader.class), + JacksonCodecs.jsonWithDefaultEnums() + .readValue(jwtPayload.getBytes(), MetadataBLOBPayload.class)), + jwtHeader, + jwtPayload, + jwtSignature); } private static ByteArray readAll(InputStream is) throws IOException { @@ -1158,4 +1204,12 @@ private static ByteArray verifyHash(ByteArray contents, Set acceptedC return null; } } + + @Value + private static class ParseResult { + private MetadataBLOB blob; + private ByteArray jwtHeader; + private ByteArray jwtPayload; + private ByteArray jwtSignature; + } } diff --git a/webauthn-server-attestation/src/test/scala/com/yubico/fido/metadata/FidoMetadataDownloaderSpec.scala b/webauthn-server-attestation/src/test/scala/com/yubico/fido/metadata/FidoMetadataDownloaderSpec.scala index 4cc0f14cb..22b8461da 100644 --- a/webauthn-server-attestation/src/test/scala/com/yubico/fido/metadata/FidoMetadataDownloaderSpec.scala +++ b/webauthn-server-attestation/src/test/scala/com/yubico/fido/metadata/FidoMetadataDownloaderSpec.scala @@ -39,11 +39,13 @@ import java.security.SecureRandom import java.security.cert.CRL import java.security.cert.CertPathValidatorException import java.security.cert.CertPathValidatorException.BasicReason +import java.security.cert.CertificateExpiredException import java.security.cert.X509Certificate import java.time.Clock import java.time.Instant import java.time.LocalDate import java.time.ZoneOffset +import java.time.temporal.ChronoUnit import java.util.Optional import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletResponse @@ -157,16 +159,19 @@ class FidoMetadataDownloaderSpec certs } + private def formatJwtTbs(header: String, body: String): String = + new ByteArray( + header.getBytes(StandardCharsets.UTF_8) + ).getBase64Url + "." + new ByteArray( + body.getBytes(StandardCharsets.UTF_8) + ).getBase64Url + private def makeBlob( blobKeypair: KeyPair, header: String, body: String, ): String = { - val blobTbs = new ByteArray( - header.getBytes(StandardCharsets.UTF_8) - ).getBase64Url + "." + new ByteArray( - body.getBytes(StandardCharsets.UTF_8) - ).getBase64Url + val blobTbs = formatJwtTbs(header, body) val blobSignature = TestAuthenticator.sign( new ByteArray(blobTbs.getBytes(StandardCharsets.UTF_8)), blobKeypair.getPrivate, @@ -195,6 +200,22 @@ class FidoMetadataDownloaderSpec makeBlob(blobKeypair, blobHeader, blobBody) } + private def makeUnsignedBlob( + nextUpdate: LocalDate, + legalHeader: String = "Kom ihåg att du aldrig får snyta dig i mattan!", + no: Int = 1, + ): String = { + val blobHeader = + s"""{"alg":"None"}""" + val blobBody = s"""{ + "legalHeader": "${legalHeader}", + "no": ${no}, + "nextUpdate": "${nextUpdate}", + "entries": [] + }""" + formatJwtTbs(blobHeader, blobBody) + ".AAAA" + } + private def makeHttpServer( path: String, response: String, @@ -1848,6 +1869,158 @@ class FidoMetadataDownloaderSpec blob should not be null blob.getNo should equal(oldBlobNo) } + + it("If verifyDownloadsOnly is not set, a cached BLOB may expire.") { + val oldBlobNo = 1 + val newBlobNo = 2 + + val (trustRootCert, caKeypair, caName) = makeTrustRootCert() + val (blobCert, blobKeypair, _) = makeCert(caKeypair, caName) + val oldBlobJwt = + makeBlob( + List(blobCert), + blobKeypair, + CertValidTo.atOffset(ZoneOffset.UTC).toLocalDate, + no = oldBlobNo, + ) + val newBlobJwt = + makeBlob( + List(blobCert), + blobKeypair, + CertValidTo.atOffset(ZoneOffset.UTC).toLocalDate, + no = newBlobNo, + ) + val crls = List[CRL]( + TestAuthenticator.buildCrl( + caName, + caKeypair.getPrivate, + "SHA256withECDSA", + CertValidFrom, + CertValidTo, + ) + ) + + val (server, serverUrl, httpsCert) = + makeHttpServer("/blob.jwt", newBlobJwt) + startServer(server) + + val thrown = the[CertPathValidatorException] thrownBy { + load( + FidoMetadataDownloader + .builder() + .expectLegalHeader( + "Kom ihåg att du aldrig får snyta dig i mattan!" + ) + .useTrustRoot(trustRootCert) + .downloadBlob(new URL(s"${serverUrl}/blob.jwt")) + .useBlobCache( + () => + Optional.of( + new ByteArray(oldBlobJwt.getBytes(StandardCharsets.UTF_8)) + ), + _ => {}, + ) + .clock( + Clock + .fixed(CertValidTo.plus(1, ChronoUnit.DAYS), ZoneOffset.UTC) + ) + .useCrls(crls.asJava) + .trustHttpsCerts(httpsCert) + .build() + ).getPayload + } + thrown.getCause shouldBe a[CertificateExpiredException] + } + + it("If verifyDownloadsOnly is set, the signature is ignored when loading from cache.") { + val oldBlobNo = 1 + val newBlobNo = 2 + + val (trustRootCert, caKeypair, caName) = makeTrustRootCert() + val (blobCert, blobKeypair, _) = makeCert(caKeypair, caName) + val oldBlobJwt = + makeUnsignedBlob( + CertValidFrom.atOffset(ZoneOffset.UTC).toLocalDate, + no = oldBlobNo, + ) + val newBlobJwt = + makeBlob( + List(blobCert), + blobKeypair, + CertValidTo.atOffset(ZoneOffset.UTC).toLocalDate, + no = newBlobNo, + ) + val crls = List[CRL]( + TestAuthenticator.buildCrl( + caName, + caKeypair.getPrivate, + "SHA256withECDSA", + CertValidFrom, + CertValidTo, + ) + ) + + val (server, serverUrl, httpsCert) = + makeHttpServer("/blob.jwt", newBlobJwt) + startServer(server) + + val blob = load( + FidoMetadataDownloader + .builder() + .expectLegalHeader( + "Kom ihåg att du aldrig får snyta dig i mattan!" + ) + .useTrustRoot(trustRootCert) + .downloadBlob(new URL(s"${serverUrl}/blob.jwt")) + .useBlobCache( + () => + Optional.of( + new ByteArray(oldBlobJwt.getBytes(StandardCharsets.UTF_8)) + ), + _ => {}, + ) + .clock( + Clock.fixed( + CertValidTo.plus(1, ChronoUnit.DAYS), + ZoneOffset.UTC, + ) + ) + .useCrls(crls.asJava) + .trustHttpsCerts(httpsCert) + .verifyDownloadsOnly(true) + .build() + ).getPayload + blob should not be null + blob.getNo should equal(oldBlobNo) + } + + it("If verifyDownloadsOnly is set, the signature is ignored when loading an explicitly given BLOB.") { + val blobNo = 1 + val (trustRootCert, caKeypair, caName) = makeTrustRootCert() + val blobJwt = + makeUnsignedBlob( + CertValidFrom.atOffset(ZoneOffset.UTC).toLocalDate, + no = blobNo, + ) + + val blob = load( + FidoMetadataDownloader + .builder() + .expectLegalHeader( + "Kom ihåg att du aldrig får snyta dig i mattan!" + ) + .useTrustRoot(trustRootCert) + .useBlob(blobJwt) + .clock( + Clock + .fixed(CertValidTo.plus(1, ChronoUnit.DAYS), ZoneOffset.UTC) + ) + .verifyDownloadsOnly(true) + .build() + ).getPayload + blob should not be null + blob.getNo should equal(blobNo) + } } describe("7. Write the verified object to a local cache as required.") { From 617318898d40d329c69a3a01266008ef65546552 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Wed, 14 Jun 2023 15:46:35 +0200 Subject: [PATCH 28/42] Apply signing plugin settings only if yubicoPublish is set --- .../src/main/groovy/project-convention-publish.gradle | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/buildSrc/src/main/groovy/project-convention-publish.gradle b/buildSrc/src/main/groovy/project-convention-publish.gradle index 6623e509c..b8e998ab5 100644 --- a/buildSrc/src/main/groovy/project-convention-publish.gradle +++ b/buildSrc/src/main/groovy/project-convention-publish.gradle @@ -49,8 +49,10 @@ project.afterEvaluate { } } - signing { - useGpgCmd() - sign(publishing.publications.jars) + if (project.findProperty("yubicoPublish") == "true") { + signing { + useGpgCmd() + sign(publishing.publications.jars) + } } } From 4d1485ece72c7f23d8f895cf12dbe509c976945a Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Fri, 16 Jun 2023 12:22:14 +0200 Subject: [PATCH 29/42] Remove unnecessary ObjectMapper setting --- .../src/main/java/com/yubico/internal/util/JacksonCodecs.java | 2 -- 1 file changed, 2 deletions(-) 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 1c4e1a960..3df8f00c9 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 @@ -5,7 +5,6 @@ import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.dataformat.cbor.CBORFactory; @@ -23,7 +22,6 @@ public static ObjectMapper cbor() { public static ObjectMapper json() { return JsonMapper.builder() .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true) - .configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false) .configure(MapperFeature.PROPAGATE_TRANSIENT_MARKER, true) .serializationInclusion(Include.NON_ABSENT) .defaultBase64Variant(Base64Variants.MODIFIED_FOR_URL) From 3d52a7e7c0f319bdfb51be9463919247caed86c4 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Fri, 16 Jun 2023 12:49:35 +0200 Subject: [PATCH 30/42] Include backup flags in RegisteredCredential generator --- .../com/yubico/webauthn/Generators.scala | 22 +++++++++++++------ .../com/yubico/webauthn/data/Generators.scala | 7 ++++-- 2 files changed, 20 insertions(+), 9 deletions(-) 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 99eab5f0d..bcad72216 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 @@ -73,13 +73,21 @@ object Generators { userHandle <- arbitrary[ByteArray] publicKeyCose <- arbitrary[ByteArray] signatureCount <- arbitrary[Int] - } yield RegisteredCredential - .builder() - .credentialId(credentialId) - .userHandle(userHandle) - .publicKeyCose(publicKeyCose) - .signatureCount(signatureCount) - .build() + backupFlags <- Gen.option(arbitraryBackupFlags.arbitrary) + } yield { + val b = RegisteredCredential + .builder() + .credentialId(credentialId) + .userHandle(userHandle) + .publicKeyCose(publicKeyCose) + .signatureCount(signatureCount) + backupFlags.foreach({ + case ((be, bs)) => + b.backupEligible(be) + b.backupState(bs) + }) + b.build() + } ) ) 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 24d1fe2d0..a9609f9b7 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 @@ -273,13 +273,16 @@ object Generators { ) ) + val arbitraryBackupFlags: Arbitrary[(Boolean, Boolean)] = Arbitrary( + arbitrary[(Boolean, Boolean)].map({ case (be, bs) => (be, be && bs) }) + ) + def authenticatorDataBytes( extensionsGen: Gen[Option[CBORObject]], rpIdHashGen: Gen[ByteArray] = byteArray(32), upFlagGen: Gen[Boolean] = Gen.const(true), uvFlagGen: Gen[Boolean] = arbitrary[Boolean], - backupFlagsGen: Gen[(Boolean, Boolean)] = - arbitrary[(Boolean, Boolean)].map({ case (be, bs) => (be, be && bs) }), + backupFlagsGen: Gen[(Boolean, Boolean)] = arbitraryBackupFlags.arbitrary, signatureCountGen: Gen[ByteArray] = byteArray(4), ): Gen[ByteArray] = halfsized( From 1245a4087d55b7e0b49cd4587d9c8e1715e923ef Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Fri, 16 Jun 2023 12:52:32 +0200 Subject: [PATCH 31/42] Fix backupState JSON property name in RegisteredCredential --- NEWS | 7 +++++++ .../yubico/webauthn/RegisteredCredential.java | 5 ++++- .../com/yubico/webauthn/data/JsonIoSpec.scala | 20 +++++++++++++++++++ 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/NEWS b/NEWS index 43ce9ef0a..f6fd756ab 100644 --- a/NEWS +++ b/NEWS @@ -1,5 +1,12 @@ == Version 2.5.0 (unreleased) == +Breaking changes to experimental features: + +* Added Jackson annotation `@JsonProperty` to method + `RegisteredCredential.isBackedUp()`, changing the property name from + `backedUp` to `backupState`. `backedUp` is still accepted during + deserialization but will no longer be emitted during serialization. + New features: * Added method `.isUserVerified()` to `RegistrationResult` and `AssertionResult` 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 eeba1d362..a33ac9793 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 @@ -24,6 +24,7 @@ package com.yubico.webauthn; +import com.fasterxml.jackson.annotation.JsonAlias; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import com.yubico.webauthn.data.AttestedCredentialData; @@ -153,7 +154,7 @@ private RegisteredCredential( @NonNull @JsonProperty("publicKeyCose") ByteArray publicKeyCose, @JsonProperty("signatureCount") long signatureCount, @JsonProperty("backupEligible") Boolean backupEligible, - @JsonProperty("backupState") Boolean backupState) { + @JsonProperty("backupState") @JsonAlias("backedUp") Boolean backupState) { this.credentialId = credentialId; this.userHandle = userHandle; this.publicKeyCose = publicKeyCose; @@ -183,6 +184,7 @@ private RegisteredCredential( * the standard matures. */ @Deprecated + @JsonProperty("backupEligible") public Optional isBackupEligible() { return Optional.ofNullable(backupEligible); } @@ -206,6 +208,7 @@ public Optional isBackupEligible() { * the standard matures. */ @Deprecated + @JsonProperty("backupState") public Optional isBackedUp() { return Optional.ofNullable(backupState); } 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 0b2602b1c..3e5e68052 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 @@ -27,6 +27,7 @@ package com.yubico.webauthn.data import com.fasterxml.jackson.core.`type`.TypeReference import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.exc.ValueInstantiationException +import com.fasterxml.jackson.databind.node.BooleanNode import com.fasterxml.jackson.databind.node.ObjectNode import com.fasterxml.jackson.databind.node.TextNode import com.yubico.internal.util.JacksonCodecs @@ -533,4 +534,23 @@ class JsonIoSpec } } + describe("The class RegisteredCredential") { + it("""does not have a "backedUp" property when newly serialized.""") { + forAll { cred: RegisteredCredential => + val tree = json.valueToTree(cred).asInstanceOf[ObjectNode] + tree.has("backedUp") should be(false) + } + } + + it("""can be parsed with the previous "backedUp" property name.""") { + forAll { cred: RegisteredCredential => + val tree = json.valueToTree(cred).asInstanceOf[ObjectNode] + tree.set[ObjectNode]("backedUp", BooleanNode.TRUE) + tree.remove("backupState") + val cred2 = json.treeToValue(tree, classOf[RegisteredCredential]) + cred2.isBackedUp.toScala should equal(Some(true)) + } + } + } + } From 618eb6b7688451f3a4ee34f95e8d39b969916356 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Fri, 16 Jun 2023 13:50:54 +0200 Subject: [PATCH 32/42] Use plain JsonMapper in JsonIoSpec --- webauthn-server-core/build.gradle.kts | 1 + .../test/scala/com/yubico/webauthn/data/JsonIoSpec.scala | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/webauthn-server-core/build.gradle.kts b/webauthn-server-core/build.gradle.kts index 69cf488b8..ecf121483 100644 --- a/webauthn-server-core/build.gradle.kts +++ b/webauthn-server-core/build.gradle.kts @@ -26,6 +26,7 @@ dependencies { testImplementation(platform(project(":test-platform"))) testImplementation(project(":yubico-util-scala")) testImplementation("com.fasterxml.jackson.core:jackson-databind") + testImplementation("com.fasterxml.jackson.datatype:jackson-datatype-jdk8") testImplementation("com.upokecenter:cbor") testImplementation("junit:junit") testImplementation("org.bouncycastle:bcpkix-jdk18on") 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 3e5e68052..1bcdb5d37 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 @@ -27,9 +27,11 @@ package com.yubico.webauthn.data import com.fasterxml.jackson.core.`type`.TypeReference import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.exc.ValueInstantiationException +import com.fasterxml.jackson.databind.json.JsonMapper import com.fasterxml.jackson.databind.node.BooleanNode import com.fasterxml.jackson.databind.node.ObjectNode import com.fasterxml.jackson.databind.node.TextNode +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module import com.yubico.internal.util.JacksonCodecs import com.yubico.webauthn.AssertionRequest import com.yubico.webauthn.AssertionResult @@ -55,7 +57,11 @@ class JsonIoSpec with Matchers with ScalaCheckDrivenPropertyChecks { - def json: ObjectMapper = JacksonCodecs.json() + val json: ObjectMapper = + JsonMapper + .builder() + .addModule(new Jdk8Module()) + .build() describe("The class") { From ecc9f50589c87e4b3416ed34280adf05910f24b8 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Fri, 16 Jun 2023 14:16:07 +0200 Subject: [PATCH 33/42] Make Jackson option PROPAGATE_TRANSIENT_MARKER not needed --- NEWS | 5 +++++ .../webauthn/data/AuthenticatorAttestationResponse.java | 5 ++++- .../main/java/com/yubico/internal/util/JacksonCodecs.java | 2 -- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/NEWS b/NEWS index f6fd756ab..655453a17 100644 --- a/NEWS +++ b/NEWS @@ -14,6 +14,11 @@ New features: * Updated README and JavaDoc to use the "passkey" term and provide more guidance around passkey use cases. +Fixes: + +* Made Jackson setting `PROPAGATE_TRANSIENT_MARKER` unnecessary for JSON + serialization with Jackson version 2.15.0-rc1 and later. + == Version 2.4.1 == 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 22ba3bce3..0a33448db 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 @@ -82,7 +82,10 @@ public class AuthenticatorAttestationResponse implements AuthenticatorResponse { private final SortedSet transports; /** The {@link #attestationObject} parsed as a domain object. */ - @NonNull @JsonIgnore private final transient AttestationObject attestation; + @NonNull + @JsonIgnore + @Getter(onMethod = @__({@JsonIgnore})) + private final transient AttestationObject attestation; @NonNull @JsonIgnore 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 3df8f00c9..24722af25 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 @@ -3,7 +3,6 @@ import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.core.Base64Variants; import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.databind.node.ObjectNode; @@ -22,7 +21,6 @@ public static ObjectMapper cbor() { public static ObjectMapper json() { return JsonMapper.builder() .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true) - .configure(MapperFeature.PROPAGATE_TRANSIENT_MARKER, true) .serializationInclusion(Include.NON_ABSENT) .defaultBase64Variant(Base64Variants.MODIFIED_FOR_URL) .addModule(new Jdk8Module()) From e371103fd38003ae790f5113b9632db226df9f5d Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Wed, 21 Jun 2023 17:06:05 +0200 Subject: [PATCH 34/42] Fix demo registration failure when authenticator extensions are present --- .../java/demo/webauthn/WebAuthnServer.java | 12 +++++- .../demo/webauthn/WebAuthnServerSpec.scala | 37 +++++++++++-------- 2 files changed, 32 insertions(+), 17 deletions(-) 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 bf7f661ce..d3fc6d4cd 100644 --- a/webauthn-server-demo/src/main/java/demo/webauthn/WebAuthnServer.java +++ b/webauthn-server-demo/src/main/java/demo/webauthn/WebAuthnServer.java @@ -647,7 +647,17 @@ public void serialize( throw new RuntimeException(e); } }); - gen.writeObjectField("extensions", value.getExtensions()); + value + .getExtensions() + .ifPresent( + extensions -> { + try { + gen.writeObjectField( + "extensions", JacksonCodecs.cbor().readTree(extensions.EncodeToBytes())); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); gen.writeEndObject(); } } 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 1542d51f4..854ef65bb 100644 --- a/webauthn-server-demo/src/test/scala/demo/webauthn/WebAuthnServerSpec.scala +++ b/webauthn-server-demo/src/test/scala/demo/webauthn/WebAuthnServerSpec.scala @@ -38,6 +38,7 @@ import com.yubico.webauthn.data.Generators.arbitraryAuthenticatorTransport import com.yubico.webauthn.data.PublicKeyCredentialRequestOptions import com.yubico.webauthn.data.RelyingPartyIdentity import com.yubico.webauthn.data.ResidentKeyRequirement +import com.yubico.webauthn.test.RealExamples import demo.webauthn.data.AssertionRequestWrapper import demo.webauthn.data.CredentialRegistration import demo.webauthn.data.RegistrationRequest @@ -91,23 +92,27 @@ class WebAuthnServerSpec } it("has a finish method which accepts and outputs JSON.") { - val requestId = ByteArray.fromBase64Url("request1") - - val server = newServerWithRegistrationRequest( - RegistrationTestData.FidoU2f.BasicAttestation - ) + for { + testData <- List( + RegistrationTestData.FidoU2f.BasicAttestation, // This test case for no particular reason + RealExamples.LargeBlobWrite.asRegistrationTestData, // This test case because it has authenticator extensions + ) + } { + val requestId = ByteArray.fromBase64Url("request1") + val server = newServerWithRegistrationRequest(testData) - 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 = + s"""{"attestationObject":"${testData.attestationObject.getBase64Url}","clientDataJSON":"${testData.clientDataJsonBytes.getBase64Url}"}""" + val publicKeyCredentialJson = + s"""{"id":"${testData.response.getId.getBase64Url}","response":${authenticationAttestationResponseJson},"clientExtensionResults":{},"type":"public-key"}""" + val responseJson = + s"""{"requestId":"${requestId.getBase64Url}","credential":${publicKeyCredentialJson}}""" - val response = server.finishRegistration(responseJson) - val json = jsonMapper.writeValueAsString(response.right.get) + val response = server.finishRegistration(responseJson) + val json = jsonMapper.writeValueAsString(response.right.get) - json should not be null + json should not be null + } } } @@ -393,8 +398,8 @@ class WebAuthnServerSpec new InMemoryRegistrationStorage, registrationRequests, newCache(), - rpId, - origins, + testData.rpId, + Set(testData.response.getResponse.getClientData.getOrigin).asJava, ) } From 882c82d94a47c03f7e39eb0fe4d9234d6aaf68aa Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Thu, 22 Jun 2023 12:51:08 +0200 Subject: [PATCH 35/42] Update terminology on demo buttons --- webauthn-server-demo/src/main/webapp/index.html | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/webauthn-server-demo/src/main/webapp/index.html b/webauthn-server-demo/src/main/webapp/index.html index 2c27d1f11..dcbdab089 100644 --- a/webauthn-server-demo/src/main/webapp/index.html +++ b/webauthn-server-demo/src/main/webapp/index.html @@ -102,11 +102,11 @@ function updateRegisterButtons() { if (session.sessionToken) { - document.getElementById('registerButton').textContent = 'Add credential'; - document.getElementById('registerRkButton').textContent = 'Add resident credential'; + document.getElementById('registerButton').textContent = 'Add non-discoverable credential'; + document.getElementById('registerRkButton').textContent = 'Add passkey'; } else { - document.getElementById('registerButton').textContent = 'Register new account'; - document.getElementById('registerRkButton').textContent = 'Register new account with resident credential'; + document.getElementById('registerButton').textContent = 'Create account with non-discoverable credential'; + document.getElementById('registerRkButton').textContent = 'Create account with passkey'; } } @@ -535,12 +535,12 @@

Test your WebAuthn device

@@ -550,12 +550,12 @@

Test your WebAuthn device

From 62b7312b3f966b5ec384af07029b8e95de10be85 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Thu, 22 Jun 2023 12:51:40 +0200 Subject: [PATCH 36/42] Update demo header; remove Yubico logo --- .../src/main/webapp/img/yubico-logo.png | Bin 2879 -> 0 bytes webauthn-server-demo/src/main/webapp/index.html | 8 +------- 2 files changed, 1 insertion(+), 7 deletions(-) delete mode 100644 webauthn-server-demo/src/main/webapp/img/yubico-logo.png diff --git a/webauthn-server-demo/src/main/webapp/img/yubico-logo.png b/webauthn-server-demo/src/main/webapp/img/yubico-logo.png deleted file mode 100644 index a77538b617f26f1084508cabe0a5c8ef84ce8ece..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2879 zcmV-F3&8Y=P)1yeSxABkAwmKcqKLdy3VC#58Xr&yS}Ej_LLfi_Mq-JtE+tka zCP0BeN8$p4h{z)pMag4PA%die2Dc#;2#G+7A_T!0Km#!eNg&DY`p4~o%P=$d+?lz% zv+Mn;wn*)s+mCa zqZ~kQu8G`-Zz))>EB&q@k!$5SRGUXueOj-J*6SUu*B=T$Bdyo%_2{sW)qsN)8uXVX z9Y-zMdQMtP?OCsP0DcIZ2<#J+{TI*;%s0xt)(SeP_MG*4Kj0)l0|x>-1n%?$TY=?( z1MV=&JyCDJWqs}sd>fbyjH$raK43lY0Pq{5+#`~H3)X7~9GWnee**ghTT>8dHZT|1 zQbTNepu;G)CBB-0)xb^(JJaWla?1qcxEh#V!=G&j?lQ_vsnJF~gg+XX156M+_XJ>! za!x{&V9k1fgA>|4-zfLXIte!em;xLQbf)0#+rVSMJwTUHu20&p^?DpI5BNH;Ysxjc z5m*GwHOf6(O^@xs)xe~L#y$y*HOj3LDy}W8&SD^SHT*hG_82-6o=dYkZ5P_Bg%Qol zo>;F(1J?qlqzTln4d{Nyu#28(y}lCo39z}6rIhYF^HV7A!?A6s8v1~)Vdl0UN|Y`y0}l!sIXPG_iQ-NdC~a-< z{e}~64gPIIFUxxU*#QnpM_aF75xBNZk1ed%&j1Hx(8U-F?3)0HvOEv$p={{{XEJOZT7Fok|63u4E(H!v4C4mc+KdxPM)0&uYP`WNp8 ztuglB1?-p5z%kbAPNUrB8WTA|Fp%zHOx9jtA+Q)&0jvW?g~0y|KzbqLG^5;av-H`m z*ZTmcrpSVv2i$FxTU)kxIzo^-ohW@N!OcRs9~k8pl>P30V4n4QDsYpajq|M67XxeJ z2Twm24&Z`>ze^^3Wh)=HG=POhxpt%6nMS!=jB<|}<(@OjEeU@+Dutep9DwouStz%{ zC^yn5cSAYZ!{T`dxZfyuGVmFqGlp8b%S>Rjpxx^>e#&uUE;O++gEokr3c+grx7s&-jdjh*u`J2Rg z{b(4wGX)9Rh|WGFn4Fb66EZTATB@l=xi@MNuY6Gh{YJUt!!BjZ=3W3kCFr+HG7qC% zFR(z+-cHu*kCzkvZ@@c(m-@2`7Go6fxrApo0gn$;cScM$qud{a;uA6)iX_(B0CcB) zU$NcH8ReFj{eC3TIZYBCNzw0BLb*xBvVm>^E)zUBBLph(iF|_ES^N)4zrl^#K!sD)3fR9`#Sh&kc=))o6)16MzLWF(T}aZv z#qz4aCSo^%%+d_bO7;`Vy^&@&L6#ofwOFBnKHwq2!>5)4mEg`Cogly1DEC&*^LHT$ zvr*=8oe=tHaiAW z=fx^P)$M}d73=k!u%#pgKz&BJg#(=tX>oj1Ak)X--fYSuhWL+AE^Psu#Pxf-mX(1lnRCrq}5kAB`-;?OTDEDB7hjiV7fVhpQ5KeqaI#z|bul2g$dVN#b z+zZIEKT}gItjmSEf3%n&un5@)f2!d{quj%FyfjI6;LFzQb83(Z>-FWxs{seXvSYoz zL+Bg-G2^j3FElLLdOf?8f)-lbvx#c#sgV5WC#ob(!o;LnqR`jw$S$s2BaLkD19T&gJ_6aF7Vx+am~Xun z5x%Pd!e3X%%jmaWFBfJpASy>biPpd(>vew!%vJ?obr0Z+DcgC}D7QGvfd=x-vtCaV z8ke(xv#i(4f+2f0%x`>{->DM!G4#96dVL`Cw_YzLs&h_*dm0En{a(54O`Lt2) z)`8enD&Q5eD}mk$Ofbql@V-mjLWR5u{3S(aT~$7?l;$No*Cc zn+D@Eg`v-GV0WU6+08^}f~9{lBpOyWvCr3leT{Pd1K*`d(a1SMxn85(MU8HG;U)Y& zY)NTifhE8fjB+KHN^(-ztkcB3fLoB=s3-WOHvpYRxyMtIT~b=nF4cFyt46s=z}JW} zU)vhcMGw&#%v3@DtBrDd6P-^qQlYsDm}ivR4Om%8pJ{5XrS8S8&GPfY>b%n^cc=Aw zI#CL3OjxLeSjwrS;BOh_R+O!33*25~`~~23!Q8)^po6!8myL2$gVj71m_d{x?TC%H zpXgZsDx=(OB@0_dJPBryd=dKjIs@5zjdJ&fu}voW8o`$WUkYW0vOoOv5#`>0CHS;n zEzh9KdVK(JVbFOYWPb`}I_xJpN9ZN`WXK$&++!8y@`0Bv-S&C_$r0|6fXixI4z>5A zgH`SgL|bc=+Zx-RDgU={ThD6DZA4`leT}+$b-F9fYHLTLZ#Z@Y@|V6Iyou=2bzd{b z8w|lNM1^GA^Iiqzw(#E918*ATwn^tIy>vX5sIDfdWOI#CZVWkcv?vmN_^@IoBdbrI zma8
- - -

Test your WebAuthn device

+

java-webauthn-server demo

From d7b4fd44e05f25b1b6feb968df45e991caa1b112 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Thu, 22 Jun 2023 14:28:54 +0200 Subject: [PATCH 37/42] Add Automatic-Module-Name to jar manifests --- NEWS | 2 ++ webauthn-server-attestation/build.gradle.kts | 1 + webauthn-server-core/build.gradle.kts | 1 + yubico-util/build.gradle.kts | 1 + 4 files changed, 5 insertions(+) diff --git a/NEWS b/NEWS index 7d8c68ac3..0746178c5 100644 --- a/NEWS +++ b/NEWS @@ -15,6 +15,7 @@ New features: as a shortcut for accessing the UV flag in authenticator data. * Updated README and JavaDoc to use the "passkey" term and provide more guidance around passkey use cases. +* Added `Automatic-Module-Name` to jar manifest. `webauthn-server-attestation`: @@ -24,6 +25,7 @@ New features: set to `true`, the BLOB signature will not be verified when loading a BLOB from cache or when explicitly given. Default setting is `false`, which preserves the previous behaviour. +* Added `Automatic-Module-Name` to jar manifest. Fixes: diff --git a/webauthn-server-attestation/build.gradle.kts b/webauthn-server-attestation/build.gradle.kts index fdcbb8e4c..9535ce0f1 100644 --- a/webauthn-server-attestation/build.gradle.kts +++ b/webauthn-server-attestation/build.gradle.kts @@ -72,6 +72,7 @@ tasks["check"].dependsOn(integrationTest) tasks.jar { manifest { attributes(mapOf( + "Automatic-Module-Name" to "com.yubico.webauthn.attestation", "Implementation-Id" to "java-webauthn-server-attestation", "Implementation-Title" to project.description, )) diff --git a/webauthn-server-core/build.gradle.kts b/webauthn-server-core/build.gradle.kts index ecf121483..79ed2a335 100644 --- a/webauthn-server-core/build.gradle.kts +++ b/webauthn-server-core/build.gradle.kts @@ -56,6 +56,7 @@ configurations.jmhRuntimeClasspath { tasks.withType(Jar::class) { manifest { attributes(mapOf( + "Automatic-Module-Name" to "com.yubico.webauthn", "Implementation-Title" to "Yubico Web Authentication server library", "Specification-Title" to "Web Authentication: An API for accessing Public Key Credentials", diff --git a/yubico-util/build.gradle.kts b/yubico-util/build.gradle.kts index c5849fdfa..e8319c120 100644 --- a/yubico-util/build.gradle.kts +++ b/yubico-util/build.gradle.kts @@ -43,6 +43,7 @@ configurations.jmhRuntimeClasspath { tasks.jar { manifest { attributes(mapOf( + "Automatic-Module-Name" to "com.yubico.internal.util", "Implementation-Id" to "yubico-util", "Implementation-Title" to project.description, )) From f0da07bcbb280c8497d7a16bdeac388b17ff916b Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 26 Jun 2023 15:28:30 +0200 Subject: [PATCH 38/42] Rename tests of PublicKeyCredential parsing functions --- .../com/yubico/webauthn/data/JsonIoSpec.scala | 94 ++++++++++--------- 1 file changed, 48 insertions(+), 46 deletions(-) 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 1bcdb5d37..64c397857 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 @@ -133,52 +133,6 @@ class JsonIoSpec } describe("The class PublicKeyCredential") { - 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 altRecoded: String = json.writeValueAsString(altDecoded) - - altDecoded should equal(decoded) - altRecoded should equal(encoded) - } - } - test( - new TypeReference[PublicKeyCredential[ - AuthenticatorAttestationResponse, - ClientRegistrationExtensionOutputs, - ]]() {} - ) - } - - 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 altRecoded: String = json.writeValueAsString(altDecoded) - - altDecoded should equal(decoded) - altRecoded should equal(encoded) - } - } - 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] @@ -416,6 +370,54 @@ class JsonIoSpec } } + describe("The function PublicKeyCredential.parseRegistrationResponseJson") { + it("can parse registration responses.") { + 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 altRecoded: String = json.writeValueAsString(altDecoded) + + altDecoded should equal(decoded) + altRecoded should equal(encoded) + } + } + + test( + new TypeReference[PublicKeyCredential[ + AuthenticatorAttestationResponse, + ClientRegistrationExtensionOutputs, + ]]() {} + ) + } + } + + describe("The function PublicKeyCredential.parseAssertionResponseJson") { + it("can parse assertion responses.") { + 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 altRecoded: String = json.writeValueAsString(altDecoded) + + altDecoded should equal(decoded) + altRecoded should equal(encoded) + } + } + + test( + new TypeReference[PublicKeyCredential[ + AuthenticatorAssertionResponse, + ClientAssertionExtensionOutputs, + ]]() {} + ) + } + } + describe("The class PublicKeyCredentialCreationOptions") { it("""has a toCredentialsCreateJson() method which returns a JSON object with the PublicKeyCredentialCreationOptions set as a top-level "publicKey" property.""") { forAll { pkcco: PublicKeyCredentialCreationOptions => From 0b21631f49cfc86b7565e7fcf545c56b28940a7f Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 26 Jun 2023 17:01:02 +0200 Subject: [PATCH 39/42] Tolerate "publicKey" and "publicKeyAlgorithm" properties in parseRegistrationResponseJson --- NEWS | 8 ++++ .../AuthenticatorAttestationResponse.java | 2 + .../com/yubico/webauthn/data/JsonIoSpec.scala | 42 +++++++++++++++++++ 3 files changed, 52 insertions(+) diff --git a/NEWS b/NEWS index 0746178c5..069bb2c08 100644 --- a/NEWS +++ b/NEWS @@ -17,6 +17,14 @@ New features: around passkey use cases. * Added `Automatic-Module-Name` to jar manifest. +Fixes: + +* `AuthenticatorAttestationResponse` now tolerates and ignores properties + `"publicKey"` and `"publicKeyAlgorithm"` during JSON deserialization. These + properties are emitted by the `PublicKeyCredential.toJSON()` method added in + WebAuthn Level 3. + + `webauthn-server-attestation`: New features: 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 0a33448db..29ba7e3ec 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 @@ -26,6 +26,7 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import com.yubico.internal.util.CollectionUtil; import com.yubico.webauthn.data.exception.Base64UrlException; @@ -49,6 +50,7 @@ * Information About Public Key Credential (interface AuthenticatorAttestationResponse) */ @Value +@JsonIgnoreProperties({"publicKey", "publicKeyAlgorithm"}) public class AuthenticatorAttestationResponse implements AuthenticatorResponse { /** 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 64c397857..3fe9a73c5 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 @@ -25,10 +25,12 @@ package com.yubico.webauthn.data import com.fasterxml.jackson.core.`type`.TypeReference +import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.exc.ValueInstantiationException import com.fasterxml.jackson.databind.json.JsonMapper import com.fasterxml.jackson.databind.node.BooleanNode +import com.fasterxml.jackson.databind.node.JsonNodeFactory import com.fasterxml.jackson.databind.node.ObjectNode import com.fasterxml.jackson.databind.node.TextNode import com.fasterxml.jackson.datatype.jdk8.Jdk8Module @@ -44,6 +46,7 @@ import com.yubico.webauthn.extension.appid.Generators._ import org.junit.runner.RunWith import org.scalacheck.Arbitrary import org.scalacheck.Arbitrary.arbitrary +import org.scalacheck.Gen import org.scalatest.funspec.AnyFunSpec import org.scalatest.matchers.should.Matchers import org.scalatestplus.junit.JUnitRunner @@ -62,6 +65,7 @@ class JsonIoSpec .builder() .addModule(new Jdk8Module()) .build() + val jf: JsonNodeFactory = JsonNodeFactory.instance describe("The class") { @@ -392,6 +396,44 @@ class JsonIoSpec ]]() {} ) } + + describe("""tolerates and ignores the "response" sub-attribute:""") { + def test[T <: JsonNode](attrName: String, genAttrValue: Gen[T]): Unit = { + type P = PublicKeyCredential[ + AuthenticatorAttestationResponse, + ClientRegistrationExtensionOutputs, + ] + it(s"${attrName}.") { + forAll( + arbitrary[P], + genAttrValue, + ) { (value: P, attrValue: T) => + val tree: ObjectNode = json.valueToTree(value) + tree + .get("response") + .asInstanceOf[ObjectNode] + .set(attrName, attrValue) + val encoded = json.writeValueAsString(tree) + val decoded = + PublicKeyCredential.parseRegistrationResponseJson(encoded) + val recoded: ObjectNode = json.valueToTree[ObjectNode](decoded) + recoded.has(attrName) should be(false) + } + } + } + + test( + "publicKeyAlgorithm", + arbitraryCOSEAlgorithmIdentifier.arbitrary.map(i => + jf.numberNode(i.getId) + ), + ) + + test( + "publicKey", + arbitrary[String].map(new TextNode(_)), + ) + } } describe("The function PublicKeyCredential.parseAssertionResponseJson") { From 1c8a8adc561b4e80de043dabbf13972d8a4864ba Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 27 Jun 2023 13:16:55 +0200 Subject: [PATCH 40/42] Relax Guava dependency version constraint to allow major version 32 --- NEWS | 1 + settings.gradle.kts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/NEWS b/NEWS index 069bb2c08..c3088166b 100644 --- a/NEWS +++ b/NEWS @@ -23,6 +23,7 @@ Fixes: `"publicKey"` and `"publicKeyAlgorithm"` during JSON deserialization. These properties are emitted by the `PublicKeyCredential.toJSON()` method added in WebAuthn Level 3. +* Relaxed Guava dependency version constraint to include major version 32. `webauthn-server-attestation`: diff --git a/settings.gradle.kts b/settings.gradle.kts index 4c4394666..3cb500697 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -16,7 +16,7 @@ dependencyResolutionManagement { create("constraintLibs") { library("cbor", "com.upokecenter:cbor:[4.5.1,5)") library("cose", "com.augustcellars.cose:cose-java:[1.0.0,2)") - library("guava", "com.google.guava:guava:[24.1.1,32)") + library("guava", "com.google.guava:guava:[24.1.1,33)") library("httpclient5", "org.apache.httpcomponents.client5:httpclient5:[5.0.0,6)") library("slf4j", "org.slf4j:slf4j-api:[1.7.25,3)") From 1576b3d0756f738e8c892855104fa4cb188faf59 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 4 Jul 2023 13:15:32 +0200 Subject: [PATCH 41/42] Handle empty allowCredentials the same as absent in finishAssertion --- NEWS | 8 +++++++ .../yubico/webauthn/FinishAssertionSteps.java | 1 + .../webauthn/RelyingPartyAssertionSpec.scala | 21 ++++++++++++------- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/NEWS b/NEWS index b33a0d5fc..b377ff5cc 100644 --- a/NEWS +++ b/NEWS @@ -1,8 +1,16 @@ == Version 2.4.2 (unreleased) == +Changes: + * Updated README and JavaDoc to use the "passkey" term and provide more guidance around passkey use cases. +Fixes: + +* `RelyingParty.finishAssertion` now behaves the same if + `StartAssertionOptions.allowCredentials` is explicitly set to a present, empty + list as when absent. + == Version 2.4.1 == 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 033b7a363..88b792435 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 @@ -111,6 +111,7 @@ public void validate() { request .getPublicKeyCredentialRequestOptions() .getAllowCredentials() + .filter(allowCredentials -> !allowCredentials.isEmpty()) .ifPresent( allowed -> { assertTrue( 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 78daeb962..1cc5f8d27 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 @@ -594,14 +594,21 @@ class RelyingPartyAssertionSpec } it("Succeeds if no credential IDs were requested.") { - val steps = finishAssertion( - allowCredentials = None, - credentialId = new ByteArray(Array(0, 1, 2, 3)), - ) - val step: FinishAssertionSteps#Step5 = steps.begin + for { + allowCredentials <- List( + None, + Some(List.empty[PublicKeyCredentialDescriptor].asJava), + ) + } { + val steps = finishAssertion( + allowCredentials = allowCredentials, + credentialId = new ByteArray(Array(0, 1, 2, 3)), + ) + val step: FinishAssertionSteps#Step5 = steps.begin - step.validations shouldBe a[Success[_]] - step.tryNext shouldBe a[Success[_]] + step.validations shouldBe a[Success[_]] + step.tryNext shouldBe a[Success[_]] + } } } From e1ed27c3ca777b4dfdf11fbf6a06f9bee6615b7b Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 4 Jul 2023 16:20:19 +0200 Subject: [PATCH 42/42] Pin OpenJDK version in release-verify-signatures workflow --- .github/workflows/release-verify-signatures.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-verify-signatures.yml b/.github/workflows/release-verify-signatures.yml index 0dbc92fe3..e2ff48b54 100644 --- a/.github/workflows/release-verify-signatures.yml +++ b/.github/workflows/release-verify-signatures.yml @@ -39,7 +39,7 @@ jobs: strategy: matrix: - java: [17] + java: ["17.0.7"] distribution: [temurin, zulu, microsoft] steps: