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/.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: diff --git a/NEWS b/NEWS index 581fc2c80..979a4eafd 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,50 @@ +== Version 2.5.0 == + +`webauthn-server-core`: + +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` + 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. + +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. +* Relaxed Guava dependency version constraint to include major version 32. +* `RelyingParty.finishAssertion` now behaves the same if + `StartAssertionOptions.allowCredentials` is explicitly set to a present, empty + list as when absent. + + +`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. +* Added `Automatic-Module-Name` to jar manifest. + +Fixes: + +* Made Jackson setting `PROPAGATE_TRANSIENT_MARKER` unnecessary for JSON + serialization with Jackson version 2.15.0-rc1 and later. + + == Version 2.4.1 == Changes: diff --git a/README b/README index 66ec489e6..88fae3067 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::[] @@ -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 @@ -64,7 +64,7 @@ Maven: com.yubico webauthn-server-core - 2.4.1 + 2.5.0 compile ---------- @@ -72,7 +72,7 @@ Maven: Gradle: ---------- -compile 'com.yubico:webauthn-server-core:2.4.1' +implementation("com.yubico:webauthn-server-core:2.5.0") ---------- NOTE: You may need additional dependencies with JCA providers to support some signature algorithms. @@ -85,7 +85,7 @@ The library will log warnings if you try to configure it for algorithms with no This library uses link:https://semver.org/[semantic versioning]. The public API consists of all public classes, methods and fields in the `com.yubico.webauthn` package and its subpackages, i.e., everything covered by the -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/package-summary.html[Javadoc], +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.5.0/com/yubico/webauthn/package-summary.html[Javadoc], *with the exception* of features annotated with a `@Deprecated` annotation and a `@deprecated EXPERIMENTAL:` tag in JavaDoc. Such features are considered unstable and may receive breaking changes without a @@ -108,7 +108,7 @@ In addition to the main `webauthn-server-core` module, there is also: == Documentation See the -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/package-summary.html[Javadoc] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.5.0/com/yubico/webauthn/package-summary.html[Javadoc] for in-depth API documentation. @@ -118,22 +118,22 @@ Using this library comes in two parts: the server side and the client side. The server side involves: 1. Implement the - link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/CredentialRepository.html[`CredentialRepository`] + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.5.0/com/yubico/webauthn/CredentialRepository.html[`CredentialRepository`] interface with your database access logic. 2. Instantiate the - link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/RelyingParty.html[`RelyingParty`] + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.5.0/com/yubico/webauthn/RelyingParty.html[`RelyingParty`] class. 3. Use the - link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/RelyingParty.html#startRegistration(com.yubico.webauthn.StartRegistrationOptions)[`RelyingParty.startRegistration(...)`] + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.5.0/com/yubico/webauthn/RelyingParty.html#startRegistration(com.yubico.webauthn.StartRegistrationOptions)[`RelyingParty.startRegistration(...)`] and - 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(...)`] + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.5.0/com/yubico/webauthn/RelyingParty.html#finishRegistration(com.yubico.webauthn.FinishRegistrationOptions)[`RelyingParty.finishRegistration(...)`] methods to perform registration ceremonies. 4. Use the - 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(...)`] + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.5.0/com/yubico/webauthn/RelyingParty.html#startAssertion(com.yubico.webauthn.StartAssertionOptions)[`RelyingParty.startAssertion(...)`] 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(...)`] + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.5.0/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: @@ -151,7 +151,7 @@ link:webauthn-server-demo[`webauthn-server-demo`] for a complete demo server. === 1. Implement a `CredentialRepository` The -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/CredentialRepository.html[`CredentialRepository`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.5.0/com/yubico/webauthn/CredentialRepository.html[`CredentialRepository`] interface abstracts your database in a database-agnostic way. The concrete implementation will be different for every project, but you can use link:https://github.com/Yubico/java-webauthn-server/blob/main/webauthn-server-demo/src/main/java/demo/webauthn/InMemoryRegistrationStorage.java[`InMemoryRegistrationStorage`] @@ -160,11 +160,11 @@ as a simple example. === 2. Instantiate a `RelyingParty` The -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/RelyingParty.html[`RelyingParty`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.5.0/com/yubico/webauthn/RelyingParty.html[`RelyingParty`] class is the main entry point to the library. You can instantiate it using its builder methods, passing in your -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/CredentialRepository.html[`CredentialRepository`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.5.0/com/yubico/webauthn/CredentialRepository.html[`CredentialRepository`] implementation (called `MyCredentialRepository` here) as an argument: [source,java] @@ -186,7 +186,7 @@ RelyingParty rp = RelyingParty.builder() A registration ceremony consists of 5 main steps: 1. Generate registration parameters using - link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/RelyingParty.html#startRegistration(com.yubico.webauthn.StartRegistrationOptions)[`RelyingParty.startRegistration(...)`]. + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.5.0/com/yubico/webauthn/RelyingParty.html#startRegistration(com.yubico.webauthn.StartRegistrationOptions)[`RelyingParty.startRegistration(...)`]. 2. Send registration parameters to the client and call https://developer.mozilla.org/en-US/docs/Web/API/CredentialsContainer/create[`navigator.credentials.create()`]. 3. With `cred` as the result of the successfully resolved promise, @@ -194,7 +194,7 @@ A registration ceremony consists of 5 main steps: and https://www.w3.org/TR/webauthn-2/#ref-for-dom-authenticatorattestationresponse-gettransports[`cred.response.getTransports()`] and return their results along with `cred` to the server. 4. Validate the response using - 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(...)`]. + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.5.0/com/yubico/webauthn/RelyingParty.html#finishRegistration(com.yubico.webauthn.FinishRegistrationOptions)[`RelyingParty.finishRegistration(...)`]. 5. Update your database using the `finishRegistration` output. This example uses GitHub's link:https://github.com/github/webauthn-json[webauthn-json] library to do both (2) and (3) in one function call. @@ -226,15 +226,15 @@ return credentialCreateJson; // Send to client ---------- You will need to keep this -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/data/PublicKeyCredentialCreationOptions.html[`PublicKeyCredentialCreationOptions`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.5.0/com/yubico/webauthn/data/PublicKeyCredentialCreationOptions.html[`PublicKeyCredentialCreationOptions`] object in temporary storage so you can also pass it into -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(...)`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.5.0/com/yubico/webauthn/RelyingParty.html#finishRegistration(com.yubico.webauthn.FinishRegistrationOptions)[`RelyingParty.finishRegistration(...)`] later. If needed, you can use the -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/data/PublicKeyCredentialCreationOptions.html#toJson()[`toJson()`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.5.0/com/yubico/webauthn/data/PublicKeyCredentialCreationOptions.html#toJson()[`toJson()`] and -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/data/PublicKeyCredentialCreationOptions.html#fromJson(java.lang.String)[`fromJson(String)`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.5.0/com/yubico/webauthn/data/PublicKeyCredentialCreationOptions.html#fromJson(java.lang.String)[`fromJson(String)`] methods to serialize and deserialize the value for storage. Now call the WebAuthn API on the client side: @@ -275,24 +275,27 @@ 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: 1. Generate authentication parameters using - 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(...)`]. + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.5.0/com/yubico/webauthn/RelyingParty.html#startAssertion(com.yubico.webauthn.StartAssertionOptions)[`RelyingParty.startAssertion(...)`]. 2. Send authentication parameters to the client, call https://developer.mozilla.org/en-US/docs/Web/API/CredentialsContainer/get[`navigator.credentials.get()`] and return the response. @@ -300,7 +303,7 @@ Like registration ceremonies, an authentication ceremony consists of 5 main step https://www.w3.org/TR/webauthn-2/#ref-for-dom-publickeycredential-getclientextensionresults[`cred.getClientExtensionResults()`] and return the result along with `cred` to the server. 4. Validate the response using - 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(...)`]. + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.5.0/com/yubico/webauthn/RelyingParty.html#finishAssertion(com.yubico.webauthn.FinishAssertionOptions)[`RelyingParty.finishAssertion(...)`]. 5. Update your database using the `finishAssertion` output, and act upon the result (for example, grant login access). This example uses GitHub's link:https://github.com/github/webauthn-json[webauthn-json] library to do both (2) and (3) in one function call. @@ -317,15 +320,15 @@ return credentialGetJson; // Send to client ---------- Again, you will need to keep this -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/AssertionRequest.html[`AssertionRequest`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.5.0/com/yubico/webauthn/AssertionRequest.html[`AssertionRequest`] object in temporary storage so you can also pass it into -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(...)`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.5.0/com/yubico/webauthn/RelyingParty.html#finishAssertion(com.yubico.webauthn.FinishAssertionOptions)[`RelyingParty.finishAssertion(...)`] later. If needed, you can use the -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/AssertionRequest.html#toJson()[`toJson()`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.5.0/com/yubico/webauthn/AssertionRequest.html#toJson()[`toJson()`] and -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/AssertionRequest.html#fromJson(java.lang.String)[`fromJson(String)`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.5.0/com/yubico/webauthn/AssertionRequest.html#fromJson(java.lang.String)[`fromJson(String)`] methods to serialize and deserialize the value for storage. Now call the WebAuthn API on the client side: @@ -366,7 +369,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`]. +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.5.0/com/yubico/webauthn/AssertionResult.html[`AssertionResult`]. Most importantly, you should update the signature counter. That might look something like this: [source,java] @@ -374,7 +377,8 @@ Most importantly, you should update the signature counter. That might look somet 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,18 +386,43 @@ 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 -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: +- <>: 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://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.5.0/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.5.0/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.5.0/com/yubico/webauthn/data/ResidentKeyRequirement.html#REQUIRED[`REQUIRED`]: [source,java] ---------- @@ -413,13 +442,25 @@ 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 +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.5.0/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.5.0/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.5.0/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.5.0/com/yubico/webauthn/data/RegistrationExtensionInputs.RegistrationExtensionInputsBuilder.html#credProps()[`credProps` extension] +to be enabled, which it is by default. + + +[#user-verification] +==== User verification: passwordless multi-factor authentication -User verification can be enforced independently per authentication ceremony: +link:https://passkeys.dev/docs/reference/terms/#user-verification-uv[User verification] +can be enforced independently per authentication ceremony: [source,java] ---------- @@ -430,7 +471,7 @@ AssertionRequest request = rp.startAssertion(StartAssertionOptions.builder() ---------- Then -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(...)`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.5.0/com/yubico/webauthn/RelyingParty.html#finishAssertion(com.yubico.webauthn.FinishAssertionOptions)[`RelyingParty.finishAssertion(...)`] will enforce that user verification was performed. However, there is no guarantee that the user's authenticator will support this unless the user has some credential created with the @@ -448,6 +489,98 @@ 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.5.0/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.5.0/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. + + +[#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.5.0/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.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`]. +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` @@ -467,14 +600,14 @@ To migrate to using the WebAuthn API, you need to do the following: 1. Follow the link:#getting-started[Getting started] guide above to set up WebAuthn support in general. + -Note that unlike a U2F AppID, the WebAuthn link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/data/RelyingPartyIdentity.RelyingPartyIdentityBuilder.html#id(java.lang.String)[RP ID] +Note that unlike a U2F AppID, the WebAuthn link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.5.0/com/yubico/webauthn/data/RelyingPartyIdentity.RelyingPartyIdentityBuilder.html#id(java.lang.String)[RP ID] consists of only the domain name of the AppID. WebAuthn does not support link:https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-appid-and-facets-v1.2-ps-20170411.html[U2F Trusted Facet Lists]. 2. Set the - link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/RelyingParty.RelyingPartyBuilder.html#appId(com.yubico.webauthn.extension.appid.AppId)[`appId()`] + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.5.0/com/yubico/webauthn/RelyingParty.RelyingPartyBuilder.html#appId(com.yubico.webauthn.extension.appid.AppId)[`appId()`] setting on your - link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/RelyingParty.html[`RelyingParty`] + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.5.0/com/yubico/webauthn/RelyingParty.html[`RelyingParty`] instance. The argument to the `appid()` setting should be the same as you used for the `appId` argument to the link:https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-javascript-api-v1.2-ps-20170411.html#high-level-javascript-api[U2F `register` and `sign` functions]. @@ -492,22 +625,22 @@ extensions and configure the `RelyingParty` to accept the given AppId when verif privacy consideration. 4. When your - link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/CredentialRepository.html[`CredentialRepository`] + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.5.0/com/yubico/webauthn/CredentialRepository.html[`CredentialRepository`] creates a - link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/RegisteredCredential.html[`RegisteredCredential`] + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.5.0/com/yubico/webauthn/RegisteredCredential.html[`RegisteredCredential`] for a U2F credential, use the U2F key handle as the - link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/RegisteredCredential.RegisteredCredentialBuilder.html#credentialId(com.yubico.webauthn.data.ByteArray)[credential ID]. + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.5.0/com/yubico/webauthn/RegisteredCredential.RegisteredCredentialBuilder.html#credentialId(com.yubico.webauthn.data.ByteArray)[credential ID]. If you store key handles base64 encoded, you should decode them using - link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/data/ByteArray.html#fromBase64(java.lang.String)[`ByteArray.fromBase64`] + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.5.0/com/yubico/webauthn/data/ByteArray.html#fromBase64(java.lang.String)[`ByteArray.fromBase64`] or - link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/data/ByteArray.html#fromBase64Url(java.lang.String)[`ByteArray.fromBase64Url`] + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.5.0/com/yubico/webauthn/data/ByteArray.html#fromBase64Url(java.lang.String)[`ByteArray.fromBase64Url`] as appropriate before passing them to the `RegisteredCredential`. 5. When your `CredentialRepository` creates a `RegisteredCredential` for a U2F credential, use the - link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/RegisteredCredential.RegisteredCredentialBuilder.html#publicKeyEs256Raw(com.yubico.webauthn.data.ByteArray)[`publicKeyEs256Raw()`] - method instead of link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/RegisteredCredential.RegisteredCredentialBuilder.html#publicKeyCose(com.yubico.webauthn.data.ByteArray)[`publicKeyCose()`] + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.5.0/com/yubico/webauthn/RegisteredCredential.RegisteredCredentialBuilder.html#publicKeyEs256Raw(com.yubico.webauthn.data.ByteArray)[`publicKeyEs256Raw()`] + method instead of link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.5.0/com/yubico/webauthn/RegisteredCredential.RegisteredCredentialBuilder.html#publicKeyCose(com.yubico.webauthn.data.ByteArray)[`publicKeyCose()`] to set the credential public key. 6. Replace calls to the U2F @@ -641,17 +774,17 @@ provides optional additional features for working with attestation. See the module documentation for more details. Alternatively, you can use the -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/attestation/AttestationTrustSource.html[`AttestationTrustSource`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.5.0/com/yubico/webauthn/attestation/AttestationTrustSource.html[`AttestationTrustSource`] interface to implement your own source of attestation root certificates and set it as the -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/RelyingParty.RelyingPartyBuilder.html#attestationTrustSource(com.yubico.webauthn.attestation.AttestationTrustSource)[`attestationTrustSource`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.5.0/com/yubico/webauthn/RelyingParty.RelyingPartyBuilder.html#attestationTrustSource(com.yubico.webauthn.attestation.AttestationTrustSource)[`attestationTrustSource`] for your -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/RelyingParty.html[`RelyingParty`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.5.0/com/yubico/webauthn/RelyingParty.html[`RelyingParty`] instance. Note that depending on your JCA provider configuration, you may need to set the -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/attestation/AttestationTrustSource.TrustRootsResult.TrustRootsResultBuilder.html#enableRevocationChecking(boolean)[`enableRevocationChecking`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.5.0/com/yubico/webauthn/attestation/AttestationTrustSource.TrustRootsResult.TrustRootsResultBuilder.html#enableRevocationChecking(boolean)[`enableRevocationChecking`] and/or -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/attestation/AttestationTrustSource.TrustRootsResult.TrustRootsResultBuilder.html#policyTreeValidator(java.util.function.Predicate)[`policyTreeValidator`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.5.0/com/yubico/webauthn/attestation/AttestationTrustSource.TrustRootsResult.TrustRootsResultBuilder.html#policyTreeValidator(java.util.function.Predicate)[`policyTreeValidator`] settings for compatibility with some authenticators' attestation certificates. See the JavaDoc for these settings for more information. @@ -689,6 +822,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 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 { diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index e6e5dc5e5..8e2ebbf9b 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.13.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.19.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/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) + } } } 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") } diff --git a/buildSrc/src/main/kotlin/project-convention-java.gradle.kts b/buildSrc/src/main/kotlin/project-convention-java.gradle.kts index 100a52a62..bf19301ad 100644 --- a/buildSrc/src/main/kotlin/project-convention-java.gradle.kts +++ b/buildSrc/src/main/kotlin/project-convention-java.gradle.kts @@ -2,22 +2,15 @@ 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") options.encoding = "UTF-8" - options.release.set(8) -} -tasks.withType(Test::class) { - javaLauncher.set(javaToolchains.launcherFor { - languageVersion.set(JavaLanguageVersion.of(8)) - }) + if (JavaVersion.current().isJava9Compatible) { + options.release.set(8) + } else { + targetCompatibility = "1.8" + sourceCompatibility = "1.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)) - }) } 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 --- 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. 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)") diff --git a/webauthn-server-attestation/README.adoc b/webauthn-server-attestation/README.adoc index 11eb29a4c..bd6df8ed4 100644 --- a/webauthn-server-attestation/README.adoc +++ b/webauthn-server-attestation/README.adoc @@ -21,7 +21,7 @@ This module does four things: - Re-download the metadata BLOB when out of date or invalid. - Provide utilities for selecting trusted metadata entries and authenticators. - Integrate with the - link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/RelyingParty.html[`RelyingParty`] + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.5.0/com/yubico/webauthn/RelyingParty.html[`RelyingParty`] class in the base library, to provide trust root certificates for verifying attestation statements during credential registrations. @@ -30,18 +30,18 @@ Notable *non-features* include: - *Scheduled BLOB downloads.* + The -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.4.1/com/yubico/fido/metadata/FidoMetadataDownloader.html[`FidoMetadataDownloader`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.5.0/com/yubico/fido/metadata/FidoMetadataDownloader.html[`FidoMetadataDownloader`] class will attempt to download a new BLOB only when its -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.4.1/com/yubico/fido/metadata/FidoMetadataDownloader.html#loadCachedBlob()[`loadCachedBlob()`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.5.0/com/yubico/fido/metadata/FidoMetadataDownloader.html#loadCachedBlob()[`loadCachedBlob()`] or -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.4.1/com/yubico/fido/metadata/FidoMetadataDownloader.html#refreshBlob()[`refreshBlob()`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.5.0/com/yubico/fido/metadata/FidoMetadataDownloader.html#refreshBlob()[`refreshBlob()`] method is executed. As the names suggest, `loadCachedBlob()` downloads a new BLOB only if the cache is empty or the cached BLOB is invalid or out of date, while `refreshBlob()` always downloads a new BLOB and falls back to the cached BLOB only when the new BLOB is invalid in some way. -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.4.1/com/yubico/fido/metadata/FidoMetadataService.html[`FidoMetadataService`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.5.0/com/yubico/fido/metadata/FidoMetadataService.html[`FidoMetadataService`] will never re-download a new BLOB once instantiated. + You should use some external scheduling mechanism to re-run `loadCachedBlob()` @@ -54,12 +54,12 @@ classes keep no internal mutable state. + The FIDO Metadata Service may from time to time report security issues with particular authenticator models. The -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.4.1/com/yubico/fido/metadata/FidoMetadataService.html[`FidoMetadataService`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.5.0/com/yubico/fido/metadata/FidoMetadataService.html[`FidoMetadataService`] class can be configured with a filter for which authenticators to trust, and untrusted authenticators can be rejected during registration by setting -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/RelyingParty.RelyingPartyBuilder.html#allowUntrustedAttestation(boolean)[`.allowUntrustedAttestation(false)`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.5.0/com/yubico/webauthn/RelyingParty.RelyingPartyBuilder.html#allowUntrustedAttestation(boolean)[`.allowUntrustedAttestation(false)`] on -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/RelyingParty.html[`RelyingParty`], +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.5.0/com/yubico/webauthn/RelyingParty.html[`RelyingParty`], but this will not affect any credentials already registered. @@ -94,7 +94,7 @@ Maven: com.yubico webauthn-server-attestation - 2.4.1 + 2.5.0 compile ---------- @@ -102,7 +102,7 @@ Maven: Gradle: ---------- -compile 'com.yubico:webauthn-server-attestation:2.4.1' +implementation("com.yubico:webauthn-server-attestation:2.5.0") ---------- @@ -111,7 +111,7 @@ compile 'com.yubico:webauthn-server-attestation:2.4.1' This library uses link:https://semver.org/[semantic versioning]. The public API consists of all public classes, methods and fields in the `com.yubico.fido.metadata` package and its subpackages, i.e., everything covered by the -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.4.1/com/yubico/fido/metadata/package-summary.html[Javadoc]. +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.5.0/com/yubico/fido/metadata/package-summary.html[Javadoc]. Package-private classes and methods are NOT part of the public API. The `com.yubico:yubico-util` module is NOT part of the public API. @@ -123,23 +123,23 @@ Breaking changes to these will NOT be reflected in version numbers. Using this module consists of 5 major steps: 1. Create a - link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.4.1/com/yubico/fido/metadata/FidoMetadataDownloader.html[`FidoMetadataDownloader`] + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.5.0/com/yubico/fido/metadata/FidoMetadataDownloader.html[`FidoMetadataDownloader`] instance to download and cache metadata BLOBs, and a - link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.4.1/com/yubico/fido/metadata/FidoMetadataService.html[`FidoMetadataService`] + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.5.0/com/yubico/fido/metadata/FidoMetadataService.html[`FidoMetadataService`] instance to make use of the downloaded BLOB. See the JavaDoc for these classes for details on how to construct them. + [WARNING] ===== Unlike other classes in this module and the core library, -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.4.1/com/yubico/fido/metadata/FidoMetadataDownloader.html[`FidoMetadataDownloader`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.5.0/com/yubico/fido/metadata/FidoMetadataDownloader.html[`FidoMetadataDownloader`] is NOT THREAD SAFE since its -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.4.1/com/yubico/fido/metadata/FidoMetadataDownloader.html#loadCachedBlob()[`loadCachedBlob()`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.5.0/com/yubico/fido/metadata/FidoMetadataDownloader.html#loadCachedBlob()[`loadCachedBlob()`] and -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.4.1/com/yubico/fido/metadata/FidoMetadataDownloader.html#refreshBlob()[`refreshBlob()`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.5.0/com/yubico/fido/metadata/FidoMetadataDownloader.html#refreshBlob()[`refreshBlob()`] methods read and write caches. -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.4.1/com/yubico/fido/metadata/FidoMetadataService.html[`FidoMetadataService`], +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.5.0/com/yubico/fido/metadata/FidoMetadataService.html[`FidoMetadataService`], on the other hand, is thread safe, and `FidoMetadataDownloader` instances can be reused for subsequent `loadCachedBlob()` and `refreshBlob()` calls @@ -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() @@ -162,18 +164,18 @@ FidoMetadataService mds = FidoMetadataService.builder() ---------- 2. Set the - link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.4.1/com/yubico/fido/metadata/FidoMetadataService.html[`FidoMetadataService`] + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.5.0/com/yubico/fido/metadata/FidoMetadataService.html[`FidoMetadataService`] as the - link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/RelyingParty.RelyingPartyBuilder.html#attestationTrustSource(com.yubico.webauthn.attestation.AttestationTrustSource)[`attestationTrustSource`] + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.5.0/com/yubico/webauthn/RelyingParty.RelyingPartyBuilder.html#attestationTrustSource(com.yubico.webauthn.attestation.AttestationTrustSource)[`attestationTrustSource`] on your - link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/RelyingParty.html[`RelyingParty`] + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.5.0/com/yubico/webauthn/RelyingParty.html[`RelyingParty`] instance, and set - link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/RelyingParty.RelyingPartyBuilder.html#attestationConveyancePreference(com.yubico.webauthn.data.AttestationConveyancePreference)[`attestationConveyancePreference(AttestationConveyancePreference.DIRECT)`] + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.5.0/com/yubico/webauthn/RelyingParty.RelyingPartyBuilder.html#attestationConveyancePreference(com.yubico.webauthn.data.AttestationConveyancePreference)[`attestationConveyancePreference(AttestationConveyancePreference.DIRECT)`] on `RelyingParty` to request an attestation statement for new registrations. Optionally also set - link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/RelyingParty.RelyingPartyBuilder.html#allowUntrustedAttestation(boolean)[`.allowUntrustedAttestation(false)`] + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.5.0/com/yubico/webauthn/RelyingParty.RelyingPartyBuilder.html#allowUntrustedAttestation(boolean)[`.allowUntrustedAttestation(false)`] on `RelyingParty` to require trusted attestation for new registrations. + [source,java] @@ -188,9 +190,9 @@ RelyingParty rp = RelyingParty.builder() ---------- 3. After performing registrations, inspect the - link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/RegistrationResult.html#isAttestationTrusted()[`isAttestationTrusted()`] + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.5.0/com/yubico/webauthn/RegistrationResult.html#isAttestationTrusted()[`isAttestationTrusted()`] result in - link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/RegistrationResult.html[`RegistrationResult`] + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.5.0/com/yubico/webauthn/RegistrationResult.html[`RegistrationResult`] to determine whether the authenticator presented an attestation statement that could be verified by any of the trusted attestation certificates in the FIDO Metadata Service. + @@ -207,7 +209,7 @@ if (result.isAttestationTrusted()) { ---------- 4. If needed, use the `findEntries` methods of - link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.4.1/com/yubico/fido/metadata/FidoMetadataService.html[`FidoMetadataService`] + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.5.0/com/yubico/fido/metadata/FidoMetadataService.html[`FidoMetadataService`] to retrieve additional authenticator metadata for new registrations. + [source,java] @@ -233,19 +235,19 @@ This step may not be necessary if you use a different provider for the `PKIX` ce == Selecting trusted authenticators The -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.4.1/com/yubico/fido/metadata/FidoMetadataService.html[`FidoMetadataService`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.5.0/com/yubico/fido/metadata/FidoMetadataService.html[`FidoMetadataService`] class can be configured with filters for which authenticators to trust. When the `FidoMetadataService` is used as the -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/RelyingParty.RelyingPartyBuilder.html#attestationTrustSource(com.yubico.webauthn.attestation.AttestationTrustSource)[`attestationTrustSource`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.5.0/com/yubico/webauthn/RelyingParty.RelyingPartyBuilder.html#attestationTrustSource(com.yubico.webauthn.attestation.AttestationTrustSource)[`attestationTrustSource`] in -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/RelyingParty.html[`RelyingParty`], +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.5.0/com/yubico/webauthn/RelyingParty.html[`RelyingParty`], this will be reflected in the -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/RegistrationResult.html#isAttestationTrusted()[`.isAttestationTrusted()`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.5.0/com/yubico/webauthn/RegistrationResult.html#isAttestationTrusted()[`.isAttestationTrusted()`] result in -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/RegistrationResult.html[`RegistrationResult`]. +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.5.0/com/yubico/webauthn/RegistrationResult.html[`RegistrationResult`]. Any authenticators not trusted will also be rejected for new registrations if you set -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/RelyingParty.RelyingPartyBuilder.html#allowUntrustedAttestation(boolean)[`.allowUntrustedAttestation(false)`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.5.0/com/yubico/webauthn/RelyingParty.RelyingPartyBuilder.html#allowUntrustedAttestation(boolean)[`.allowUntrustedAttestation(false)`] on `RelyingParty`. The filter has two stages: a "prefilter" which selects metadata entries to include in the data source, @@ -308,17 +310,17 @@ entry, and the default registration-time filter excludes any authenticator with a matching `ATTESTATION_KEY_COMPROMISE` status report entry. To customize the filters, configure the -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.4.1/com/yubico/fido/metadata/FidoMetadataService.FidoMetadataServiceBuilder.html#prefilter(java.util.function.Predicate)[`.prefilter(Predicate)`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.5.0/com/yubico/fido/metadata/FidoMetadataService.FidoMetadataServiceBuilder.html#prefilter(java.util.function.Predicate)[`.prefilter(Predicate)`] and -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.4.1/com/yubico/fido/metadata/FidoMetadataService.FidoMetadataServiceBuilder.html#filter(java.util.function.Predicate)[`.filter(Predicate)`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.5.0/com/yubico/fido/metadata/FidoMetadataService.FidoMetadataServiceBuilder.html#filter(java.util.function.Predicate)[`.filter(Predicate)`] settings in -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.4.1/com/yubico/fido/metadata/FidoMetadataService.html[`FidoMetadataService`]. +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.5.0/com/yubico/fido/metadata/FidoMetadataService.html[`FidoMetadataService`]. The filters are predicate functions; each metadata entry will be included in the data source if and only if the prefilter predicate returns `true` for that entry. Similarly during registration or metadata lookup, the authenticator will be matched with each metadata entry only if the registration-time filter returns `true` for that pair of authenticator and metadata entry. You can also use the -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.4.1/com/yubico/fido/metadata/FidoMetadataService.Filters.html#allOf(java.util.function.Predicate\...)[`FidoMetadataService.Filters.allOf()`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.5.0/com/yubico/fido/metadata/FidoMetadataService.Filters.html#allOf(java.util.function.Predicate\...)[`FidoMetadataService.Filters.allOf()`] combinator to merge several predicates into one. [NOTE] @@ -328,10 +330,10 @@ This is true for both the prefilter and the registration-time filter. If you want to maintain the default filter in addition to the new behaviour, you must include the default condition in the new filter. For example, you can use -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.4.1/com/yubico/fido/metadata/FidoMetadataService.Filters.html#allOf(java.util.function.Predicate\...)[`FidoMetadataService.Filters.allOf()`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.5.0/com/yubico/fido/metadata/FidoMetadataService.Filters.html#allOf(java.util.function.Predicate\...)[`FidoMetadataService.Filters.allOf()`] to combine a predefined filter with a custom one. The default filters are available via static functions in -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.4.1/com/yubico/fido/metadata/FidoMetadataService.Filters.html[`FidoMetadataService.Filters`]. +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.5.0/com/yubico/fido/metadata/FidoMetadataService.Filters.html[`FidoMetadataService.Filters`]. ===== @@ -352,9 +354,9 @@ This is why any enforceable attestation policy must disallow unknown trust roots Note that unknown and untrusted attestation is allowed by default, but can be disallowed by explicitly configuring -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/RelyingParty.html[`RelyingParty`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.5.0/com/yubico/webauthn/RelyingParty.html[`RelyingParty`] with -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/RelyingParty.RelyingPartyBuilder.html#allowUntrustedAttestation(boolean)[`.allowUntrustedAttestation(false)`]. +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.5.0/com/yubico/webauthn/RelyingParty.RelyingPartyBuilder.html#allowUntrustedAttestation(boolean)[`.allowUntrustedAttestation(false)`]. == Alignment with FIDO MDS spec @@ -364,17 +366,17 @@ link:https://fidoalliance.org/specs/mds/fido-metadata-service-v3.0-ps-20210518.h The library implements these as closely as possible, but with some slight departures from the spec: * Processing rules steps 1-7 are implemented as specified, by the - link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.4.1/com/yubico/fido/metadata/FidoMetadataDownloader.html[`FidoMetadataDownloader`] + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.5.0/com/yubico/fido/metadata/FidoMetadataDownloader.html[`FidoMetadataDownloader`] class. All "SHOULD" clauses are also respected, with some caveats: ** Step 3 states "The `nextUpdate` field of the Metadata BLOB specifies a date when the download SHOULD occur at latest". `FidoMetadataDownloader` does not automatically re-download the BLOB. Instead, each time the - link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.4.1/com/yubico/fido/metadata/FidoMetadataDownloader.html#loadCachedBlob()[`loadCachedBlob()`] + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.5.0/com/yubico/fido/metadata/FidoMetadataDownloader.html#loadCachedBlob()[`loadCachedBlob()`] method is executed it checks whether a new BLOB should be downloaded. The - link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.4.1/com/yubico/fido/metadata/FidoMetadataDownloader.html#refreshBlob()[`refreshBlob()`] + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.5.0/com/yubico/fido/metadata/FidoMetadataDownloader.html#refreshBlob()[`refreshBlob()`] method always attempts to download a new BLOB when executed, but also does not trigger re-downloads automatically. + @@ -386,7 +388,7 @@ until the next execution of `.loadCachedBlob()` or `.refreshBlob()`. * Metadata entries are not stored or cached individually, instead the BLOB is cached as a whole. In processing rules step 8, neither `FidoMetadataDownloader` nor - link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.4.1/com/yubico/fido/metadata/FidoMetadataService.html[`FidoMetadataService`] + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.5.0/com/yubico/fido/metadata/FidoMetadataService.html[`FidoMetadataService`] performs any comparison between versions of a metadata entry. Policy for ignoring metadata entries can be configured via the filter settings in `FidoMetadataService`. See above for details. @@ -398,7 +400,7 @@ There are also some other requirements throughout the spec, which may not be obv states that "The Relying party MUST reject the Metadata Statement if the `authenticatorVersion` has not increased" in an `UPDATE_AVAILABLE` status report. Thus, - link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.4.1/com/yubico/fido/metadata/FidoMetadataService.html[`FidoMetadataService`] + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.5.0/com/yubico/fido/metadata/FidoMetadataService.html[`FidoMetadataService`] silently ignores any `MetadataBLOBPayloadEntry` whose `metadataStatement.authenticatorVersion` is present and not greater than or equal to the `authenticatorVersion` in the respective status report. @@ -408,16 +410,16 @@ There are also some other requirements throughout the spec, which may not be obv link:https://fidoalliance.org/specs/mds/fido-metadata-service-v3.0-ps-20210518.html#info-statuses[AuthenticatorStatus section] states that "FIDO Servers MUST silently ignore all unknown AuthenticatorStatus values". Thus any unknown status values will be parsed as - link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.4.1/com/yubico/fido/metadata/AuthenticatorStatus.html#UNKNOWN[`AuthenticatorStatus.UNKNOWN`], + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.5.0/com/yubico/fido/metadata/AuthenticatorStatus.html#UNKNOWN[`AuthenticatorStatus.UNKNOWN`], and - link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.4.1/com/yubico/fido/metadata/MetadataBLOBPayloadEntry.html[`MetadataBLOBPayloadEntry`] + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.5.0/com/yubico/fido/metadata/MetadataBLOBPayloadEntry.html[`MetadataBLOBPayloadEntry`] will silently ignore any status report with that status. == Overriding certificate path validation The -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.4.1/com/yubico/fido/metadata/FidoMetadataDownloader.html[`FidoMetadataDownloader`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.5.0/com/yubico/fido/metadata/FidoMetadataDownloader.html[`FidoMetadataDownloader`] class uses `CertPathValidator.getInstance("PKIX")` to retrieve a `CertPathValidator` instance. If you need to override any aspect of certificate path validation, such as CRL retrieval or OCSP, you may provide a custom `CertPathValidator` provider for the `"PKIX"` algorithm. 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-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..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; + } } /** @@ -677,9 +701,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 +804,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 +835,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,12 +973,18 @@ 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( + parseAndMaybeVerifyBlob( new ByteArray(blobJwt.getBytes(StandardCharsets.UTF_8)), trustRootCertificate)); } else { @@ -960,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; @@ -1008,30 +1059,45 @@ private ByteArray download(URL url) throws IOException { } private MetadataBLOB parseAndVerifyBlob(ByteArray jwt, X509Certificate trustRootCertificate) - 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()); - final ByteArray signature = ByteArray.fromBase64Url(s.next()); - return verifyBlob(header, payload, signature, trustRootCertificate); + throws CertPathValidatorException, + InvalidAlgorithmParameterException, + CertificateException, + IOException, + NoSuchAlgorithmException, + SignatureException, + InvalidKeyException, + Base64UrlException, + FidoMetadataDownloaderException { + return verifyBlob(parseBlob(jwt), trustRootCertificate); } - private MetadataBLOB verifyBlob( - ByteArray jwtHeader, - ByteArray jwtPayload, - ByteArray jwtSignature, - X509Certificate trustRootCertificate) - throws IOException, CertificateException, NoSuchAlgorithmException, InvalidKeyException, - SignatureException, CertPathValidatorException, InvalidAlgorithmParameterException, + private MetadataBLOB parseAndMaybeVerifyBlob(ByteArray jwt, X509Certificate trustRootCertificate) + throws CertPathValidatorException, + InvalidAlgorithmParameterException, + CertificateException, + IOException, + NoSuchAlgorithmException, + SignatureException, + InvalidKeyException, + Base64UrlException, 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); + if (verifyDownloadsOnly) { + return parseBlob(jwt).blob; + } else { + return verifyBlob(parseBlob(jwt), trustRootCertificate); + } + } + + private MetadataBLOB verifyBlob(ParseResult parseResult, X509Certificate trustRootCertificate) + throws IOException, + CertificateException, + NoSuchAlgorithmException, + InvalidKeyException, + SignatureException, + CertPathValidatorException, + InvalidAlgorithmParameterException, + FidoMetadataDownloaderException { + final MetadataBLOBHeader header = parseResult.blob.getHeader(); final List certChain; if (header.getX5u().isPresent()) { @@ -1079,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); } @@ -1096,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 { @@ -1120,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/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-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.") { diff --git a/webauthn-server-core/build.gradle.kts b/webauthn-server-core/build.gradle.kts index 69cf488b8..79ed2a335 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") @@ -55,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/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..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,8 +59,11 @@ 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). Identification of the user is therefore deferred until the response + * is received. + * + * @see Passkey in passkeys.dev reference */ private final String username; @@ -74,8 +77,11 @@ 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). Identification of the user is therefore deferred until the response + * is received. + * + * @see Passkey in passkeys.dev reference */ private final ByteArray userHandle; @@ -105,8 +111,11 @@ 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). Identification of the user is therefore deferred until the response + * is received. + * + * @see Passkey in passkeys.dev reference */ public Optional getUsername() { return Optional.ofNullable(username); @@ -121,8 +130,11 @@ 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). Identification of the user is therefore deferred until the response + * is received. + * + * @see Passkey in passkeys.dev reference */ public Optional getUserHandle() { return Optional.ofNullable(userHandle); @@ -215,8 +227,11 @@ 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). Identification of the user is therefore deferred until the response + * is received. + * + * @see Passkey in passkeys.dev reference */ public AssertionRequestBuilder username(@NonNull Optional username) { return this.username(username.orElse(null)); @@ -230,8 +245,11 @@ 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). Identification of the user is therefore deferred until the response + * is received. + * + * @see Passkey in passkeys.dev reference */ public AssertionRequestBuilder username(String username) { this.username = username; @@ -250,8 +268,11 @@ 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). Identification of the user is therefore deferred until the response + * is received. + * + * @see Passkey in passkeys.dev reference */ public AssertionRequestBuilder userHandle(@NonNull Optional userHandle) { return this.userHandle(userHandle.orElse(null)); @@ -266,8 +287,11 @@ 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). Identification of the user is therefore deferred until the response + * is received. + * + * @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/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 !allowCredentials.isEmpty()) .ifPresent( allowed -> { assertTrue( 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/main/java/com/yubico/webauthn/RegistrationResult.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/RegistrationResult.java index 73c8d6e14..b113b3072 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 @@ -33,7 +33,10 @@ import com.yubico.webauthn.data.AttestationType; import com.yubico.webauthn.data.AuthenticatorAttachment; import com.yubico.webauthn.data.AuthenticatorAttestationResponse; +import com.yubico.webauthn.data.AuthenticatorData; +import com.yubico.webauthn.data.AuthenticatorDataFlags; import com.yubico.webauthn.data.AuthenticatorRegistrationExtensionOutputs; +import com.yubico.webauthn.data.AuthenticatorResponse; import com.yubico.webauthn.data.ByteArray; import com.yubico.webauthn.data.ClientRegistrationExtensionOutputs; import com.yubico.webauthn.data.PublicKeyCredential; @@ -125,6 +128,25 @@ private static RegistrationResult fromJson( .collect(Collectors.toList()))); } + /** + * Check whether 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 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 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 465d1a0d8..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 @@ -89,15 +89,17 @@ 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 in passkeys.dev reference */ public Optional getUsername() { return Optional.ofNullable(username); @@ -113,9 +115,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 +126,8 @@ public Optional getUsername() { * @see Client-side-discoverable * credential + * @see Passkey in passkeys.dev reference */ public Optional getUserHandle() { return Optional.ofNullable(userHandle); @@ -175,9 +179,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 +191,8 @@ public static class StartAssertionOptionsBuilder { * @see Client-side-discoverable * credential + * @see Passkey in passkeys.dev reference */ public StartAssertionOptionsBuilder username(@NonNull Optional username) { this.username = username.orElse(null); @@ -207,9 +213,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 +225,8 @@ public StartAssertionOptionsBuilder username(@NonNull Optional username) * @see Client-side-discoverable * credential + * @see Passkey in passkeys.dev reference */ public StartAssertionOptionsBuilder username(String username) { return this.username(Optional.ofNullable(username)); @@ -235,9 +243,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 +257,8 @@ public StartAssertionOptionsBuilder username(String username) { * @see Client-side-discoverable * credential + * @see Passkey in passkeys.dev reference */ public StartAssertionOptionsBuilder userHandle(@NonNull Optional userHandle) { this.userHandle = userHandle.orElse(null); @@ -269,9 +279,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 +291,8 @@ public StartAssertionOptionsBuilder userHandle(@NonNull Optional user * @see Client-side-discoverable * credential + * @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/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); 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..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 { /** @@ -82,7 +84,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/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..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 @@ -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 in passkeys.dev reference */ private final ResidentKeyRequirement residentKey; @@ -95,10 +107,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 +129,8 @@ public Optional getAuthenticatorAttachment() { * @see Client-side * discoverable Credential + * @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 db831a05c..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 @@ -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,8 @@ public static class CredentialPropertiesOutput { * @see Server-side * Credential + * @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/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; 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..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 @@ -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,15 @@ * @see Client-side * discoverable Credential + * @see Passkey in passkeys.dev reference */ @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 +62,14 @@ public enum ResidentKeyRequirement { * @see Server-side * Credential + * @see Passkey in passkeys.dev reference */ 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 +80,14 @@ public enum ResidentKeyRequirement { * @see Server-side * Credential + * @see Passkey in passkeys.dev reference */ 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 +98,8 @@ public enum ResidentKeyRequirement { * @see Server-side * Credential + * @see Passkey in passkeys.dev reference */ REQUIRED("required"); 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/RelyingPartyAssertionSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyAssertionSpec.scala index 78daeb962..07b0b7301 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[_]] + } } } @@ -2575,6 +2582,43 @@ class RelyingPartyAssertionSpec .username(user.getName) .build() + it("exposes isUserVerified() with the UV flag value in authenticator data.") { + val pkcWithoutUv = + TestAuthenticator.createAssertion( + flags = Some(new AuthenticatorDataFlags(0x00.toByte)), + challenge = + request.getPublicKeyCredentialRequestOptions.getChallenge, + credentialKey = credentialKeypair, + credentialId = credential.getId, + ) + val pkcWithUv = + TestAuthenticator.createAssertion( + flags = Some(new AuthenticatorDataFlags(0x04.toByte)), + challenge = + request.getPublicKeyCredentialRequestOptions.getChallenge, + credentialKey = credentialKeypair, + credentialId = credential.getId, + ) + + val resultWithoutUv = rp.finishAssertion( + FinishAssertionOptions + .builder() + .request(request) + .response(pkcWithoutUv) + .build() + ) + val resultWithUv = rp.finishAssertion( + FinishAssertionOptions + .builder() + .request(request) + .response(pkcWithUv) + .build() + ) + + resultWithoutUv.isUserVerified should be(false) + resultWithUv.isUserVerified should be(true) + } + it("exposes isBackupEligible() with the BE flag value in authenticator data.") { val pkcWithoutBackup = TestAuthenticator.createAssertion( diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyRegistrationSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyRegistrationSpec.scala index 2519fa756..39f20a1b2 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyRegistrationSpec.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyRegistrationSpec.scala @@ -4623,6 +4623,37 @@ class RelyingPartyRegistrationSpec .pubKeyCredParams(List(PublicKeyCredentialParameters.ES256).asJava) .build() + it("exposes isUserVerified() with the UV flag value in authenticator data.") { + val (pkcWithoutUv, _, _) = + TestAuthenticator.createUnattestedCredential( + flags = Some(new AuthenticatorDataFlags(0x00.toByte)), + challenge = request.getChallenge, + ) + val (pkcWithUv, _, _) = + TestAuthenticator.createUnattestedCredential( + flags = Some(new AuthenticatorDataFlags(0x04.toByte)), + challenge = request.getChallenge, + ) + + val resultWithoutUv = rp.finishRegistration( + FinishRegistrationOptions + .builder() + .request(request) + .response(pkcWithoutUv) + .build() + ) + val resultWithUv = rp.finishRegistration( + FinishRegistrationOptions + .builder() + .request(request) + .response(pkcWithUv) + .build() + ) + + resultWithoutUv.isUserVerified should be(false) + resultWithUv.isUserVerified should be(true) + } + it("exposes isBackupEligible() with the BE flag value in authenticator data.") { val (pkcWithoutBackup, _, _) = TestAuthenticator.createUnattestedCredential( 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( 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..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,15 @@ 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 import com.yubico.internal.util.JacksonCodecs import com.yubico.webauthn.AssertionRequest import com.yubico.webauthn.AssertionResult @@ -41,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 @@ -54,7 +60,12 @@ class JsonIoSpec with Matchers with ScalaCheckDrivenPropertyChecks { - def json: ObjectMapper = JacksonCodecs.json() + val json: ObjectMapper = + JsonMapper + .builder() + .addModule(new Jdk8Module()) + .build() + val jf: JsonNodeFactory = JsonNodeFactory.instance describe("The class") { @@ -126,52 +137,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] @@ -409,6 +374,92 @@ 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("""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") { + 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 => @@ -533,4 +584,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)) + } + } + } + } 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..6735d1b79 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 @@ -38,7 +44,7 @@ layer. This layer manages the general architecture of the system, and is where most business logic and integration code would go. The demo server implements the "persistent" storage of users and credential registrations - the -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/CredentialRepository.html[`CredentialRepository`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.5.0/com/yubico/webauthn/CredentialRepository.html[`CredentialRepository`] integration point - as the link:src/main/java/demo/webauthn/InMemoryRegistrationStorage.java[`InMemoryRegistrationStorage`] class, which simply keeps them stored in memory for a limited time. The @@ -52,7 +58,7 @@ would be specific to a particular Relying Party (RP) would go in this layer. - The server layer in turn calls the *library layer*, which is where the link:../webauthn-server-core/[`webauthn-server-core`] library gets involved. The entry point into the library is the - link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/RelyingParty.html[`RelyingParty`] + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.5.0/com/yubico/webauthn/RelyingParty.html[`RelyingParty`] class. + This layer implements the Web Authentication @@ -63,11 +69,11 @@ and exposes integration points for storage of challenges and credentials. Some notable integration points are: + ** The library user must provide an implementation of the -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/CredentialRepository.html[`CredentialRepository`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.5.0/com/yubico/webauthn/CredentialRepository.html[`CredentialRepository`] interface to use for looking up stored public keys, user handles and signature counters. ** The library user can optionally provide an instance of the -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/attestation/AttestationTrustSource.html[`AttestationTrustSource`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.5.0/com/yubico/webauthn/attestation/AttestationTrustSource.html[`AttestationTrustSource`] interface to enable identification and validation of authenticator models. This instance is then used to look up trusted attestation root certificates. The link:../webauthn-server-attestation/[`webauthn-server-attestation`] @@ -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.5.0/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/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 217c64a4c..d3fc6d4cd 100644 --- a/webauthn-server-demo/src/main/java/demo/webauthn/WebAuthnServer.java +++ b/webauthn-server-demo/src/main/java/demo/webauthn/WebAuthnServer.java @@ -32,8 +32,9 @@ 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.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; @@ -46,26 +47,24 @@ 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; 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.File; import java.io.IOException; import java.security.DigestException; import java.security.InvalidAlgorithmParameterException; @@ -80,9 +79,7 @@ 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; @@ -105,7 +102,44 @@ 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(); @@ -113,10 +147,17 @@ public class WebAuthnServer { private final RelyingParty rp; public WebAuthnServer() - throws InvalidAppIdException, CertificateException, CertPathValidatorException, - InvalidAlgorithmParameterException, Base64UrlException, DigestException, - FidoMetadataDownloaderException, UnexpectedLegalHeader, IOException, - NoSuchAlgorithmException, SignatureException, InvalidKeyException { + throws CertificateException, + CertPathValidatorException, + InvalidAlgorithmParameterException, + Base64UrlException, + DigestException, + FidoMetadataDownloaderException, + UnexpectedLegalHeader, + IOException, + NoSuchAlgorithmException, + SignatureException, + InvalidKeyException { this( new InMemoryRegistrationStorage(), newCache(), @@ -131,10 +172,17 @@ public WebAuthnServer( Cache assertRequestStorage, RelyingPartyIdentity rpIdentity, Set origins) - throws InvalidAppIdException, CertificateException, CertPathValidatorException, - InvalidAlgorithmParameterException, Base64UrlException, DigestException, - FidoMetadataDownloaderException, UnexpectedLegalHeader, IOException, - NoSuchAlgorithmException, SignatureException, InvalidKeyException { + throws CertificateException, + CertPathValidatorException, + InvalidAlgorithmParameterException, + Base64UrlException, + DigestException, + FidoMetadataDownloaderException, + UnexpectedLegalHeader, + IOException, + NoSuchAlgorithmException, + SignatureException, + InvalidKeyException { this.userStorage = userStorage; this.registerRequestStorage = registerRequestStorage; this.assertRequestStorage = assertRequestStorage; @@ -548,10 +596,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( @@ -559,7 +604,7 @@ private CredentialRegistration addRegistration( Optional nickname, RegisteredCredential credential, SortedSet transports, - Optional attestationMetadata) { + Optional attestationMetadata) { CredentialRegistration reg = CredentialRegistration.builder() .userIdentity(userIdentity) @@ -579,29 +624,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( @@ -625,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/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/img/yubico-logo.png b/webauthn-server-demo/src/main/webapp/img/yubico-logo.png deleted file mode 100644 index a77538b61..000000000 Binary files a/webauthn-server-demo/src/main/webapp/img/yubico-logo.png and /dev/null differ diff --git a/webauthn-server-demo/src/main/webapp/index.html b/webauthn-server-demo/src/main/webapp/index.html index df79e27d9..cd304f8cf 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'; } } @@ -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.imageUrl) { + if (params?.metadataStatement) { + showDeviceInfoFidoMds(params.metadataStatement); + } else { + showDeviceInfoYubico(params); + } + + 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 { @@ -489,13 +512,7 @@
- - -

Test your WebAuthn device

+

java-webauthn-server demo

@@ -512,12 +529,12 @@

Test your WebAuthn device

@@ -527,12 +544,12 @@

Test your WebAuthn device

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, ) } 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, )) 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..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,9 +3,7 @@ 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.SerializationFeature; import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.dataformat.cbor.CBORFactory; @@ -23,8 +21,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) .addModule(new Jdk8Module())