From 6af31c1cd0333f1b9575cb548f9bc085b931c868 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 4 May 2022 13:01:17 +0000 Subject: [PATCH 001/145] Bump spotless-plugin-gradle from 6.5.1 to 6.5.2 Bumps [spotless-plugin-gradle](https://github.com/diffplug/spotless) from 6.5.1 to 6.5.2. - [Release notes](https://github.com/diffplug/spotless/releases) - [Changelog](https://github.com/diffplug/spotless/blob/main/CHANGES.md) - [Commits](https://github.com/diffplug/spotless/compare/gradle/6.5.1...gradle/6.5.2) --- updated-dependencies: - dependency-name: com.diffplug.spotless:spotless-plugin-gradle dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index fd0009c32..cbd093eac 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ buildscript { } dependencies { classpath 'com.cinnober.gradle:semver-git:2.5.0' - classpath 'com.diffplug.spotless:spotless-plugin-gradle:6.5.1' + classpath 'com.diffplug.spotless:spotless-plugin-gradle:6.5.2' classpath 'io.github.cosmicsilence:gradle-scalafix:0.1.13' } } From f06676c0617c7be211531cb749d4ef445b69a771 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 10 May 2022 22:39:01 +0200 Subject: [PATCH 002/145] Make README links to project sources branch-agnostic where possible --- webauthn-server-demo/README | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/webauthn-server-demo/README b/webauthn-server-demo/README index 4808d4019..062c3656c 100644 --- a/webauthn-server-demo/README +++ b/webauthn-server-demo/README @@ -7,9 +7,9 @@ one can perform auxiliary actions such as adding an additional authenticator or deregistering a credential. The central part is the -https://github.com/Yubico/java-webauthn-server/blob/master/webauthn-server-demo/src/main/java/demo/webauthn/WebAuthnServer.java[WebAuthnServer] +link:src/main/java/demo/webauthn/WebAuthnServer.java[WebAuthnServer] class, and the -https://github.com/Yubico/java-webauthn-server/blob/master/webauthn-server-demo/src/main/java/demo/webauthn/WebAuthnRestResource.java[WebAuthnRestResource] +link:src/main/java/demo/webauthn/WebAuthnRestResource.java[WebAuthnRestResource] class which provides the REST API on top of it. @@ -24,25 +24,25 @@ $ $BROWSER https://localhost:8443/ == Architecture The example webapp is made up of three main layers, the bottom of which is the -https://github.com/Yubico/java-webauthn-server/blob/master/webauthn-server-core/[`webauthn-server-core`] +link:../webauthn-server-core/[`webauthn-server-core`] library: - The front end interacts with the server via a *REST API*, implemented in - https://github.com/Yubico/java-webauthn-server/blob/master/webauthn-server-demo/src/main/java/demo/webauthn/WebAuthnRestResource.java[WebAuthnRestResource]. + link:src/main/java/demo/webauthn/WebAuthnRestResource.java[WebAuthnRestResource]. + This layer manages translation between JSON request/response payloads and domain objects, and most methods simply call into analogous methods in the server layer. - The REST API then delegates to the *server layer*, implemented in - https://github.com/Yubico/java-webauthn-server/blob/master/webauthn-server-demo/src/main/java/demo/webauthn/WebAuthnServer.java[WebAuthnServer]. + link:src/main/java/demo/webauthn/WebAuthnServer.java[WebAuthnServer]. + This layer manages the general architecture of the system, and is where most business logic and integration code would go. The demo server implements the "persistent" storage of users and credential registrations - the -https://github.com/Yubico/java-webauthn-server/blob/master/webauthn-server-core/src/main/java/com/yubico/webauthn/CredentialRepository.java[CredentialRepository] +link:../webauthn-server-core/src/main/java/com/yubico/webauthn/CredentialRepository.java[CredentialRepository] integration point - as the -https://github.com/Yubico/java-webauthn-server/blob/master/webauthn-server-demo/src/main/java/demo/webauthn/InMemoryRegistrationStorage.java[InMemoryRegistrationStorage] +link:src/main/java/demo/webauthn/InMemoryRegistrationStorage.java[InMemoryRegistrationStorage] class, which simply keeps them stored in memory for a limited time. The transient storage of pending challenges is also kept in memory, but for a shorter duration. @@ -52,9 +52,9 @@ deregistration of credentials, is also in this layer. In general, anything that would be specific to a particular Relying Party (RP) would go in this layer. - The server layer in turn calls the *library layer*, which is where the - https://github.com/Yubico/java-webauthn-server/blob/master/webauthn-server-core/[`webauthn-server-core`] + link:../webauthn-server-core/[`webauthn-server-core`] library gets involved. The entry point into the library is the - https://github.com/Yubico/java-webauthn-server/blob/master/webauthn-server-core/src/main/java/com/yubico/webauthn/RelyingParty.java[RelyingParty] + link:../webauthn-server-core/src/main/java/com/yubico/webauthn/RelyingParty.java[RelyingParty] class. + This layer implements the Web Authentication @@ -65,14 +65,14 @@ and exposes integration points for storage of challenges and credentials. Some notable integration points are: + ** The library user must provide an implementation of the -https://github.com/Yubico/java-webauthn-server/blob/master/webauthn-server-core/src/main/java/com/yubico/webauthn/CredentialRepository.java[CredentialRepository] +link:../webauthn-server-core/src/main/java/com/yubico/webauthn/CredentialRepository.java[CredentialRepository] interface to use for looking up stored public keys, user handles and signature counters. ** The library user can optionally provide an instance of the -https://github.com/Yubico/java-webauthn-server/blob/master/webauthn-server-core/src/main/java/com/yubico/webauthn/attestation/MetadataService.java[MetadataService] +link:../webauthn-server-core/src/main/java/com/yubico/webauthn/attestation/MetadataService.java[MetadataService] interface to enable identification and validation of authenticator models. This instance is then used to look up trusted attestation root certificates. The -https://github.com/Yubico/java-webauthn-server/blob/master/webauthn-server-attestation/[`webauthn-server-attestation`] +link:../webauthn-server-attestation/[`webauthn-server-attestation`] sibling library provides implementations of this interface that are pre-seeded with Yubico device metadata. @@ -99,7 +99,7 @@ To build it, run === Standalone Java executable The standalone Java executable has the main class -https://github.com/Yubico/java-webauthn-server/blob/master/webauthn-server-demo/src/main/java/demo/webauthn/EmbeddedServer.java[`demo.webauthn.EmbeddedServer`]. +link:src/main/java/demo/webauthn/EmbeddedServer.java[`demo.webauthn.EmbeddedServer`]. This server also serves the REST API at `/api/v1/`, and static resources for the GUI under `/`. From 2b958b5e72dd27b36de8af9df0a1fdb28d211a5d Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 10 May 2022 22:39:13 +0200 Subject: [PATCH 003/145] Rename main branch from master to main --- README | 6 +++--- doc/releasing.md | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README b/README index ccb5cdb7c..d67c2aa18 100644 --- a/README +++ b/README @@ -139,7 +139,7 @@ link:webauthn-server-demo[`webauthn-server-demo`] for a complete demo server. The link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core-minimal/latest/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/master/webauthn-server-demo/src/main/java/demo/webauthn/InMemoryRegistrationStorage.java[`InMemoryRegistrationStorage`] +link:https://github.com/Yubico/java-webauthn-server/blob/main/webauthn-server-demo/src/main/java/demo/webauthn/InMemoryRegistrationStorage.java[`InMemoryRegistrationStorage`] as a simple example. === 2. Instantiate a `RelyingParty` @@ -478,14 +478,14 @@ effects, and does not directly interact with any database. This means it is database agnostic and thread safe. The following diagram illustrates an example architecture for an application using the library. -image::https://raw.githubusercontent.com/Yubico/java-webauthn-server/master/docs/img/demo-architecture.svg?sanitize=true["Example application architecture",align="center"] +image::https://raw.githubusercontent.com/Yubico/java-webauthn-server/main/docs/img/demo-architecture.svg?sanitize=true["Example application architecture",align="center"] The application manages all state and database access, and communicates with the library via POJO representations of requests and responses. The following diagram illustrates the data flow during a WebAuthn registration or authentication ceremony. -image::https://raw.githubusercontent.com/Yubico/java-webauthn-server/master/docs/img/demo-sequence-diagram.svg?sanitize=true["WebAuthn ceremony sequence diagram",align="center"] +image::https://raw.githubusercontent.com/Yubico/java-webauthn-server/main/docs/img/demo-sequence-diagram.svg?sanitize=true["WebAuthn ceremony sequence diagram",align="center"] In this diagram, the *Client* is the user's browser and the application's client-side scripts. The *Server* is the application and its business logic, the diff --git a/doc/releasing.md b/doc/releasing.md index 0394948ba..b63834abe 100644 --- a/doc/releasing.md +++ b/doc/releasing.md @@ -46,10 +46,10 @@ Release candidate versions ``` If the README still accurately reflects the latest non-pre-release version, - you can simply push to master instead: + you can simply push to main instead: ``` - $ git push origin master 1.4.0-RC1 + $ git push origin main 1.4.0-RC1 ``` 8. Make GitHub release. @@ -79,7 +79,7 @@ Release versions ``` $ git checkout 1.3.0 $ git checkout -b release-1.4.0 - $ git merge --no-ff master + $ git merge --no-ff main ``` Copy the release notes for this version from `NEWS` into the merge commit @@ -90,7 +90,7 @@ Release versions commits for examples. ``` - $ git checkout master + $ git checkout main $ git merge --ff-only release-1.4.0 $ git branch -d release-1.4.0 ``` @@ -135,7 +135,7 @@ Release versions 11. Push to GitHub: ``` - $ git push origin master 1.4.0 + $ git push origin main 1.4.0 ``` 12. Make GitHub release. From 0664baa48e384d4cab75a989d2d47c0750c74f32 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 10 May 2022 22:05:02 +0200 Subject: [PATCH 004/145] Add binary reproducibility badge to README --- .github/workflows/release-verify-signatures.yml | 2 +- README | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release-verify-signatures.yml b/.github/workflows/release-verify-signatures.yml index fe85b844f..2d11ac54d 100644 --- a/.github/workflows/release-verify-signatures.yml +++ b/.github/workflows/release-verify-signatures.yml @@ -1,4 +1,4 @@ -name: Verify release signatures +name: Reproducible binary on: release: diff --git a/README b/README index d67c2aa18..6fd303373 100644 --- a/README +++ b/README @@ -6,6 +6,7 @@ java-webauthn-server image:https://github.com/Yubico/java-webauthn-server/workflows/build/badge.svg["Build Status", link="https://github.com/Yubico/java-webauthn-server/actions"] image:https://coveralls.io/repos/github/Yubico/java-webauthn-server/badge.svg["Coverage Status", link="https://coveralls.io/github/Yubico/java-webauthn-server"] +image:https://github.com/Yubico/java-webauthn-server/actions/workflows/release-verify-signatures.yml/badge.svg["Binary reproducibility", link="https://github.com/Yubico/java-webauthn-server/actions/workflows/release-verify-signatures.yml"] Server-side https://www.w3.org/TR/webauthn/[Web Authentication] library for Java. Provides implementations of the From 1494a163fe834011e2f304e98922c550cdb7f251 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 2 May 2022 18:11:07 +0200 Subject: [PATCH 005/145] Update and add more JavaDoc URLs --- README | 103 ++++++++++++++++-------- doc/releasing.md | 16 ++-- webauthn-server-attestation/README.adoc | 8 +- 3 files changed, 83 insertions(+), 44 deletions(-) diff --git a/README b/README index 6fd303373..4f1f08f74 100644 --- a/README +++ b/README @@ -60,7 +60,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-minimal/latest/com/yubico/webauthn/package-summary.html[Javadoc]. +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.0.0/com/yubico/webauthn/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. @@ -71,7 +71,7 @@ Breaking changes to these will NOT be reflected in version numbers. In addition to the main `webauthn-server-core` module, there is also: -- `webauthn-server-attestation`: Integration with the https://fidoalliance.org/metadata/[FIDO Metadata Service] +- link:webauthn-server-attestation[`webauthn-server-attestation`]: Integration with the https://fidoalliance.org/metadata/[FIDO Metadata Service] for retrieving and selecting trust roots to use for verifying https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#sctn-attestation[attestation statements]. @@ -102,7 +102,7 @@ but the authentication mechanism alone does not make a security system. == Documentation See the -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core-minimal/latest/com/yubico/webauthn/package-summary.html[Javadoc] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.0.0/com/yubico/webauthn/package-summary.html[Javadoc] for in-depth API documentation. @@ -116,10 +116,22 @@ See link:doc/Migrating_from_v1.adoc[the migration guide]. Using this library comes in two parts: the server side and the client side. The server side involves: - 1. Implement the `CredentialRepository` interface with your database access logic. - 2. Instantiate the `RelyingParty` class. - 3. Use the `RelyingParty.startRegistration(...)` and `RelyingParty.fininshRegistration(...)` methods to perform registration ceremonies. - 4. Use the `RelyingParty.startAssertion(...)` and `RelyingParty.fininshAssertion(...)` methods to perform authentication ceremonies. + 1. Implement the + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.0.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.0.0/com/yubico/webauthn/RelyingParty.html[`RelyingParty`] + class. + 3. Use the + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.0.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.0.0/com/yubico/webauthn/RelyingParty.html#finishRegistration(com.yubico.webauthn.FinishRegistrationOptions)[`RelyingParty.fininshRegistration(...)`] + methods to perform registration ceremonies. + 4. Use the + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.0.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.0.0/com/yubico/webauthn/RelyingParty.html#finishAssertion(com.yubico.webauthn.FinishAssertionOptions)[`RelyingParty.fininshAssertion(...)`] + methods to perform authentication ceremonies. 5. Use the outputs of `finishRegistration` and `finishAssertion` to update your database, initiate sessions, etc. The client side involves: @@ -137,18 +149,22 @@ 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-minimal/latest/com/yubico/webauthn/CredentialRepository.html[`CredentialRepository`] interface -abstracts your database in a database-agnostic way. +The +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.0.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`] as a simple example. === 2. Instantiate a `RelyingParty` -The link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core-minimal/latest/com/yubico/webauthn/RelyingParty.html[`RelyingParty`] class -is the main entry point to the library. +The +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.0.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 `CredentialRepository` implementation (called `MyCredentialRepository` here) as an argument: +passing in your +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.0.0/com/yubico/webauthn/CredentialRepository.html[`CredentialRepository`] +implementation (called `MyCredentialRepository` here) as an argument: [source,java] ---------- @@ -168,14 +184,16 @@ RelyingParty rp = RelyingParty.builder() A registration ceremony consists of 5 main steps: - 1. Generate registration parameters using `RelyingParty.startRegistration(...)`. + 1. Generate registration parameters using + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.0.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, call https://www.w3.org/TR/webauthn-2/#ref-for-dom-publickeycredential-getclientextensionresults[`cred.getClientExtensionResults()`] 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 `RelyingParty.finishRegistration(...)`. + 4. Validate the response using + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.0.0/com/yubico/webauthn/RelyingParty.html#finishRegistration(com.yubico.webauthn.FinishRegistrationOptions)[`RelyingParty.fininshRegistration(...)`]. 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. @@ -206,8 +224,12 @@ String credentialCreateJson = request.toCredentialsCreateJson(); return credentialCreateJson; // Send to client ---------- -You will need to keep this `PublicKeyCredentialCreationOptions` object in temporary storage -so you can also pass it into `finishRegistration(...)` later. +You will need to keep this +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.0.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.0.0/com/yubico/webauthn/RelyingParty.html#finishRegistration(com.yubico.webauthn.FinishRegistrationOptions)[`RelyingParty.fininshRegistration(...)`] +later. Now call the WebAuthn API on the client side: @@ -263,14 +285,16 @@ storeCredential( // Some database access method of your own design Like registration ceremonies, an authentication ceremony consists of 5 main steps: - 1. Generate authentication parameters using `RelyingParty.startAssertion(...)`. + 1. Generate authentication parameters using + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.0.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. 3. With `cred` as the result of the successfully resolved promise, call 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 `RelyingParty.finishAssertion(...)`. + 4. Validate the response using + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.0.0/com/yubico/webauthn/RelyingParty.html#finishAssertion(com.yubico.webauthn.FinishAssertionOptions)[`RelyingParty.fininshAssertion(...)`]. 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. @@ -286,8 +310,12 @@ String credentialGetJson = request.toCredentialsGetJson(); return credentialGetJson; // Send to client ---------- -Again, you will need to keep this `PublicKeyCredentialRequestOptions` object in temporary storage -so you can also pass it into `finishAssertion(...)` later. +Again, you will need to keep this +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.0.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.0.0/com/yubico/webauthn/RelyingParty.html#finishAssertion(com.yubico.webauthn.FinishAssertionOptions)[`RelyingParty.fininshAssertion(...)`] +later. Now call the WebAuthn API on the client side: @@ -326,7 +354,8 @@ try { throw new RuntimeException("Authentication failed"); ---------- -Finally, if the previous step was successful, update your database using the `AssertionResult`. +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.0.0/com/yubico/webauthn/AssertionResult.html[`AssertionResult`]. Most importantly, you should update the signature counter. That might look something like this: [source,java] @@ -389,9 +418,11 @@ AssertionRequest request = rp.startAssertion(StartAssertionOptions.builder() .build()); ---------- -Then `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 +Then +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.0.0/com/yubico/webauthn/RelyingParty.html#finishAssertion(com.yubico.webauthn.FinishAssertionOptions)[`RelyingParty.fininshAssertion(...)`] +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 link:https://www.w3.org/TR/webauthn-2/#dom-publickeycredentialcreationoptions-authenticatorselection[`authenticatorSelection`].link:https://www.w3.org/TR/webauthn-2/#dom-authenticatorselectioncriteria-userverification[`userVerification`] option set: @@ -420,13 +451,15 @@ 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-minimal/latest/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.0.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-minimal/latest/com/yubico/webauthn/RelyingParty.RelyingPartyBuilder.html#appId(com.yubico.webauthn.extension.appid.AppId)[`appId()`] - setting on your `RelyingParty` instance. + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.0.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.0.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]. + @@ -442,19 +475,23 @@ extensions and configure the `RelyingParty` to accept the given AppId when verif for more on this, see the link:https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#sctn-user-handle-privacy[User Handle Contents] privacy consideration. - 4. When your `CredentialRepository` creates a `RegisteredCredential` for a U2F credential, + 4. When your + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.0.0/com/yubico/webauthn/CredentialRepository.html[`CredentialRepository`] + creates a + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.0.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-minimal/latest/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.0.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-minimal/latest/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.0.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-minimal/latest/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.0.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-minimal/latest/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-minimal/latest/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.0.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.0.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 diff --git a/doc/releasing.md b/doc/releasing.md index b63834abe..1aff7522a 100644 --- a/doc/releasing.md +++ b/doc/releasing.md @@ -99,20 +99,22 @@ Release versions 5. Update the version in the dependency snippets in the README. - 6. Amend these changes into the merge commit: + 6. Update the version in JavaDoc links in the READMEs. + + 7. Amend these changes into the merge commit: ``` $ git add NEWS $ 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" @@ -120,25 +122,25 @@ 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. Wait for the artifacts to become downloadable at +11. Wait for the artifacts to become downloadable at https://repo1.maven.org/maven2/com/yubico/webauthn-server-core/ . This is needed for one of the GitHub Actions release workflows and usually takes less than 30 minutes (long before the artifacts become searchable on the main Maven Central website). -11. Push to GitHub: +12. Push to GitHub: ``` $ git push origin main 1.4.0 ``` -12. Make GitHub release. +13. Make GitHub release. - Use the new tag as the release tag - Copy the release notes from `NEWS` into the GitHub release notes; reformat diff --git a/webauthn-server-attestation/README.adoc b/webauthn-server-attestation/README.adoc index 5eee6ddc0..35b3e7c86 100644 --- a/webauthn-server-attestation/README.adoc +++ b/webauthn-server-attestation/README.adoc @@ -19,7 +19,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-minimal/latest/com/yubico/webauthn/RelyingParty.html[`RelyingParty`] + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/latest/com/yubico/webauthn/RelyingParty.html[`RelyingParty`] class in the base library, to provide trust root certificates for verifying attestation statements during credential registrations. @@ -106,7 +106,7 @@ FidoMetadataService mds = FidoMetadataService.builder() ---------- 2. Set the `FidoMetadataService` as the `attestationTrustSource` on your - link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core-minimal/latest/com/yubico/webauthn/RelyingParty.html[`RelyingParty`] + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/latest/com/yubico/webauthn/RelyingParty.html[`RelyingParty`] instance, and set `attestationConveyancePreference(AttestationConveyancePreference.DIRECT)` on `RelyingParty` to request an attestation statement for new registrations. @@ -124,7 +124,7 @@ RelyingParty rp = RelyingParty.builder() ---------- 3. After performing registrations, inspect the `isAttestationTrusted()` result in - link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core-minimal/latest/com/yubico/webauthn/RelyingParty.html[`RelyingParty`] + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/latest/com/yubico/webauthn/RelyingParty.html[`RelyingParty`] 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. + @@ -164,7 +164,7 @@ The class can be configured with filters for which authenticators to trust. When the `FidoMetadataService` is used as the `.attestationTrustSource()` in `RelyingParty`, this will be reflected in the `.isAttestationTrusted()` result in -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core-minimal/latest/com/yubico/webauthn/RegistrationResult.html[`RegistrationResult`]. +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/latest/com/yubico/webauthn/RegistrationResult.html[`RegistrationResult`]. Any authenticators not trusted will also be rejected for new registrations if you set `.allowUntrustedAttestation(false)` on `RelyingParty`. The filter has two stages: a "prefilter" which selects metadata entries to include in the data source, From e5d6626862739f8f0ddacad49e1f7726d807bbeb Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 2 May 2022 18:25:20 +0200 Subject: [PATCH 006/145] Add JavaDoc URLs to webauthn-server-attestation README --- webauthn-server-attestation/README.adoc | 130 +++++++++++++++++------- 1 file changed, 94 insertions(+), 36 deletions(-) diff --git a/webauthn-server-attestation/README.adoc b/webauthn-server-attestation/README.adoc index 35b3e7c86..fdd8f88b6 100644 --- a/webauthn-server-attestation/README.adoc +++ b/webauthn-server-attestation/README.adoc @@ -19,7 +19,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/latest/com/yubico/webauthn/RelyingParty.html[`RelyingParty`] + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.0.0/com/yubico/webauthn/RelyingParty.html[`RelyingParty`] class in the base library, to provide trust root certificates for verifying attestation statements during credential registrations. @@ -27,10 +27,13 @@ Notable *non-features* include: - *Scheduled BLOB downloads.* + -The `FidoMetadataDownloader` -class will attempt to download a new BLOB only when its `loadCachedBlob()` is executed, +The +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.0.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.0.0/com/yubico/fido/metadata/FidoMetadataDownloader.html#loadCachedBlob()[`loadCachedBlob()`] +is executed, and then only if the cache is empty or if the cached BLOB is invalid or out of date. -`FidoMetadataService` +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.0.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()` periodically @@ -41,8 +44,13 @@ classes keep no internal mutable state. - *Revocation of already-registered credentials* + The FIDO Metadata Service may from time to time report security issues with particular authenticator models. -The `FidoMetadataService` class can be configured with a filter for which authenticators to trust, -and untrusted authenticators can be rejected during registration by setting `.allowUntrustedAttestation(false)` on `RelyingParty`, +The +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.0.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.0.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.0.0/com/yubico/webauthn/RelyingParty.html[`RelyingParty`], but this will not affect any credentials already registered. @@ -74,18 +82,22 @@ See link:doc/Migrating_from_v1.adoc[the migration guide]. Using this module consists of 4 major steps: 1. Create a - `FidoMetadataDownloader` + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.0.0/com/yubico/fido/metadata/FidoMetadataDownloader.html[`FidoMetadataDownloader`] instance to download and cache metadata BLOBs, and a - `FidoMetadataService` + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.0.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, -`FidoMetadataDownloader` is NOT THREAD SAFE since its `loadCachedBlob()` method reads and writes caches. -`FidoMetadataService`, on the other hand, is thread safe, +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.0.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.0.0/com/yubico/fido/metadata/FidoMetadataDownloader.html#loadCachedBlob()[`loadCachedBlob()`] +method reads and writes caches. +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.0.0/com/yubico/fido/metadata/FidoMetadataService.html[`FidoMetadataService`], +on the other hand, is thread safe, and `FidoMetadataDownloader` instances can be reused for subsequent `loadCachedBlob()` calls as long as only one `loadCachedBlob()` call executes at a time. ===== @@ -105,12 +117,20 @@ FidoMetadataService mds = FidoMetadataService.builder() .build(); ---------- - 2. Set the `FidoMetadataService` as the `attestationTrustSource` on your - link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/latest/com/yubico/webauthn/RelyingParty.html[`RelyingParty`] + 2. Set the + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.0.0/com/yubico/fido/metadata/FidoMetadataService.html[`FidoMetadataService`] + as the + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.0.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.0.0/com/yubico/webauthn/RelyingParty.html[`RelyingParty`] instance, - and set `attestationConveyancePreference(AttestationConveyancePreference.DIRECT)` on `RelyingParty` + and set + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.0.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 `.allowUntrustedAttestation(false)` on `RelyingParty` to require trusted attestation for new registrations. + Optionally also set + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.0.0/com/yubico/webauthn/RelyingParty.RelyingPartyBuilder.html#allowUntrustedAttestation(boolean)[`.allowUntrustedAttestation(false)`] + on `RelyingParty` to require trusted attestation for new registrations. + [source,java] ---------- @@ -123,8 +143,10 @@ RelyingParty rp = RelyingParty.builder() .build(); ---------- - 3. After performing registrations, inspect the `isAttestationTrusted()` result in - link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/latest/com/yubico/webauthn/RelyingParty.html[`RelyingParty`] + 3. After performing registrations, inspect the + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.0.0/com/yubico/webauthn/RegistrationResult.html#isAttestationTrusted()[`isAttestationTrusted()`] + result in + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.0.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. + @@ -140,7 +162,9 @@ if (result.isAttestationTrusted()) { } ---------- - 4. If needed, use the `findEntries` methods of `FidoMetadataService` to retrieve additional authenticator metadata for new registrations. + 4. If needed, use the `findEntries` methods of + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.0.0/com/yubico/fido/metadata/FidoMetadataService.html[`FidoMetadataService`] + to retrieve additional authenticator metadata for new registrations. + [source,java] ---------- @@ -150,7 +174,9 @@ RegistrationResult result = rp.finishRegistration(/* ... */); Set metadata = mds.findEntries(result); ---------- -By default, `FidoMetadataDownloader` will probably use the SUN provider for the `PKIX` certificate path validation algorithm. +By default, +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.0.0/com/yubico/fido/metadata/FidoMetadataDownloader.html[`FidoMetadataDownloader`] +will probably use the SUN provider for the `PKIX` certificate path validation algorithm. This requires the `com.sun.security.enableCRLDP` system property set to `true` in order to verify the BLOB signature. For example, this can be done on the JVM command line using a `-Dcom.sun.security.enableCRLDP=true` option. See the https://docs.oracle.com/javase/9/security/java-pki-programmers-guide.htm#JSSEC-GUID-EB250086-0AC1-4D60-AE2A-FC7461374746[Java PKI Programmers Guide] @@ -160,12 +186,20 @@ for details. == Selecting trusted authenticators The -`FidoMetadataService` +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.0.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 `.attestationTrustSource()` in `RelyingParty`, -this will be reflected in the `.isAttestationTrusted()` result in -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/latest/com/yubico/webauthn/RegistrationResult.html[`RegistrationResult`]. -Any authenticators not trusted will also be rejected for new registrations if you set `.allowUntrustedAttestation(false)` on `RelyingParty`. +When the `FidoMetadataService` is used as the +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.0.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.0.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.0.0/com/yubico/webauthn/RegistrationResult.html#isAttestationTrusted()[`.isAttestationTrusted()`] +result in +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.0.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.0.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, and a registration-time filter which decides whether to associate a metadata entry @@ -226,13 +260,19 @@ link:https://fidoalliance.org/specs/mds/fido-metadata-service-v3.0-ps-20210518.h 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 `.prefilter(Predicate)` and `.filter(Predicate)` settings -in the `FidoMetadataService` builder. +To customize the filters, configure the +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.0.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.0.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.0.0/com/yubico/fido/metadata/FidoMetadataService.html[`FidoMetadataService`]. The filters are predicate functions; each metadata entry will be trusted 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 `FidoMetadataService.Filters.allOf()` combinator to merge several predicates into one. +You can also use the +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.0.0/com/yubico/fido/metadata/FidoMetadataService.Filters.html#allOf(java.util.function.Predicate\...)[`FidoMetadataService.Filters.allOf()`] +combinator to merge several predicates into one. [NOTE] ===== @@ -240,8 +280,11 @@ Setting a custom filter will replace the default filter. 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 `FidoMetadataService.Filters.allOf()` to combine a predefined filter with a custom one. -The default filters are available via static functions in `FidoMetadataService.Filters`. +For example, you can use +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.0.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.0.0/com/yubico/fido/metadata/FidoMetadataService.Filters.html[`FidoMetadataService.Filters`]. ===== @@ -261,7 +304,10 @@ Since it will have an unknown trust root, it would then be implicitly trusted. 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 `RelyingParty` with `.allowUntrustedAttestation(false)`. +but can be disallowed by explicitly configuring +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.0.0/com/yubico/webauthn/RelyingParty.html[`RelyingParty`] +with +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.0.0/com/yubico/webauthn/RelyingParty.RelyingPartyBuilder.html#allowUntrustedAttestation(boolean)[`.allowUntrustedAttestation(false)`]. == Alignment with FIDO MDS spec @@ -270,12 +316,16 @@ The FIDO Metadata Service specification defines link:https://fidoalliance.org/specs/mds/fido-metadata-service-v3.0-ps-20210518.html#metadata-blob-object-processing-rules[processing rules for servers]. 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 `FidoMetadataDownloader` class. +* Processing rules steps 1-7 are implemented as specified, by the + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.0.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 its `.loadCachedBlob()` method is executed it checks whether a new BLOB should be downloaded. + Instead, each time its + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.0.0/com/yubico/fido/metadata/FidoMetadataDownloader.html#loadCachedBlob()[`loadCachedBlob()`] + method is executed it checks whether a new BLOB should be downloaded. + If no BLOB exists in cache, or the cached BLOB is invalid, or if the current date is greater than or equal to `nextUpdate`, then a new BLOB is downloaded. @@ -284,7 +334,8 @@ then the new BLOB replaces the cached one; otherwise, the new BLOB is discarded and the cached one is kept until the next execution of `.loadCachedBlob()`. * Metadata entries are not stored or cached individually, instead the BLOB is cached as a whole. - In processing rules step 8, neither `FidoMetadataDownloader` nor `FidoMetadataService` + In processing rules step 8, neither `FidoMetadataDownloader` nor + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.0.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. @@ -295,7 +346,9 @@ 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 "The Relying party MUST reject the Metadata Statement if the `authenticatorVersion` has not increased" in an `UPDATE_AVAILABLE` status report. - Thus, `FidoMetadataService` silently ignores any `MetadataBLOBPayloadEntry` + Thus, + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.0.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. Again, no comparison is made between metadata entries from different BLOB versions. @@ -303,12 +356,17 @@ There are also some other requirements throughout the spec, which may not be obv * The 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 valus will be parsed as `AuthenticatorStatus.UNKNOWN`, - and `MetadataBLOBPayloadEntry` will silently ignore any status report with that status. + Thus any unknown status values will be parsed as + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.0.0/com/yubico/fido/metadata/AuthenticatorStatus.html#UNKNOWN[`AuthenticatorStatus.UNKNOWN`], + and + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.0.0/com/yubico/fido/metadata/MetadataBLOBPayloadEntry.html[`MetadataBLOBPayloadEntry`] + will silently ignore any status report with that status. == Overriding certificate path validation -The `FidoMetadataDownloader` class uses `CertPathValidator.getInstance("PKIX")` to retrieve a `CertPathValidator` instance. +The +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.0.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. From d238f706069ad6b23b22021a53bcd8483175850f Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 10 May 2022 21:37:12 +0200 Subject: [PATCH 007/145] Don't run GitHub Actions workflows on tmp** branches --- .github/workflows/build.yml | 8 +++++++- .github/workflows/code-formatting.yml | 8 +++++++- .github/workflows/codeql-analysis.yml | 6 +++++- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d81b970af..2d6c0bec4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,7 +1,13 @@ # This name is shown in the status badge in the README name: build -on: [push, pull_request] +on: + push: + branches-ignore: + - 'tmp**' + pull_request: + branches-ignore: + - 'tmp**' jobs: test: diff --git a/.github/workflows/code-formatting.yml b/.github/workflows/code-formatting.yml index 9ab0f3888..3bc259afc 100644 --- a/.github/workflows/code-formatting.yml +++ b/.github/workflows/code-formatting.yml @@ -1,7 +1,13 @@ # This name is shown in the status badge in the README name: code-formatting -on: [push, pull_request] +on: + push: + branches-ignore: + - 'tmp**' + pull_request: + branches-ignore: + - 'tmp**' jobs: test: diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index ced88f0f7..f65521105 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -2,8 +2,12 @@ name: "Code scanning - action" on: push: - branches-ignore: 'dependabot/**' + branches-ignore: + - 'dependabot/**' + - 'tmp**' pull_request: + branches-ignore: + - 'tmp**' schedule: - cron: '0 12 * * 2' From e966845b6722cc783db26ca933cf637bb09f7d5f Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 10 May 2022 22:57:24 +0200 Subject: [PATCH 008/145] Fix broken link in demo README --- webauthn-server-demo/README | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webauthn-server-demo/README b/webauthn-server-demo/README index 062c3656c..88381cb97 100644 --- a/webauthn-server-demo/README +++ b/webauthn-server-demo/README @@ -69,7 +69,7 @@ link:../webauthn-server-core/src/main/java/com/yubico/webauthn/CredentialReposit 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:../webauthn-server-core/src/main/java/com/yubico/webauthn/attestation/MetadataService.java[MetadataService] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.0.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`] From 66028095b230c056d640439cc87a6452634e62a6 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 10 May 2022 22:58:10 +0200 Subject: [PATCH 009/145] Link to JavaDoc where appropriate in demo README --- webauthn-server-demo/README | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/webauthn-server-demo/README b/webauthn-server-demo/README index 88381cb97..8ca367763 100644 --- a/webauthn-server-demo/README +++ b/webauthn-server-demo/README @@ -40,7 +40,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:../webauthn-server-core/src/main/java/com/yubico/webauthn/CredentialRepository.java[CredentialRepository] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.0.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 @@ -54,7 +54,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:../webauthn-server-core/src/main/java/com/yubico/webauthn/RelyingParty.java[RelyingParty] + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.0.0/com/yubico/webauthn/RelyingParty.html[RelyingParty] class. + This layer implements the Web Authentication From e7a45781555f679a14ac3d770c8be920d0d344cb Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 10 May 2022 22:58:29 +0200 Subject: [PATCH 010/145] Use fixed-width formatting for class names in demo README --- webauthn-server-demo/README | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/webauthn-server-demo/README b/webauthn-server-demo/README index 8ca367763..b8a0813a0 100644 --- a/webauthn-server-demo/README +++ b/webauthn-server-demo/README @@ -7,9 +7,9 @@ one can perform auxiliary actions such as adding an additional authenticator or deregistering a credential. The central part is the -link:src/main/java/demo/webauthn/WebAuthnServer.java[WebAuthnServer] +link:src/main/java/demo/webauthn/WebAuthnServer.java[`WebAuthnServer`] class, and the -link:src/main/java/demo/webauthn/WebAuthnRestResource.java[WebAuthnRestResource] +link:src/main/java/demo/webauthn/WebAuthnRestResource.java[`WebAuthnRestResource`] class which provides the REST API on top of it. @@ -28,21 +28,21 @@ link:../webauthn-server-core/[`webauthn-server-core`] library: - The front end interacts with the server via a *REST API*, implemented in - link:src/main/java/demo/webauthn/WebAuthnRestResource.java[WebAuthnRestResource]. + link:src/main/java/demo/webauthn/WebAuthnRestResource.java[`WebAuthnRestResource`]. + This layer manages translation between JSON request/response payloads and domain objects, and most methods simply call into analogous methods in the server layer. - The REST API then delegates to the *server layer*, implemented in - link:src/main/java/demo/webauthn/WebAuthnServer.java[WebAuthnServer]. + link:src/main/java/demo/webauthn/WebAuthnServer.java[`WebAuthnServer`]. + This layer manages the general architecture of the system, and is where most business logic and integration code would go. The demo server implements the "persistent" storage of users and credential registrations - the -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.0.0/com/yubico/webauthn/CredentialRepository.html[CredentialRepository] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.0.0/com/yubico/webauthn/CredentialRepository.html[`CredentialRepository`] integration point - as the -link:src/main/java/demo/webauthn/InMemoryRegistrationStorage.java[InMemoryRegistrationStorage] +link:src/main/java/demo/webauthn/InMemoryRegistrationStorage.java[`InMemoryRegistrationStorage`] class, which simply keeps them stored in memory for a limited time. The transient storage of pending challenges is also kept in memory, but for a shorter duration. @@ -54,7 +54,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.0.0/com/yubico/webauthn/RelyingParty.html[RelyingParty] + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.0.0/com/yubico/webauthn/RelyingParty.html[`RelyingParty`] class. + This layer implements the Web Authentication @@ -65,11 +65,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:../webauthn-server-core/src/main/java/com/yubico/webauthn/CredentialRepository.java[CredentialRepository] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.0.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.0.0/com/yubico/webauthn/attestation/AttestationTrustSource.html[AttestationTrustSource] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.0.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`] From cd7c9a4ed0134af9b1efd1c3aa86b836891d757c Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Wed, 11 May 2022 16:37:39 +0200 Subject: [PATCH 011/145] Make dependabot ignore patch version updates of spotless-plugin-gradle --- .github/dependabot.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index ce159c58f..1dfeac7ad 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -6,3 +6,8 @@ updates: directory: "/" schedule: interval: "daily" + + ignore: + # Spotless patch updates are too noisy + - dependency-name: "spotless-plugin-gradle" + update-types: ["version-update:semver-patch"] From 947d973c7e440ed4b2459b426aaf27f7c925ffaf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 May 2022 13:01:21 +0000 Subject: [PATCH 012/145] Bump spotless-plugin-gradle from 6.5.2 to 6.6.1 Bumps [spotless-plugin-gradle](https://github.com/diffplug/spotless) from 6.5.2 to 6.6.1. - [Release notes](https://github.com/diffplug/spotless/releases) - [Changelog](https://github.com/diffplug/spotless/blob/main/CHANGES.md) - [Commits](https://github.com/diffplug/spotless/compare/gradle/6.5.2...gradle/6.6.1) --- updated-dependencies: - dependency-name: com.diffplug.spotless:spotless-plugin-gradle dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index cbd093eac..cee16d713 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ buildscript { } dependencies { classpath 'com.cinnober.gradle:semver-git:2.5.0' - classpath 'com.diffplug.spotless:spotless-plugin-gradle:6.5.2' + classpath 'com.diffplug.spotless:spotless-plugin-gradle:6.6.1' classpath 'io.github.cosmicsilence:gradle-scalafix:0.1.13' } } From a77876305f56e42360a7db7abc294129142f5113 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Wed, 11 May 2022 14:20:19 +0200 Subject: [PATCH 013/145] Eliminate unnecessary RelyingPartyIdentity parameter in RelyingPartyRegistrationSpec --- .../RelyingPartyRegistrationSpec.scala | 29 +++---------------- 1 file changed, 4 insertions(+), 25 deletions(-) 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 ddae4cfa4..d1bf10d9b 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 @@ -133,17 +133,12 @@ class RelyingPartyRegistrationSpec attestationTrustSource: Option[AttestationTrustSource] = None, origins: Option[Set[String]] = None, preferredPubkeyParams: List[PublicKeyCredentialParameters] = Nil, - rp: RelyingPartyIdentity = RelyingPartyIdentity - .builder() - .id("localhost") - .name("Test party") - .build(), testData: RegistrationTestData, clock: Clock = Clock.systemUTC(), ): FinishRegistrationSteps = { var builder = RelyingParty .builder() - .identity(rp) + .identity(testData.rpId) .credentialRepository(credentialRepository) .preferredPubkeyParams(preferredPubkeyParams.asJava) .allowOriginPort(allowOriginPort) @@ -2088,7 +2083,6 @@ class RelyingPartyRegistrationSpec val steps = finishRegistration( testData = defaultTestData, allowUntrustedAttestation = false, - rp = defaultTestData.rpId, ) val step: FinishRegistrationSteps#Step19 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next @@ -2224,8 +2218,7 @@ class RelyingPartyRegistrationSpec describe("5. If successful, return implementation-specific values representing attestation type Basic and attestation trust path x5c.") { it("The real example succeeds.") { val steps = finishRegistration( - testData = testDataContainer.RealExample, - rp = testDataContainer.RealExample.rpId, + testData = testDataContainer.RealExample ) val step: FinishRegistrationSteps#Step19 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next @@ -2256,12 +2249,7 @@ class RelyingPartyRegistrationSpec it("The android-safetynet statement format is supported.") { val steps = finishRegistration( - testData = RegistrationTestData.AndroidSafetynet.RealExample, - rp = RelyingPartyIdentity - .builder() - .id("demo.yubico.com") - .name("") - .build(), + testData = RegistrationTestData.AndroidSafetynet.RealExample ) val step: FinishRegistrationSteps#Step19 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next @@ -2272,9 +2260,7 @@ class RelyingPartyRegistrationSpec it("The apple statement format is supported.") { val steps = finishRegistration( - testData = - RealExamples.AppleAttestationIos.asRegistrationTestData, - rp = RealExamples.AppleAttestationIos.rp, + testData = RealExamples.AppleAttestationIos.asRegistrationTestData ) val step: FinishRegistrationSteps#Step19 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next @@ -2352,7 +2338,6 @@ class RelyingPartyRegistrationSpec val steps = finishRegistration( testData = testData, attestationTrustSource = Some(attestationTrustSource), - rp = testData.rpId, ) val step: FinishRegistrationSteps#Step20 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next @@ -2372,7 +2357,6 @@ class RelyingPartyRegistrationSpec val steps = finishRegistration( testData = testData, attestationTrustSource = None, - rp = testData.rpId, ) val step: FinishRegistrationSteps#Step20 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next @@ -2546,7 +2530,6 @@ class RelyingPartyRegistrationSpec allowUntrustedAttestation = false, testData = testData, attestationTrustSource = Some(emptyTrustSource), - rp = testData.rpId, clock = clock, ) val step: FinishRegistrationSteps#Step21 = @@ -2562,7 +2545,6 @@ class RelyingPartyRegistrationSpec allowUntrustedAttestation = true, testData = testData, attestationTrustSource = Some(emptyTrustSource), - rp = testData.rpId, clock = clock, ) val step: FinishRegistrationSteps#Step21 = @@ -2599,7 +2581,6 @@ class RelyingPartyRegistrationSpec val steps = finishRegistration( testData = testData, attestationTrustSource = attestationTrustSource, - rp = testData.rpId, clock = clock, ) val step: FinishRegistrationSteps#Step21 = @@ -2650,7 +2631,6 @@ class RelyingPartyRegistrationSpec val steps = finishRegistration( testData = testData, attestationTrustSource = Some(attestationTrustSource), - rp = testData.rpId, clock = clock, ) val step: FinishRegistrationSteps#Step21 = @@ -2678,7 +2658,6 @@ class RelyingPartyRegistrationSpec val steps = finishRegistration( testData = testData, attestationTrustSource = Some(attestationTrustSource), - rp = testData.rpId, clock = clock, ) val step: FinishRegistrationSteps#Step21 = From 65d83c905522c457d48dec5c671f4a0eae4cf26e Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 16 May 2022 17:26:10 +0200 Subject: [PATCH 014/145] Fix outdated references to FidoMetadataDownloader.loadBlob() --- webauthn-server-attestation/doc/Migrating_from_v1.adoc | 4 ++-- .../yubico/fido/metadata/FidoMetadataDownloader.java | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/webauthn-server-attestation/doc/Migrating_from_v1.adoc b/webauthn-server-attestation/doc/Migrating_from_v1.adoc index cf0f035b7..8f5bada11 100644 --- a/webauthn-server-attestation/doc/Migrating_from_v1.adoc +++ b/webauthn-server-attestation/doc/Migrating_from_v1.adoc @@ -49,12 +49,12 @@ FidoMetadataService metadataService = FidoMetadataService.builder() .useDefaultBlob() .useBlobCacheFile(new File("fido-mds-blob-cache.bin")) .build() - .loadBlob() + .loadCachedBlob() ) .build(); ---------- -You may also need to add external logic to occasionally re-run `loadBlob()` +You may also need to add external logic to occasionally re-run `loadCachedBlob()` and reconstruct the `FidoMetadataService`, as `FidoMetadataService` will not automatically update the BLOB on its own. 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 d9261251c..d6e3a4913 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 @@ -642,12 +642,12 @@ public FidoMetadataDownloaderBuilder trustHttpsCerts(@NonNull X509Certificate... * Consumer} (see {@link FidoMetadataDownloaderBuilder.Step5}). * * - * No internal mutable state is maintained between invocations of loadBlob(); each - * invocation will reload/rewrite caches, perform downloads and check the "legalHeader" + * No internal mutable state is maintained between invocations of this method; each invocation + * will reload/rewrite caches, perform downloads and check the "legalHeader" * as necessary. You may therefore reuse a {@link FidoMetadataDownloader} instance and, - * for example, call loadBlob() periodically to refresh the BLOB when appropriate. - * Each call will return a new {@link MetadataBLOB} instance; ones already returned will not be - * updated by subsequent loadBlob() calls. + * for example, call this method periodically to refresh the BLOB when appropriate. Each call will + * return a new {@link MetadataBLOB} instance; ones already returned will not be updated by + * subsequent calls. * * @return the successfully retrieved and validated metadata BLOB. * @throws Base64UrlException if the metadata BLOB is not a well-formed JWT in compact From aef06cfdd668259cdd53c99a4793bd6ff1021c5c Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 16 May 2022 17:44:16 +0200 Subject: [PATCH 015/145] Add JavaDoc links to migration guides --- doc/Migrating_from_v1.adoc | 49 +++++++++++++------ .../doc/Migrating_from_v1.adoc | 48 +++++++++++++----- 2 files changed, 68 insertions(+), 29 deletions(-) diff --git a/doc/Migrating_from_v1.adoc b/doc/Migrating_from_v1.adoc index d4cb5e6dc..ade815996 100644 --- a/doc/Migrating_from_v1.adoc +++ b/doc/Migrating_from_v1.adoc @@ -21,7 +21,7 @@ Here is a high-level outline of what needs to be updated: - Remove uses of removed features. - Update uses of renamed and replaced features. - Replace any implementations of `MetadataService` with - `AttestationTrustSource`. + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.0.0/com/yubico/webauthn/attestation/AttestationTrustSource.html[`AttestationTrustSource`]. - Rename imports of classes in `com.yubico.fido.metadata`. - Update `getUserVerification()` and `getResidentKey()` calls to expect `Optional` values. @@ -84,7 +84,8 @@ Gradle: implementation 'org.bouncycastle:bcprov-jdk15on:1.70' ---------- -Then set up the provider. This should be done before instantiating `RelyingParty`. +Then set up the provider. This should be done before instantiating +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.0.0/com/yubico/webauthn/RelyingParty.html[`RelyingParty`]. Example: @@ -100,7 +101,10 @@ Security.addProvider(new BouncyCastleProvider()); Several fields, methods and settings have been removed: -- The `icon` field in `RelyingPartyIdentity` and `UserIdentity`, +- The `icon` field in + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.0.0/com/yubico/webauthn/data/RelyingPartyIdentity.html[`RelyingPartyIdentity`] + and + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.0.0/com/yubico/webauthn/data/UserIdentity.html[`UserIdentity`], and its associated methods. They were removed in WebAuthn Level 2 and have no replacement. + @@ -122,7 +126,8 @@ Example: .build(); ---------- -- The setting `allowUnrequestedExtensions(boolean)` in `RelyingParty`. +- The setting `allowUnrequestedExtensions(boolean)` in + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.0.0/com/yubico/webauthn/RelyingParty.html[`RelyingParty`]. + WebAuthn Level 2 now recommends that unrequested extensions should be allowed, so this setting has been removed and is now always enabled. @@ -201,9 +206,13 @@ Example: == Update uses of renamed and replaced features -- Methods `requireResidentKey(boolean)` and `isRequireResidentKey()` - in `AuthenticatorSelectionCriteria` have been replaced - by `residentKey(ResidentKeyRequirement)` and `getResidentKey()`, respectively. +- Methods `requireResidentKey(boolean)` and `isRequireResidentKey()` in + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.0.0/com/yubico/webauthn/data/AuthenticatorSelectionCriteria.html[`AuthenticatorSelectionCriteria`] + have been replaced by + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.0.0/com/yubico/webauthn/data/AuthenticatorSelectionCriteria.AuthenticatorSelectionCriteriaBuilder.html#residentKey(com.yubico.webauthn.data.ResidentKeyRequirement)[`residentKey(ResidentKeyRequirement)`] + and + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.0.0/com/yubico/webauthn/data/AuthenticatorSelectionCriteria.html#getResidentKey()[`getResidentKey()`], + respectively. + Replace `requireResidentKey(false)` with `residentKey(ResidentKeyRequirement.DISCOURAGED)`. @@ -252,16 +261,18 @@ Example: == Replace implementations of `MetadataService` -The `MetadataService` interface has been replaced with `AttestationTrustSource`. +The `MetadataService` interface has been replaced with +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.0.0/com/yubico/webauthn/attestation/AttestationTrustSource.html[`AttestationTrustSource`]. The new interface has some key differences: - `MetadataService` implementations were expected to validate the attestation certificate path. `AttestationTrustSource` implementations are not; instead they only need to retrieve the trust root certificates. - The `RelyingParty.finishRegistration` method will perform - certificate path validation internally - and report the result via `RegistrationResult.isAttestationTrusted()`. + The + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.0.0/com/yubico/webauthn/RelyingParty.html#finishRegistration(com.yubico.webauthn.FinishRegistrationOptions)[`RelyingParty.finishRegistration`] + method will perform certificate path validation internally and report the result via + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.0.0/com/yubico/webauthn/RegistrationResult.html#isAttestationTrusted()[`RegistrationResult.isAttestationTrusted()`]. The `AttestationTrustSource` may also return a `CertStore` of untrusted certificates and CRLs that may be needed for certificate path validation, @@ -274,8 +285,12 @@ The new interface has some key differences: for accessing attestation metadata, but `RelyingParty` will not integrate them in the core result types. -See the JavaDoc for `AttestationTrustSource` for details on how to implement it, -and see the `FidoMetadataService` class in the +See the +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.0.0/com/yubico/webauthn/attestation/AttestationTrustSource.html[JavaDoc +for `AttestationTrustSource`] for details on how to implement it, +and see the +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.0.0/com/yubico/fido/metadata/FidoMetadataService.html[`FidoMetadataService`] +class in the link:../webauthn-server-attestation[`webauthn-server-attestation` module] for a reference implementation. @@ -308,15 +323,17 @@ link:https://github.com/w3c/webauthn/issues/1253[turned out to cause confusion]. Therefore, browsers have started issuing console warnings when `userVerification` is not set explicitly. This library has mirrored the defaults for -`PublicKeyCredentialRequestOptions.userVerification` and -`AuthenticatorSelectionCriteria.userVerification`, +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.0.0/com/yubico/webauthn/data/PublicKeyCredentialRequestOptions.PublicKeyCredentialRequestOptionsBuilder.html#userVerification(com.yubico.webauthn.data.UserVerificationRequirement)[`PublicKeyCredentialRequestOptions.userVerification`] +and +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.0.0/com/yubico/webauthn/data/AuthenticatorSelectionCriteria.AuthenticatorSelectionCriteriaBuilder.html#userVerification(com.yubico.webauthn.data.UserVerificationRequirement)[`AuthenticatorSelectionCriteria.userVerification`], but this inadvertently suppresses any browser console warnings since the library emits parameter objects with an explicit value set, even if the value was not explicitly set at the library level. The defaults have therefore been removed, and the corresponding getters now return `Optional` values. For consistency, the same change applies to -`AuthenticatorSelectionCriteria.residentKey` as well. +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.0.0/com/yubico/webauthn/data/AuthenticatorSelectionCriteria.AuthenticatorSelectionCriteriaBuilder.html#residentKey(com.yubico.webauthn.data.ResidentKeyRequirement)[`AuthenticatorSelectionCriteria.residentKey`] +as well. The setters for these settings remain unchanged, but if you use the getters you need to expect `Optional` values instead. diff --git a/webauthn-server-attestation/doc/Migrating_from_v1.adoc b/webauthn-server-attestation/doc/Migrating_from_v1.adoc index 8f5bada11..06f54de16 100644 --- a/webauthn-server-attestation/doc/Migrating_from_v1.adoc +++ b/webauthn-server-attestation/doc/Migrating_from_v1.adoc @@ -11,18 +11,28 @@ link:https://github.com/Yubico/java-webauthn-server/issues/new[let us know!] Here is a high-level outline of what needs to be updated: - Replace uses of `StandardMetadataService` and its related classes - with `FidoMetadataService` and `FidoMetadataDownloader`. -- Update the name of the `RelyingParty` integration point - from `metadataService` to `attestationTrustSource`. -- `RegistrationResult` no longer includes attestation metadata, + with + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.0.0/com/yubico/fido/metadata/FidoMetadataService.html[`FidoMetadataService`] + and + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.0.0/com/yubico/fido/metadata/FidoMetadataDownloader.html[`FidoMetadataDownloader`]. +- Update the name of the + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.0.0/com/yubico/webauthn/RelyingParty.html[`RelyingParty`] + integration point from `metadataService` to + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.0.0/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.0.0/com/yubico/webauthn/RegistrationResult.html[`RegistrationResult`] + no longer includes attestation metadata, instead you'll need to retrieve it separately after a successful registration. -- Replace uses of the `Attestation` result type with `MetadataBLOBPayloadEntry`. +- Replace uses of the `Attestation` result type with + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.0.0/com/yubico/fido/metadata/MetadataBLOBPayloadEntry.html[`MetadataBLOBPayloadEntry`]. == Replace `StandardMetadataService` `StandardMetadataService` and its constituent classes have been removed -in favour of `FidoMetadataService` and `FidoMetadataDownloader`. +in favour of +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.0.0/com/yubico/fido/metadata/FidoMetadataService.html[`FidoMetadataService`] +and +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.0.0/com/yubico/fido/metadata/FidoMetadataDownloader.html[`FidoMetadataDownloader`]. See the link:../#getting-started[Getting started] documentation for details on how to configure and construct them. @@ -54,14 +64,18 @@ FidoMetadataService metadataService = FidoMetadataService.builder() .build(); ---------- -You may also need to add external logic to occasionally re-run `loadCachedBlob()` +You may also need to add external logic to occasionally re-run +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.0.0/com/yubico/fido/metadata/FidoMetadataDownloader.html#loadCachedBlob()[`loadCachedBlob()`] and reconstruct the `FidoMetadataService`, as `FidoMetadataService` will not automatically update the BLOB on its own. == Update `RelyingParty` integration point -`FidoMetadataService` integrates with `RelyingParty` in much the same way as `StandardMetadataService`, +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.0.0/com/yubico/fido/metadata/FidoMetadataService.html[`FidoMetadataService`] +integrates with +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.0.0/com/yubico/webauthn/RelyingParty.html[`RelyingParty`] +in much the same way as `StandardMetadataService`, although the name of the setting has changed. Example `1.x` code: @@ -93,11 +107,17 @@ Example `2.0` code: == Retrieve attestation metadata separately -In `1.x`, `RegistrationResult` could include an `Attestation` object with attestation metadata, +In `1.x`, +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.0.0/com/yubico/webauthn/RegistrationResult.html[`RegistrationResult`] +could include an `Attestation` object with attestation metadata, if a metadata service was configured and the authenticator matched anything in the metadata service. -In order to keep `RelyingParty` and the new `AttestationTrustSource` interface -decoupled from any particular format of attestation metadata, this result field has been removed. -Instead, use the `findEntries` methods of `FidoMetadataService` +In order to keep +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.0.0/com/yubico/webauthn/RelyingParty.html[`RelyingParty`] +and the new +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.0.0/com/yubico/webauthn/attestation/AttestationTrustSource.html[`AttestationTrustSource`] +interface decoupled from any particular format of attestation metadata, this result field has been removed. +Instead, use the `findEntries` methods of +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.0.0/com/yubico/fido/metadata/FidoMetadataService.html[`FidoMetadataService`] to retrieve attestation metadata after a successful registration, if needed. Example `1.x` code: @@ -128,7 +148,9 @@ Optional authenticatorName = mds.findEntries(result) This ties in with the previous step, and much of it will likely be done already. However if your front-end accesses and/or displays contents of an `Attestation` object, -it will need to be updated to work with `MetadataBLOBPayloadEntry` or similar types instead. +it will need to be updated to work with +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.0.0/com/yubico/fido/metadata/MetadataBLOBPayloadEntry.html[`MetadataBLOBPayloadEntry`] +or similar types instead. Example `1.x` code: From 49ee771e483fe565a85ecf91fc77c5234f6f18f9 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 16 May 2022 18:18:07 +0200 Subject: [PATCH 016/145] Remove fetch polyfill from demo All browsers that support WebAuthn also support the Fetch API. --- .../src/main/webapp/index.html | 1 - .../src/main/webapp/lib/fetch/LICENSE | 20 - .../src/main/webapp/lib/fetch/fetch-3.0.0.js | 516 ------------------ .../src/main/webapp/lib/fetch/package.json | 40 -- 4 files changed, 577 deletions(-) delete mode 100644 webauthn-server-demo/src/main/webapp/lib/fetch/LICENSE delete mode 100644 webauthn-server-demo/src/main/webapp/lib/fetch/fetch-3.0.0.js delete mode 100644 webauthn-server-demo/src/main/webapp/lib/fetch/package.json diff --git a/webauthn-server-demo/src/main/webapp/index.html b/webauthn-server-demo/src/main/webapp/index.html index 103e35a1d..032da8a43 100644 --- a/webauthn-server-demo/src/main/webapp/index.html +++ b/webauthn-server-demo/src/main/webapp/index.html @@ -52,7 +52,6 @@ - diff --git a/webauthn-server-demo/src/main/webapp/lib/fetch/LICENSE b/webauthn-server-demo/src/main/webapp/lib/fetch/LICENSE deleted file mode 100644 index 0e319d55d..000000000 --- a/webauthn-server-demo/src/main/webapp/lib/fetch/LICENSE +++ /dev/null @@ -1,20 +0,0 @@ -Copyright (c) 2014-2016 GitHub, Inc. - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/webauthn-server-demo/src/main/webapp/lib/fetch/fetch-3.0.0.js b/webauthn-server-demo/src/main/webapp/lib/fetch/fetch-3.0.0.js deleted file mode 100644 index 06e4d1dcb..000000000 --- a/webauthn-server-demo/src/main/webapp/lib/fetch/fetch-3.0.0.js +++ /dev/null @@ -1,516 +0,0 @@ -var support = { - searchParams: 'URLSearchParams' in self, - iterable: 'Symbol' in self && 'iterator' in Symbol, - blob: - 'FileReader' in self && - 'Blob' in self && - (function() { - try { - new Blob() - return true - } catch (e) { - return false - } - })(), - formData: 'FormData' in self, - arrayBuffer: 'ArrayBuffer' in self -} - -function isDataView(obj) { - return obj && DataView.prototype.isPrototypeOf(obj) -} - -if (support.arrayBuffer) { - var viewClasses = [ - '[object Int8Array]', - '[object Uint8Array]', - '[object Uint8ClampedArray]', - '[object Int16Array]', - '[object Uint16Array]', - '[object Int32Array]', - '[object Uint32Array]', - '[object Float32Array]', - '[object Float64Array]' - ] - - var isArrayBufferView = - ArrayBuffer.isView || - function(obj) { - return obj && viewClasses.indexOf(Object.prototype.toString.call(obj)) > -1 - } -} - -function normalizeName(name) { - if (typeof name !== 'string') { - name = String(name) - } - if (/[^a-z0-9\-#$%&'*+.^_`|~]/i.test(name)) { - throw new TypeError('Invalid character in header field name') - } - return name.toLowerCase() -} - -function normalizeValue(value) { - if (typeof value !== 'string') { - value = String(value) - } - return value -} - -// Build a destructive iterator for the value list -function iteratorFor(items) { - var iterator = { - next: function() { - var value = items.shift() - return {done: value === undefined, value: value} - } - } - - if (support.iterable) { - iterator[Symbol.iterator] = function() { - return iterator - } - } - - return iterator -} - -export function Headers(headers) { - this.map = {} - - if (headers instanceof Headers) { - headers.forEach(function(value, name) { - this.append(name, value) - }, this) - } else if (Array.isArray(headers)) { - headers.forEach(function(header) { - this.append(header[0], header[1]) - }, this) - } else if (headers) { - Object.getOwnPropertyNames(headers).forEach(function(name) { - this.append(name, headers[name]) - }, this) - } -} - -Headers.prototype.append = function(name, value) { - name = normalizeName(name) - value = normalizeValue(value) - var oldValue = this.map[name] - this.map[name] = oldValue ? oldValue + ', ' + value : value -} - -Headers.prototype['delete'] = function(name) { - delete this.map[normalizeName(name)] -} - -Headers.prototype.get = function(name) { - name = normalizeName(name) - return this.has(name) ? this.map[name] : null -} - -Headers.prototype.has = function(name) { - return this.map.hasOwnProperty(normalizeName(name)) -} - -Headers.prototype.set = function(name, value) { - this.map[normalizeName(name)] = normalizeValue(value) -} - -Headers.prototype.forEach = function(callback, thisArg) { - for (var name in this.map) { - if (this.map.hasOwnProperty(name)) { - callback.call(thisArg, this.map[name], name, this) - } - } -} - -Headers.prototype.keys = function() { - var items = [] - this.forEach(function(value, name) { - items.push(name) - }) - return iteratorFor(items) -} - -Headers.prototype.values = function() { - var items = [] - this.forEach(function(value) { - items.push(value) - }) - return iteratorFor(items) -} - -Headers.prototype.entries = function() { - var items = [] - this.forEach(function(value, name) { - items.push([name, value]) - }) - return iteratorFor(items) -} - -if (support.iterable) { - Headers.prototype[Symbol.iterator] = Headers.prototype.entries -} - -function consumed(body) { - if (body.bodyUsed) { - return Promise.reject(new TypeError('Already read')) - } - body.bodyUsed = true -} - -function fileReaderReady(reader) { - return new Promise(function(resolve, reject) { - reader.onload = function() { - resolve(reader.result) - } - reader.onerror = function() { - reject(reader.error) - } - }) -} - -function readBlobAsArrayBuffer(blob) { - var reader = new FileReader() - var promise = fileReaderReady(reader) - reader.readAsArrayBuffer(blob) - return promise -} - -function readBlobAsText(blob) { - var reader = new FileReader() - var promise = fileReaderReady(reader) - reader.readAsText(blob) - return promise -} - -function readArrayBufferAsText(buf) { - var view = new Uint8Array(buf) - var chars = new Array(view.length) - - for (var i = 0; i < view.length; i++) { - chars[i] = String.fromCharCode(view[i]) - } - return chars.join('') -} - -function bufferClone(buf) { - if (buf.slice) { - return buf.slice(0) - } else { - var view = new Uint8Array(buf.byteLength) - view.set(new Uint8Array(buf)) - return view.buffer - } -} - -function Body() { - this.bodyUsed = false - - this._initBody = function(body) { - this._bodyInit = body - if (!body) { - this._bodyText = '' - } else if (typeof body === 'string') { - this._bodyText = body - } else if (support.blob && Blob.prototype.isPrototypeOf(body)) { - this._bodyBlob = body - } else if (support.formData && FormData.prototype.isPrototypeOf(body)) { - this._bodyFormData = body - } else if (support.searchParams && URLSearchParams.prototype.isPrototypeOf(body)) { - this._bodyText = body.toString() - } else if (support.arrayBuffer && support.blob && isDataView(body)) { - this._bodyArrayBuffer = bufferClone(body.buffer) - // IE 10-11 can't handle a DataView body. - this._bodyInit = new Blob([this._bodyArrayBuffer]) - } else if (support.arrayBuffer && (ArrayBuffer.prototype.isPrototypeOf(body) || isArrayBufferView(body))) { - this._bodyArrayBuffer = bufferClone(body) - } else { - this._bodyText = body = Object.prototype.toString.call(body) - } - - if (!this.headers.get('content-type')) { - if (typeof body === 'string') { - this.headers.set('content-type', 'text/plain;charset=UTF-8') - } else if (this._bodyBlob && this._bodyBlob.type) { - this.headers.set('content-type', this._bodyBlob.type) - } else if (support.searchParams && URLSearchParams.prototype.isPrototypeOf(body)) { - this.headers.set('content-type', 'application/x-www-form-urlencoded;charset=UTF-8') - } - } - } - - if (support.blob) { - this.blob = function() { - var rejected = consumed(this) - if (rejected) { - return rejected - } - - if (this._bodyBlob) { - return Promise.resolve(this._bodyBlob) - } else if (this._bodyArrayBuffer) { - return Promise.resolve(new Blob([this._bodyArrayBuffer])) - } else if (this._bodyFormData) { - throw new Error('could not read FormData body as blob') - } else { - return Promise.resolve(new Blob([this._bodyText])) - } - } - - this.arrayBuffer = function() { - if (this._bodyArrayBuffer) { - return consumed(this) || Promise.resolve(this._bodyArrayBuffer) - } else { - return this.blob().then(readBlobAsArrayBuffer) - } - } - } - - this.text = function() { - var rejected = consumed(this) - if (rejected) { - return rejected - } - - if (this._bodyBlob) { - return readBlobAsText(this._bodyBlob) - } else if (this._bodyArrayBuffer) { - return Promise.resolve(readArrayBufferAsText(this._bodyArrayBuffer)) - } else if (this._bodyFormData) { - throw new Error('could not read FormData body as text') - } else { - return Promise.resolve(this._bodyText) - } - } - - if (support.formData) { - this.formData = function() { - return this.text().then(decode) - } - } - - this.json = function() { - return this.text().then(JSON.parse) - } - - return this -} - -// HTTP methods whose capitalization should be normalized -var methods = ['DELETE', 'GET', 'HEAD', 'OPTIONS', 'POST', 'PUT'] - -function normalizeMethod(method) { - var upcased = method.toUpperCase() - return methods.indexOf(upcased) > -1 ? upcased : method -} - -export function Request(input, options) { - options = options || {} - var body = options.body - - if (input instanceof Request) { - if (input.bodyUsed) { - throw new TypeError('Already read') - } - this.url = input.url - this.credentials = input.credentials - if (!options.headers) { - this.headers = new Headers(input.headers) - } - this.method = input.method - this.mode = input.mode - this.signal = input.signal - if (!body && input._bodyInit != null) { - body = input._bodyInit - input.bodyUsed = true - } - } else { - this.url = String(input) - } - - this.credentials = options.credentials || this.credentials || 'same-origin' - if (options.headers || !this.headers) { - this.headers = new Headers(options.headers) - } - this.method = normalizeMethod(options.method || this.method || 'GET') - this.mode = options.mode || this.mode || null - this.signal = options.signal || this.signal - this.referrer = null - - if ((this.method === 'GET' || this.method === 'HEAD') && body) { - throw new TypeError('Body not allowed for GET or HEAD requests') - } - this._initBody(body) -} - -Request.prototype.clone = function() { - return new Request(this, {body: this._bodyInit}) -} - -function decode(body) { - var form = new FormData() - body - .trim() - .split('&') - .forEach(function(bytes) { - if (bytes) { - var split = bytes.split('=') - var name = split.shift().replace(/\+/g, ' ') - var value = split.join('=').replace(/\+/g, ' ') - form.append(decodeURIComponent(name), decodeURIComponent(value)) - } - }) - return form -} - -function parseHeaders(rawHeaders) { - var headers = new Headers() - // Replace instances of \r\n and \n followed by at least one space or horizontal tab with a space - // https://tools.ietf.org/html/rfc7230#section-3.2 - var preProcessedHeaders = rawHeaders.replace(/\r?\n[\t ]+/g, ' ') - preProcessedHeaders.split(/\r?\n/).forEach(function(line) { - var parts = line.split(':') - var key = parts.shift().trim() - if (key) { - var value = parts.join(':').trim() - headers.append(key, value) - } - }) - return headers -} - -Body.call(Request.prototype) - -export function Response(bodyInit, options) { - if (!options) { - options = {} - } - - this.type = 'default' - this.status = options.status === undefined ? 200 : options.status - this.ok = this.status >= 200 && this.status < 300 - this.statusText = 'statusText' in options ? options.statusText : 'OK' - this.headers = new Headers(options.headers) - this.url = options.url || '' - this._initBody(bodyInit) -} - -Body.call(Response.prototype) - -Response.prototype.clone = function() { - return new Response(this._bodyInit, { - status: this.status, - statusText: this.statusText, - headers: new Headers(this.headers), - url: this.url - }) -} - -Response.error = function() { - var response = new Response(null, {status: 0, statusText: ''}) - response.type = 'error' - return response -} - -var redirectStatuses = [301, 302, 303, 307, 308] - -Response.redirect = function(url, status) { - if (redirectStatuses.indexOf(status) === -1) { - throw new RangeError('Invalid status code') - } - - return new Response(null, {status: status, headers: {location: url}}) -} - -export var DOMException = self.DOMException -try { - new DOMException() -} catch (err) { - DOMException = function(message, name) { - this.message = message - this.name = name - var error = Error(message) - this.stack = error.stack - } - DOMException.prototype = Object.create(Error.prototype) - DOMException.prototype.constructor = DOMException -} - -export function fetch(input, init) { - return new Promise(function(resolve, reject) { - var request = new Request(input, init) - - if (request.signal && request.signal.aborted) { - return reject(new DOMException('Aborted', 'AbortError')) - } - - var xhr = new XMLHttpRequest() - - function abortXhr() { - xhr.abort() - } - - xhr.onload = function() { - var options = { - status: xhr.status, - statusText: xhr.statusText, - headers: parseHeaders(xhr.getAllResponseHeaders() || '') - } - options.url = 'responseURL' in xhr ? xhr.responseURL : options.headers.get('X-Request-URL') - var body = 'response' in xhr ? xhr.response : xhr.responseText - resolve(new Response(body, options)) - } - - xhr.onerror = function() { - reject(new TypeError('Network request failed')) - } - - xhr.ontimeout = function() { - reject(new TypeError('Network request failed')) - } - - xhr.onabort = function() { - reject(new DOMException('Aborted', 'AbortError')) - } - - xhr.open(request.method, request.url, true) - - if (request.credentials === 'include') { - xhr.withCredentials = true - } else if (request.credentials === 'omit') { - xhr.withCredentials = false - } - - if ('responseType' in xhr && support.blob) { - xhr.responseType = 'blob' - } - - request.headers.forEach(function(value, name) { - xhr.setRequestHeader(name, value) - }) - - if (request.signal) { - request.signal.addEventListener('abort', abortXhr) - - xhr.onreadystatechange = function() { - // DONE (success or failure) - if (xhr.readyState === 4) { - request.signal.removeEventListener('abort', abortXhr) - } - } - } - - xhr.send(typeof request._bodyInit === 'undefined' ? null : request._bodyInit) - }) -} - -fetch.polyfill = true - -if (!self.fetch) { - self.fetch = fetch - self.Headers = Headers - self.Request = Request - self.Response = Response -} diff --git a/webauthn-server-demo/src/main/webapp/lib/fetch/package.json b/webauthn-server-demo/src/main/webapp/lib/fetch/package.json deleted file mode 100644 index 874b605de..000000000 --- a/webauthn-server-demo/src/main/webapp/lib/fetch/package.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "name": "whatwg-fetch", - "description": "A window.fetch polyfill.", - "version": "3.0.0", - "main": "./dist/fetch.umd.js", - "module": "./fetch.js", - "repository": "github/fetch", - "license": "MIT", - "devDependencies": { - "abortcontroller-polyfill": "^1.1.9", - "chai": "^4.1.2", - "eslint": "^4.19.1", - "eslint-plugin-github": "^1.0.0", - "karma": "^3.0.0", - "karma-chai": "^0.1.0", - "karma-chrome-launcher": "^2.2.0", - "karma-detect-browsers": "^2.3.2", - "karma-firefox-launcher": "^1.1.0", - "karma-mocha": "^1.3.0", - "karma-safari-launcher": "^1.0.0", - "karma-safaritechpreview-launcher": "0.0.6", - "mocha": "^4.0.1", - "promise-polyfill": "6.0.2", - "rollup": "^0.59.1", - "url-search-params": "0.6.1" - }, - "files": [ - "LICENSE", - "dist/fetch.umd.js", - "dist/fetch.umd.js.flow", - "fetch.js", - "fetch.js.flow" - ], - "scripts": { - "karma": "karma start ./test/karma.config.js --no-single-run --auto-watch", - "prepare": "make dist/fetch.umd.js dist/fetch.umd.js.flow", - "pretest": "make", - "test": "karma start ./test/karma.config.js && karma start ./test/karma-worker.config.js" - } -} From 1f823bc0d6748114e4dcfa719194920e883ea86a Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 16 May 2022 18:25:53 +0200 Subject: [PATCH 017/145] Drop U2F registration from demo app Chrome has fully dropped support for the U2F API, so this is unlikely to work anymore. The main purpose of this feature in the demo was to illustrate use of the `appid` and `appidExclude` extensions. See: https://groups.google.com/a/chromium.org/g/blink-dev/c/xHC3AtU_65A --- .../java/com/yubico/webauthn/U2fVerifier.java | 81 -- .../demo/webauthn/WebAuthnRestResource.java | 15 - .../java/demo/webauthn/WebAuthnServer.java | 115 --- .../demo/webauthn/data/U2fCredential.java | 41 - .../webauthn/data/U2fCredentialResponse.java | 52 -- .../data/U2fRegistrationResponse.java | 50 -- .../webauthn/data/U2fRegistrationResult.java | 27 - .../src/main/webapp/index.html | 93 +- .../src/main/webapp/lib/u2f-api-1.1.js | 822 ------------------ 9 files changed, 5 insertions(+), 1291 deletions(-) delete mode 100644 webauthn-server-demo/src/main/java/com/yubico/webauthn/U2fVerifier.java delete mode 100644 webauthn-server-demo/src/main/java/demo/webauthn/data/U2fCredential.java delete mode 100644 webauthn-server-demo/src/main/java/demo/webauthn/data/U2fCredentialResponse.java delete mode 100644 webauthn-server-demo/src/main/java/demo/webauthn/data/U2fRegistrationResponse.java delete mode 100644 webauthn-server-demo/src/main/java/demo/webauthn/data/U2fRegistrationResult.java delete mode 100644 webauthn-server-demo/src/main/webapp/lib/u2f-api-1.1.js diff --git a/webauthn-server-demo/src/main/java/com/yubico/webauthn/U2fVerifier.java b/webauthn-server-demo/src/main/java/com/yubico/webauthn/U2fVerifier.java deleted file mode 100644 index 8f922fadd..000000000 --- a/webauthn-server-demo/src/main/java/com/yubico/webauthn/U2fVerifier.java +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright (c) 2018, Yubico AB -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, this -// list of conditions and the following disclaimer. -// -// 2. Redistributions in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -package com.yubico.webauthn; - -import com.fasterxml.jackson.databind.JsonNode; -import com.yubico.internal.util.CertificateParser; -import com.yubico.internal.util.ExceptionUtil; -import com.yubico.internal.util.JacksonCodecs; -import com.yubico.webauthn.data.ByteArray; -import com.yubico.webauthn.data.exception.Base64UrlException; -import com.yubico.webauthn.extension.appid.AppId; -import demo.webauthn.data.RegistrationRequest; -import demo.webauthn.data.U2fRegistrationResponse; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; - -public class U2fVerifier { - - public static boolean verify( - AppId appId, RegistrationRequest request, U2fRegistrationResponse response) - throws CertificateException, IOException, Base64UrlException { - final ByteArray appIdHash = Crypto.sha256(appId.getId()); - final ByteArray clientDataHash = - Crypto.sha256(response.getCredential().getU2fResponse().getClientDataJSON()); - - final JsonNode clientData = - JacksonCodecs.json() - .readTree(response.getCredential().getU2fResponse().getClientDataJSON().getBytes()); - final String challengeBase64 = clientData.get("challenge").textValue(); - - ExceptionUtil.assure( - request - .getPublicKeyCredentialCreationOptions() - .getChallenge() - .equals(ByteArray.fromBase64Url(challengeBase64)), - "Wrong challenge."); - - InputStream attestationCertAndSignatureStream = - new ByteArrayInputStream( - response.getCredential().getU2fResponse().getAttestationCertAndSignature().getBytes()); - - final X509Certificate attestationCert = - CertificateParser.parseDer(attestationCertAndSignatureStream); - - byte[] signatureBytes = new byte[attestationCertAndSignatureStream.available()]; - attestationCertAndSignatureStream.read(signatureBytes); - final ByteArray signature = new ByteArray(signatureBytes); - - return new U2fRawRegisterResponse( - response.getCredential().getU2fResponse().getPublicKey(), - response.getCredential().getU2fResponse().getKeyHandle(), - attestationCert, - signature) - .verifySignature(appIdHash, clientDataHash); - } -} diff --git a/webauthn-server-demo/src/main/java/demo/webauthn/WebAuthnRestResource.java b/webauthn-server-demo/src/main/java/demo/webauthn/WebAuthnRestResource.java index 370a0eb89..788881882 100644 --- a/webauthn-server-demo/src/main/java/demo/webauthn/WebAuthnRestResource.java +++ b/webauthn-server-demo/src/main/java/demo/webauthn/WebAuthnRestResource.java @@ -154,8 +154,6 @@ private StartRegistrationResponse(RegistrationRequest request) throws MalformedU private final class StartRegistrationActions { public final URL finish = uriInfo.getAbsolutePathBuilder().path("finish").build().toURL(); - public final URL finishU2f = - uriInfo.getAbsolutePathBuilder().path("finish-u2f").build().toURL(); private StartRegistrationActions() throws MalformedURLException {} } @@ -215,19 +213,6 @@ public Response finishRegistration(@NonNull String responseJson) { responseJson); } - @Path("register/finish-u2f") - @POST - public Response finishU2fRegistration(@NonNull String responseJson) throws ExecutionException { - logger.trace("finishRegistration responseJson: {}", responseJson); - Either, WebAuthnServer.SuccessfulU2fRegistrationResult> result = - server.finishU2fRegistration(responseJson); - return finishResponse( - result, - "U2F registration failed; further error message(s) were unfortunately lost to an internal server error.", - "finishU2fRegistration", - responseJson); - } - private final class StartAuthenticationResponse { public final boolean success = true; public final AssertionRequestWrapper request; 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 7f535e821..85e0eb634 100644 --- a/webauthn-server-demo/src/main/java/demo/webauthn/WebAuthnServer.java +++ b/webauthn-server-demo/src/main/java/demo/webauthn/WebAuthnServer.java @@ -36,7 +36,6 @@ import com.yubico.fido.metadata.FidoMetadataDownloaderException; import com.yubico.fido.metadata.UnexpectedLegalHeader; import com.yubico.internal.util.CertificateParser; -import com.yubico.internal.util.ExceptionUtil; import com.yubico.internal.util.JacksonCodecs; import com.yubico.util.Either; import com.yubico.webauthn.AssertionResult; @@ -47,7 +46,6 @@ import com.yubico.webauthn.RelyingParty; import com.yubico.webauthn.StartAssertionOptions; import com.yubico.webauthn.StartRegistrationOptions; -import com.yubico.webauthn.U2fVerifier; import com.yubico.webauthn.attestation.Attestation; import com.yubico.webauthn.attestation.YubicoJsonMetadataService; import com.yubico.webauthn.data.AttestationConveyancePreference; @@ -56,7 +54,6 @@ import com.yubico.webauthn.data.AuthenticatorTransport; import com.yubico.webauthn.data.ByteArray; import com.yubico.webauthn.data.COSEAlgorithmIdentifier; -import com.yubico.webauthn.data.PublicKeyCredentialDescriptor; import com.yubico.webauthn.data.RelyingPartyIdentity; import com.yubico.webauthn.data.ResidentKeyRequirement; import com.yubico.webauthn.data.UserIdentity; @@ -70,8 +67,6 @@ import demo.webauthn.data.CredentialRegistration; import demo.webauthn.data.RegistrationRequest; import demo.webauthn.data.RegistrationResponse; -import demo.webauthn.data.U2fRegistrationResponse; -import demo.webauthn.data.U2fRegistrationResult; import java.io.IOException; import java.security.DigestException; import java.security.InvalidAlgorithmParameterException; @@ -275,18 +270,6 @@ public SuccessfulRegistrationResult( } } - @Value - public class SuccessfulU2fRegistrationResult { - final boolean success = true; - final RegistrationRequest request; - final U2fRegistrationResponse response; - final CredentialRegistration registration; - boolean attestationTrusted; - Optional attestationCert; - final String username; - final ByteArray sessionToken; - } - @Value public static class AttestationCertInfo { final ByteArray der; @@ -392,86 +375,6 @@ public Either, SuccessfulRegistrationResult> finishRegistration( } } - public Either, SuccessfulU2fRegistrationResult> finishU2fRegistration( - String responseJson) throws ExecutionException { - logger.trace("finishU2fRegistration responseJson: {}", responseJson); - U2fRegistrationResponse response = null; - try { - response = jsonMapper.readValue(responseJson, U2fRegistrationResponse.class); - } catch (IOException e) { - logger.error("JSON error in finishU2fRegistration; responseJson: {}", responseJson, e); - return Either.left( - Arrays.asList( - "Registration failed!", "Failed to decode response object.", e.getMessage())); - } - - RegistrationRequest request = registerRequestStorage.getIfPresent(response.getRequestId()); - registerRequestStorage.invalidate(response.getRequestId()); - - if (request == null) { - logger.debug("fail finishU2fRegistration responseJson: {}", responseJson); - return Either.left( - Arrays.asList("Registration failed!", "No such registration in progress.")); - } else { - - try { - ExceptionUtil.assure( - U2fVerifier.verify(rp.getAppId().get(), request, response), - "Failed to verify signature."); - } catch (Exception e) { - logger.debug("Failed to verify U2F signature.", e); - return Either.left(Arrays.asList("Failed to verify signature.", e.getMessage())); - } - - X509Certificate attestationCert = null; - try { - attestationCert = - CertificateParser.parseDer( - response - .getCredential() - .getU2fResponse() - .getAttestationCertAndSignature() - .getBytes()); - } catch (CertificateException e) { - logger.error( - "Failed to parse attestation certificate: {}", - response.getCredential().getU2fResponse().getAttestationCertAndSignature(), - e); - } - - Optional attestation = metadataService.findMetadata(attestationCert); - - final U2fRegistrationResult result = - U2fRegistrationResult.builder() - .keyId( - PublicKeyCredentialDescriptor.builder() - .id(response.getCredential().getU2fResponse().getKeyHandle()) - .build()) - .attestationTrusted(attestation.isPresent()) - .publicKeyCose( - rawEcdaKeyToCose(response.getCredential().getU2fResponse().getPublicKey())) - .attestationMetadata(attestation) - .build(); - - return Either.right( - new SuccessfulU2fRegistrationResult( - request, - response, - addRegistration( - request.getPublicKeyCredentialCreationOptions().getUser(), - request.getCredentialNickname(), - 0, - result), - result.isAttestationTrusted(), - Optional.of( - new AttestationCertInfo( - response.getCredential().getU2fResponse().getAttestationCertAndSignature())), - request.getUsername(), - sessions.createSession( - request.getPublicKeyCredentialCreationOptions().getUser().getId()))); - } - } - public Either, AssertionRequestWrapper> startAuthentication( Optional username) { logger.trace("startAuthentication username: {}", username); @@ -655,24 +558,6 @@ private CredentialRegistration addRegistration( .flatMap(metadataService::findMetadata)); } - private CredentialRegistration addRegistration( - UserIdentity userIdentity, - Optional nickname, - long signatureCount, - U2fRegistrationResult result) { - return addRegistration( - userIdentity, - nickname, - RegisteredCredential.builder() - .credentialId(result.getKeyId().getId()) - .userHandle(userIdentity.getId()) - .publicKeyCose(result.getPublicKeyCose()) - .signatureCount(signatureCount) - .build(), - Collections.emptySortedSet(), - result.getAttestationMetadata()); - } - private CredentialRegistration addRegistration( UserIdentity userIdentity, Optional nickname, diff --git a/webauthn-server-demo/src/main/java/demo/webauthn/data/U2fCredential.java b/webauthn-server-demo/src/main/java/demo/webauthn/data/U2fCredential.java deleted file mode 100644 index 769b3666e..000000000 --- a/webauthn-server-demo/src/main/java/demo/webauthn/data/U2fCredential.java +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) 2018, Yubico AB -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, this -// list of conditions and the following disclaimer. -// -// 2. Redistributions in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -package demo.webauthn.data; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.NonNull; -import lombok.Value; - -@Value -public class U2fCredential { - - private final U2fCredentialResponse u2fResponse; - - @JsonCreator - public U2fCredential(@NonNull @JsonProperty("u2fResponse") U2fCredentialResponse u2fResponse) { - this.u2fResponse = u2fResponse; - } -} diff --git a/webauthn-server-demo/src/main/java/demo/webauthn/data/U2fCredentialResponse.java b/webauthn-server-demo/src/main/java/demo/webauthn/data/U2fCredentialResponse.java deleted file mode 100644 index 1ad3f5fc4..000000000 --- a/webauthn-server-demo/src/main/java/demo/webauthn/data/U2fCredentialResponse.java +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright (c) 2018, Yubico AB -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, this -// list of conditions and the following disclaimer. -// -// 2. Redistributions in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -package demo.webauthn.data; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.yubico.webauthn.data.ByteArray; -import lombok.NonNull; -import lombok.Value; - -@Value -public class U2fCredentialResponse { - - private final ByteArray keyHandle; - private final ByteArray publicKey; - private final ByteArray attestationCertAndSignature; - private final ByteArray clientDataJSON; - - @JsonCreator - public U2fCredentialResponse( - @NonNull @JsonProperty("keyHandle") ByteArray keyHandle, - @NonNull @JsonProperty("publicKey") ByteArray publicKey, - @NonNull @JsonProperty("attestationCertAndSignature") ByteArray attestationCertAndSignature, - @NonNull @JsonProperty("clientDataJSON") ByteArray clientDataJSON) { - this.keyHandle = keyHandle; - this.publicKey = publicKey; - this.attestationCertAndSignature = attestationCertAndSignature; - this.clientDataJSON = clientDataJSON; - } -} diff --git a/webauthn-server-demo/src/main/java/demo/webauthn/data/U2fRegistrationResponse.java b/webauthn-server-demo/src/main/java/demo/webauthn/data/U2fRegistrationResponse.java deleted file mode 100644 index ef0d612a0..000000000 --- a/webauthn-server-demo/src/main/java/demo/webauthn/data/U2fRegistrationResponse.java +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright (c) 2018, Yubico AB -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, this -// list of conditions and the following disclaimer. -// -// 2. Redistributions in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -package demo.webauthn.data; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.yubico.webauthn.data.ByteArray; -import java.util.Optional; -import lombok.NonNull; -import lombok.Value; - -@Value -public class U2fRegistrationResponse { - - private final ByteArray requestId; - private final U2fCredential credential; - private final Optional sessionToken; - - @JsonCreator - public U2fRegistrationResponse( - @NonNull @JsonProperty("requestId") ByteArray requestId, - @NonNull @JsonProperty("credential") U2fCredential credential, - @NonNull @JsonProperty("sessionToken") Optional sessionToken) { - this.requestId = requestId; - this.credential = credential; - this.sessionToken = sessionToken; - } -} diff --git a/webauthn-server-demo/src/main/java/demo/webauthn/data/U2fRegistrationResult.java b/webauthn-server-demo/src/main/java/demo/webauthn/data/U2fRegistrationResult.java deleted file mode 100644 index aaaf0c94d..000000000 --- a/webauthn-server-demo/src/main/java/demo/webauthn/data/U2fRegistrationResult.java +++ /dev/null @@ -1,27 +0,0 @@ -package demo.webauthn.data; - -import com.yubico.webauthn.attestation.Attestation; -import com.yubico.webauthn.data.ByteArray; -import com.yubico.webauthn.data.PublicKeyCredentialDescriptor; -import java.util.Collections; -import java.util.List; -import java.util.Optional; -import lombok.Builder; -import lombok.NonNull; -import lombok.Value; - -@Value -@Builder -public class U2fRegistrationResult { - - @NonNull private final PublicKeyCredentialDescriptor keyId; - - private final boolean attestationTrusted; - - @NonNull private final ByteArray publicKeyCose; - - @NonNull @Builder.Default private final List warnings = Collections.emptyList(); - - @NonNull @Builder.Default - private final Optional attestationMetadata = Optional.empty(); -} diff --git a/webauthn-server-demo/src/main/webapp/index.html b/webauthn-server-demo/src/main/webapp/index.html index 032da8a43..df79e27d9 100644 --- a/webauthn-server-demo/src/main/webapp/index.html +++ b/webauthn-server-demo/src/main/webapp/index.html @@ -49,7 +49,6 @@ - @@ -143,7 +142,7 @@ } function showRequest(data) { return showJson('request', data); } function showAuthenticatorResponse(data) { - const clientDataJson = data && (data.response && data.response.clientDataJSON || data.u2fResponse.clientDataJSON); + const clientDataJson = data && (data.response && data.response.clientDataJSON); return showJson('authenticator-response', extend( data, { _clientDataJson: data && JSON.parse(new TextDecoder('utf-8').decode(base64url.toByteArray(clientDataJson))), @@ -213,78 +212,10 @@ ; } -function executeRegisterRequest(request, useU2f) { +function executeRegisterRequest(request) { console.log('executeRegisterRequest', request); - if (useU2f) { - return executeU2fRegisterRequest(request); - } else { - return webauthnJson.create({ publicKey: request.publicKeyCredentialCreationOptions }); - } -} - -async function executeU2fRegisterRequest(request) { - const appId = 'https://localhost:8443'; - console.log('appId', appId); - const result = await u2fRegister( - appId, - [{ - version: 'U2F_V2', - challenge: request.publicKeyCredentialCreationOptions.challenge, - attestation: 'direct', - }], - request.publicKeyCredentialCreationOptions.excludeCredentials.map(cred => ({ - version: 'U2F_V2', - keyHandle: cred.id, - })) - ); - - const registrationDataBase64 = result.registrationData; - const clientDataBase64 = result.clientData; - const registrationDataBytes = base64url.toByteArray(registrationDataBase64); - - const publicKeyBytes = registrationDataBytes.slice(1, 1 + 65); - const L = registrationDataBytes[1 + 65]; - const keyHandleBytes = registrationDataBytes.slice(1 + 65 + 1, 1 + 65 + 1 + L); - - const attestationCertAndTrailingBytes = registrationDataBytes.slice(1 + 65 + 1 + L); - - return { - u2fResponse: { - keyHandle: base64url.fromByteArray(keyHandleBytes), - publicKey: base64url.fromByteArray(publicKeyBytes), - attestationCertAndSignature: base64url.fromByteArray(attestationCertAndTrailingBytes), - clientDataJSON: clientDataBase64, - }, - }; -} - -function u2fRegister(appId, registerRequests, registeredKeys) { - return new Promise((resolve, reject) => { - u2f.register( - appId, - registerRequests, - registeredKeys, - data => { - if (data.errorCode) { - switch (data.errorCode) { - case 2: - reject(new Error('Bad request.')); - break; - - case 4: - reject(new Error('This device is already registered.')); - break; - - default: - reject(new Error(`U2F failed with error: ${data.errorCode}`)); - } - } else { - resolve(data); - } - } - ); - }); + return webauthnJson.create({ publicKey: request.publicKeyCredentialCreationOptions }); } function submitResponse(url, request, response) { @@ -312,7 +243,6 @@ const statusStrings = params.statusStrings; /* { init, authenticatorRequest, serverRequest, success, } */ const executeRequest = params.executeRequest; /* function({ publicKeyCredentialCreationOptions: object } | { publicKeyCredentialRequestOptions: object }): Promise[PublicKeyCredential] */ const handleError = params.handleError; /* function(err): ? */ - const useU2f = params.useU2f; /* boolean */ setStatus('Looking up API paths...'); resetDisplays(); @@ -335,7 +265,6 @@ request, statusStrings, urls, - useU2f, }; const webauthnResponse = await executeRequest(request); @@ -349,7 +278,6 @@ const request = ceremonyState.request; const statusStrings = ceremonyState.statusStrings; const urls = ceremonyState.urls; - const useU2f = ceremonyState.useU2f; setStatus(statusStrings.serverRequest || 'Sending response to server...'); if (callbacks.serverRequest) { @@ -357,7 +285,7 @@ } showAuthenticatorResponse(response); - const data = await submitResponse(useU2f ? urls.finishU2f : urls.finish, request, response); + const data = await submitResponse(urls.finish, request, response); if (data && data.success) { setStatus(statusStrings.success); @@ -376,7 +304,6 @@ const username = document.getElementById('username').value; const displayName = document.getElementById('displayName').value; const credentialNickname = document.getElementById('credentialNickname').value; - const useU2f = document.getElementById('useU2f').checked; var request; @@ -391,9 +318,8 @@ }, executeRequest: req => { request = req; - return executeRegisterRequest(req, useU2f); + return executeRegisterRequest(req); }, - useU2f, }); if (data.registration) { @@ -611,15 +537,6 @@

Test your WebAuthn device

-
-
-
-
- - -
-
-
diff --git a/webauthn-server-demo/src/main/webapp/lib/u2f-api-1.1.js b/webauthn-server-demo/src/main/webapp/lib/u2f-api-1.1.js deleted file mode 100644 index 17d4a1f36..000000000 --- a/webauthn-server-demo/src/main/webapp/lib/u2f-api-1.1.js +++ /dev/null @@ -1,822 +0,0 @@ -/* eslint-disable */ -//Copyright 2014-2015 Google Inc. All rights reserved. - -//Use of this source code is governed by a BSD-style -//license that can be found in the LICENSE file or at -//https://developers.google.com/open-source/licenses/bsd - -/** - * @fileoverview The U2F api. - */ -"use strict"; - -/** - * Namespace for the U2F api. - * @type {Object} - */ -var u2f = u2f || {}; - -/** - * FIDO U2F Javascript API Version - * @number - */ -var js_api_version; - -/** - * The U2F extension id - * @const {string} - */ -// The Chrome packaged app extension ID. -// Uncomment this if you want to deploy a server instance that uses -// the package Chrome app and does not require installing the U2F Chrome extension. -u2f.EXTENSION_ID = "kmendfapggjehodndflmmgagdbamhnfd"; -// The U2F Chrome extension ID. -// Uncomment this if you want to deploy a server instance that uses -// the U2F Chrome extension to authenticate. -// u2f.EXTENSION_ID = 'pfboblefjcgdjicmnffhdgionmgcdmne'; - -/** - * Message types for messsages to/from the extension - * @const - * @enum {string} - */ -u2f.MessageTypes = { - U2F_REGISTER_REQUEST: "u2f_register_request", - U2F_REGISTER_RESPONSE: "u2f_register_response", - U2F_SIGN_REQUEST: "u2f_sign_request", - U2F_SIGN_RESPONSE: "u2f_sign_response", - U2F_GET_API_VERSION_REQUEST: "u2f_get_api_version_request", - U2F_GET_API_VERSION_RESPONSE: "u2f_get_api_version_response" -}; - -/** - * Response status codes - * @const - * @enum {number} - */ -u2f.ErrorCodes = { - OK: 0, - OTHER_ERROR: 1, - BAD_REQUEST: 2, - CONFIGURATION_UNSUPPORTED: 3, - DEVICE_INELIGIBLE: 4, - TIMEOUT: 5 -}; - -/** - * A message for registration requests - * @typedef {{ - * type: u2f.MessageTypes, - * appId: ?string, - * timeoutSeconds: ?number, - * requestId: ?number - * }} - */ -u2f.U2fRequest; - -/** - * A message for registration responses - * @typedef {{ - * type: u2f.MessageTypes, - * responseData: (u2f.Error | u2f.RegisterResponse | u2f.SignResponse), - * requestId: ?number - * }} - */ -u2f.U2fResponse; - -/** - * An error object for responses - * @typedef {{ - * errorCode: u2f.ErrorCodes, - * errorMessage: ?string - * }} - */ -u2f.Error; - -/** - * Data object for a single sign request. - * @typedef {enum {BLUETOOTH_RADIO, BLUETOOTH_LOW_ENERGY, USB, NFC}} - */ -u2f.Transport; - -/** - * Data object for a single sign request. - * @typedef {Array} - */ -u2f.Transports; - -/** - * Data object for a single sign request. - * @typedef {{ - * version: string, - * challenge: string, - * keyHandle: string, - * appId: string - * }} - */ -u2f.SignRequest; - -/** - * Data object for a sign response. - * @typedef {{ - * keyHandle: string, - * signatureData: string, - * clientData: string - * }} - */ -u2f.SignResponse; - -/** - * Data object for a registration request. - * @typedef {{ - * version: string, - * challenge: string - * }} - */ -u2f.RegisterRequest; - -/** - * Data object for a registration response. - * @typedef {{ - * version: string, - * keyHandle: string, - * transports: Transports, - * appId: string - * }} - */ -u2f.RegisterResponse; - -/** - * Data object for a registered key. - * @typedef {{ - * version: string, - * keyHandle: string, - * transports: ?Transports, - * appId: ?string - * }} - */ -u2f.RegisteredKey; - -/** - * Data object for a get API register response. - * @typedef {{ - * js_api_version: number - * }} - */ -u2f.GetJsApiVersionResponse; - -//Low level MessagePort API support - -/** - * Sets up a MessagePort to the U2F extension using the - * available mechanisms. - * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback - */ -u2f.getMessagePort = function(callback) { - if (typeof chrome != "undefined" && chrome.runtime) { - // The actual message here does not matter, but we need to get a reply - // for the callback to run. Thus, send an empty signature request - // in order to get a failure response. - var msg = { - type: u2f.MessageTypes.U2F_SIGN_REQUEST, - signRequests: [] - }; - chrome.runtime.sendMessage(u2f.EXTENSION_ID, msg, function() { - if (!chrome.runtime.lastError) { - // We are on a whitelisted origin and can talk directly - // with the extension. - u2f.getChromeRuntimePort_(callback); - } else { - // chrome.runtime was available, but we couldn't message - // the extension directly, use iframe - u2f.getIframePort_(callback); - } - }); - } else if (u2f.isAndroidChrome_()) { - u2f.getAuthenticatorPort_(callback); - } else if (u2f.isIosChrome_()) { - u2f.getIosPort_(callback); - } else { - // chrome.runtime was not available at all, which is normal - // when this origin doesn't have access to any extensions. - u2f.getIframePort_(callback); - } -}; - -/** - * Detect chrome running on android based on the browser's useragent. - * @private - */ -u2f.isAndroidChrome_ = function() { - var userAgent = navigator.userAgent; - return ( - userAgent.indexOf("Chrome") != -1 && userAgent.indexOf("Android") != -1 - ); -}; - -/** - * Detect chrome running on iOS based on the browser's platform. - * @private - */ -u2f.isIosChrome_ = function() { - return ["iPhone", "iPad", "iPod"].indexOf(navigator.platform) > -1; -}; - -/** - * Connects directly to the extension via chrome.runtime.connect. - * @param {function(u2f.WrappedChromeRuntimePort_)} callback - * @private - */ -u2f.getChromeRuntimePort_ = function(callback) { - var port = chrome.runtime.connect(u2f.EXTENSION_ID, { - includeTlsChannelId: true - }); - setTimeout(function() { - callback(new u2f.WrappedChromeRuntimePort_(port)); - }, 0); -}; - -/** - * Return a 'port' abstraction to the Authenticator app. - * @param {function(u2f.WrappedAuthenticatorPort_)} callback - * @private - */ -u2f.getAuthenticatorPort_ = function(callback) { - setTimeout(function() { - callback(new u2f.WrappedAuthenticatorPort_()); - }, 0); -}; - -/** - * Return a 'port' abstraction to the iOS client app. - * @param {function(u2f.WrappedIosPort_)} callback - * @private - */ -u2f.getIosPort_ = function(callback) { - setTimeout(function() { - callback(new u2f.WrappedIosPort_()); - }, 0); -}; - -/** - * A wrapper for chrome.runtime.Port that is compatible with MessagePort. - * @param {Port} port - * @constructor - * @private - */ -u2f.WrappedChromeRuntimePort_ = function(port) { - this.port_ = port; -}; - -/** - * Format and return a sign request compliant with the JS API version supported by the extension. - * @param {Array} signRequests - * @param {number} timeoutSeconds - * @param {number} reqId - * @return {Object} - */ -u2f.formatSignRequest_ = function( - appId, - challenge, - registeredKeys, - timeoutSeconds, - reqId -) { - if (js_api_version === undefined || js_api_version < 1.1) { - // Adapt request to the 1.0 JS API - var signRequests = []; - for (var i = 0; i < registeredKeys.length; i++) { - signRequests[i] = { - version: registeredKeys[i].version, - challenge: challenge, - keyHandle: registeredKeys[i].keyHandle, - appId: appId - }; - } - return { - type: u2f.MessageTypes.U2F_SIGN_REQUEST, - signRequests: signRequests, - timeoutSeconds: timeoutSeconds, - requestId: reqId - }; - } - // JS 1.1 API - return { - type: u2f.MessageTypes.U2F_SIGN_REQUEST, - appId: appId, - challenge: challenge, - registeredKeys: registeredKeys, - timeoutSeconds: timeoutSeconds, - requestId: reqId - }; -}; - -/** - * Format and return a register request compliant with the JS API version supported by the extension.. - * @param {Array} signRequests - * @param {Array} signRequests - * @param {number} timeoutSeconds - * @param {number} reqId - * @return {Object} - */ -u2f.formatRegisterRequest_ = function( - appId, - registeredKeys, - registerRequests, - timeoutSeconds, - reqId -) { - if (js_api_version === undefined || js_api_version < 1.1) { - // Adapt request to the 1.0 JS API - for (var i = 0; i < registerRequests.length; i++) { - registerRequests[i].appId = appId; - } - var signRequests = []; - for (var i = 0; i < registeredKeys.length; i++) { - signRequests[i] = { - version: registeredKeys[i].version, - challenge: registerRequests[0], - keyHandle: registeredKeys[i].keyHandle, - appId: appId - }; - } - return { - type: u2f.MessageTypes.U2F_REGISTER_REQUEST, - signRequests: signRequests, - registerRequests: registerRequests, - timeoutSeconds: timeoutSeconds, - requestId: reqId - }; - } - // JS 1.1 API - return { - type: u2f.MessageTypes.U2F_REGISTER_REQUEST, - appId: appId, - registerRequests: registerRequests, - registeredKeys: registeredKeys, - timeoutSeconds: timeoutSeconds, - requestId: reqId - }; -}; - -/** - * Posts a message on the underlying channel. - * @param {Object} message - */ -u2f.WrappedChromeRuntimePort_.prototype.postMessage = function(message) { - this.port_.postMessage(message); -}; - -/** - * Emulates the HTML 5 addEventListener interface. Works only for the - * onmessage event, which is hooked up to the chrome.runtime.Port.onMessage. - * @param {string} eventName - * @param {function({data: Object})} handler - */ -u2f.WrappedChromeRuntimePort_.prototype.addEventListener = function( - eventName, - handler -) { - var name = eventName.toLowerCase(); - if (name == "message" || name == "onmessage") { - this.port_.onMessage.addListener(function(message) { - // Emulate a minimal MessageEvent object - handler({ data: message }); - }); - } else { - console.error("WrappedChromeRuntimePort only supports onMessage"); - } -}; - -/** - * Wrap the Authenticator app with a MessagePort interface. - * @constructor - * @private - */ -u2f.WrappedAuthenticatorPort_ = function() { - this.requestId_ = -1; - this.requestObject_ = null; -}; - -/** - * Launch the Authenticator intent. - * @param {Object} message - */ -u2f.WrappedAuthenticatorPort_.prototype.postMessage = function(message) { - var intentUrl = - u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ + - ";S.request=" + - encodeURIComponent(JSON.stringify(message)) + - ";end"; - document.location = intentUrl; -}; - -/** - * Tells what type of port this is. - * @return {String} port type - */ -u2f.WrappedAuthenticatorPort_.prototype.getPortType = function() { - return "WrappedAuthenticatorPort_"; -}; - -/** - * Emulates the HTML 5 addEventListener interface. - * @param {string} eventName - * @param {function({data: Object})} handler - */ -u2f.WrappedAuthenticatorPort_.prototype.addEventListener = function( - eventName, - handler -) { - var name = eventName.toLowerCase(); - if (name == "message") { - var self = this; - /* Register a callback to that executes when - * chrome injects the response. */ - window.addEventListener( - "message", - self.onRequestUpdate_.bind(self, handler), - false - ); - } else { - console.error("WrappedAuthenticatorPort only supports message"); - } -}; - -/** - * Callback invoked when a response is received from the Authenticator. - * @param function({data: Object}) callback - * @param {Object} message message Object - */ -u2f.WrappedAuthenticatorPort_.prototype.onRequestUpdate_ = function( - callback, - message -) { - var messageObject = JSON.parse(message.data); - var intentUrl = messageObject["intentURL"]; - - var errorCode = messageObject["errorCode"]; - var responseObject = null; - if (messageObject.hasOwnProperty("data")) { - responseObject = /** @type {Object} */ (JSON.parse(messageObject["data"])); - } - - callback({ data: responseObject }); -}; - -/** - * Base URL for intents to Authenticator. - * @const - * @private - */ -u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ = - "intent:#Intent;action=com.google.android.apps.authenticator.AUTHENTICATE"; - -/** - * Wrap the iOS client app with a MessagePort interface. - * @constructor - * @private - */ -u2f.WrappedIosPort_ = function() {}; - -/** - * Launch the iOS client app request - * @param {Object} message - */ -u2f.WrappedIosPort_.prototype.postMessage = function(message) { - var str = JSON.stringify(message); - var url = "u2f://auth?" + encodeURI(str); - location.replace(url); -}; - -/** - * Tells what type of port this is. - * @return {String} port type - */ -u2f.WrappedIosPort_.prototype.getPortType = function() { - return "WrappedIosPort_"; -}; - -/** - * Emulates the HTML 5 addEventListener interface. - * @param {string} eventName - * @param {function({data: Object})} handler - */ -u2f.WrappedIosPort_.prototype.addEventListener = function(eventName, handler) { - var name = eventName.toLowerCase(); - if (name !== "message") { - console.error("WrappedIosPort only supports message"); - } -}; - -/** - * Sets up an embedded trampoline iframe, sourced from the extension. - * @param {function(MessagePort)} callback - * @private - */ -u2f.getIframePort_ = function(callback) { - // Create the iframe - var iframeOrigin = "chrome-extension://" + u2f.EXTENSION_ID; - var iframe = document.createElement("iframe"); - iframe.src = iframeOrigin + "/u2f-comms.html"; - iframe.setAttribute("style", "display:none"); - document.body.appendChild(iframe); - - var channel = new MessageChannel(); - var ready = function(message) { - if (message.data == "ready") { - channel.port1.removeEventListener("message", ready); - callback(channel.port1); - } else { - console.error('First event on iframe port was not "ready"'); - } - }; - channel.port1.addEventListener("message", ready); - channel.port1.start(); - - iframe.addEventListener("load", function() { - // Deliver the port to the iframe and initialize - iframe.contentWindow.postMessage("init", iframeOrigin, [channel.port2]); - }); -}; - -//High-level JS API - -/** - * Default extension response timeout in seconds. - * @const - */ -u2f.EXTENSION_TIMEOUT_SEC = 30; - -/** - * A singleton instance for a MessagePort to the extension. - * @type {MessagePort|u2f.WrappedChromeRuntimePort_} - * @private - */ -u2f.port_ = null; - -/** - * Callbacks waiting for a port - * @type {Array} - * @private - */ -u2f.waitingForPort_ = []; - -/** - * A counter for requestIds. - * @type {number} - * @private - */ -u2f.reqCounter_ = 0; - -/** - * A map from requestIds to client callbacks - * @type {Object.} - * @private - */ -u2f.callbackMap_ = {}; - -/** - * Creates or retrieves the MessagePort singleton to use. - * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback - * @private - */ -u2f.getPortSingleton_ = function(callback) { - if (u2f.port_) { - callback(u2f.port_); - } else { - if (u2f.waitingForPort_.length == 0) { - u2f.getMessagePort(function(port) { - u2f.port_ = port; - u2f.port_.addEventListener( - "message", - /** @type {function(Event)} */ (u2f.responseHandler_) - ); - - // Careful, here be async callbacks. Maybe. - while (u2f.waitingForPort_.length) - u2f.waitingForPort_.shift()(u2f.port_); - }); - } - u2f.waitingForPort_.push(callback); - } -}; - -/** - * Handles response messages from the extension. - * @param {MessageEvent.} message - * @private - */ -u2f.responseHandler_ = function(message) { - var response = message.data; - var reqId = response["requestId"]; - if (!reqId || !u2f.callbackMap_[reqId]) { - console.error("Unknown or missing requestId in response."); - return; - } - var cb = u2f.callbackMap_[reqId]; - delete u2f.callbackMap_[reqId]; - cb(response["responseData"]); -}; - -/** - * Dispatches an array of sign requests to available U2F tokens. - * If the JS API version supported by the extension is unknown, it first sends a - * message to the extension to find out the supported API version and then it sends - * the sign request. - * @param {string=} appId - * @param {string=} challenge - * @param {Array} registeredKeys - * @param {function((u2f.Error|u2f.SignResponse))} callback - * @param {number=} opt_timeoutSeconds - */ -u2f.sign = function( - appId, - challenge, - registeredKeys, - callback, - opt_timeoutSeconds -) { - if (js_api_version === undefined) { - // Send a message to get the extension to JS API version, then send the actual sign request. - u2f.getApiVersion(function(response) { - js_api_version = - response["js_api_version"] === undefined - ? 0 - : response["js_api_version"]; - console.log("Extension JS API Version: ", js_api_version); - u2f.sendSignRequest( - appId, - challenge, - registeredKeys, - callback, - opt_timeoutSeconds - ); - }); - } else { - // We know the JS API version. Send the actual sign request in the supported API version. - u2f.sendSignRequest( - appId, - challenge, - registeredKeys, - callback, - opt_timeoutSeconds - ); - } -}; - -/** - * Dispatches an array of sign requests to available U2F tokens. - * @param {string=} appId - * @param {string=} challenge - * @param {Array} registeredKeys - * @param {function((u2f.Error|u2f.SignResponse))} callback - * @param {number=} opt_timeoutSeconds - */ -u2f.sendSignRequest = function( - appId, - challenge, - registeredKeys, - callback, - opt_timeoutSeconds -) { - u2f.getPortSingleton_(function(port) { - var reqId = ++u2f.reqCounter_; - u2f.callbackMap_[reqId] = callback; - var timeoutSeconds = - typeof opt_timeoutSeconds !== "undefined" - ? opt_timeoutSeconds - : u2f.EXTENSION_TIMEOUT_SEC; - var req = u2f.formatSignRequest_( - appId, - challenge, - registeredKeys, - timeoutSeconds, - reqId - ); - port.postMessage(req); - }); -}; - -/** - * Dispatches register requests to available U2F tokens. An array of sign - * requests identifies already registered tokens. - * If the JS API version supported by the extension is unknown, it first sends a - * message to the extension to find out the supported API version and then it sends - * the register request. - * @param {string=} appId - * @param {Array} registerRequests - * @param {Array} registeredKeys - * @param {function((u2f.Error|u2f.RegisterResponse))} callback - * @param {number=} opt_timeoutSeconds - */ -u2f.register = function( - appId, - registerRequests, - registeredKeys, - callback, - opt_timeoutSeconds -) { - if (js_api_version === undefined) { - // Send a message to get the extension to JS API version, then send the actual register request. - u2f.getApiVersion(function(response) { - js_api_version = - response["js_api_version"] === undefined - ? 0 - : response["js_api_version"]; - console.log("Extension JS API Version: ", js_api_version); - u2f.sendRegisterRequest( - appId, - registerRequests, - registeredKeys, - callback, - opt_timeoutSeconds - ); - }); - } else { - // We know the JS API version. Send the actual register request in the supported API version. - u2f.sendRegisterRequest( - appId, - registerRequests, - registeredKeys, - callback, - opt_timeoutSeconds - ); - } -}; - -/** - * Dispatches register requests to available U2F tokens. An array of sign - * requests identifies already registered tokens. - * @param {string=} appId - * @param {Array} registerRequests - * @param {Array} registeredKeys - * @param {function((u2f.Error|u2f.RegisterResponse))} callback - * @param {number=} opt_timeoutSeconds - */ -u2f.sendRegisterRequest = function( - appId, - registerRequests, - registeredKeys, - callback, - opt_timeoutSeconds -) { - u2f.getPortSingleton_(function(port) { - var reqId = ++u2f.reqCounter_; - u2f.callbackMap_[reqId] = callback; - var timeoutSeconds = - typeof opt_timeoutSeconds !== "undefined" - ? opt_timeoutSeconds - : u2f.EXTENSION_TIMEOUT_SEC; - var req = u2f.formatRegisterRequest_( - appId, - registeredKeys, - registerRequests, - timeoutSeconds, - reqId - ); - port.postMessage(req); - }); -}; - -/** - * Dispatches a message to the extension to find out the supported - * JS API version. - * If the user is on a mobile phone and is thus using Google Authenticator instead - * of the Chrome extension, don't send the request and simply return 0. - * @param {function((u2f.Error|u2f.GetJsApiVersionResponse))} callback - * @param {number=} opt_timeoutSeconds - */ -u2f.getApiVersion = function(callback, opt_timeoutSeconds) { - u2f.getPortSingleton_(function(port) { - // If we are using Android Google Authenticator or iOS client app, - // do not fire an intent to ask which JS API version to use. - if (port.getPortType) { - var apiVersion; - switch (port.getPortType()) { - case "WrappedIosPort_": - case "WrappedAuthenticatorPort_": - apiVersion = 1.1; - break; - - default: - apiVersion = 0; - break; - } - callback({ js_api_version: apiVersion }); - return; - } - var reqId = ++u2f.reqCounter_; - u2f.callbackMap_[reqId] = callback; - var req = { - type: u2f.MessageTypes.U2F_GET_API_VERSION_REQUEST, - timeoutSeconds: - typeof opt_timeoutSeconds !== "undefined" - ? opt_timeoutSeconds - : u2f.EXTENSION_TIMEOUT_SEC, - requestId: reqId - }; - port.postMessage(req); - }); -}; From 0d1116cc3fcbf3a5282ed332a5d2dd5af4e63f31 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 17 May 2022 18:38:12 +0200 Subject: [PATCH 018/145] Add method FidoMetadataDownloader.refreshBlob() --- NEWS | 7 + webauthn-server-attestation/README.adoc | 36 +- .../doc/Migrating_from_v1.adoc | 2 + .../fido/metadata/FidoMetadataDownloader.java | 279 +- .../metadata/FidoMetadataDownloaderSpec.scala | 2871 +++++++++-------- 5 files changed, 1734 insertions(+), 1461 deletions(-) diff --git a/NEWS b/NEWS index 57dea19b5..b9b6cd622 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,10 @@ +== Version 2.1.0 (unreleased) == + +New features: + +- Added method `FidoMetadataDownloader.refreshBlob()`. + + == Version 2.0.0 == This release removes deprecated APIs and changes some defaults to better align diff --git a/webauthn-server-attestation/README.adoc b/webauthn-server-attestation/README.adoc index fdd8f88b6..e9d662a58 100644 --- a/webauthn-server-attestation/README.adoc +++ b/webauthn-server-attestation/README.adoc @@ -31,12 +31,19 @@ The link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.0.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.0.0/com/yubico/fido/metadata/FidoMetadataDownloader.html#loadCachedBlob()[`loadCachedBlob()`] -is executed, -and then only if the cache is empty or if the cached BLOB is invalid or out of date. +or +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.1.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.0.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()` periodically +You should use some external scheduling mechanism to re-run `loadCachedBlob()` +and/or `refreshBlob()` periodically and rebuild new `FidoMetadataService` instances with the updated metadata contents. You can do this with minimal disruption since the `FidoMetadataService` and `RelyingParty` classes keep no internal mutable state. @@ -95,11 +102,14 @@ Unlike other classes in this module and the core library, link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.0.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.0.0/com/yubico/fido/metadata/FidoMetadataDownloader.html#loadCachedBlob()[`loadCachedBlob()`] -method reads and writes caches. +and +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.1.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.0.0/com/yubico/fido/metadata/FidoMetadataService.html[`FidoMetadataService`], on the other hand, is thread safe, -and `FidoMetadataDownloader` instances can be reused for subsequent `loadCachedBlob()` calls -as long as only one `loadCachedBlob()` call executes at a time. +and `FidoMetadataDownloader` instances can be reused +for subsequent `loadCachedBlob()` and `refreshBlob()` calls +as long as only one call executes at a time. ===== + [source,java] @@ -323,15 +333,19 @@ The library implements these as closely as possible, but with some slight depart ** 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 its + Instead, each time the link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.0.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.1.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. + -If no BLOB exists in cache, or the cached BLOB is invalid, or if the current date is greater than or equal to `nextUpdate`, -then a new BLOB is downloaded. -If the new BLOB is valid, has a correct signature, and has a `no` field greater than the cached BLOB, +Whenever a newly downloaded BLOB is valid, has a correct signature, +and has a `no` field greater than the cached BLOB (if any), then the new BLOB replaces the cached one; -otherwise, the new BLOB is discarded and the cached one is kept until the next execution of `.loadCachedBlob()`. +otherwise, the new BLOB is discarded and the cached one is kept +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 diff --git a/webauthn-server-attestation/doc/Migrating_from_v1.adoc b/webauthn-server-attestation/doc/Migrating_from_v1.adoc index 06f54de16..5b99aa166 100644 --- a/webauthn-server-attestation/doc/Migrating_from_v1.adoc +++ b/webauthn-server-attestation/doc/Migrating_from_v1.adoc @@ -66,6 +66,8 @@ FidoMetadataService metadataService = FidoMetadataService.builder() You may also need to add external logic to occasionally re-run link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.0.0/com/yubico/fido/metadata/FidoMetadataDownloader.html#loadCachedBlob()[`loadCachedBlob()`] +and/or +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.1.0/com/yubico/fido/metadata/FidoMetadataDownloader.html#refreshBlob()[`refreshBlob()`] and reconstruct the `FidoMetadataService`, as `FidoMetadataService` will not automatically update the BLOB on its own. 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 d6e3a4913..0252ddc39 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 @@ -95,10 +95,10 @@ * *

This class is NOT THREAD SAFE since it reads and writes caches. However, it has no internal * mutable state, so instances MAY be reused in single-threaded or externally synchronized contexts. - * See also the {@link #loadCachedBlob()} method. + * See also the {@link #loadCachedBlob()} and {@link #refreshBlob()} methods. * *

Use the {@link #builder() builder} to configure settings, then use the {@link - * #loadCachedBlob()} method to load the metadata BLOB. + * #loadCachedBlob()} and {@link #refreshBlob()} methods to load the metadata BLOB. */ @Slf4j @AllArgsConstructor(access = AccessLevel.PRIVATE) @@ -650,17 +650,17 @@ public FidoMetadataDownloaderBuilder trustHttpsCerts(@NonNull X509Certificate... * subsequent calls. * * @return the successfully retrieved and validated metadata BLOB. - * @throws Base64UrlException if the metadata BLOB is not a well-formed JWT in compact - * serialization. - * @throws CertPathValidatorException if the downloaded or explicitly configured BLOB fails + * @throws Base64UrlException if the explicitly configured or newly downloaded BLOB is not a + * well-formed JWT in compact serialization. + * @throws CertPathValidatorException if the explicitly configured or newly downloaded BLOB fails * certificate path validation. * @throws CertificateException if the trust root certificate was downloaded and passed the * SHA-256 integrity check, but does not contain a currently valid X.509 DER certificate; or * if the BLOB signing certificate chain fails to parse. * @throws DigestException if the trust root certificate was downloaded but failed the SHA-256 * integrity check. - * @throws FidoMetadataDownloaderException if the explicitly configured BLOB (if any) has a bad - * signature. + * @throws FidoMetadataDownloaderException if the explicitly configured or newly downloaded BLOB + * (if any) has a bad signature and there is no cached BLOB to fall back to. * @throws IOException if any of the following fails: downloading the trust root certificate, * downloading the BLOB, reading or writing any cache file (if any), or parsing the BLOB * contents. @@ -680,8 +680,164 @@ public MetadataBLOB loadCachedBlob() CertificateException, IOException, NoSuchAlgorithmException, SignatureException, InvalidKeyException, UnexpectedLegalHeader, DigestException, FidoMetadataDownloaderException { - X509Certificate trustRoot = retrieveTrustRootCert(); - return retrieveBlob(trustRoot); + final X509Certificate trustRoot = retrieveTrustRootCert(); + + final Optional explicit = loadExplicitBlobOnly(trustRoot); + if (explicit.isPresent()) { + log.debug("Explicit BLOB is set - disregarding cache and download."); + return explicit.get(); + } + + final Optional cached = loadCachedBlobOnly(trustRoot); + if (cached.isPresent()) { + log.debug("Cached BLOB exists, checking expiry date..."); + if (cached + .get() + .getPayload() + .getNextUpdate() + .atStartOfDay() + .atZone(clock.getZone()) + .isAfter(clock.instant().atZone(clock.getZone()))) { + log.debug("Cached BLOB has not yet expired - using cached BLOB."); + return cached.get(); + } else { + log.debug("Cached BLOB has expired."); + } + + } else { + log.debug("Cached BLOB does not exist or is invalid."); + } + + return refreshBlobInternal(trustRoot, cached).get(); + } + + /** + * Download and cache a fresh metadata BLOB, or read it from cache if the downloaded BLOB is not + * up to date. + * + *

This method is NOT THREAD SAFE since it reads and writes caches. + * + *

On each execution this will, in order: + * + *

    + *
  1. Download the trust root certificate, if necessary: if the cache is empty, the cache fails + * to load, or the cached cert is not valid at the current time (as determined by the {@link + * FidoMetadataDownloaderBuilder#clock(Clock) clock} setting). + *
  2. If downloaded, cache the trust root certificate using the configured {@link File} or + * {@link Consumer} (see {@link FidoMetadataDownloaderBuilder.Step3}) + *
  3. Download the metadata BLOB. + *
  4. Check the "no" property of the downloaded BLOB and compare it with the + * "no" of the cached BLOB, if any. The one with a greater "no" + * overrides the other, even if its "nextUpdate" is in the past. + *
  5. If the downloaded BLOB has a newer "no", or if no BLOB was cached, verify + * that the value of the downloaded BLOB's "legalHeader" appears in the + * configured {@link FidoMetadataDownloaderBuilder.Step1#expectLegalHeader(String...) + * expectLegalHeader} setting. If not, throw an {@link UnexpectedLegalHeader} exception + * containing the cached BLOB, if any, and the downloaded BLOB. + *
  6. If the downloaded BLOB has an expected + * "legalHeader", cache it using the configured {@link File} or {@link Consumer} (see + * {@link FidoMetadataDownloaderBuilder.Step5}). + *
+ * + * No internal mutable state is maintained between invocations of this method; each invocation + * will reload/rewrite caches, perform downloads and check the "legalHeader" + * as necessary. You may therefore reuse a {@link FidoMetadataDownloader} instance and, + * for example, call this method periodically to refresh the BLOB. Each call will return a new + * {@link MetadataBLOB} instance; ones already returned will not be updated by subsequent calls. + * + * @return the successfully retrieved and validated metadata BLOB. + * @throws Base64UrlException if the explicitly configured or newly downloaded BLOB is not a + * well-formed JWT in compact serialization. + * @throws CertPathValidatorException if the explicitly configured or newly downloaded BLOB fails + * certificate path validation. + * @throws CertificateException if the trust root certificate was downloaded and passed the + * SHA-256 integrity check, but does not contain a currently valid X.509 DER certificate; or + * if the BLOB signing certificate chain fails to parse. + * @throws DigestException if the trust root certificate was downloaded but failed the SHA-256 + * integrity check. + * @throws FidoMetadataDownloaderException if the explicitly configured or newly downloaded BLOB + * (if any) has a bad signature and there is no cached BLOB to fall back to. + * @throws IOException if any of the following fails: downloading the trust root certificate, + * downloading the BLOB, reading or writing any cache file (if any), or parsing the BLOB + * contents. + * @throws InvalidAlgorithmParameterException if certificate path validation fails. + * @throws InvalidKeyException if signature verification fails. + * @throws NoSuchAlgorithmException if signature verification fails, or if the SHA-256 algorithm + * is not available. + * @throws SignatureException if signature verification fails. + * @throws UnexpectedLegalHeader if the downloaded BLOB (if any) contains a "legalHeader" + * value not configured in {@link + * FidoMetadataDownloaderBuilder.Step1#expectLegalHeader(String...) + * expectLegalHeader(String...)} but is otherwise valid. The downloaded BLOB will not be + * written to cache in this case. + */ + public MetadataBLOB refreshBlob() + throws CertPathValidatorException, InvalidAlgorithmParameterException, Base64UrlException, + CertificateException, IOException, NoSuchAlgorithmException, SignatureException, + InvalidKeyException, UnexpectedLegalHeader, DigestException, + FidoMetadataDownloaderException { + final X509Certificate trustRoot = retrieveTrustRootCert(); + + final Optional explicit = loadExplicitBlobOnly(trustRoot); + if (explicit.isPresent()) { + log.debug("Explicit BLOB is set - disregarding cache and download."); + return explicit.get(); + } + + final Optional cached = loadCachedBlobOnly(trustRoot); + if (cached.isPresent()) { + log.debug("Cached BLOB exists, proceeding to compare against fresh BLOB..."); + } else { + log.debug("Cached BLOB does not exist or is invalid."); + } + + return refreshBlobInternal(trustRoot, cached).get(); + } + + private Optional refreshBlobInternal( + @NonNull X509Certificate trustRoot, @NonNull Optional cached) + throws CertPathValidatorException, InvalidAlgorithmParameterException, Base64UrlException, + CertificateException, IOException, NoSuchAlgorithmException, SignatureException, + InvalidKeyException, UnexpectedLegalHeader, FidoMetadataDownloaderException { + + try { + log.debug("Attempting to download new BLOB..."); + final ByteArray downloadedBytes = download(blobUrl); + final MetadataBLOB downloadedBlob = parseAndVerifyBlob(downloadedBytes, trustRoot); + log.debug("New BLOB downloaded."); + + if (cached.isPresent()) { + log.debug("Cached BLOB exists - checking if new BLOB has a higher \"no\"..."); + if (downloadedBlob.getPayload().getNo() <= cached.get().getPayload().getNo()) { + log.debug("New BLOB does not have a higher \"no\" - using cached BLOB instead."); + return cached; + } + log.debug("New BLOB has a higher \"no\" - proceeding with new BLOB."); + } + + log.debug("Checking legalHeader in new BLOB..."); + if (!expectedLegalHeaders.contains(downloadedBlob.getPayload().getLegalHeader())) { + throw new UnexpectedLegalHeader(cached.orElse(null), downloadedBlob); + } + + log.debug("Writing new BLOB to cache..."); + if (blobCacheFile != null) { + new FileOutputStream(blobCacheFile).write(downloadedBytes.getBytes()); + } + + if (blobCacheConsumer != null) { + blobCacheConsumer.accept(downloadedBytes); + } + + return Optional.of(downloadedBlob); + } catch (FidoMetadataDownloaderException e) { + if (e.getReason() == Reason.BAD_SIGNATURE && cached.isPresent()) { + log.debug("New BLOB has bad signature - falling back to cached BLOB."); + return cached; + } else { + throw e; + } + } } /** @@ -745,95 +901,56 @@ private X509Certificate retrieveTrustRootCert() /** * @throws Base64UrlException if the metadata BLOB is not a well-formed JWT in compact * serialization. - * @throws CertPathValidatorException if the downloaded or explicitly configured BLOB fails - * certificate path validation. + * @throws CertPathValidatorException if the explicitly configured BLOB fails certificate path + * validation. * @throws CertificateException if the BLOB signing certificate chain fails to parse. - * @throws IOException if any of the following fails: downloading the BLOB, reading or writing the - * cache file (if any), or parsing the BLOB contents. + * @throws IOException on failure to parse the BLOB contents. * @throws InvalidAlgorithmParameterException if certificate path validation fails. * @throws InvalidKeyException if signature verification fails. - * @throws UnexpectedLegalHeader if the downloaded BLOB (if any) contains a "legalHeader" - * value not configured in {@link - * FidoMetadataDownloaderBuilder.Step1#expectLegalHeader(String...) - * expectLegalHeader(String...)} but is otherwise valid. The downloaded BLOB will not be - * written to cache in this case. * @throws NoSuchAlgorithmException if signature verification fails. * @throws SignatureException if signature verification fails. * @throws FidoMetadataDownloaderException if the explicitly configured BLOB (if any) has a bad * signature. */ - private MetadataBLOB retrieveBlob(X509Certificate trustRootCertificate) + private Optional loadExplicitBlobOnly(X509Certificate trustRootCertificate) throws Base64UrlException, CertPathValidatorException, CertificateException, IOException, - InvalidAlgorithmParameterException, InvalidKeyException, UnexpectedLegalHeader, - NoSuchAlgorithmException, SignatureException, FidoMetadataDownloaderException { + InvalidAlgorithmParameterException, InvalidKeyException, NoSuchAlgorithmException, + SignatureException, FidoMetadataDownloaderException { if (blobJwt != null) { - return parseAndVerifyBlob( - new ByteArray(blobJwt.getBytes(StandardCharsets.UTF_8)), trustRootCertificate); + return Optional.of( + parseAndVerifyBlob( + new ByteArray(blobJwt.getBytes(StandardCharsets.UTF_8)), trustRootCertificate)); } else { + return Optional.empty(); + } + } - final Optional cachedContents; - if (blobCacheFile != null) { - cachedContents = readCacheFile(blobCacheFile); - } else { - cachedContents = blobCacheSupplier.get(); - } + private Optional loadCachedBlobOnly(X509Certificate trustRootCertificate) { - final MetadataBLOB cachedBlob = - cachedContents - .map( - cached -> { - try { - return parseAndVerifyBlob(cached, trustRootCertificate); - } catch (Exception e) { - return null; - } - }) - .orElse(null); - - if (cachedBlob != null - && cachedBlob - .getPayload() - .getNextUpdate() - .atStartOfDay() - .atZone(clock.getZone()) - .isAfter(clock.instant().atZone(clock.getZone()))) { - return cachedBlob; + final Optional cachedContents; + if (blobCacheFile != null) { + log.debug("Attempting to read BLOB from cache file..."); - } else { - final ByteArray downloaded = download(blobUrl); - try { - final MetadataBLOB downloadedBlob = parseAndVerifyBlob(downloaded, trustRootCertificate); - - if (cachedBlob == null - || downloadedBlob.getPayload().getNo() > cachedBlob.getPayload().getNo()) { - if (expectedLegalHeaders.contains(downloadedBlob.getPayload().getLegalHeader())) { - if (blobCacheFile != null) { - new FileOutputStream(blobCacheFile).write(downloaded.getBytes()); - } - - if (blobCacheConsumer != null) { - blobCacheConsumer.accept(downloaded); - } - - return downloadedBlob; - } else { - throw new UnexpectedLegalHeader(cachedBlob, downloadedBlob); - } - - } else { - return cachedBlob; - } - } catch (FidoMetadataDownloaderException e) { - if (e.getReason() == FidoMetadataDownloaderException.Reason.BAD_SIGNATURE - && cachedBlob != null) { - return cachedBlob; - } else { - throw e; - } - } + try { + cachedContents = readCacheFile(blobCacheFile); + } catch (IOException e) { + return Optional.empty(); } + } else { + log.debug("Attempting to read BLOB from cache Supplier..."); + cachedContents = blobCacheSupplier.get(); } + + return cachedContents.map( + cached -> { + try { + return parseAndVerifyBlob(cached, trustRootCertificate); + } catch (Exception e) { + log.debug("Failed to read or parse cached BLOB.", e); + return null; + } + }); } private Optional readCacheFile(File cacheFile) throws IOException { 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 e23c2b126..c5b41317c 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 @@ -262,768 +262,347 @@ class FidoMetadataDownloaderSpec (server, s"https://localhost:${port}", tlsCert) } - describe("§3.2. Metadata BLOB object processing rules") { - describe("1. Download and cache the root signing trust anchor from the respective MDS root location e.g. More information can be found at https://fidoalliance.org/metadata/") { - it( - "The trust root is downloaded and cached if there isn't a supplier-cached one." - ) { - val random = new SecureRandom() - val trustRootDistinguishedName = - s"CN=Test trust root ${random.nextInt(10000)}" - val (trustRootCert, caKeypair, caName) = - makeTrustRootCert(distinguishedName = trustRootDistinguishedName) - val (blobCert, blobKeypair, _) = makeCert(caKeypair, caName) - val blobJwt = - makeBlob(List(blobCert), blobKeypair, LocalDate.now()) - val crls = List[CRL]( - TestAuthenticator.buildCrl( - caName, - caKeypair.getPrivate, - "SHA256withECDSA", - CertValidFrom, - CertValidTo, - ) - ) - - var writtenCache: Option[ByteArray] = None - - val (server, serverUrl, httpsCert) = - makeHttpServer("/trust-root.der", trustRootCert.getEncoded) - startServer(server) - - val blob = FidoMetadataDownloader - .builder() - .expectLegalHeader("Kom ihåg att du aldrig får snyta dig i mattan!") - .downloadTrustRoot( - new URL(s"${serverUrl}/trust-root.der"), - Set( - TestAuthenticator.sha256(new ByteArray(trustRootCert.getEncoded)) - ).asJava, - ) - .useTrustRootCache( - () => Optional.empty(), - newCache => { writtenCache = Some(newCache) }, - ) - .useBlob(blobJwt) - .clock(Clock.fixed(CertValidFrom, ZoneOffset.UTC)) - .useCrls(crls.asJava) - .trustHttpsCerts(httpsCert) - .build() - .loadCachedBlob - blob should not be null - blob.getHeader.getX5c.get.asScala.last.getIssuerDN.getName should equal( - trustRootDistinguishedName - ) - writtenCache should equal(Some(new ByteArray(trustRootCert.getEncoded))) - } - - it("The trust root is downloaded and cached if there's an expired one in supplier-cache.") { - val random = new SecureRandom() - - val oldTrustRootDistinguishedName = - s"CN=Test trust root ${random.nextInt(10000)}" - val newTrustRootDistinguishedName = - s"CN=Test trust root ${random.nextInt(10000) + 10000}" - val (oldTrustRootCert, _, _) = - makeTrustRootCert( - distinguishedName = oldTrustRootDistinguishedName, - validFrom = CertValidFrom.minusSeconds(600), - validTo = CertValidFrom.minusSeconds(1), - ) - val (newTrustRootCert, caKeypair, caName) = - makeTrustRootCert(distinguishedName = newTrustRootDistinguishedName) - - val (blobCert, blobKeypair, _) = makeCert(caKeypair, caName) - val blobJwt = - makeBlob(List(blobCert), blobKeypair, LocalDate.now()) - val crls = List[CRL]( - TestAuthenticator.buildCrl( - caName, - caKeypair.getPrivate, - "SHA256withECDSA", - CertValidFrom, - CertValidTo, - ) - ) - - var writtenCache: Option[ByteArray] = None - - val (server, serverUrl, httpsCert) = - makeHttpServer("/trust-root.der", newTrustRootCert.getEncoded) - startServer(server) - - val blob = FidoMetadataDownloader - .builder() - .expectLegalHeader("Kom ihåg att du aldrig får snyta dig i mattan!") - .downloadTrustRoot( - new URL(s"${serverUrl}/trust-root.der"), - Set( - TestAuthenticator.sha256( - new ByteArray(newTrustRootCert.getEncoded) - ) - ).asJava, - ) - .useTrustRootCache( - () => Optional.of(new ByteArray(oldTrustRootCert.getEncoded)), - newCache => { writtenCache = Some(newCache) }, - ) - .useBlob(blobJwt) - .clock(Clock.fixed(CertValidFrom, ZoneOffset.UTC)) - .useCrls(crls.asJava) - .trustHttpsCerts(httpsCert) - .build() - .loadCachedBlob - blob should not be null - blob.getHeader.getX5c.get.asScala.last.getIssuerDN.getName should equal( - newTrustRootDistinguishedName - ) - writtenCache should equal( - Some(new ByteArray(newTrustRootCert.getEncoded)) - ) - } + private def withEachLoadMethod( + body: (FidoMetadataDownloader => MetadataBLOB) => Unit + ): Unit = { + describe("[using loadCachedBlob()]") { + body(_.loadCachedBlob()) + } + describe("[using refreshBlob()]") { + body(_.refreshBlob()) + } + } - it( - "The trust root is not downloaded if there's a valid one in file cache." - ) { - val random = new SecureRandom() - val trustRootDistinguishedName = - s"CN=Test trust root ${random.nextInt(10000)}" - val (trustRootCert, caKeypair, caName) = - makeTrustRootCert(distinguishedName = trustRootDistinguishedName) - val (blobCert, blobKeypair, _) = makeCert(caKeypair, caName) - val blobJwt = - makeBlob(List(blobCert), blobKeypair, LocalDate.now()) - val crls = List[CRL]( - TestAuthenticator.buildCrl( - caName, - caKeypair.getPrivate, - "SHA256withECDSA", - CertValidFrom, - CertValidTo, + describe("§3.2. Metadata BLOB object processing rules") { + withEachLoadMethod { load => + describe("1. Download and cache the root signing trust anchor from the respective MDS root location e.g. More information can be found at https://fidoalliance.org/metadata/") { + it( + "The trust root is downloaded and cached if there isn't a supplier-cached one." + ) { + val random = new SecureRandom() + val trustRootDistinguishedName = + s"CN=Test trust root ${random.nextInt(10000)}" + val (trustRootCert, caKeypair, caName) = + makeTrustRootCert(distinguishedName = trustRootDistinguishedName) + val (blobCert, blobKeypair, _) = makeCert(caKeypair, caName) + val blobJwt = + makeBlob(List(blobCert), blobKeypair, LocalDate.now()) + val crls = List[CRL]( + TestAuthenticator.buildCrl( + caName, + caKeypair.getPrivate, + "SHA256withECDSA", + CertValidFrom, + CertValidTo, + ) ) - ) - - val cacheFile = File.createTempFile( - s"${getClass.getCanonicalName}_test_cache_", - ".tmp", - ) - val f = new FileOutputStream(cacheFile) - f.write(trustRootCert.getEncoded) - f.close() - cacheFile.deleteOnExit() - - val blob = FidoMetadataDownloader - .builder() - .expectLegalHeader("Kom ihåg att du aldrig får snyta dig i mattan!") - .useDefaultTrustRoot() - .useTrustRootCacheFile(cacheFile) - .useBlob(blobJwt) - .clock(Clock.fixed(CertValidFrom, ZoneOffset.UTC)) - .useCrls(crls.asJava) - .build() - .loadCachedBlob - blob should not be null - blob.getHeader.getX5c.get.asScala.last.getIssuerDN.getName should equal( - trustRootDistinguishedName - ) - } - it( - "The trust root is downloaded and cached if there isn't a file-cached one." - ) { - val random = new SecureRandom() - val trustRootDistinguishedName = - s"CN=Test trust root ${random.nextInt(10000)}" - val (trustRootCert, caKeypair, caName) = - makeTrustRootCert(distinguishedName = trustRootDistinguishedName) - val (blobCert, blobKeypair, _) = makeCert(caKeypair, caName) - val blobJwt = - makeBlob(List(blobCert), blobKeypair, LocalDate.now()) - val crls = List[CRL]( - TestAuthenticator.buildCrl( - caName, - caKeypair.getPrivate, - "SHA256withECDSA", - CertValidFrom, - CertValidTo, - ) - ) - - val (server, serverUrl, httpsCert) = - makeHttpServer("/trust-root.der", trustRootCert.getEncoded) - startServer(server) - - val cacheFile = File.createTempFile( - s"${getClass.getCanonicalName}_test_cache_", - ".tmp", - ) - cacheFile.delete() - cacheFile.deleteOnExit() - - val blob = FidoMetadataDownloader - .builder() - .expectLegalHeader("Kom ihåg att du aldrig får snyta dig i mattan!") - .downloadTrustRoot( - new URL(s"${serverUrl}/trust-root.der"), - Set( - TestAuthenticator.sha256(new ByteArray(trustRootCert.getEncoded)) - ).asJava, - ) - .useTrustRootCacheFile(cacheFile) - .useBlob(blobJwt) - .clock(Clock.fixed(CertValidFrom, ZoneOffset.UTC)) - .useCrls(crls.asJava) - .trustHttpsCerts(httpsCert) - .build() - .loadCachedBlob - blob should not be null - blob.getHeader.getX5c.get.asScala.last.getIssuerDN.getName should equal( - trustRootDistinguishedName - ) - cacheFile.exists() should be(true) - BinaryUtil.readAll(new FileInputStream(cacheFile)) should equal( - trustRootCert.getEncoded - ) - } + var writtenCache: Option[ByteArray] = None - it("The trust root is downloaded and cached if there's an expired one in file cache.") { - val random = new SecureRandom() - - val oldTrustRootDistinguishedName = - s"CN=Test trust root ${random.nextInt(10000)}" - val newTrustRootDistinguishedName = - s"CN=Test trust root ${random.nextInt(10000) + 10000}" - val (oldTrustRootCert, _, _) = - makeTrustRootCert( - distinguishedName = oldTrustRootDistinguishedName, - validFrom = CertValidFrom.minusSeconds(600), - validTo = CertValidFrom.minusSeconds(1), - ) - val (newTrustRootCert, caKeypair, caName) = - makeTrustRootCert(distinguishedName = newTrustRootDistinguishedName) + val (server, serverUrl, httpsCert) = + makeHttpServer("/trust-root.der", trustRootCert.getEncoded) + startServer(server) - val (blobCert, blobKeypair, _) = makeCert(caKeypair, caName) - val blobJwt = - makeBlob(List(blobCert), blobKeypair, LocalDate.now()) - val crls = List[CRL]( - TestAuthenticator.buildCrl( - caName, - caKeypair.getPrivate, - "SHA256withECDSA", - CertValidFrom, - CertValidTo, - ) - ) - - val (server, serverUrl, httpsCert) = - makeHttpServer("/trust-root.der", newTrustRootCert.getEncoded) - startServer(server) - - val cacheFile = File.createTempFile( - s"${getClass.getCanonicalName}_test_cache_", - ".tmp", - ) - val f = new FileOutputStream(cacheFile) - f.write(oldTrustRootCert.getEncoded) - f.close() - cacheFile.deleteOnExit() - - val blob = FidoMetadataDownloader - .builder() - .expectLegalHeader("Kom ihåg att du aldrig får snyta dig i mattan!") - .downloadTrustRoot( - new URL(s"${serverUrl}/trust-root.der"), - Set( - TestAuthenticator.sha256( - new ByteArray(newTrustRootCert.getEncoded) + val blob = load( + FidoMetadataDownloader + .builder() + .expectLegalHeader( + "Kom ihåg att du aldrig får snyta dig i mattan!" ) - ).asJava, - ) - .useTrustRootCacheFile(cacheFile) - .useBlob(blobJwt) - .clock(Clock.fixed(CertValidFrom, ZoneOffset.UTC)) - .useCrls(crls.asJava) - .trustHttpsCerts(httpsCert) - .build() - .loadCachedBlob - blob should not be null - blob.getHeader.getX5c.get.asScala.last.getIssuerDN.getName should equal( - newTrustRootDistinguishedName - ) - cacheFile.exists() should be(true) - BinaryUtil.readAll(new FileInputStream(cacheFile)) should equal( - newTrustRootCert.getEncoded - ) - } - - it("The trust root is not downloaded if there's a valid one in supplier-cache.") { - val random = new SecureRandom() - val trustRootDistinguishedName = - s"CN=Test trust root ${random.nextInt(10000)}" - val (trustRootCert, caKeypair, caName) = - makeTrustRootCert(distinguishedName = trustRootDistinguishedName) - val (blobCert, blobKeypair, _) = makeCert(caKeypair, caName) - val blobJwt = - makeBlob(List(blobCert), blobKeypair, LocalDate.now()) - val crls = List[CRL]( - TestAuthenticator.buildCrl( - caName, - caKeypair.getPrivate, - "SHA256withECDSA", - CertValidFrom, - CertValidTo, + .downloadTrustRoot( + new URL(s"${serverUrl}/trust-root.der"), + Set( + TestAuthenticator.sha256( + new ByteArray(trustRootCert.getEncoded) + ) + ).asJava, + ) + .useTrustRootCache( + () => Optional.empty(), + newCache => { + writtenCache = Some(newCache) + }, + ) + .useBlob(blobJwt) + .clock(Clock.fixed(CertValidFrom, ZoneOffset.UTC)) + .useCrls(crls.asJava) + .trustHttpsCerts(httpsCert) + .build() ) - ) - - var writtenCache: Option[ByteArray] = None - val blob = FidoMetadataDownloader - .builder() - .expectLegalHeader("Kom ihåg att du aldrig får snyta dig i mattan!") - .useDefaultTrustRoot() - .useTrustRootCache( - () => Optional.of(new ByteArray(trustRootCert.getEncoded)), - newCache => { writtenCache = Some(newCache) }, + blob should not be null + blob.getHeader.getX5c.get.asScala.last.getIssuerDN.getName should equal( + trustRootDistinguishedName ) - .useBlob(blobJwt) - .clock(Clock.fixed(CertValidFrom, ZoneOffset.UTC)) - .useCrls(crls.asJava) - .build() - .loadCachedBlob - blob should not be null - blob.getHeader.getX5c.get.asScala.last.getIssuerDN.getName should equal( - trustRootDistinguishedName - ) - writtenCache should equal(None) - } - - it("The downloaded trust root cert must match one of the expected SHA256 hashes.") { - val (trustRootCert, caKeypair, caName) = makeTrustRootCert() - val (blobCert, blobKeypair, _) = makeCert(caKeypair, caName) - val blobJwt = makeBlob(List(blobCert), blobKeypair, LocalDate.now()) - val crls = List[CRL]( - TestAuthenticator.buildCrl( - caName, - caKeypair.getPrivate, - "SHA256withECDSA", - CertValidFrom, - CertValidTo, + writtenCache should equal( + Some(new ByteArray(trustRootCert.getEncoded)) ) - ) - - val (server, serverUrl, httpsCert) = - makeHttpServer("/trust-root.der", trustRootCert.getEncoded) - startServer(server) - - def testWithHashes(hashes: Set[ByteArray]): MetadataBLOB = { - FidoMetadataDownloader - .builder() - .expectLegalHeader("Kom ihåg att du aldrig får snyta dig i mattan!") - .downloadTrustRoot( - new URL(s"${serverUrl}/trust-root.der"), - hashes.asJava, - ) - .useTrustRootCache(() => Optional.empty(), _ => {}) - .useBlob(blobJwt) - .useCrls(crls.asJava) - .trustHttpsCerts(httpsCert) - .clock(Clock.fixed(CertValidFrom, ZoneOffset.UTC)) - .build() - .loadCachedBlob } - val goodHash = - TestAuthenticator.sha256(new ByteArray(trustRootCert.getEncoded)) - val badHash = TestAuthenticator.sha256(goodHash) - - a[DigestException] should be thrownBy { testWithHashes(Set(badHash)) } - testWithHashes(Set(goodHash)) should not be null - testWithHashes(Set(badHash, goodHash)) should not be null - } - } - - describe("2. To validate the digital certificates used in the digital signature, the certificate revocation information MUST be available in the form of CRLs at the respective MDS CRL location e.g. More information can be found at https://fidoalliance.org/metadata/") { - val (trustRootCert, caKeypair, caName) = makeTrustRootCert() - val (blobCert, blobKeypair, _) = makeCert(caKeypair, caName) - val blobJwt = - makeBlob(List(blobCert), blobKeypair, LocalDate.parse("2022-01-19")) - - it( - "Verification fails if the certs don't declare CRL distribution points." - ) { - val thrown = the[CertPathValidatorException] thrownBy { - FidoMetadataDownloader - .builder() - .expectLegalHeader("Kom ihåg att du aldrig får snyta dig i mattan!") - .useTrustRoot(trustRootCert) - .useBlob(blobJwt) - .clock(Clock.fixed(CertValidFrom, ZoneOffset.UTC)) - .build() - .loadCachedBlob() - } - thrown.getReason should equal( - BasicReason.UNDETERMINED_REVOCATION_STATUS - ) - } + it("The trust root is downloaded and cached if there's an expired one in supplier-cache.") { + val random = new SecureRandom() + + val oldTrustRootDistinguishedName = + s"CN=Test trust root ${random.nextInt(10000)}" + val newTrustRootDistinguishedName = + s"CN=Test trust root ${random.nextInt(10000) + 10000}" + val (oldTrustRootCert, _, _) = + makeTrustRootCert( + distinguishedName = oldTrustRootDistinguishedName, + validFrom = CertValidFrom.minusSeconds(600), + validTo = CertValidFrom.minusSeconds(1), + ) + val (newTrustRootCert, caKeypair, caName) = + makeTrustRootCert(distinguishedName = newTrustRootDistinguishedName) - it("Verification succeeds if explicitly given appropriate CRLs.") { - val crls = List[CRL]( - TestAuthenticator.buildCrl( - caName, - caKeypair.getPrivate, - "SHA256withECDSA", - CertValidFrom, - CertValidTo, + val (blobCert, blobKeypair, _) = makeCert(caKeypair, caName) + val blobJwt = + makeBlob(List(blobCert), blobKeypair, LocalDate.now()) + val crls = List[CRL]( + TestAuthenticator.buildCrl( + caName, + caKeypair.getPrivate, + "SHA256withECDSA", + CertValidFrom, + CertValidTo, + ) ) - ) - - val blob = FidoMetadataDownloader - .builder() - .expectLegalHeader("Kom ihåg att du aldrig får snyta dig i mattan!") - .useTrustRoot(trustRootCert) - .useBlob(blobJwt) - .useCrls(crls.asJava) - .clock(Clock.fixed(CertValidFrom, ZoneOffset.UTC)) - .build() - .loadCachedBlob() - blob should not be null - } - describe("Intermediate certificates") { + var writtenCache: Option[ByteArray] = None - val (intermediateCert, intermediateKeypair, intermediateName) = - makeCert( - caKeypair, - caName, - isCa = true, - name = "CN=Yubico java-webauthn-server unit tests intermediate CA, O=Yubico", - ) - val (blobCert, blobKeypair, _) = - makeCert(intermediateKeypair, intermediateName) - val blobJwt = makeBlob( - List(blobCert, intermediateCert), - blobKeypair, - LocalDate.parse("2022-01-19"), - ) - - it("each require their own CRL.") { - val thrown = the[CertPathValidatorException] thrownBy { + val (server, serverUrl, httpsCert) = + makeHttpServer("/trust-root.der", newTrustRootCert.getEncoded) + startServer(server) + + val blob = load( FidoMetadataDownloader .builder() .expectLegalHeader( "Kom ihåg att du aldrig får snyta dig i mattan!" ) - .useTrustRoot(trustRootCert) + .downloadTrustRoot( + new URL(s"${serverUrl}/trust-root.der"), + Set( + TestAuthenticator.sha256( + new ByteArray(newTrustRootCert.getEncoded) + ) + ).asJava, + ) + .useTrustRootCache( + () => Optional.of(new ByteArray(oldTrustRootCert.getEncoded)), + newCache => { + writtenCache = Some(newCache) + }, + ) .useBlob(blobJwt) .clock(Clock.fixed(CertValidFrom, ZoneOffset.UTC)) + .useCrls(crls.asJava) + .trustHttpsCerts(httpsCert) .build() - .loadCachedBlob() - } - thrown.getReason should equal( - BasicReason.UNDETERMINED_REVOCATION_STATUS ) - - val rootCrl = TestAuthenticator.buildCrl( - caName, - caKeypair.getPrivate, - "SHA256withECDSA", - CertValidFrom, - CertValidTo, + blob should not be null + blob.getHeader.getX5c.get.asScala.last.getIssuerDN.getName should equal( + newTrustRootDistinguishedName + ) + writtenCache should equal( + Some(new ByteArray(newTrustRootCert.getEncoded)) ) + } - val thrown2 = the[CertPathValidatorException] thrownBy { - FidoMetadataDownloader - .builder() - .expectLegalHeader( - "Kom ihåg att du aldrig får snyta dig i mattan!" - ) - .useTrustRoot(trustRootCert) - .useBlob(blobJwt) - .clock(Clock.fixed(CertValidFrom, ZoneOffset.UTC)) - .useCrls(List[CRL](rootCrl).asJava) - .build() - .loadCachedBlob() - } - thrown2.getReason should equal( - BasicReason.UNDETERMINED_REVOCATION_STATUS + it( + "The trust root is not downloaded if there's a valid one in file cache." + ) { + val random = new SecureRandom() + val trustRootDistinguishedName = + s"CN=Test trust root ${random.nextInt(10000)}" + val (trustRootCert, caKeypair, caName) = + makeTrustRootCert(distinguishedName = trustRootDistinguishedName) + val (blobCert, blobKeypair, _) = makeCert(caKeypair, caName) + val blobJwt = + makeBlob(List(blobCert), blobKeypair, LocalDate.now()) + val crls = List[CRL]( + TestAuthenticator.buildCrl( + caName, + caKeypair.getPrivate, + "SHA256withECDSA", + CertValidFrom, + CertValidTo, + ) ) - val intermediateCrl = TestAuthenticator.buildCrl( - intermediateName, - intermediateKeypair.getPrivate, - "SHA256withECDSA", - CertValidFrom, - CertValidTo, + val cacheFile = File.createTempFile( + s"${getClass.getCanonicalName}_test_cache_", + ".tmp", ) + val f = new FileOutputStream(cacheFile) + f.write(trustRootCert.getEncoded) + f.close() + cacheFile.deleteOnExit() - val thrown3 = the[CertPathValidatorException] thrownBy { + val blob = load( FidoMetadataDownloader .builder() .expectLegalHeader( "Kom ihåg att du aldrig får snyta dig i mattan!" ) - .useTrustRoot(trustRootCert) + .useDefaultTrustRoot() + .useTrustRootCacheFile(cacheFile) .useBlob(blobJwt) .clock(Clock.fixed(CertValidFrom, ZoneOffset.UTC)) - .useCrls(List[CRL](intermediateCrl).asJava) + .useCrls(crls.asJava) .build() - .loadCachedBlob() - } - thrown3.getReason should equal( - BasicReason.UNDETERMINED_REVOCATION_STATUS ) - - val blob = FidoMetadataDownloader - .builder() - .expectLegalHeader("Kom ihåg att du aldrig får snyta dig i mattan!") - .useTrustRoot(trustRootCert) - .useBlob(blobJwt) - .useCrls(List[CRL](rootCrl, intermediateCrl).asJava) - .clock(Clock.fixed(CertValidFrom, ZoneOffset.UTC)) - .build() - .loadCachedBlob() blob should not be null + blob.getHeader.getX5c.get.asScala.last.getIssuerDN.getName should equal( + trustRootDistinguishedName + ) } - it("can revoke downstream certificates too.") { - val rootCrl = TestAuthenticator.buildCrl( - caName, - caKeypair.getPrivate, - "SHA256withECDSA", - CertValidFrom, - CertValidTo, + it( + "The trust root is downloaded and cached if there isn't a file-cached one." + ) { + val random = new SecureRandom() + val trustRootDistinguishedName = + s"CN=Test trust root ${random.nextInt(10000)}" + val (trustRootCert, caKeypair, caName) = + makeTrustRootCert(distinguishedName = trustRootDistinguishedName) + val (blobCert, blobKeypair, _) = makeCert(caKeypair, caName) + val blobJwt = + makeBlob(List(blobCert), blobKeypair, LocalDate.now()) + val crls = List[CRL]( + TestAuthenticator.buildCrl( + caName, + caKeypair.getPrivate, + "SHA256withECDSA", + CertValidFrom, + CertValidTo, + ) ) - val intermediateCrl = TestAuthenticator.buildCrl( - intermediateName, - intermediateKeypair.getPrivate, - "SHA256withECDSA", - CertValidFrom, - CertValidTo, - revoked = Set(blobCert), + + val (server, serverUrl, httpsCert) = + makeHttpServer("/trust-root.der", trustRootCert.getEncoded) + startServer(server) + + val cacheFile = File.createTempFile( + s"${getClass.getCanonicalName}_test_cache_", + ".tmp", ) - val crls = List(rootCrl, intermediateCrl) + cacheFile.delete() + cacheFile.deleteOnExit() - val thrown = the[CertPathValidatorException] thrownBy { + val blob = load( FidoMetadataDownloader .builder() .expectLegalHeader( "Kom ihåg att du aldrig får snyta dig i mattan!" ) - .useTrustRoot(trustRootCert) + .downloadTrustRoot( + new URL(s"${serverUrl}/trust-root.der"), + Set( + TestAuthenticator.sha256( + new ByteArray(trustRootCert.getEncoded) + ) + ).asJava, + ) + .useTrustRootCacheFile(cacheFile) .useBlob(blobJwt) + .clock(Clock.fixed(CertValidFrom, ZoneOffset.UTC)) .useCrls(crls.asJava) - .clock(Clock.fixed(CertValidFrom.plusSeconds(1), ZoneOffset.UTC)) + .trustHttpsCerts(httpsCert) .build() - .loadCachedBlob() - } - thrown.getReason should equal( - BasicReason.REVOKED ) - } - } - } - - describe("3. The FIDO Server MUST be able to download the latest metadata BLOB object from the well-known URL when appropriate, e.g. https://mds.fidoalliance.org/. The nextUpdate field of the Metadata BLOB specifies a date when the download SHOULD occur at latest.") { - it("The BLOB is downloaded if there isn't a cached one.") { - val random = new SecureRandom() - val blobLegalHeader = - s"Kom ihåg att du aldrig får snyta dig i mattan! ${random.nextInt(10000)}" - val blobNo = random.nextInt(10000); - - val (trustRootCert, caKeypair, caName) = makeTrustRootCert() - val (blobCert, blobKeypair, _) = makeCert(caKeypair, caName) - val blobJwt = - makeBlob( - List(blobCert), - blobKeypair, - LocalDate.parse("2022-01-19"), - no = blobNo, - legalHeader = blobLegalHeader, + blob should not be null + blob.getHeader.getX5c.get.asScala.last.getIssuerDN.getName should equal( + trustRootDistinguishedName ) - val crls = List[CRL]( - TestAuthenticator.buildCrl( - caName, - caKeypair.getPrivate, - "SHA256withECDSA", - CertValidFrom, - CertValidTo, + cacheFile.exists() should be(true) + BinaryUtil.readAll(new FileInputStream(cacheFile)) should equal( + trustRootCert.getEncoded ) - ) - - val (server, serverUrl, httpsCert) = - makeHttpServer("/blob.jwt", blobJwt) - startServer(server) - - val blob = FidoMetadataDownloader - .builder() - .expectLegalHeader(blobLegalHeader) - .useTrustRoot(trustRootCert) - .downloadBlob(new URL(s"${serverUrl}/blob.jwt")) - .useBlobCache(() => Optional.empty(), _ => {}) - .clock(Clock.fixed(CertValidFrom, ZoneOffset.UTC)) - .useCrls(crls.asJava) - .trustHttpsCerts(httpsCert) - .build() - .loadCachedBlob() - .getPayload - blob should not be null - blob.getLegalHeader should equal(blobLegalHeader) - blob.getNo should equal(blobNo) - } + } - it("The BLOB is downloaded if the cached one is out of date.") { - val oldBlobNo = 1 - val newBlobNo = 2 + it("The trust root is downloaded and cached if there's an expired one in file cache.") { + val random = new SecureRandom() + + val oldTrustRootDistinguishedName = + s"CN=Test trust root ${random.nextInt(10000)}" + val newTrustRootDistinguishedName = + s"CN=Test trust root ${random.nextInt(10000) + 10000}" + val (oldTrustRootCert, _, _) = + makeTrustRootCert( + distinguishedName = oldTrustRootDistinguishedName, + validFrom = CertValidFrom.minusSeconds(600), + validTo = CertValidFrom.minusSeconds(1), + ) + val (newTrustRootCert, caKeypair, caName) = + makeTrustRootCert(distinguishedName = newTrustRootDistinguishedName) - val (trustRootCert, caKeypair, caName) = makeTrustRootCert() - val (blobCert, blobKeypair, _) = makeCert(caKeypair, caName) - val oldBlobJwt = - makeBlob( - List(blobCert), - blobKeypair, - 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 = 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)) - ), - _ => {}, + val (blobCert, blobKeypair, _) = makeCert(caKeypair, caName) + val blobJwt = + makeBlob(List(blobCert), blobKeypair, LocalDate.now()) + val crls = List[CRL]( + TestAuthenticator.buildCrl( + caName, + caKeypair.getPrivate, + "SHA256withECDSA", + CertValidFrom, + CertValidTo, + ) ) - .clock(Clock.fixed(CertValidFrom, ZoneOffset.UTC)) - .useCrls(crls.asJava) - .trustHttpsCerts(httpsCert) - .build() - .loadCachedBlob() - .getPayload - blob should not be null - blob.getNo should equal(newBlobNo) - } - it( - "The BLOB is not downloaded if the cached one is not yet out of date." - ) { - val oldBlobNo = 1 - val newBlobNo = 2 + val (server, serverUrl, httpsCert) = + makeHttpServer("/trust-root.der", newTrustRootCert.getEncoded) + startServer(server) - 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 cacheFile = File.createTempFile( + s"${getClass.getCanonicalName}_test_cache_", + ".tmp", ) - val newBlobJwt = - makeBlob( - List(blobCert), - blobKeypair, - CertValidTo.atOffset(ZoneOffset.UTC).toLocalDate, - no = newBlobNo, + val f = new FileOutputStream(cacheFile) + f.write(oldTrustRootCert.getEncoded) + f.close() + cacheFile.deleteOnExit() + + val blob = load( + FidoMetadataDownloader + .builder() + .expectLegalHeader( + "Kom ihåg att du aldrig får snyta dig i mattan!" + ) + .downloadTrustRoot( + new URL(s"${serverUrl}/trust-root.der"), + Set( + TestAuthenticator.sha256( + new ByteArray(newTrustRootCert.getEncoded) + ) + ).asJava, + ) + .useTrustRootCacheFile(cacheFile) + .useBlob(blobJwt) + .clock(Clock.fixed(CertValidFrom, ZoneOffset.UTC)) + .useCrls(crls.asJava) + .trustHttpsCerts(httpsCert) + .build() ) - val crls = List[CRL]( - TestAuthenticator.buildCrl( - caName, - caKeypair.getPrivate, - "SHA256withECDSA", - CertValidFrom, - CertValidTo, + blob should not be null + blob.getHeader.getX5c.get.asScala.last.getIssuerDN.getName should equal( + newTrustRootDistinguishedName ) - ) - - val (server, serverUrl, httpsCert) = - makeHttpServer("/blob.jwt", newBlobJwt) - startServer(server) - - val blob = 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)) - ), - _ => {}, + cacheFile.exists() should be(true) + BinaryUtil.readAll(new FileInputStream(cacheFile)) should equal( + newTrustRootCert.getEncoded ) - .clock(Clock.fixed(CertValidFrom, ZoneOffset.UTC)) - .useCrls(crls.asJava) - .trustHttpsCerts(httpsCert) - .build() - .loadCachedBlob() - .getPayload - blob should not be null - blob.getNo should equal(oldBlobNo) - } - - } - - describe("4. If the x5u attribute is present in the JWT Header, then:") { + } - describe("1. The FIDO Server MUST verify that the URL specified by the x5u attribute has the same web-origin as the URL used to download the metadata BLOB from. The FIDO Server SHOULD ignore the file if the web-origin differs (in order to prevent loading objects from arbitrary sites).") { - it("x5u on a different host is rejected.") { - val (trustRootCert, caKeypair, caName) = makeTrustRootCert() + it("The trust root is not downloaded if there's a valid one in supplier-cache.") { + val random = new SecureRandom() + val trustRootDistinguishedName = + s"CN=Test trust root ${random.nextInt(10000)}" + val (trustRootCert, caKeypair, caName) = + makeTrustRootCert(distinguishedName = trustRootDistinguishedName) val (blobCert, blobKeypair, _) = makeCert(caKeypair, caName) - - val certChain = List(blobCert) - val certChainPem = certChain - .map(cert => new ByteArray(cert.getEncoded).getBase64) - .mkString( - "-----BEGIN CERTIFICATE-----\n", - "\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\n", - "\n-----END CERTIFICATE-----", - ) - val blobJwt = - makeBlob( - blobKeypair, - s"""{"alg":"ES256","x5u": "https://localhost:8444/chain.pem"}""", - s"""{ - "legalHeader": "Kom ihåg att du aldrig får snyta dig i mattan!", - "no": 1, - "nextUpdate": "2022-01-19", - "entries": [] - }""", - ) - - val (server, _, httpsCert) = - makeHttpServer( - Map( - "/chain.pem" -> certChainPem.getBytes(StandardCharsets.UTF_8), - "/blob.jwt" -> blobJwt.getBytes(StandardCharsets.UTF_8), - ) - ) - startServer(server) - + makeBlob(List(blobCert), blobKeypair, LocalDate.now()) val crls = List[CRL]( TestAuthenticator.buildCrl( caName, @@ -1034,53 +613,37 @@ class FidoMetadataDownloaderSpec ) ) - val thrown = the[IllegalArgumentException] thrownBy { + var writtenCache: Option[ByteArray] = None + + val blob = load( FidoMetadataDownloader .builder() .expectLegalHeader( "Kom ihåg att du aldrig får snyta dig i mattan!" ) - .useTrustRoot(trustRootCert) - .downloadBlob(new URL("https://localhost:8443/blob.jwt")) - .useBlobCache(() => Optional.empty(), _ => {}) + .useDefaultTrustRoot() + .useTrustRootCache( + () => Optional.of(new ByteArray(trustRootCert.getEncoded)), + newCache => { + writtenCache = Some(newCache) + }, + ) + .useBlob(blobJwt) + .clock(Clock.fixed(CertValidFrom, ZoneOffset.UTC)) .useCrls(crls.asJava) - .trustHttpsCerts(httpsCert) .build() - .loadCachedBlob - } - thrown should not be null + ) + blob should not be null + blob.getHeader.getX5c.get.asScala.last.getIssuerDN.getName should equal( + trustRootDistinguishedName + ) + writtenCache should equal(None) } - } - describe("2. The FIDO Server MUST download the certificate (chain) from the URL specified by the x5u attribute [JWS]. The certificate chain MUST be verified to properly chain to the metadata BLOB signing trust anchor according to [RFC5280]. All certificates in the chain MUST be checked for revocation according to [RFC5280].") { - it("x5u with one cert is accepted.") { + it("The downloaded trust root cert must match one of the expected SHA256 hashes.") { val (trustRootCert, caKeypair, caName) = makeTrustRootCert() val (blobCert, blobKeypair, _) = makeCert(caKeypair, caName) - - val certChain = List(blobCert) - val certChainPem = certChain - .map(cert => new ByteArray(cert.getEncoded).getBase64) - .mkString( - "-----BEGIN CERTIFICATE-----\n", - "\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\n", - "\n-----END CERTIFICATE-----", - ) - - val (server, serverUrl, httpsCert) = - makeHttpServer("/chain.pem", certChainPem) - startServer(server) - - val blobJwt = - makeBlob( - blobKeypair, - s"""{"alg":"ES256","x5u": "${serverUrl}/chain.pem"}""", - s"""{ - "legalHeader": "Kom ihåg att du aldrig får snyta dig i mattan!", - "no": 1, - "nextUpdate": "2022-01-19", - "entries": [] - }""", - ) + val blobJwt = makeBlob(List(blobCert), blobKeypair, LocalDate.now()) val crls = List[CRL]( TestAuthenticator.buildCrl( caName, @@ -1091,49 +654,70 @@ class FidoMetadataDownloaderSpec ) ) - val blob = FidoMetadataDownloader - .builder() - .expectLegalHeader("Kom ihåg att du aldrig får snyta dig i mattan!") - .useTrustRoot(trustRootCert) - .useBlob(blobJwt) - .useCrls(crls.asJava) - .trustHttpsCerts(httpsCert) - .clock(Clock.fixed(CertValidFrom, ZoneOffset.UTC)) - .build() - .loadCachedBlob - blob should not be null - } - - it("x5u with an unknown trust anchor is rejected.") { - val (trustRootCert, caKeypair, caName) = makeTrustRootCert() - val (_, untrustedCaKeypair, untrustedCaName) = makeTrustRootCert() - val (blobCert, blobKeypair, _) = - makeCert(untrustedCaKeypair, untrustedCaName) + val (server, serverUrl, httpsCert) = + makeHttpServer("/trust-root.der", trustRootCert.getEncoded) + startServer(server) - val certChain = List(blobCert) - val certChainPem = certChain - .map(cert => new ByteArray(cert.getEncoded).getBase64) - .mkString( - "-----BEGIN CERTIFICATE-----\n", - "\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\n", - "\n-----END CERTIFICATE-----", + def testWithHashes(hashes: Set[ByteArray]): MetadataBLOB = { + load( + FidoMetadataDownloader + .builder() + .expectLegalHeader( + "Kom ihåg att du aldrig får snyta dig i mattan!" + ) + .downloadTrustRoot( + new URL(s"${serverUrl}/trust-root.der"), + hashes.asJava, + ) + .useTrustRootCache(() => Optional.empty(), _ => {}) + .useBlob(blobJwt) + .useCrls(crls.asJava) + .trustHttpsCerts(httpsCert) + .clock(Clock.fixed(CertValidFrom, ZoneOffset.UTC)) + .build() ) + } - val (server, serverUrl, httpsCert) = - makeHttpServer("/chain.pem", certChainPem) - startServer(server) + val goodHash = + TestAuthenticator.sha256(new ByteArray(trustRootCert.getEncoded)) + val badHash = TestAuthenticator.sha256(goodHash) - val blobJwt = - makeBlob( - blobKeypair, - s"""{"alg":"ES256","x5u": "${serverUrl}/chain.pem"}""", - s"""{ - "legalHeader": "Kom ihåg att du aldrig får snyta dig i mattan!", - "no": 1, - "nextUpdate": "2022-01-19", - "entries": [] - }""", + a[DigestException] should be thrownBy { + testWithHashes(Set(badHash)) + } + testWithHashes(Set(goodHash)) should not be null + testWithHashes(Set(badHash, goodHash)) should not be null + } + } + + describe("2. To validate the digital certificates used in the digital signature, the certificate revocation information MUST be available in the form of CRLs at the respective MDS CRL location e.g. More information can be found at https://fidoalliance.org/metadata/") { + val (trustRootCert, caKeypair, caName) = makeTrustRootCert() + val (blobCert, blobKeypair, _) = makeCert(caKeypair, caName) + val blobJwt = + makeBlob(List(blobCert), blobKeypair, LocalDate.parse("2022-01-19")) + + it( + "Verification fails if the certs don't declare CRL distribution points." + ) { + val thrown = the[CertPathValidatorException] thrownBy { + load( + FidoMetadataDownloader + .builder() + .expectLegalHeader( + "Kom ihåg att du aldrig får snyta dig i mattan!" + ) + .useTrustRoot(trustRootCert) + .useBlob(blobJwt) + .clock(Clock.fixed(CertValidFrom, ZoneOffset.UTC)) + .build() ) + } + thrown.getReason should equal( + BasicReason.UNDETERMINED_REVOCATION_STATUS + ) + } + + it("Verification succeeds if explicitly given appropriate CRLs.") { val crls = List[CRL]( TestAuthenticator.buildCrl( caName, @@ -1144,7 +728,7 @@ class FidoMetadataDownloaderSpec ) ) - val thrown = the[CertPathValidatorException] thrownBy { + val blob = load( FidoMetadataDownloader .builder() .expectLegalHeader( @@ -1153,77 +737,100 @@ class FidoMetadataDownloaderSpec .useTrustRoot(trustRootCert) .useBlob(blobJwt) .useCrls(crls.asJava) - .trustHttpsCerts(httpsCert) .clock(Clock.fixed(CertValidFrom, ZoneOffset.UTC)) .build() - .loadCachedBlob - } - thrown should not be null - thrown.getReason should be( - CertPathValidatorException.BasicReason.INVALID_SIGNATURE ) + blob should not be null } - it("x5u with three certs requires a CRL for each CA certificate.") { - val (trustRootCert, caKeypair, caName) = makeTrustRootCert() - val certChain = makeCertChain(caKeypair, caName, 3) - certChain.length should be(3) - val certChainPem = certChain - .map({ - case (cert, _, _) => new ByteArray(cert.getEncoded).getBase64 - }) - .mkString( - "-----BEGIN CERTIFICATE-----\n", - "\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\n", - "\n-----END CERTIFICATE-----", + describe("Intermediate certificates") { + + val (intermediateCert, intermediateKeypair, intermediateName) = + makeCert( + caKeypair, + caName, + isCa = true, + name = "CN=Yubico java-webauthn-server unit tests intermediate CA, O=Yubico", ) + val (blobCert, blobKeypair, _) = + makeCert(intermediateKeypair, intermediateName) + val blobJwt = makeBlob( + List(blobCert, intermediateCert), + blobKeypair, + LocalDate.parse("2022-01-19"), + ) - val crls = - (certChain.tail :+ (trustRootCert, caKeypair, caName)).map({ - case (_, keypair, name) => - TestAuthenticator.buildCrl( - name, - keypair.getPrivate, - "SHA256withECDSA", - CertValidFrom, - CertValidTo, - ) - }) + it("each require their own CRL.") { + val thrown = the[CertPathValidatorException] thrownBy { + load( + FidoMetadataDownloader + .builder() + .expectLegalHeader( + "Kom ihåg att du aldrig får snyta dig i mattan!" + ) + .useTrustRoot(trustRootCert) + .useBlob(blobJwt) + .clock(Clock.fixed(CertValidFrom, ZoneOffset.UTC)) + .build() + ) + } + thrown.getReason should equal( + BasicReason.UNDETERMINED_REVOCATION_STATUS + ) - val (server, serverUrl, httpsCert) = - makeHttpServer("/chain.pem", certChainPem) - startServer(server) + val rootCrl = TestAuthenticator.buildCrl( + caName, + caKeypair.getPrivate, + "SHA256withECDSA", + CertValidFrom, + CertValidTo, + ) - val blobJwt = - makeBlob( - certChain.head._2, - s"""{"alg":"ES256","x5u": "${serverUrl}/chain.pem"}""", - s"""{ - "legalHeader": "Kom ihåg att du aldrig får snyta dig i mattan!", - "no": 1, - "nextUpdate": "2022-01-19", - "entries": [] - }""", + val thrown2 = the[CertPathValidatorException] thrownBy { + load( + FidoMetadataDownloader + .builder() + .expectLegalHeader( + "Kom ihåg att du aldrig får snyta dig i mattan!" + ) + .useTrustRoot(trustRootCert) + .useBlob(blobJwt) + .clock(Clock.fixed(CertValidFrom, ZoneOffset.UTC)) + .useCrls(List[CRL](rootCrl).asJava) + .build() + ) + } + thrown2.getReason should equal( + BasicReason.UNDETERMINED_REVOCATION_STATUS ) - val clock = Clock.fixed(CertValidFrom, ZoneOffset.UTC) + val intermediateCrl = TestAuthenticator.buildCrl( + intermediateName, + intermediateKeypair.getPrivate, + "SHA256withECDSA", + CertValidFrom, + CertValidTo, + ) - val blob = FidoMetadataDownloader - .builder() - .expectLegalHeader("Kom ihåg att du aldrig får snyta dig i mattan!") - .useTrustRoot(trustRootCert) - .useBlob(blobJwt) - .useCrls(crls.asJava) - .trustHttpsCerts(httpsCert) - .clock(clock) - .build() - .loadCachedBlob - blob should not be null + val thrown3 = the[CertPathValidatorException] thrownBy { + load( + FidoMetadataDownloader + .builder() + .expectLegalHeader( + "Kom ihåg att du aldrig får snyta dig i mattan!" + ) + .useTrustRoot(trustRootCert) + .useBlob(blobJwt) + .clock(Clock.fixed(CertValidFrom, ZoneOffset.UTC)) + .useCrls(List[CRL](intermediateCrl).asJava) + .build() + ) + } + thrown3.getReason should equal( + BasicReason.UNDETERMINED_REVOCATION_STATUS + ) - for { i <- certChain.indices } { - val splicedCrls = crls.take(i) ++ crls.drop(i + 1) - splicedCrls.length should be(crls.length - 1) - val thrown = the[CertPathValidatorException] thrownBy { + val blob = load( FidoMetadataDownloader .builder() .expectLegalHeader( @@ -1231,619 +838,1145 @@ class FidoMetadataDownloaderSpec ) .useTrustRoot(trustRootCert) .useBlob(blobJwt) - .useCrls(splicedCrls.asJava) - .trustHttpsCerts(httpsCert) - .clock(clock) + .useCrls(List[CRL](rootCrl, intermediateCrl).asJava) + .clock(Clock.fixed(CertValidFrom, ZoneOffset.UTC)) .build() - .loadCachedBlob - } - thrown should not be null - thrown.getReason should be( - CertPathValidatorException.BasicReason.UNDETERMINED_REVOCATION_STATUS ) + blob should not be null } - } - } - describe("3. The FIDO Server SHOULD ignore the file if the chain cannot be verified or if one of the chain certificates is revoked.") { - it("Verification fails if explicitly given CRLs where a cert in the chain is revoked.") { - val (trustRootCert, caKeypair, caName) = makeTrustRootCert() - val certChain = makeCertChain(caKeypair, caName, 3) - certChain.length should be(3) - val certChainPem = certChain - .map({ - case (cert, _, _) => new ByteArray(cert.getEncoded).getBase64 - }) - .mkString( - "-----BEGIN CERTIFICATE-----\n", - "\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\n", - "\n-----END CERTIFICATE-----", + it("can revoke downstream certificates too.") { + val rootCrl = TestAuthenticator.buildCrl( + caName, + caKeypair.getPrivate, + "SHA256withECDSA", + CertValidFrom, + CertValidTo, ) - - val crls = - (certChain.tail :+ (trustRootCert, caKeypair, caName)).map({ - case (_, keypair, name) => - TestAuthenticator.buildCrl( - name, - keypair.getPrivate, - "SHA256withECDSA", - CertValidFrom, - CertValidTo, - ) - }) - - val (server, serverUrl, httpsCert) = - makeHttpServer("/chain.pem", certChainPem) - startServer(server) - - val blobJwt = - makeBlob( - certChain.head._2, - s"""{"alg":"ES256","x5u": "${serverUrl}/chain.pem"}""", - s"""{ - "legalHeader": "Kom ihåg att du aldrig får snyta dig i mattan!", - "no": 1, - "nextUpdate": "2022-01-19", - "entries": [] - }""", + val intermediateCrl = TestAuthenticator.buildCrl( + intermediateName, + intermediateKeypair.getPrivate, + "SHA256withECDSA", + CertValidFrom, + CertValidTo, + revoked = Set(blobCert), ) + val crls = List(rootCrl, intermediateCrl) - val clock = Clock.fixed(CertValidFrom.plusSeconds(1), ZoneOffset.UTC) - - val blob = FidoMetadataDownloader - .builder() - .expectLegalHeader("Kom ihåg att du aldrig får snyta dig i mattan!") - .useTrustRoot(trustRootCert) - .useBlob(blobJwt) - .useCrls(crls.asJava) - .trustHttpsCerts(httpsCert) - .clock(clock) - .build() - .loadCachedBlob - blob should not be null - - for { i <- certChain.indices } { - val crlsWithRevocation = - crls.take(i) ++ crls.drop(i + 1) :+ TestAuthenticator.buildCrl( - certChain.lift(i + 1).map(_._3).getOrElse(caName), - certChain.lift(i + 1).map(_._2).getOrElse(caKeypair).getPrivate, - "SHA256withECDSA", - CertValidFrom, - CertValidTo, - revoked = Set(certChain(i)._1), - ) - crlsWithRevocation.length should equal(crls.length) val thrown = the[CertPathValidatorException] thrownBy { - FidoMetadataDownloader - .builder() - .expectLegalHeader( - "Kom ihåg att du aldrig får snyta dig i mattan!" - ) - .useTrustRoot(trustRootCert) - .useBlob(blobJwt) - .useCrls(crlsWithRevocation.asJava) - .trustHttpsCerts(httpsCert) - .clock(clock) - .build() - .loadCachedBlob + load( + FidoMetadataDownloader + .builder() + .expectLegalHeader( + "Kom ihåg att du aldrig får snyta dig i mattan!" + ) + .useTrustRoot(trustRootCert) + .useBlob(blobJwt) + .useCrls(crls.asJava) + .clock( + Clock.fixed(CertValidFrom.plusSeconds(1), ZoneOffset.UTC) + ) + .build() + ) } - thrown should not be null - thrown.getReason should be(BasicReason.REVOKED) - thrown.getIndex should equal(i) + thrown.getReason should equal( + BasicReason.REVOKED + ) } } } - } - - describe("5. If the x5u attribute is missing, the chain should be retrieved from the x5c attribute. If that attribute is missing as well, Metadata BLOB signing trust anchor is considered the BLOB signing certificate chain.") { - it("x5c with one cert is accepted.") { - val (trustRootCert, caKeypair, caName) = makeTrustRootCert() - val (blobCert, blobKeypair, _) = makeCert(caKeypair, caName) - val certChain = List(blobCert) - val certChainJson = certChain - .map(cert => new ByteArray(cert.getEncoded).getBase64) - .mkString("[\"", "\",\"", "\"]") - val blobJwt = - makeBlob( - blobKeypair, - s"""{"alg":"ES256","x5c": ${certChainJson}}""", - s"""{ - "legalHeader": "Kom ihåg att du aldrig får snyta dig i mattan!", - "no": 1, - "nextUpdate": "2022-01-19", - "entries": [] - }""", - ) - val crls = List[CRL]( - TestAuthenticator.buildCrl( - caName, - caKeypair.getPrivate, - "SHA256withECDSA", - CertValidFrom, - CertValidTo, - ) - ) - - val blob = FidoMetadataDownloader - .builder() - .expectLegalHeader("Kom ihåg att du aldrig får snyta dig i mattan!") - .useTrustRoot(trustRootCert) - .useBlob(blobJwt) - .useCrls(crls.asJava) - .clock(Clock.fixed(CertValidFrom, ZoneOffset.UTC)) - .build() - .loadCachedBlob - blob should not be null - } - it("x5c with three certs requires a CRL for each CA certificate.") { - val (trustRootCert, caKeypair, caName) = makeTrustRootCert() - val certChain = makeCertChain(caKeypair, caName, 3) - certChain.length should be(3) - val certChainJson = certChain - .map({ - case (cert, _, _) => new ByteArray(cert.getEncoded).getBase64 - }) - .mkString("[\"", "\",\"", "\"]") + describe("3. The FIDO Server MUST be able to download the latest metadata BLOB object from the well-known URL when appropriate, e.g. https://mds.fidoalliance.org/. The nextUpdate field of the Metadata BLOB specifies a date when the download SHOULD occur at latest.") { + it("The BLOB is downloaded if there isn't a cached one.") { + val random = new SecureRandom() + val blobLegalHeader = + s"Kom ihåg att du aldrig får snyta dig i mattan! ${random.nextInt(10000)}" + val blobNo = random.nextInt(10000); - val blobJwt = - makeBlob( - certChain.head._2, - s"""{"alg":"ES256","x5c": ${certChainJson}}""", - s"""{ - "legalHeader": "Kom ihåg att du aldrig får snyta dig i mattan!", - "no": 1, - "nextUpdate": "2022-01-19", - "entries": [] - }""", - ) - val crls = (certChain.tail :+ (trustRootCert, caKeypair, caName)).map({ - case (_, keypair, name) => + val (trustRootCert, caKeypair, caName) = makeTrustRootCert() + val (blobCert, blobKeypair, _) = makeCert(caKeypair, caName) + val blobJwt = + makeBlob( + List(blobCert), + blobKeypair, + LocalDate.parse("2022-01-19"), + no = blobNo, + legalHeader = blobLegalHeader, + ) + val crls = List[CRL]( TestAuthenticator.buildCrl( - name, - keypair.getPrivate, + caName, + caKeypair.getPrivate, "SHA256withECDSA", CertValidFrom, CertValidTo, ) - }) - - val clock = Clock.fixed(CertValidFrom, ZoneOffset.UTC) - - val blob = Try( - FidoMetadataDownloader - .builder() - .expectLegalHeader("Kom ihåg att du aldrig får snyta dig i mattan!") - .useTrustRoot(trustRootCert) - .useBlob(blobJwt) - .useCrls(crls.asJava) - .clock(clock) - .build() - .loadCachedBlob - ) - blob should not be null - blob shouldBe a[Success[_]] - - for { i <- certChain.indices } { - val splicedCrls = crls.take(i) ++ crls.drop(i + 1) - splicedCrls.length should be(crls.length - 1) - val thrown = the[CertPathValidatorException] thrownBy { + ) + + val (server, serverUrl, httpsCert) = + makeHttpServer("/blob.jwt", blobJwt) + startServer(server) + + val blob = load( FidoMetadataDownloader .builder() - .expectLegalHeader( - "Kom ihåg att du aldrig får snyta dig i mattan!" - ) + .expectLegalHeader(blobLegalHeader) .useTrustRoot(trustRootCert) - .useBlob(blobJwt) - .useCrls(splicedCrls.asJava) - .clock(clock) + .downloadBlob(new URL(s"${serverUrl}/blob.jwt")) + .useBlobCache(() => Optional.empty(), _ => {}) + .clock(Clock.fixed(CertValidFrom, ZoneOffset.UTC)) + .useCrls(crls.asJava) + .trustHttpsCerts(httpsCert) .build() - .loadCachedBlob - } - thrown should not be null - thrown.getReason should be( - CertPathValidatorException.BasicReason.UNDETERMINED_REVOCATION_STATUS - ) + ).getPayload + blob should not be null + blob.getLegalHeader should equal(blobLegalHeader) + blob.getNo should equal(blobNo) } - } - it("Missing x5c means the trust root cert is used as the signer.") { - val (trustRootCert, caKeypair, caName) = makeTrustRootCert() - val blobJwt = - makeBlob( - caKeypair, - s"""{"alg":"ES256"}""", - s"""{ + it("The BLOB is downloaded if the cached one is out of date.") { + val oldBlobNo = 1 + val newBlobNo = 2 + + val (trustRootCert, caKeypair, caName) = makeTrustRootCert() + val (blobCert, blobKeypair, _) = makeCert(caKeypair, caName) + val oldBlobJwt = + makeBlob( + List(blobCert), + blobKeypair, + 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(CertValidFrom, ZoneOffset.UTC)) + .useCrls(crls.asJava) + .trustHttpsCerts(httpsCert) + .build() + ).getPayload + blob should not be null + blob.getNo should equal(newBlobNo) + } + } + + describe("4. If the x5u attribute is present in the JWT Header, then:") { + + describe("1. The FIDO Server MUST verify that the URL specified by the x5u attribute has the same web-origin as the URL used to download the metadata BLOB from. The FIDO Server SHOULD ignore the file if the web-origin differs (in order to prevent loading objects from arbitrary sites).") { + it("x5u on a different host is rejected.") { + val (trustRootCert, caKeypair, caName) = makeTrustRootCert() + val (blobCert, blobKeypair, _) = makeCert(caKeypair, caName) + + val certChain = List(blobCert) + val certChainPem = certChain + .map(cert => new ByteArray(cert.getEncoded).getBase64) + .mkString( + "-----BEGIN CERTIFICATE-----\n", + "\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\n", + "\n-----END CERTIFICATE-----", + ) + + val blobJwt = + makeBlob( + blobKeypair, + s"""{"alg":"ES256","x5u": "https://localhost:8444/chain.pem"}""", + s"""{ + "legalHeader": "Kom ihåg att du aldrig får snyta dig i mattan!", + "no": 1, + "nextUpdate": "2022-01-19", + "entries": [] + }""", + ) + + val (server, _, httpsCert) = + makeHttpServer( + Map( + "/chain.pem" -> certChainPem.getBytes(StandardCharsets.UTF_8), + "/blob.jwt" -> blobJwt.getBytes(StandardCharsets.UTF_8), + ) + ) + startServer(server) + + val crls = List[CRL]( + TestAuthenticator.buildCrl( + caName, + caKeypair.getPrivate, + "SHA256withECDSA", + CertValidFrom, + CertValidTo, + ) + ) + + val thrown = the[IllegalArgumentException] thrownBy { + load( + FidoMetadataDownloader + .builder() + .expectLegalHeader( + "Kom ihåg att du aldrig får snyta dig i mattan!" + ) + .useTrustRoot(trustRootCert) + .downloadBlob(new URL("https://localhost:8443/blob.jwt")) + .useBlobCache(() => Optional.empty(), _ => {}) + .useCrls(crls.asJava) + .trustHttpsCerts(httpsCert) + .build() + ) + } + thrown should not be null + } + } + + describe("2. The FIDO Server MUST download the certificate (chain) from the URL specified by the x5u attribute [JWS]. The certificate chain MUST be verified to properly chain to the metadata BLOB signing trust anchor according to [RFC5280]. All certificates in the chain MUST be checked for revocation according to [RFC5280].") { + it("x5u with one cert is accepted.") { + val (trustRootCert, caKeypair, caName) = makeTrustRootCert() + val (blobCert, blobKeypair, _) = makeCert(caKeypair, caName) + + val certChain = List(blobCert) + val certChainPem = certChain + .map(cert => new ByteArray(cert.getEncoded).getBase64) + .mkString( + "-----BEGIN CERTIFICATE-----\n", + "\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\n", + "\n-----END CERTIFICATE-----", + ) + + val (server, serverUrl, httpsCert) = + makeHttpServer("/chain.pem", certChainPem) + startServer(server) + + val blobJwt = + makeBlob( + blobKeypair, + s"""{"alg":"ES256","x5u": "${serverUrl}/chain.pem"}""", + s"""{ "legalHeader": "Kom ihåg att du aldrig får snyta dig i mattan!", "no": 1, "nextUpdate": "2022-01-19", "entries": [] }""", + ) + val crls = List[CRL]( + TestAuthenticator.buildCrl( + caName, + caKeypair.getPrivate, + "SHA256withECDSA", + CertValidFrom, + CertValidTo, + ) + ) + + val blob = load( + FidoMetadataDownloader + .builder() + .expectLegalHeader( + "Kom ihåg att du aldrig får snyta dig i mattan!" + ) + .useTrustRoot(trustRootCert) + .useBlob(blobJwt) + .useCrls(crls.asJava) + .trustHttpsCerts(httpsCert) + .clock(Clock.fixed(CertValidFrom, ZoneOffset.UTC)) + .build() + ) + blob should not be null + } + + it("x5u with an unknown trust anchor is rejected.") { + val (trustRootCert, caKeypair, caName) = makeTrustRootCert() + val (_, untrustedCaKeypair, untrustedCaName) = makeTrustRootCert() + val (blobCert, blobKeypair, _) = + makeCert(untrustedCaKeypair, untrustedCaName) + + val certChain = List(blobCert) + val certChainPem = certChain + .map(cert => new ByteArray(cert.getEncoded).getBase64) + .mkString( + "-----BEGIN CERTIFICATE-----\n", + "\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\n", + "\n-----END CERTIFICATE-----", + ) + + val (server, serverUrl, httpsCert) = + makeHttpServer("/chain.pem", certChainPem) + startServer(server) + + val blobJwt = + makeBlob( + blobKeypair, + s"""{"alg":"ES256","x5u": "${serverUrl}/chain.pem"}""", + s"""{ + "legalHeader": "Kom ihåg att du aldrig får snyta dig i mattan!", + "no": 1, + "nextUpdate": "2022-01-19", + "entries": [] + }""", + ) + val crls = List[CRL]( + TestAuthenticator.buildCrl( + caName, + caKeypair.getPrivate, + "SHA256withECDSA", + CertValidFrom, + CertValidTo, + ) + ) + + val thrown = the[CertPathValidatorException] thrownBy { + load( + FidoMetadataDownloader + .builder() + .expectLegalHeader( + "Kom ihåg att du aldrig får snyta dig i mattan!" + ) + .useTrustRoot(trustRootCert) + .useBlob(blobJwt) + .useCrls(crls.asJava) + .trustHttpsCerts(httpsCert) + .clock(Clock.fixed(CertValidFrom, ZoneOffset.UTC)) + .build() + ) + } + thrown should not be null + thrown.getReason should be( + CertPathValidatorException.BasicReason.INVALID_SIGNATURE + ) + } + + it("x5u with three certs requires a CRL for each CA certificate.") { + val (trustRootCert, caKeypair, caName) = makeTrustRootCert() + val certChain = makeCertChain(caKeypair, caName, 3) + certChain.length should be(3) + val certChainPem = certChain + .map({ + case (cert, _, _) => new ByteArray(cert.getEncoded).getBase64 + }) + .mkString( + "-----BEGIN CERTIFICATE-----\n", + "\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\n", + "\n-----END CERTIFICATE-----", + ) + + val crls = + (certChain.tail :+ (trustRootCert, caKeypair, caName)).map({ + case (_, keypair, name) => + TestAuthenticator.buildCrl( + name, + keypair.getPrivate, + "SHA256withECDSA", + CertValidFrom, + CertValidTo, + ) + }) + + val (server, serverUrl, httpsCert) = + makeHttpServer("/chain.pem", certChainPem) + startServer(server) + + val blobJwt = + makeBlob( + certChain.head._2, + s"""{"alg":"ES256","x5u": "${serverUrl}/chain.pem"}""", + s"""{ + "legalHeader": "Kom ihåg att du aldrig får snyta dig i mattan!", + "no": 1, + "nextUpdate": "2022-01-19", + "entries": [] + }""", + ) + + val clock = Clock.fixed(CertValidFrom, ZoneOffset.UTC) + + val blob = load( + FidoMetadataDownloader + .builder() + .expectLegalHeader( + "Kom ihåg att du aldrig får snyta dig i mattan!" + ) + .useTrustRoot(trustRootCert) + .useBlob(blobJwt) + .useCrls(crls.asJava) + .trustHttpsCerts(httpsCert) + .clock(clock) + .build() + ) + blob should not be null + + for { i <- certChain.indices } { + val splicedCrls = crls.take(i) ++ crls.drop(i + 1) + splicedCrls.length should be(crls.length - 1) + val thrown = the[CertPathValidatorException] thrownBy { + load( + FidoMetadataDownloader + .builder() + .expectLegalHeader( + "Kom ihåg att du aldrig får snyta dig i mattan!" + ) + .useTrustRoot(trustRootCert) + .useBlob(blobJwt) + .useCrls(splicedCrls.asJava) + .trustHttpsCerts(httpsCert) + .clock(clock) + .build() + ) + } + thrown should not be null + thrown.getReason should be( + CertPathValidatorException.BasicReason.UNDETERMINED_REVOCATION_STATUS + ) + } + } + } + + describe("3. The FIDO Server SHOULD ignore the file if the chain cannot be verified or if one of the chain certificates is revoked.") { + it("Verification fails if explicitly given CRLs where a cert in the chain is revoked.") { + val (trustRootCert, caKeypair, caName) = makeTrustRootCert() + val certChain = makeCertChain(caKeypair, caName, 3) + certChain.length should be(3) + val certChainPem = certChain + .map({ + case (cert, _, _) => new ByteArray(cert.getEncoded).getBase64 + }) + .mkString( + "-----BEGIN CERTIFICATE-----\n", + "\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\n", + "\n-----END CERTIFICATE-----", + ) + + val crls = + (certChain.tail :+ (trustRootCert, caKeypair, caName)).map({ + case (_, keypair, name) => + TestAuthenticator.buildCrl( + name, + keypair.getPrivate, + "SHA256withECDSA", + CertValidFrom, + CertValidTo, + ) + }) + + val (server, serverUrl, httpsCert) = + makeHttpServer("/chain.pem", certChainPem) + startServer(server) + + val blobJwt = + makeBlob( + certChain.head._2, + s"""{"alg":"ES256","x5u": "${serverUrl}/chain.pem"}""", + s"""{ + "legalHeader": "Kom ihåg att du aldrig får snyta dig i mattan!", + "no": 1, + "nextUpdate": "2022-01-19", + "entries": [] + }""", + ) + + val clock = + Clock.fixed(CertValidFrom.plusSeconds(1), ZoneOffset.UTC) + + val blob = load( + FidoMetadataDownloader + .builder() + .expectLegalHeader( + "Kom ihåg att du aldrig får snyta dig i mattan!" + ) + .useTrustRoot(trustRootCert) + .useBlob(blobJwt) + .useCrls(crls.asJava) + .trustHttpsCerts(httpsCert) + .clock(clock) + .build() + ) + blob should not be null + + for { i <- certChain.indices } { + val crlsWithRevocation = + crls.take(i) ++ crls.drop(i + 1) :+ TestAuthenticator.buildCrl( + certChain.lift(i + 1).map(_._3).getOrElse(caName), + certChain + .lift(i + 1) + .map(_._2) + .getOrElse(caKeypair) + .getPrivate, + "SHA256withECDSA", + CertValidFrom, + CertValidTo, + revoked = Set(certChain(i)._1), + ) + crlsWithRevocation.length should equal(crls.length) + val thrown = the[CertPathValidatorException] thrownBy { + load( + FidoMetadataDownloader + .builder() + .expectLegalHeader( + "Kom ihåg att du aldrig får snyta dig i mattan!" + ) + .useTrustRoot(trustRootCert) + .useBlob(blobJwt) + .useCrls(crlsWithRevocation.asJava) + .trustHttpsCerts(httpsCert) + .clock(clock) + .build() + ) + } + thrown should not be null + thrown.getReason should be(BasicReason.REVOKED) + thrown.getIndex should equal(i) + } + } + } + } + + describe("5. If the x5u attribute is missing, the chain should be retrieved from the x5c attribute. If that attribute is missing as well, Metadata BLOB signing trust anchor is considered the BLOB signing certificate chain.") { + it("x5c with one cert is accepted.") { + val (trustRootCert, caKeypair, caName) = makeTrustRootCert() + val (blobCert, blobKeypair, _) = makeCert(caKeypair, caName) + val certChain = List(blobCert) + val certChainJson = certChain + .map(cert => new ByteArray(cert.getEncoded).getBase64) + .mkString("[\"", "\",\"", "\"]") + val blobJwt = + makeBlob( + blobKeypair, + s"""{"alg":"ES256","x5c": ${certChainJson}}""", + s"""{ + "legalHeader": "Kom ihåg att du aldrig får snyta dig i mattan!", + "no": 1, + "nextUpdate": "2022-01-19", + "entries": [] + }""", + ) + val crls = List[CRL]( + TestAuthenticator.buildCrl( + caName, + caKeypair.getPrivate, + "SHA256withECDSA", + CertValidFrom, + CertValidTo, + ) + ) + + val blob = load( + FidoMetadataDownloader + .builder() + .expectLegalHeader( + "Kom ihåg att du aldrig får snyta dig i mattan!" + ) + .useTrustRoot(trustRootCert) + .useBlob(blobJwt) + .useCrls(crls.asJava) + .clock(Clock.fixed(CertValidFrom, ZoneOffset.UTC)) + .build() + ) + blob should not be null + } + + it("x5c with three certs requires a CRL for each CA certificate.") { + val (trustRootCert, caKeypair, caName) = makeTrustRootCert() + val certChain = makeCertChain(caKeypair, caName, 3) + certChain.length should be(3) + val certChainJson = certChain + .map({ + case (cert, _, _) => new ByteArray(cert.getEncoded).getBase64 + }) + .mkString("[\"", "\",\"", "\"]") + + val blobJwt = + makeBlob( + certChain.head._2, + s"""{"alg":"ES256","x5c": ${certChainJson}}""", + s"""{ + "legalHeader": "Kom ihåg att du aldrig får snyta dig i mattan!", + "no": 1, + "nextUpdate": "2022-01-19", + "entries": [] + }""", + ) + val crls = + (certChain.tail :+ (trustRootCert, caKeypair, caName)).map({ + case (_, keypair, name) => + TestAuthenticator.buildCrl( + name, + keypair.getPrivate, + "SHA256withECDSA", + CertValidFrom, + CertValidTo, + ) + }) + + val clock = Clock.fixed(CertValidFrom, ZoneOffset.UTC) + + val blob = Try( + load( + FidoMetadataDownloader + .builder() + .expectLegalHeader( + "Kom ihåg att du aldrig får snyta dig i mattan!" + ) + .useTrustRoot(trustRootCert) + .useBlob(blobJwt) + .useCrls(crls.asJava) + .clock(clock) + .build() + ) ) + blob should not be null + blob shouldBe a[Success[_]] - val crls = List( - TestAuthenticator.buildCrl( - caName, - caKeypair.getPrivate, - "SHA256withECDSA", - CertValidFrom, - CertValidTo, + for { i <- certChain.indices } { + val splicedCrls = crls.take(i) ++ crls.drop(i + 1) + splicedCrls.length should be(crls.length - 1) + val thrown = the[CertPathValidatorException] thrownBy { + load( + FidoMetadataDownloader + .builder() + .expectLegalHeader( + "Kom ihåg att du aldrig får snyta dig i mattan!" + ) + .useTrustRoot(trustRootCert) + .useBlob(blobJwt) + .useCrls(splicedCrls.asJava) + .clock(clock) + .build() + ) + } + thrown should not be null + thrown.getReason should be( + CertPathValidatorException.BasicReason.UNDETERMINED_REVOCATION_STATUS + ) + } + } + + it("Missing x5c means the trust root cert is used as the signer.") { + val (trustRootCert, caKeypair, caName) = makeTrustRootCert() + val blobJwt = + makeBlob( + caKeypair, + s"""{"alg":"ES256"}""", + s"""{ + "legalHeader": "Kom ihåg att du aldrig får snyta dig i mattan!", + "no": 1, + "nextUpdate": "2022-01-19", + "entries": [] + }""", + ) + + val crls = List( + TestAuthenticator.buildCrl( + caName, + caKeypair.getPrivate, + "SHA256withECDSA", + CertValidFrom, + CertValidTo, + ) + ) + + val blob = load( + FidoMetadataDownloader + .builder() + .expectLegalHeader( + "Kom ihåg att du aldrig får snyta dig i mattan!" + ) + .useTrustRoot(trustRootCert) + .useBlob(blobJwt) + .useCrls(crls.asJava) + .clock(Clock.fixed(CertValidFrom, ZoneOffset.UTC)) + .build() ) - ) - - val blob = FidoMetadataDownloader - .builder() - .expectLegalHeader("Kom ihåg att du aldrig får snyta dig i mattan!") - .useTrustRoot(trustRootCert) - .useBlob(blobJwt) - .useCrls(crls.asJava) - .clock(Clock.fixed(CertValidFrom, ZoneOffset.UTC)) - .build() - .loadCachedBlob - blob should not be null + blob should not be null + } } - } - describe("6. Verify the signature of the Metadata BLOB object using the BLOB signing certificate chain (as determined by the steps above). The FIDO Server SHOULD ignore the file if the signature is invalid. It SHOULD also ignore the file if its number (no) is less or equal to the number of the last Metadata BLOB object cached locally.") { - it("Invalid signatures are detected.") { - val (trustRootCert, caKeypair, caName) = makeTrustRootCert() - val (blobCert, blobKeypair, _) = makeCert(caKeypair, caName) + describe("6. Verify the signature of the Metadata BLOB object using the BLOB signing certificate chain (as determined by the steps above). The FIDO Server SHOULD ignore the file if the signature is invalid. It SHOULD also ignore the file if its number (no) is less or equal to the number of the last Metadata BLOB object cached locally.") { + it("Invalid signatures are detected.") { + val (trustRootCert, caKeypair, caName) = makeTrustRootCert() + val (blobCert, blobKeypair, _) = makeCert(caKeypair, caName) - val validBlobJwt = - makeBlob(List(blobCert), blobKeypair, LocalDate.parse("2022-01-19")) - val crls = List( - TestAuthenticator.buildCrl( - caName, - caKeypair.getPrivate, - "SHA256withECDSA", - CertValidFrom, - CertValidTo, + val validBlobJwt = + makeBlob(List(blobCert), blobKeypair, LocalDate.parse("2022-01-19")) + val crls = List( + TestAuthenticator.buildCrl( + caName, + caKeypair.getPrivate, + "SHA256withECDSA", + CertValidFrom, + CertValidTo, + ) ) - ) - val badBlobJwt = validBlobJwt - .split(raw"\.") - .updated( - 1, { - val json = JacksonCodecs.json() - val badBlobBody = json - .readTree( - ByteArray - .fromBase64Url(validBlobJwt.split(raw"\.")(1)) - .getBytes + val badBlobJwt = validBlobJwt + .split(raw"\.") + .updated( + 1, { + val json = JacksonCodecs.json() + val badBlobBody = json + .readTree( + ByteArray + .fromBase64Url(validBlobJwt.split(raw"\.")(1)) + .getBytes + ) + .asInstanceOf[ObjectNode] + badBlobBody.set("no", new IntNode(7)) + new ByteArray( + json + .writeValueAsString(badBlobBody) + .getBytes(StandardCharsets.UTF_8) + ).getBase64 + }, + ) + .mkString(".") + + val thrown = the[FidoMetadataDownloaderException] thrownBy { + load( + FidoMetadataDownloader + .builder() + .expectLegalHeader( + "Kom ihåg att du aldrig får snyta dig i mattan!" ) - .asInstanceOf[ObjectNode] - badBlobBody.set("no", new IntNode(7)) - new ByteArray( - json - .writeValueAsString(badBlobBody) - .getBytes(StandardCharsets.UTF_8) - ).getBase64 - }, - ) - .mkString(".") - - val thrown = the[FidoMetadataDownloaderException] thrownBy { - FidoMetadataDownloader - .builder() - .expectLegalHeader("Kom ihåg att du aldrig får snyta dig i mattan!") - .useTrustRoot(trustRootCert) - .useBlob(badBlobJwt) - .useCrls(crls.asJava) - .build() - .loadCachedBlob + .useTrustRoot(trustRootCert) + .useBlob(badBlobJwt) + .useCrls(crls.asJava) + .build() + ) + } + thrown.getReason should be(Reason.BAD_SIGNATURE) } - thrown.getReason should be(Reason.BAD_SIGNATURE) - } - it("""A newly downloaded BLOB is disregarded if the cached one has a greater "no".""") { - val oldBlobNo = 2 - val newBlobNo = 1 + it("""A newly downloaded BLOB is disregarded if the cached one has a greater "no".""") { + val oldBlobNo = 2 + val newBlobNo = 1 - val (trustRootCert, caKeypair, caName) = makeTrustRootCert() - val (blobCert, blobKeypair, _) = makeCert(caKeypair, caName) - val oldBlobJwt = - makeBlob( - List(blobCert), - blobKeypair, - 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 = 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)) - ), - _ => {}, + val (trustRootCert, caKeypair, caName) = makeTrustRootCert() + val (blobCert, blobKeypair, _) = makeCert(caKeypair, caName) + val oldBlobJwt = + makeBlob( + List(blobCert), + blobKeypair, + 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, + ) ) - .clock(Clock.fixed(CertValidFrom, ZoneOffset.UTC)) - .useCrls(crls.asJava) - .trustHttpsCerts(httpsCert) - .build() - .loadCachedBlob - .getPayload - blob should not be null - blob.getNo should equal(oldBlobNo) - } - it("A newly downloaded BLOB is disregarded if it has an invalid signature but the cached one has a valid signature.") { - val oldBlobNo = 1 - val newBlobNo = 2 + val (server, serverUrl, httpsCert) = + makeHttpServer("/blob.jwt", newBlobJwt) + startServer(server) - val (trustRootCert, caKeypair, caName) = makeTrustRootCert() - val (blobCert, blobKeypair, _) = makeCert(caKeypair, caName) - val oldBlobJwt = - makeBlob( - List(blobCert), - blobKeypair, - 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 badNewBlobJwt = newBlobJwt - .split(raw"\.") - .updated( - 1, { - val json = JacksonCodecs.json() - val badBlobBody = json - .readTree( - ByteArray.fromBase64Url(newBlobJwt.split(raw"\.")(1)).getBytes - ) - .asInstanceOf[ObjectNode] - badBlobBody.set("no", new IntNode(7)) - new ByteArray( - json - .writeValueAsString(badBlobBody) - .getBytes(StandardCharsets.UTF_8) - ).getBase64 - }, - ) - .mkString(".") - - val (server, serverUrl, httpsCert) = - makeHttpServer("/blob.jwt", badNewBlobJwt) - startServer(server) - - val blob = 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(CertValidFrom, ZoneOffset.UTC)) - .useCrls(crls.asJava) - .trustHttpsCerts(httpsCert) - .build() - .loadCachedBlob - .getPayload - blob should not be null - blob.getNo should equal(oldBlobNo) - } - } + 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(CertValidFrom, ZoneOffset.UTC)) + .useCrls(crls.asJava) + .trustHttpsCerts(httpsCert) + .build() + ).getPayload + blob should not be null + blob.getNo should equal(oldBlobNo) + } - describe("7. Write the verified object to a local cache as required.") { - it("Cache consumer works.") { - val (trustRootCert, caKeypair, caName) = makeTrustRootCert() - val (blobCert, blobKeypair, _) = makeCert(caKeypair, caName) - val blobJwt = - makeBlob(List(blobCert), blobKeypair, LocalDate.parse("2022-01-19")) - val crls = List( - TestAuthenticator.buildCrl( - caName, - caKeypair.getPrivate, - "SHA256withECDSA", - CertValidFrom, - CertValidTo, - ) - ) - - val (server, serverUrl, httpsCert) = - makeHttpServer("/blob.jwt", blobJwt) - startServer(server) - - var writtenCache: Option[ByteArray] = None - - val blob = 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.empty(), - cacheme => { writtenCache = Some(cacheme) }, - ) - .clock(Clock.fixed(CertValidFrom, ZoneOffset.UTC)) - .useCrls(crls.asJava) - .trustHttpsCerts(httpsCert) - .build() - .loadCachedBlob - .getPayload - blob should not be null - writtenCache should equal( - Some(new ByteArray(blobJwt.getBytes(StandardCharsets.UTF_8))) - ) - } + it("A newly downloaded BLOB is disregarded if it has an invalid signature but the cached one has a valid signature.") { + val oldBlobNo = 1 + val newBlobNo = 2 - describe("File cache") { - val (trustRootCert, caKeypair, caName) = makeTrustRootCert() - val (blobCert, blobKeypair, _) = makeCert(caKeypair, caName) - val blobJwt = - makeBlob( - List(blobCert), - blobKeypair, - LocalDate.parse("2022-01-19"), - no = 2, - ) - val oldBlobJwt = - makeBlob( - List(blobCert), - blobKeypair, - LocalDate.parse("2022-01-19"), - no = 1, - ) - val crls = List( - TestAuthenticator.buildCrl( - caName, - caKeypair.getPrivate, - "SHA256withECDSA", - CertValidFrom, - CertValidTo, + val (trustRootCert, caKeypair, caName) = makeTrustRootCert() + val (blobCert, blobKeypair, _) = makeCert(caKeypair, caName) + val oldBlobJwt = + makeBlob( + List(blobCert), + blobKeypair, + 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, + ) ) - ) - it("is overwritten if it exists.") { + val badNewBlobJwt = newBlobJwt + .split(raw"\.") + .updated( + 1, { + val json = JacksonCodecs.json() + val badBlobBody = json + .readTree( + ByteArray + .fromBase64Url(newBlobJwt.split(raw"\.")(1)) + .getBytes + ) + .asInstanceOf[ObjectNode] + badBlobBody.set("no", new IntNode(7)) + new ByteArray( + json + .writeValueAsString(badBlobBody) + .getBytes(StandardCharsets.UTF_8) + ).getBase64 + }, + ) + .mkString(".") + val (server, serverUrl, httpsCert) = - makeHttpServer("/blob.jwt", blobJwt) + makeHttpServer("/blob.jwt", badNewBlobJwt) startServer(server) - val cacheFile = File.createTempFile( - s"${getClass.getCanonicalName}_test_cache_", - ".tmp", - ) - val f = new FileOutputStream(cacheFile) - f.write(oldBlobJwt.getBytes(StandardCharsets.UTF_8)) - f.close() - cacheFile.deleteOnExit() + 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(CertValidFrom, ZoneOffset.UTC)) + .useCrls(crls.asJava) + .trustHttpsCerts(httpsCert) + .build() + ).getPayload + blob should not be null + blob.getNo should equal(oldBlobNo) + } + } - val blob = FidoMetadataDownloader - .builder() - .expectLegalHeader("Kom ihåg att du aldrig får snyta dig i mattan!") - .useTrustRoot(trustRootCert) - .downloadBlob(new URL(s"${serverUrl}/blob.jwt")) - .useBlobCacheFile(cacheFile) - .clock( - Clock.fixed(Instant.parse("2022-01-19T00:00:00Z"), ZoneOffset.UTC) + describe("7. Write the verified object to a local cache as required.") { + it("Cache consumer works.") { + val (trustRootCert, caKeypair, caName) = makeTrustRootCert() + val (blobCert, blobKeypair, _) = makeCert(caKeypair, caName) + val blobJwt = + makeBlob(List(blobCert), blobKeypair, LocalDate.parse("2022-01-19")) + val crls = List( + TestAuthenticator.buildCrl( + caName, + caKeypair.getPrivate, + "SHA256withECDSA", + CertValidFrom, + CertValidTo, ) - .useCrls(crls.asJava) - .trustHttpsCerts(httpsCert) - .clock(Clock.fixed(CertValidFrom, ZoneOffset.UTC)) - .build() - .loadCachedBlob - .getPayload - blob should not be null - blob.getNo should be(2) - cacheFile.exists() should be(true) - BinaryUtil.readAll(new FileInputStream(cacheFile)) should equal( - blobJwt.getBytes(StandardCharsets.UTF_8) ) - } - it("is created if it does not exist.") { val (server, serverUrl, httpsCert) = makeHttpServer("/blob.jwt", blobJwt) startServer(server) - val cacheFile = File.createTempFile( - s"${getClass.getCanonicalName}_test_cache_", - ".tmp", - ) - cacheFile.delete() - cacheFile.deleteOnExit() + var writtenCache: Option[ByteArray] = None - val blob = FidoMetadataDownloader - .builder() - .expectLegalHeader("Kom ihåg att du aldrig får snyta dig i mattan!") - .useTrustRoot(trustRootCert) - .downloadBlob(new URL(s"${serverUrl}/blob.jwt")) - .useBlobCacheFile(cacheFile) - .clock( - Clock.fixed(Instant.parse("2022-01-19T00:00:00Z"), ZoneOffset.UTC) - ) - .useCrls(crls.asJava) - .trustHttpsCerts(httpsCert) - .clock(Clock.fixed(CertValidFrom, ZoneOffset.UTC)) - .build() - .loadCachedBlob - .getPayload + 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.empty(), + cacheme => { + writtenCache = Some(cacheme) + }, + ) + .clock(Clock.fixed(CertValidFrom, ZoneOffset.UTC)) + .useCrls(crls.asJava) + .trustHttpsCerts(httpsCert) + .build() + ).getPayload blob should not be null - blob.getNo should be(2) - cacheFile.exists() should be(true) - BinaryUtil.readAll(new FileInputStream(cacheFile)) should equal( - blobJwt.getBytes(StandardCharsets.UTF_8) + writtenCache should equal( + Some(new ByteArray(blobJwt.getBytes(StandardCharsets.UTF_8))) ) } - it("is read from.") { - val (server, serverUrl, httpsCert) = - makeHttpServer("/blob.jwt", oldBlobJwt) - startServer(server) - - val cacheFile = File.createTempFile( - s"${getClass.getCanonicalName}_test_cache_", - ".tmp", + describe("File cache") { + val (trustRootCert, caKeypair, caName) = makeTrustRootCert() + val (blobCert, blobKeypair, _) = makeCert(caKeypair, caName) + val blobJwt = + makeBlob( + List(blobCert), + blobKeypair, + LocalDate.parse("2022-01-19"), + no = 2, + ) + val oldBlobJwt = + makeBlob( + List(blobCert), + blobKeypair, + LocalDate.parse("2022-01-19"), + no = 1, + ) + val crls = List( + TestAuthenticator.buildCrl( + caName, + caKeypair.getPrivate, + "SHA256withECDSA", + CertValidFrom, + CertValidTo, + ) ) - cacheFile.deleteOnExit() - val f = new FileOutputStream(cacheFile) - f.write(blobJwt.getBytes(StandardCharsets.UTF_8)) - f.close() - val blob = FidoMetadataDownloader - .builder() - .expectLegalHeader("Kom ihåg att du aldrig får snyta dig i mattan!") - .useTrustRoot(trustRootCert) - .downloadBlob(new URL(s"${serverUrl}/blob.jwt")) - .useBlobCacheFile(cacheFile) - .clock(Clock.fixed(CertValidFrom, ZoneOffset.UTC)) - .useCrls(crls.asJava) - .trustHttpsCerts(httpsCert) - .build() - .loadCachedBlob - .getPayload - blob should not be null - blob.getNo should be(2) + it("is overwritten if it exists.") { + val (server, serverUrl, httpsCert) = + makeHttpServer("/blob.jwt", blobJwt) + startServer(server) + + val cacheFile = File.createTempFile( + s"${getClass.getCanonicalName}_test_cache_", + ".tmp", + ) + val f = new FileOutputStream(cacheFile) + f.write(oldBlobJwt.getBytes(StandardCharsets.UTF_8)) + f.close() + cacheFile.deleteOnExit() + + 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")) + .useBlobCacheFile(cacheFile) + .clock( + Clock.fixed( + Instant.parse("2022-01-19T00:00:00Z"), + ZoneOffset.UTC, + ) + ) + .useCrls(crls.asJava) + .trustHttpsCerts(httpsCert) + .clock(Clock.fixed(CertValidFrom, ZoneOffset.UTC)) + .build() + ).getPayload + blob should not be null + blob.getNo should be(2) + cacheFile.exists() should be(true) + BinaryUtil.readAll(new FileInputStream(cacheFile)) should equal( + blobJwt.getBytes(StandardCharsets.UTF_8) + ) + } + + it("is created if it does not exist.") { + val (server, serverUrl, httpsCert) = + makeHttpServer("/blob.jwt", blobJwt) + startServer(server) + + val cacheFile = File.createTempFile( + s"${getClass.getCanonicalName}_test_cache_", + ".tmp", + ) + cacheFile.delete() + cacheFile.deleteOnExit() + + 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")) + .useBlobCacheFile(cacheFile) + .clock( + Clock.fixed( + Instant.parse("2022-01-19T00:00:00Z"), + ZoneOffset.UTC, + ) + ) + .useCrls(crls.asJava) + .trustHttpsCerts(httpsCert) + .clock(Clock.fixed(CertValidFrom, ZoneOffset.UTC)) + .build() + ).getPayload + blob should not be null + blob.getNo should be(2) + cacheFile.exists() should be(true) + BinaryUtil.readAll(new FileInputStream(cacheFile)) should equal( + blobJwt.getBytes(StandardCharsets.UTF_8) + ) + } + + it("is read from.") { + val (server, serverUrl, httpsCert) = + makeHttpServer("/blob.jwt", oldBlobJwt) + startServer(server) + + val cacheFile = File.createTempFile( + s"${getClass.getCanonicalName}_test_cache_", + ".tmp", + ) + cacheFile.deleteOnExit() + val f = new FileOutputStream(cacheFile) + f.write(blobJwt.getBytes(StandardCharsets.UTF_8)) + f.close() + + 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")) + .useBlobCacheFile(cacheFile) + .clock(Clock.fixed(CertValidFrom, ZoneOffset.UTC)) + .useCrls(crls.asJava) + .trustHttpsCerts(httpsCert) + .build() + ).getPayload + blob should not be null + blob.getNo should be(2) + } } } + + describe("8. Iterate through the individual entries (of type MetadataBLOBPayloadEntry). For each entry:") { + it("Nothing to test - see instead FidoMetadataService.") {} + } + } + } + + describe("3. The FIDO Server MUST be able to download the latest metadata BLOB object from the well-known URL when appropriate, e.g. https://mds.fidoalliance.org/. The nextUpdate field of the Metadata BLOB specifies a date when the download SHOULD occur at latest.") { + + 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) + + val downloader = 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(CertValidFrom, ZoneOffset.UTC)) + .useCrls(crls.asJava) + .trustHttpsCerts(httpsCert) + .build() + + it( + "[using loadCachedBlob] The BLOB is not downloaded if the cached one is not yet out of date." + ) { + startServer(server) + val blob = downloader.loadCachedBlob().getPayload + blob should not be null + blob.getNo should equal(oldBlobNo) } - describe("8. Iterate through the individual entries (of type MetadataBLOBPayloadEntry). For each entry:") { - it("Nothing to test - see instead FidoMetadataService.") {} + it( + "[using refreshBlob] The BLOB is always downloaded even if the cached one is not yet out of date." + ) { + startServer(server) + val blob = downloader.refreshBlob().getPayload + blob should not be null + blob.getNo should equal(newBlobNo) } } From e9ee6818dc7f19111e6d72a0f890b76b0c029bff Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 31 May 2022 17:31:10 +0200 Subject: [PATCH 019/145] Fix AsciiDoc list syntax in NEWS --- NEWS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NEWS b/NEWS index b9b6cd622..3b656507c 100644 --- a/NEWS +++ b/NEWS @@ -2,7 +2,7 @@ New features: -- Added method `FidoMetadataDownloader.refreshBlob()`. +* Added method `FidoMetadataDownloader.refreshBlob()`. == Version 2.0.0 == From 05e2452edf422022cead5d676d2f46c43e6f713a Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 30 May 2022 16:56:55 +0200 Subject: [PATCH 020/145] Add JavaDoc to COSEAlgorithmIdentifier.fromId --- .../yubico/webauthn/data/COSEAlgorithmIdentifier.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/COSEAlgorithmIdentifier.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/COSEAlgorithmIdentifier.java index 1ba31d5ca..737278388 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/COSEAlgorithmIdentifier.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/COSEAlgorithmIdentifier.java @@ -51,6 +51,16 @@ public enum COSEAlgorithmIdentifier { this.id = id; } + /** + * Attempt to parse an integer as a {@link COSEAlgorithmIdentifier}. + * + * @param id an integer equal to the {@link #getId() id} of a constant in {@link + * COSEAlgorithmIdentifier} + * @return The {@link COSEAlgorithmIdentifier} instance whose {@link #getId() id} equals id + * , if any. + * @see §5.8.5. + * Cryptographic Algorithm Identifier (typedef COSEAlgorithmIdentifier) + */ public static Optional fromId(long id) { return Stream.of(values()).filter(v -> v.id == id).findAny(); } From a7475315d81893f4cba985969d23e212e10365c3 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 30 May 2022 17:19:23 +0200 Subject: [PATCH 021/145] Add function COSEAlgorithmIdentifier.fromPublicKey(ByteArray) --- NEWS | 1 + .../yubico/webauthn/FinishAssertionSteps.java | 2 +- .../com/yubico/webauthn/WebAuthnCodecs.java | 7 ---- .../data/COSEAlgorithmIdentifier.java | 33 +++++++++++++++++++ .../webauthn/RegistrationTestData.scala | 6 ++-- .../RelyingPartyRegistrationSpec.scala | 4 +-- 6 files changed, 40 insertions(+), 13 deletions(-) diff --git a/NEWS b/NEWS index 3b656507c..d2384d656 100644 --- a/NEWS +++ b/NEWS @@ -3,6 +3,7 @@ New features: * Added method `FidoMetadataDownloader.refreshBlob()`. +* Added function `COSEAlgorithmIdentifier.fromPublicKey(ByteArray)`. == Version 2.0.0 == diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishAssertionSteps.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishAssertionSteps.java index 91496ec98..543cd2d96 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishAssertionSteps.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishAssertionSteps.java @@ -490,7 +490,7 @@ public void validate() { } final COSEAlgorithmIdentifier alg = - WebAuthnCodecs.getCoseKeyAlg(cose) + COSEAlgorithmIdentifier.fromPublicKey(cose) .orElseThrow( () -> new IllegalArgumentException( 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 34d961bae..8a5c3d627 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/WebAuthnCodecs.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/WebAuthnCodecs.java @@ -42,7 +42,6 @@ import java.util.Arrays; import java.util.HashMap; import java.util.Map; -import java.util.Optional; final class WebAuthnCodecs { @@ -140,12 +139,6 @@ private static PublicKey importCoseEd25519PublicKey(CBORObject cose) return kFact.generatePublic(new X509EncodedKeySpec(x509Key.getBytes())); } - static Optional getCoseKeyAlg(ByteArray key) { - CBORObject cose = CBORObject.DecodeFromBytes(key.getBytes()); - final int alg = cose.get(CBORObject.FromObject(3)).AsInt32(); - return COSEAlgorithmIdentifier.fromId(alg); - } - static String getJavaAlgorithmName(COSEAlgorithmIdentifier alg) { switch (alg) { case EdDSA: diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/COSEAlgorithmIdentifier.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/COSEAlgorithmIdentifier.java index 737278388..017873c9c 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/COSEAlgorithmIdentifier.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/COSEAlgorithmIdentifier.java @@ -26,9 +26,12 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonValue; +import com.upokecenter.cbor.CBORException; +import com.upokecenter.cbor.CBORObject; import java.util.Optional; import java.util.stream.Stream; import lombok.Getter; +import lombok.NonNull; /** * A number identifying a cryptographic algorithm. The algorithm identifiers SHOULD be values @@ -65,6 +68,36 @@ public static Optional fromId(long id) { return Stream.of(values()).filter(v -> v.id == id).findAny(); } + /** + * Read the {@link COSEAlgorithmIdentifier} from a public key in COSE_Key format. + * + * @param publicKeyCose a public key in COSE_Key format. + * @return The alg of the publicKeyCose parsed as a {@link + * COSEAlgorithmIdentifier}, if possible. Returns empty if the {@link COSEAlgorithmIdentifier} + * enum has no constant matching the alg value. + * @throws IllegalArgumentException if publicKeyCose is not a well-formed COSE_Key. + */ + public static Optional fromPublicKey(@NonNull ByteArray publicKeyCose) { + final CBORObject ALG = CBORObject.FromObject(3); + final int alg; + try { + CBORObject cose = CBORObject.DecodeFromBytes(publicKeyCose.getBytes()); + if (!cose.ContainsKey(ALG)) { + throw new IllegalArgumentException( + "Public key does not contain an \"alg\"(3) value: " + publicKeyCose); + } + CBORObject algCbor = cose.get(ALG); + if (!(algCbor.isNumber() && algCbor.AsNumber().IsInteger())) { + throw new IllegalArgumentException( + "Public key has non-integer \"alg\"(3) value: " + publicKeyCose); + } + alg = algCbor.AsInt32(); + } catch (CBORException e) { + throw new IllegalArgumentException("Failed to parse public key", e); + } + return fromId(alg); + } + @JsonCreator private static COSEAlgorithmIdentifier fromJson(long id) { return fromId(id) diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RegistrationTestData.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RegistrationTestData.scala index f343fcd0b..58c8b7ac5 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RegistrationTestData.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RegistrationTestData.scala @@ -642,8 +642,8 @@ case class RegistrationTestData( }) protected def validate(): Unit = { - val alg = WebAuthnCodecs - .getCoseKeyAlg( + val alg = COSEAlgorithmIdentifier + .fromPublicKey( response.getResponse.getParsedAuthenticatorData.getAttestedCredentialData.get.getCredentialPublicKey ) .get @@ -785,7 +785,7 @@ case class RegistrationTestData( val pubkey = WebAuthnCodecs.importCosePublicKey(pubKeyCoseBytes) val prikey = WebAuthnTestCodecs.importPrivateKey( privateKey, - WebAuthnCodecs.getCoseKeyAlg(pubKeyCoseBytes).get, + COSEAlgorithmIdentifier.fromPublicKey(pubKeyCoseBytes).get, ) new KeyPair(pubkey, prikey) } 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 d1bf10d9b..5157fb321 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 @@ -1781,8 +1781,8 @@ class RelyingPartyRegistrationSpec it("Succeeds for an RS1 test case.") { val testData = RegistrationTestData.Packed.SelfAttestationRs1 - val alg = WebAuthnCodecs - .getCoseKeyAlg( + val alg = COSEAlgorithmIdentifier + .fromPublicKey( testData.response.getResponse.getParsedAuthenticatorData.getAttestedCredentialData.get.getCredentialPublicKey ) .get From 5b3fe6ae5888c410c956d3e79463dece8196b421 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 31 May 2022 17:29:00 +0200 Subject: [PATCH 022/145] Add field AssertionResult.credential: RegisteredCredential --- NEWS | 8 ++ .../com/yubico/webauthn/AssertionResult.java | 124 ++++++++++++------ .../yubico/webauthn/FinishAssertionSteps.java | 49 +++---- .../com/yubico/webauthn/Generators.scala | 6 +- .../webauthn/RelyingPartyAssertionSpec.scala | 7 + 5 files changed, 120 insertions(+), 74 deletions(-) diff --git a/NEWS b/NEWS index d2384d656..83ccbaa98 100644 --- a/NEWS +++ b/NEWS @@ -1,9 +1,17 @@ == Version 2.1.0 (unreleased) == +Deprecations: + +* Deprecated method `AssertionResult.getCredentialId(): ByteArray`. Use + `.getCredential().getCredentialId()` instead. +* Deprecated method `AssertionResult.getUserHandle(): ByteArray`. Use + `.getCredential().getUserHandle()` instead. + New features: * Added method `FidoMetadataDownloader.refreshBlob()`. * Added function `COSEAlgorithmIdentifier.fromPublicKey(ByteArray)`. +* Added method `AssertionResult.getCredential(): RegisteredCredential`. == Version 2.0.0 == 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 2b8c3c0f1..1fb1d9909 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 @@ -26,6 +26,7 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; +import com.yubico.internal.util.ExceptionUtil; import com.yubico.webauthn.data.AuthenticatorAssertionExtensionOutputs; import com.yubico.webauthn.data.AuthenticatorData; import com.yubico.webauthn.data.ByteArray; @@ -46,24 +47,16 @@ public class AssertionResult { private final boolean success; /** - * The credential - * ID of the credential used for the assertion. - * - * @see Credential - * ID - * @see PublicKeyCredentialRequestOptions#getAllowCredentials() - */ - @NonNull private final ByteArray credentialId; - - /** - * The user handle - * of the authenticated user. + * The {@link RegisteredCredential} that was returned by {@link + * CredentialRepository#lookup(ByteArray, ByteArray)} and whose public key was used to + * successfully verify the assertion signature. * - * @see User Handle - * @see UserIdentity#getId() - * @see #getUsername() + *

NOTE: The {@link RegisteredCredential#getSignatureCount() signature count} in this object + * will reflect the signature counter state before the assertion operation, not the new + * counter value. When updating your database state, use the signature counter from {@link + * #getSignatureCount()} instead. */ - @NonNull private final ByteArray userHandle; + private final RegisteredCredential credential; /** * The username of the authenticated user. @@ -107,12 +100,33 @@ public class AssertionResult { private final AuthenticatorAssertionExtensionOutputs authenticatorExtensionOutputs; + private AssertionResult( + boolean success, + @NonNull @JsonProperty("credential") RegisteredCredential credential, + @NonNull String username, + long signatureCount, + boolean signatureCounterValid, + ClientAssertionExtensionOutputs clientExtensionOutputs, + AuthenticatorAssertionExtensionOutputs authenticatorExtensionOutputs) { + this( + success, + credential, + username, + null, + null, + signatureCount, + signatureCounterValid, + clientExtensionOutputs, + authenticatorExtensionOutputs); + } + @JsonCreator private AssertionResult( @JsonProperty("success") boolean success, - @NonNull @JsonProperty("credentialId") ByteArray credentialId, - @NonNull @JsonProperty("userHandle") ByteArray userHandle, + @NonNull @JsonProperty("credential") RegisteredCredential credential, @NonNull @JsonProperty("username") String username, + @JsonProperty("credentialId") ByteArray credentialId, // TODO: Delete in next major release + @JsonProperty("userHandle") ByteArray userHandle, // TODO: Delete in next major release @JsonProperty("signatureCount") long signatureCount, @JsonProperty("signatureCounterValid") boolean signatureCounterValid, @JsonProperty("clientExtensionOutputs") @@ -120,9 +134,20 @@ private AssertionResult( @JsonProperty("authenticatorExtensionOutputs") AuthenticatorAssertionExtensionOutputs authenticatorExtensionOutputs) { this.success = success; - this.credentialId = credentialId; - this.userHandle = userHandle; + this.credential = credential; this.username = username; + + if (credentialId != null) { + ExceptionUtil.assure( + credential.getCredentialId().equals(credentialId), + "Legacy credentialId is present and does not equal credential.credentialId"); + } + if (userHandle != null) { + ExceptionUtil.assure( + credential.getUserHandle().equals(userHandle), + "Legacy userHandle is present and does not equal credential.userHandle"); + } + this.signatureCount = signatureCount; this.signatureCounterValid = signatureCounterValid; this.clientExtensionOutputs = @@ -132,6 +157,36 @@ private AssertionResult( this.authenticatorExtensionOutputs = authenticatorExtensionOutputs; } + /** + * The credential + * ID of the credential used for the assertion. + * + * @see Credential + * ID + * @see PublicKeyCredentialRequestOptions#getAllowCredentials() + * @deprecated Use {@link #getCredential()}.{@link RegisteredCredential#getCredentialId() + * getCredentialId()} instead. + */ + @Deprecated + public ByteArray getCredentialId() { + return credential.getCredentialId(); + } + + /** + * The user handle + * of the authenticated user. + * + * @see User Handle + * @see UserIdentity#getId() + * @see #getUsername() + * @deprecated Use {@link #getCredential()}.{@link RegisteredCredential#getUserHandle()} () + * getUserHandle()} instead. + */ + @Deprecated + public ByteArray getUserHandle() { + return credential.getUserHandle(); + } + /** * The client @@ -180,49 +235,42 @@ public Step2 success(boolean success) { } public class Step2 { - public Step3 credentialId(ByteArray credentialId) { - builder.credentialId(credentialId); + public Step3 credential(RegisteredCredential credential) { + builder.credential(credential); return new Step3(); } } public class Step3 { - public Step4 userHandle(ByteArray userHandle) { - builder.userHandle(userHandle); + public Step4 username(String username) { + builder.username(username); return new Step4(); } } public class Step4 { - public Step5 username(String username) { - builder.username(username); + public Step5 signatureCount(long signatureCount) { + builder.signatureCount(signatureCount); return new Step5(); } } public class Step5 { - public Step6 signatureCount(long signatureCount) { - builder.signatureCount(signatureCount); + public Step6 signatureCounterValid(boolean signatureCounterValid) { + builder.signatureCounterValid(signatureCounterValid); return new Step6(); } } public class Step6 { - public Step7 signatureCounterValid(boolean signatureCounterValid) { - builder.signatureCounterValid(signatureCounterValid); - return new Step7(); - } - } - - public class Step7 { - public Step8 clientExtensionOutputs( + public Step7 clientExtensionOutputs( ClientAssertionExtensionOutputs clientExtensionOutputs) { builder.clientExtensionOutputs(clientExtensionOutputs); - return new Step8(); + return new Step7(); } } - public class Step8 { + public class Step7 { public AssertionResultBuilder assertionExtensionOutputs( AuthenticatorAssertionExtensionOutputs authenticatorExtensionOutputs) { return builder.authenticatorExtensionOutputs(authenticatorExtensionOutputs); diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishAssertionSteps.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishAssertionSteps.java index 543cd2d96..b0194b501 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishAssertionSteps.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishAssertionSteps.java @@ -203,7 +203,7 @@ class Step7 implements Step { @Override public Step8 nextStep() { - return new Step8(username, userHandle, credential.get()); + return new Step8(username, credential.get()); } @Override @@ -220,7 +220,6 @@ public void validate() { class Step8 implements Step { private final String username; - private final ByteArray userHandle; private final RegisteredCredential credential; @Override @@ -232,7 +231,7 @@ public void validate() { @Override public Step10 nextStep() { - return new Step10(username, userHandle, credential); + return new Step10(username, credential); } public ByteArray authenticatorData() { @@ -253,7 +252,6 @@ public ByteArray signature() { @Value class Step10 implements Step { private final String username; - private final ByteArray userHandle; private final RegisteredCredential credential; @Override @@ -263,7 +261,7 @@ public void validate() { @Override public Step11 nextStep() { - return new Step11(username, userHandle, credential, clientData()); + return new Step11(username, credential, clientData()); } public CollectedClientData clientData() { @@ -274,7 +272,6 @@ public CollectedClientData clientData() { @Value class Step11 implements Step { private final String username; - private final ByteArray userHandle; private final RegisteredCredential credential; private final CollectedClientData clientData; @@ -289,14 +286,13 @@ public void validate() { @Override public Step12 nextStep() { - return new Step12(username, userHandle, credential); + return new Step12(username, credential); } } @Value class Step12 implements Step { private final String username; - private final ByteArray userHandle; private final RegisteredCredential credential; @Override @@ -311,14 +307,13 @@ public void validate() { @Override public Step13 nextStep() { - return new Step13(username, userHandle, credential); + return new Step13(username, credential); } } @Value class Step13 implements Step { private final String username; - private final ByteArray userHandle; private final RegisteredCredential credential; @Override @@ -331,14 +326,13 @@ public void validate() { @Override public Step14 nextStep() { - return new Step14(username, userHandle, credential); + return new Step14(username, credential); } } @Value class Step14 implements Step { private final String username; - private final ByteArray userHandle; private final RegisteredCredential credential; @Override @@ -349,14 +343,13 @@ public void validate() { @Override public Step15 nextStep() { - return new Step15(username, userHandle, credential); + return new Step15(username, credential); } } @Value class Step15 implements Step { private final String username; - private final ByteArray userHandle; private final RegisteredCredential credential; @Override @@ -382,14 +375,13 @@ public void validate() { @Override public Step16 nextStep() { - return new Step16(username, userHandle, credential); + return new Step16(username, credential); } } @Value class Step16 implements Step { private final String username; - private final ByteArray userHandle; private final RegisteredCredential credential; @Override @@ -401,14 +393,13 @@ public void validate() { @Override public Step17 nextStep() { - return new Step17(username, userHandle, credential); + return new Step17(username, credential); } } @Value class Step17 implements Step { private final String username; - private final ByteArray userHandle; private final RegisteredCredential credential; @Override @@ -425,14 +416,13 @@ public void validate() { @Override public Step18 nextStep() { - return new Step18(username, userHandle, credential); + return new Step18(username, credential); } } @Value class Step18 implements Step { private final String username; - private final ByteArray userHandle; private final RegisteredCredential credential; @Override @@ -440,14 +430,13 @@ public void validate() {} @Override public Step19 nextStep() { - return new Step19(username, userHandle, credential); + return new Step19(username, credential); } } @Value class Step19 implements Step { private final String username; - private final ByteArray userHandle; private final RegisteredCredential credential; @Override @@ -457,7 +446,7 @@ public void validate() { @Override public Step20 nextStep() { - return new Step20(username, userHandle, credential, clientDataJsonHash()); + return new Step20(username, credential, clientDataJsonHash()); } public ByteArray clientDataJsonHash() { @@ -468,7 +457,6 @@ public ByteArray clientDataJsonHash() { @Value class Step20 implements Step { private final String username; - private final ByteArray userHandle; private final RegisteredCredential credential; private final ByteArray clientDataJsonHash; @@ -503,7 +491,7 @@ public void validate() { @Override public Step21 nextStep() { - return new Step21(username, userHandle, credential); + return new Step21(username, credential); } public ByteArray signedBytes() { @@ -514,13 +502,11 @@ public ByteArray signedBytes() { @Value class Step21 implements Step { private final String username; - private final ByteArray userHandle; private final RegisteredCredential credential; private final long storedSignatureCountBefore; - public Step21(String username, ByteArray userHandle, RegisteredCredential credential) { + public Step21(String username, RegisteredCredential credential) { this.username = username; - this.userHandle = userHandle; this.credential = credential; this.storedSignatureCountBefore = credential.getSignatureCount(); } @@ -540,7 +526,7 @@ private boolean signatureCounterValid() { @Override public Finished nextStep() { - return new Finished(username, userHandle, assertionSignatureCount(), signatureCounterValid()); + return new Finished(credential, username, assertionSignatureCount(), signatureCounterValid()); } private long assertionSignatureCount() { @@ -550,8 +536,8 @@ private long assertionSignatureCount() { @Value class Finished implements Step { + private final RegisteredCredential credential; private final String username; - private final ByteArray userHandle; private final long assertionSignatureCount; private final boolean signatureCounterValid; @@ -570,8 +556,7 @@ public Optional result() { return Optional.of( AssertionResult.builder() .success(true) - .credentialId(response.getId()) - .userHandle(userHandle) + .credential(credential) .username(username) .signatureCount(assertionSignatureCount) .signatureCounterValid(signatureCounterValid) 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 cdf29f722..15d346337 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 @@ -25,17 +25,15 @@ object Generators { authenticatorExtensionOutputs <- arbitrary[Option[AuthenticatorAssertionExtensionOutputs]] clientExtensionOutputs <- arbitrary[ClientAssertionExtensionOutputs] - credentialId <- arbitrary[ByteArray] + credential <- arbitrary[RegisteredCredential] signatureCount <- arbitrary[Long] signatureCounterValid <- arbitrary[Boolean] success <- arbitrary[Boolean] - userHandle <- arbitrary[ByteArray] username <- arbitrary[String] } yield AssertionResult .builder() .success(success) - .credentialId(credentialId) - .userHandle(userHandle) + .credential(credential) .username(username) .signatureCount(signatureCount) .signatureCounterValid(signatureCounterValid) 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 da17187b5..fc546d5ef 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 @@ -1829,6 +1829,13 @@ class RelyingPartyAssertionSpec step.result.get.isSuccess should be(true) step.result.get.getCredentialId should equal(Defaults.credentialId) step.result.get.getUserHandle should equal(Defaults.userHandle) + step.result.get.getCredential.getCredentialId should equal( + step.result.get.getCredentialId + ) + step.result.get.getCredential.getUserHandle should equal( + step.result.get.getUserHandle + ) + step.result.get.getCredential.getPublicKeyCose should not be null } } } From 0828654f8b6a66f18a7b00df198f3f20b1f578db Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 31 May 2022 17:32:47 +0200 Subject: [PATCH 023/145] Compute assertionSignatureCount only once in FinishAssertionSteps --- .../com/yubico/webauthn/FinishAssertionSteps.java | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishAssertionSteps.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishAssertionSteps.java index b0194b501..c2388207e 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishAssertionSteps.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishAssertionSteps.java @@ -503,11 +503,14 @@ public ByteArray signedBytes() { class Step21 implements Step { private final String username; private final RegisteredCredential credential; + private final long assertionSignatureCount; private final long storedSignatureCountBefore; public Step21(String username, RegisteredCredential credential) { this.username = username; this.credential = credential; + this.assertionSignatureCount = + response.getResponse().getParsedAuthenticatorData().getSignatureCounter(); this.storedSignatureCountBefore = credential.getSignatureCount(); } @@ -515,22 +518,18 @@ public Step21(String username, RegisteredCredential credential) { public void validate() throws InvalidSignatureCountException { if (validateSignatureCounter && !signatureCounterValid()) { throw new InvalidSignatureCountException( - response.getId(), storedSignatureCountBefore + 1, assertionSignatureCount()); + response.getId(), storedSignatureCountBefore + 1, assertionSignatureCount); } } private boolean signatureCounterValid() { - return (assertionSignatureCount() == 0 && storedSignatureCountBefore == 0) - || assertionSignatureCount() > storedSignatureCountBefore; + return (assertionSignatureCount == 0 && storedSignatureCountBefore == 0) + || assertionSignatureCount > storedSignatureCountBefore; } @Override public Finished nextStep() { - return new Finished(credential, username, assertionSignatureCount(), signatureCounterValid()); - } - - private long assertionSignatureCount() { - return response.getResponse().getParsedAuthenticatorData().getSignatureCounter(); + return new Finished(credential, username, assertionSignatureCount, signatureCounterValid()); } } From 6843b800ad068da2490542f8bf27463a6b3aa3f4 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 31 May 2022 18:18:47 +0200 Subject: [PATCH 024/145] Collect archive and signature files in build/dist/ --- build.gradle | 14 ++++++++++++++ doc/releasing.md | 8 ++++---- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index cee16d713..05e4d0717 100644 --- a/build.gradle +++ b/build.gradle @@ -132,6 +132,12 @@ task assembleJavadoc(type: Sync) { destinationDir = file("${rootProject.buildDir}/javadoc") } +task collectSignatures(type: Sync) { + destinationDir = file("${rootProject.buildDir}/dist") + duplicatesStrategy DuplicatesStrategy.FAIL + include '*.jar', '*.jar.asc' +} + String getGitCommit() { def proc = "git rev-parse HEAD".execute(null, projectDir) proc.waitFor() @@ -270,6 +276,14 @@ subprojects { project -> useGpgCmd() sign publishing.publications.jars } + + tasks.withType(Sign) { Sign signTask -> + rootProject.tasks.collectSignatures { + from signTask.inputs.files + from signTask.outputs.files + } + signTask.finalizedBy rootProject.tasks.collectSignatures + } } } diff --git a/doc/releasing.md b/doc/releasing.md index 1aff7522a..df217dd38 100644 --- a/doc/releasing.md +++ b/doc/releasing.md @@ -60,9 +60,9 @@ Release candidate versions from ASCIIdoc to Markdown and remove line wraps. Include only changes/additions since the previous release or pre-release. - Attach the signature files from - `webauthn-server-attestation/build/libs/webauthn-server-attestation-X.Y.Z-RCN.jar.asc` + `build/dist/webauthn-server-attestation-X.Y.Z-RCN.jar.asc` and - `webauthn-server-core/build/libs/webauthn-server-core-X.Y.Z-RCN.jar.asc`. + `build/dist/webauthn-server-core-X.Y.Z-RCN.jar.asc`. - Note which JDK version was used to build the artifacts. @@ -147,8 +147,8 @@ Release versions from ASCIIdoc to Markdown and remove line wraps. Include all changes since the previous release (not just changes since the previous pre-release). - Attach the signature files from - `webauthn-server-attestation/build/libs/webauthn-server-attestation-X.Y.Z.jar.asc` + `build/dist/webauthn-server-attestation-X.Y.Z.jar.asc` and - `webauthn-server-core/build/libs/webauthn-server-core-X.Y.Z.jar.asc`. + `build/dist/webauthn-server-core-X.Y.Z.jar.asc`. - Note which JDK version was used to build the artifacts. From d978475f3b5be467123abbd5140b47311062e1b3 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Wed, 1 Jun 2022 18:43:21 +0200 Subject: [PATCH 025/145] Fix main branch name in coverage workflow --- .github/workflows/coverage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 3dc41f85a..bee8ff4da 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -3,7 +3,7 @@ name: Test coverage on: push: - branches: [master] + branches: [main] jobs: test: From 60ee8749247789f57f77d9a93a44f0a822978aa2 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Wed, 1 Jun 2022 14:29:03 +0200 Subject: [PATCH 026/145] Host mutation test reports on GitHub Pages instead of Coveralls --- .github/workflows/coverage-index.html | 14 ++++++++ .github/workflows/coverage.yml | 49 ++++++++++++++++++++++++--- README | 2 +- build.gradle | 8 ----- 4 files changed, 59 insertions(+), 14 deletions(-) create mode 100644 .github/workflows/coverage-index.html diff --git a/.github/workflows/coverage-index.html b/.github/workflows/coverage-index.html new file mode 100644 index 000000000..ff40e3c79 --- /dev/null +++ b/.github/workflows/coverage-index.html @@ -0,0 +1,14 @@ + + + + + Mutation test report index - java-webauthn-server {shortcommit} + + +

Mutation test reports for java-webauthn-server {shortcommit}:

+ + + diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index bee8ff4da..73132e41a 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -11,6 +11,9 @@ jobs: runs-on: ubuntu-latest + permissions: + contents: write + steps: - name: Check out code uses: actions/checkout@v1 @@ -21,9 +24,45 @@ jobs: java-version: 11 - name: Run mutation test - run: ./gradlew pitest + run: ./gradlew pitestMerge + + - name: Collect HTML reports + run: | + mkdir -p mutation-coverage-reports + for sp in webauthn-server-attestation webauthn-server-core; do + cp -a "${sp}"/build/reports/pitest mutation-coverage-reports/"${sp}" + done + sed "s/{shortcommit}/${GITHUB_SHA:0:8}/g;s/{commit}/${GITHUB_SHA}/g;s#{repo}#${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}#g" .github/workflows/coverage-index.html > index.html + + - name: Create coverage badge + # This creates a file that defines a [Shields.io endpoint badge](https://shields.io/endpoint) + # which we can then include in the project README. + run: | + KILLED=$(grep -c -E 'status="(KILLED|TIMED_OUT)"' build/reports/pitest/mutations.xml) + MUTATIONS=$(grep -c 'status=' build/reports/pitest/mutations.xml) + COVERAGE_PERCENT=$(( $KILLED * 100 / $MUTATIONS )) + COVERAGE_HUE=$(( $KILLED * 120 / $MUTATIONS )) + cat > coverage-badge.json << EOF + { + "schemaVersion": 1, + "label": "mutation coverage", + "message": "${COVERAGE_PERCENT}%", + "color": "hsl(${COVERAGE_HUE}, 100%, 40%)", + "cacheSeconds": 3600 + } + EOF + + - name: Check out GitHub Pages branch + uses: actions/checkout@v3 + with: + ref: gh-pages + clean: false - - name: Report to Coveralls - env: - COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} - run: ./gradlew coveralls + - name: Push to GitHub Pages + run: | + git config user.name github-actions + git config user.email github-actions@github.com + git rm --cached -rf -- . + git add coverage-badge.json index.html mutation-coverage-reports + git commit --amend --reset-author -m "Generate GitHub Pages content" + git push -f diff --git a/README b/README index 4f1f08f74..0e2be4146 100644 --- a/README +++ b/README @@ -5,7 +5,7 @@ java-webauthn-server :toc-title: image:https://github.com/Yubico/java-webauthn-server/workflows/build/badge.svg["Build Status", link="https://github.com/Yubico/java-webauthn-server/actions"] -image:https://coveralls.io/repos/github/Yubico/java-webauthn-server/badge.svg["Coverage Status", link="https://coveralls.io/github/Yubico/java-webauthn-server"] +image:https://img.shields.io/endpoint?url=https%3A%2F%2FYubico.github.io%2Fjava-webauthn-server%2Fcoverage-badge.json["Mutation test coverage", link="https://Yubico.github.io/java-webauthn-server/"] image:https://github.com/Yubico/java-webauthn-server/actions/workflows/release-verify-signatures.yml/badge.svg["Binary reproducibility", link="https://github.com/Yubico/java-webauthn-server/actions/workflows/release-verify-signatures.yml"] Server-side https://www.w3.org/TR/webauthn/[Web Authentication] library for diff --git a/build.gradle b/build.gradle index cee16d713..c84fdd2af 100644 --- a/build.gradle +++ b/build.gradle @@ -10,7 +10,6 @@ buildscript { } plugins { id 'java-platform' - id 'com.github.kt3k.coveralls' version '2.12.0' id 'io.github.gradle-nexus.publish-plugin' version '1.1.0' id 'io.franzbecker.gradle-lombok' version '5.0.0' } @@ -325,10 +324,3 @@ if (publishEnabled) { } task pitestMerge(type: com.yubico.gradle.pitest.tasks.PitestMergeTask) - -coveralls { - sourceDirs = subprojects.findAll({ project.hasProperty('sourceSets') }).sourceSets.main.allSource.srcDirs.flatten() -} -tasks.coveralls { - inputs.files pitestMerge.outputs.files -} From df6c6506066b6389f57822e62c5bea9d67adc362 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Fri, 3 Jun 2022 15:33:36 +0200 Subject: [PATCH 027/145] Preserve output files through gh-pages checkout --- .github/workflows/coverage.yml | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 73132e41a..a3e4d8c4a 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -26,13 +26,16 @@ jobs: - name: Run mutation test run: ./gradlew pitestMerge + - name: Create output directory + run: mkdir -p build/gh-pages + - name: Collect HTML reports run: | - mkdir -p mutation-coverage-reports + mkdir -p build/gh-pages/mutation-coverage-reports for sp in webauthn-server-attestation webauthn-server-core; do - cp -a "${sp}"/build/reports/pitest mutation-coverage-reports/"${sp}" + cp -a "${sp}"/build/reports/pitest build/gh-pages/mutation-coverage-reports/"${sp}" done - sed "s/{shortcommit}/${GITHUB_SHA:0:8}/g;s/{commit}/${GITHUB_SHA}/g;s#{repo}#${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}#g" .github/workflows/coverage-index.html > index.html + sed "s/{shortcommit}/${GITHUB_SHA:0:8}/g;s/{commit}/${GITHUB_SHA}/g;s#{repo}#${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}#g" .github/workflows/coverage-index.html > build/gh-pages/index.html - name: Create coverage badge # This creates a file that defines a [Shields.io endpoint badge](https://shields.io/endpoint) @@ -42,7 +45,7 @@ jobs: MUTATIONS=$(grep -c 'status=' build/reports/pitest/mutations.xml) COVERAGE_PERCENT=$(( $KILLED * 100 / $MUTATIONS )) COVERAGE_HUE=$(( $KILLED * 120 / $MUTATIONS )) - cat > coverage-badge.json << EOF + cat > build/gh-pages/coverage-badge.json << EOF { "schemaVersion": 1, "label": "mutation coverage", @@ -62,7 +65,8 @@ jobs: run: | git config user.name github-actions git config user.email github-actions@github.com - git rm --cached -rf -- . + git rm -rf -- . + mv build/gh-pages/* . git add coverage-badge.json index.html mutation-coverage-reports git commit --amend --reset-author -m "Generate GitHub Pages content" git push -f From c95212f442a18c101a3312613622d664f7db19cd Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Fri, 3 Jun 2022 13:48:24 +0200 Subject: [PATCH 028/145] Use correct measure of mutation coverage --- .github/workflows/coverage.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index a3e4d8c4a..24eafc54f 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -41,10 +41,10 @@ jobs: # This creates a file that defines a [Shields.io endpoint badge](https://shields.io/endpoint) # which we can then include in the project README. run: | - KILLED=$(grep -c -E 'status="(KILLED|TIMED_OUT)"' build/reports/pitest/mutations.xml) - MUTATIONS=$(grep -c 'status=' build/reports/pitest/mutations.xml) - COVERAGE_PERCENT=$(( $KILLED * 100 / $MUTATIONS )) - COVERAGE_HUE=$(( $KILLED * 120 / $MUTATIONS )) + DETECTED=$(grep -c -E "detected=['\"]true['\"]" build/reports/pitest/mutations.xml) + MUTATIONS=$(grep -c "detected=" build/reports/pitest/mutations.xml) + COVERAGE_PERCENT=$(( $DETECTED * 100 / $MUTATIONS )) + COVERAGE_HUE=$(( $DETECTED * 120 / $MUTATIONS )) cat > build/gh-pages/coverage-badge.json << EOF { "schemaVersion": 1, From 41f8b0fbf811d1fd5355a664ed9c338f8fd9e0eb Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Wed, 1 Jun 2022 17:42:35 +0200 Subject: [PATCH 029/145] Include yubico-util in mutation tests --- .github/workflows/coverage-index.html | 1 + .github/workflows/coverage.yml | 2 +- yubico-util/build.gradle | 15 +++++++++++++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/.github/workflows/coverage-index.html b/.github/workflows/coverage-index.html index ff40e3c79..14908c1af 100644 --- a/.github/workflows/coverage-index.html +++ b/.github/workflows/coverage-index.html @@ -9,6 +9,7 @@

Mutation test reports for java-webauthn-server webauthn-server-attestation
  • webauthn-server-core
  • +
  • yubico-util
  • diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 24eafc54f..bd965fcc1 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -32,7 +32,7 @@ jobs: - name: Collect HTML reports run: | mkdir -p build/gh-pages/mutation-coverage-reports - for sp in webauthn-server-attestation webauthn-server-core; do + for sp in webauthn-server-attestation webauthn-server-core yubico-util; do cp -a "${sp}"/build/reports/pitest build/gh-pages/mutation-coverage-reports/"${sp}" done sed "s/{shortcommit}/${GITHUB_SHA:0:8}/g;s/{commit}/${GITHUB_SHA}/g;s#{repo}#${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}#g" .github/workflows/coverage-index.html > build/gh-pages/index.html diff --git a/yubico-util/build.gradle b/yubico-util/build.gradle index e4249774e..726602ddf 100644 --- a/yubico-util/build.gradle +++ b/yubico-util/build.gradle @@ -3,6 +3,7 @@ plugins { id 'scala' id 'maven-publish' id 'signing' + id 'info.solidsoft.pitest' id 'io.github.cosmicsilence.scalafix' } @@ -50,3 +51,17 @@ jar { } } +pitest { + pitestVersion = '1.4.11' + + timestampedReports = false + outputFormats = ['XML', 'HTML'] + + avoidCallsTo = [ + 'java.util.logging', + 'org.apache.log4j', + 'org.slf4j', + 'org.apache.commons.logging', + 'com.google.common.io.Closeables', + ] +} From 818f8ec3f5ab587d5d5f8b0d7758a9cdf23b7f4f Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Fri, 3 Jun 2022 12:28:18 +0200 Subject: [PATCH 030/145] Move coverage-index.html to .github/workflow-resources/ --- .github/{workflows => workflow-resources}/coverage-index.html | 0 .github/workflows/coverage.yml | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename .github/{workflows => workflow-resources}/coverage-index.html (100%) diff --git a/.github/workflows/coverage-index.html b/.github/workflow-resources/coverage-index.html similarity index 100% rename from .github/workflows/coverage-index.html rename to .github/workflow-resources/coverage-index.html diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index bd965fcc1..b64d2dc3a 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -35,7 +35,7 @@ jobs: for sp in webauthn-server-attestation webauthn-server-core yubico-util; do cp -a "${sp}"/build/reports/pitest build/gh-pages/mutation-coverage-reports/"${sp}" done - sed "s/{shortcommit}/${GITHUB_SHA:0:8}/g;s/{commit}/${GITHUB_SHA}/g;s#{repo}#${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}#g" .github/workflows/coverage-index.html > build/gh-pages/index.html + sed "s/{shortcommit}/${GITHUB_SHA:0:8}/g;s/{commit}/${GITHUB_SHA}/g;s#{repo}#${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}#g" .github/workflow-resources/coverage-index.html > build/gh-pages/index.html - name: Create coverage badge # This creates a file that defines a [Shields.io endpoint badge](https://shields.io/endpoint) From 8178388c8ccfabebccdd389d909ae356171fc13f Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Fri, 3 Jun 2022 17:56:13 +0200 Subject: [PATCH 031/145] Use xq to build coverage-badge.json --- .github/workflows/coverage.yml | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index b64d2dc3a..95ddb0701 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -26,6 +26,10 @@ jobs: - name: Run mutation test run: ./gradlew pitestMerge + - name: Install yq (and xq) + run: | + pip install yq + - name: Create output directory run: mkdir -p build/gh-pages @@ -41,19 +45,17 @@ jobs: # This creates a file that defines a [Shields.io endpoint badge](https://shields.io/endpoint) # which we can then include in the project README. run: | - DETECTED=$(grep -c -E "detected=['\"]true['\"]" build/reports/pitest/mutations.xml) - MUTATIONS=$(grep -c "detected=" build/reports/pitest/mutations.xml) - COVERAGE_PERCENT=$(( $DETECTED * 100 / $MUTATIONS )) - COVERAGE_HUE=$(( $DETECTED * 120 / $MUTATIONS )) - cat > build/gh-pages/coverage-badge.json << EOF - { - "schemaVersion": 1, - "label": "mutation coverage", - "message": "${COVERAGE_PERCENT}%", - "color": "hsl(${COVERAGE_HUE}, 100%, 40%)", - "cacheSeconds": 3600 - } - EOF + cat build/reports/pitest/mutations.xml \ + | xq '.mutations.mutation + | (map(select(.["@detected"] == "true")) | length) / length + | { + schemaVersion: 1, + label: "mutation coverage", + message: "\(. * 100 | floor | tostring) %", + color: "hsl(\(. * 120 | floor | tostring), 100%, 40%", + cacheSeconds: 3600, + }' \ + > build/gh-pages/coverage-badge.json - name: Check out GitHub Pages branch uses: actions/checkout@v3 From eadf13b49657fc9c96863f6634ab32bc2aecc91c Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Fri, 3 Jun 2022 18:05:40 +0200 Subject: [PATCH 032/145] Document reason for granting repo write permission --- .github/workflows/coverage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 95ddb0701..8ed09e5fa 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest permissions: - contents: write + contents: write # For push to GitHub Pages steps: - name: Check out code From 981304ac3cd1f55dba0dbcbb470dc645529ae15f Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Fri, 3 Jun 2022 20:32:24 +0200 Subject: [PATCH 033/145] Post coverage results as commit comment --- .github/workflows/coverage.yml | 27 +++++++- .github/workflows/coverage/compute-stats.sh | 10 +++ .../coverage/index.html.template} | 0 .../workflows/coverage/stats-to-comment.sh | 65 +++++++++++++++++++ 4 files changed, 100 insertions(+), 2 deletions(-) create mode 100755 .github/workflows/coverage/compute-stats.sh rename .github/{workflow-resources/coverage-index.html => workflows/coverage/index.html.template} (100%) create mode 100755 .github/workflows/coverage/stats-to-comment.sh diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 8ed09e5fa..a7a5bb3da 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -39,7 +39,7 @@ jobs: for sp in webauthn-server-attestation webauthn-server-core yubico-util; do cp -a "${sp}"/build/reports/pitest build/gh-pages/mutation-coverage-reports/"${sp}" done - sed "s/{shortcommit}/${GITHUB_SHA:0:8}/g;s/{commit}/${GITHUB_SHA}/g;s#{repo}#${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}#g" .github/workflow-resources/coverage-index.html > build/gh-pages/index.html + sed "s/{shortcommit}/${GITHUB_SHA:0:8}/g;s/{commit}/${GITHUB_SHA}/g;s#{repo}#${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}#g" .github/workflows/coverage/index.html.template > build/gh-pages/index.html - name: Create coverage badge # This creates a file that defines a [Shields.io endpoint badge](https://shields.io/endpoint) @@ -63,12 +63,35 @@ jobs: ref: gh-pages clean: false + - name: Post mutation test results as commit comment + run: | + git checkout "${GITHUB_SHA}" -- .github/workflows/coverage + + ./.github/workflows/coverage/compute-stats.sh build/reports/pitest/mutations.xml > new-stats.json + + if [[ -f prev-mutations.xml ]]; then + ./.github/workflows/coverage/compute-stats.sh prev-mutations.xml > prev-stats.json + else + cp new-stats.json prev-stats.json + fi + + touch prev-commit.txt + + ./.github/workflows/coverage/stats-to-comment.sh prev-stats.json new-stats.json $(cat prev-commit.txt) > build/results-comment.json + + curl -X POST \ + -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \ + ${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/commits/${GITHUB_SHA}/comments -d @build/results-comment.json + mv new-stats.json prev-stats.json + - name: Push to GitHub Pages run: | git config user.name github-actions git config user.email github-actions@github.com git rm -rf -- . mv build/gh-pages/* . - git add coverage-badge.json index.html mutation-coverage-reports + cp build/reports/pitest/mutations.xml prev-mutations.xml + echo "${GITHUB_SHA}" > prev-commit.txt + git add coverage-badge.json index.html mutation-coverage-reports prev-mutations.xml prev-commit.txt git commit --amend --reset-author -m "Generate GitHub Pages content" git push -f diff --git a/.github/workflows/coverage/compute-stats.sh b/.github/workflows/coverage/compute-stats.sh new file mode 100755 index 000000000..0497ea802 --- /dev/null +++ b/.github/workflows/coverage/compute-stats.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +xq '.mutations.mutation + | group_by(.mutatedClass | split(".") | .[:-1]) + | INDEX(.[0].mutatedClass | split(".") | .[:-1] | join(".")) + | map_values({ + detected: (map(select(.["@detected"] == "true")) | length), + mutations: length, + }) +' "${1}" diff --git a/.github/workflow-resources/coverage-index.html b/.github/workflows/coverage/index.html.template similarity index 100% rename from .github/workflow-resources/coverage-index.html rename to .github/workflows/coverage/index.html.template diff --git a/.github/workflows/coverage/stats-to-comment.sh b/.github/workflows/coverage/stats-to-comment.sh new file mode 100755 index 000000000..ea744fff1 --- /dev/null +++ b/.github/workflows/coverage/stats-to-comment.sh @@ -0,0 +1,65 @@ +#!/bin/bash + +make-contents() { + cat << EOF +## Mutation test results + +Package | Coverage | Stats | Prev | Prev | +------- | --------:|:-----:| ----:|:----:| +EOF + + jq -s '.[0] as $old | .[1] as $new + | { + packages: ( + $old | keys + | map({ + ("`\(.)`"): { + before: { + detected: $old[.].detected, + mutations: $old[.].mutations, + }, + after: { + detected: $new[.].detected, + mutations: $new[.].mutations, + }, + percentage_diff: (($new[.].detected / $new[.].mutations - $old[.].detected / $old[.].mutations) * 100 | round), + }, + }) + | add + ), + overall: { + before: { + detected: [($old[] | .detected)] | add, + mutations: [($old[] | .mutations)] | add, + }, + after: { + detected: [($new[] | .detected)] | add, + mutations: [($new[] | .mutations)] | add, + }, + percentage_diff: ( + ( + ([($new[] | .detected)] | add) / ([($new[] | .mutations)] | add) + - ([($old[] | .detected)] | add) / ([($old[] | .mutations)] | add) + ) * 100 | round + ), + }, + } + | { ("**Overall**"): .overall } + .packages + | to_entries + | .[] + | def difficon: if . > 0 then ":green_circle:" else if . < 0 then ":small_red_triangle_down:" else ":small_blue_diamond:" end end; + def triangles: if . > 0 then ":small_red_triangle:" else if . < 0 then ":small_red_triangle_down:" else ":small_blue_diamond:" end end; + "\(.key) | **\(.value.after.detected / .value.after.mutations * 100 | floor) %** \(.value.percentage_diff | difficon) | \(.value.after.detected) \(.value.after.detected - .value.before.detected | triangles) / \(.value.after.mutations) \(.value.after.mutations - .value.before.mutations | triangles)| \(.value.before.detected / .value.before.mutations * 100 | floor) % | \(.value.before.detected) / \(.value.before.mutations)" + ' \ + "${1}" "${2}" --raw-output + + if [[ -n "${3}" ]]; then + cat << EOF + +Previous run: ${3} +EOF + fi + +} + +make-contents "$@" | python -c 'import json; import sys; print(json.dumps({"body": sys.stdin.read()}))' From 51b367a5935b8af81ef6bd2b40b69d156fac72b8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Jun 2022 13:01:24 +0000 Subject: [PATCH 034/145] Bump spotless-plugin-gradle from 6.6.1 to 6.7.0 Bumps [spotless-plugin-gradle](https://github.com/diffplug/spotless) from 6.6.1 to 6.7.0. - [Release notes](https://github.com/diffplug/spotless/releases) - [Changelog](https://github.com/diffplug/spotless/blob/main/CHANGES.md) - [Commits](https://github.com/diffplug/spotless/compare/gradle/6.6.1...gradle/6.7.0) --- updated-dependencies: - dependency-name: com.diffplug.spotless:spotless-plugin-gradle dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index cee16d713..e56310235 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ buildscript { } dependencies { classpath 'com.cinnober.gradle:semver-git:2.5.0' - classpath 'com.diffplug.spotless:spotless-plugin-gradle:6.6.1' + classpath 'com.diffplug.spotless:spotless-plugin-gradle:6.7.0' classpath 'io.github.cosmicsilence:gradle-scalafix:0.1.13' } } From 17c4e1f574101ee5156b85d4e8ca33086330ad32 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Wed, 8 Jun 2022 15:06:11 +0200 Subject: [PATCH 035/145] Enable Dependabot for GitHub Actions --- .github/dependabot.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 1dfeac7ad..031f7e8a5 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -11,3 +11,8 @@ updates: # Spotless patch updates are too noisy - dependency-name: "spotless-plugin-gradle" update-types: ["version-update:semver-patch"] + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" From 12f77da2bd13e13269ffbe3d06ff2bc0049979c2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Jun 2022 13:07:15 +0000 Subject: [PATCH 036/145] Bump actions/upload-artifact from 2 to 3 Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 2 to 3. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v2...v3) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2d6c0bec4..8000ec7c5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -32,14 +32,14 @@ jobs: - name: Archive HTML test report if: ${{ always() }} - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: test-reports-java${{ matrix.java }}-html path: "*/build/reports/**" - name: Archive JUnit test report if: ${{ always() }} - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: test-reports-java${{ matrix.java }}-xml path: "*/build/test-results/**/*.xml" From dd53f8499a618bcb1065823d2af5e689aed0ce48 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Jun 2022 13:07:19 +0000 Subject: [PATCH 037/145] Bump actions/setup-java from 1 to 3 Bumps [actions/setup-java](https://github.com/actions/setup-java) from 1 to 3. - [Release notes](https://github.com/actions/setup-java/releases) - [Commits](https://github.com/actions/setup-java/compare/v1...v3) --- updated-dependencies: - dependency-name: actions/setup-java dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/build.yml | 2 +- .github/workflows/code-formatting.yml | 2 +- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/coverage.yml | 2 +- .github/workflows/release-verify-signatures.yml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2d6c0bec4..442a0ed6e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -23,7 +23,7 @@ jobs: uses: actions/checkout@v1 - name: Set up JDK - uses: actions/setup-java@v1 + uses: actions/setup-java@v3 with: java-version: ${{ matrix.java }} diff --git a/.github/workflows/code-formatting.yml b/.github/workflows/code-formatting.yml index 3bc259afc..37b43f9c4 100644 --- a/.github/workflows/code-formatting.yml +++ b/.github/workflows/code-formatting.yml @@ -23,7 +23,7 @@ jobs: uses: actions/checkout@v1 - name: Set up JDK - uses: actions/setup-java@v1 + uses: actions/setup-java@v3 with: java-version: ${{ matrix.java }} diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index f65521105..76ba2818c 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -20,7 +20,7 @@ jobs: - name: Checkout repository uses: actions/checkout@v2 - - uses: actions/setup-java@v1 + - uses: actions/setup-java@v3 with: java-version: '11' diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index a7a5bb3da..ccecbd950 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -19,7 +19,7 @@ jobs: uses: actions/checkout@v1 - name: Set up JDK 11 - uses: actions/setup-java@v1 + uses: actions/setup-java@v3 with: java-version: 11 diff --git a/.github/workflows/release-verify-signatures.yml b/.github/workflows/release-verify-signatures.yml index 2d11ac54d..a2151ea8b 100644 --- a/.github/workflows/release-verify-signatures.yml +++ b/.github/workflows/release-verify-signatures.yml @@ -18,7 +18,7 @@ jobs: uses: actions/checkout@v1 - name: Set up JDK - uses: actions/setup-java@v1 + uses: actions/setup-java@v3 with: java-version: ${{ matrix.java }} From 4b22ed873044560135522c4f577ddf8490360410 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Jun 2022 13:07:22 +0000 Subject: [PATCH 038/145] Bump actions/checkout from 1 to 3 Bumps [actions/checkout](https://github.com/actions/checkout) from 1 to 3. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v1...v3) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/build.yml | 2 +- .github/workflows/code-formatting.yml | 2 +- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/coverage.yml | 2 +- .github/workflows/release-verify-signatures.yml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2d6c0bec4..6f0685e39 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -20,7 +20,7 @@ jobs: steps: - name: Check out code - uses: actions/checkout@v1 + uses: actions/checkout@v3 - name: Set up JDK uses: actions/setup-java@v1 diff --git a/.github/workflows/code-formatting.yml b/.github/workflows/code-formatting.yml index 3bc259afc..c1dfb599b 100644 --- a/.github/workflows/code-formatting.yml +++ b/.github/workflows/code-formatting.yml @@ -20,7 +20,7 @@ jobs: steps: - name: Check out code - uses: actions/checkout@v1 + uses: actions/checkout@v3 - name: Set up JDK uses: actions/setup-java@v1 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index f65521105..39111ea2f 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -18,7 +18,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 - uses: actions/setup-java@v1 with: diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index a7a5bb3da..bc9259a63 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -16,7 +16,7 @@ jobs: steps: - name: Check out code - uses: actions/checkout@v1 + uses: actions/checkout@v3 - name: Set up JDK 11 uses: actions/setup-java@v1 diff --git a/.github/workflows/release-verify-signatures.yml b/.github/workflows/release-verify-signatures.yml index 2d11ac54d..34bd37c3a 100644 --- a/.github/workflows/release-verify-signatures.yml +++ b/.github/workflows/release-verify-signatures.yml @@ -15,7 +15,7 @@ jobs: steps: - name: check out code - uses: actions/checkout@v1 + uses: actions/checkout@v3 - name: Set up JDK uses: actions/setup-java@v1 From 39c33d93290052dfc0e133dee8ea1515947800f9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Jun 2022 13:07:24 +0000 Subject: [PATCH 039/145] Bump actions/download-artifact from 2 to 3 Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 2 to 3. - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/v2...v3) --- updated-dependencies: - dependency-name: actions/download-artifact dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2d6c0bec4..5914415e0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -55,7 +55,7 @@ jobs: steps: - name: Download artifacts - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 - name: Publish test results uses: EnricoMi/publish-unit-test-result-action@v1 From 8b50d6adb9939d16939a3e7409388848d7e12935 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Jun 2022 13:07:28 +0000 Subject: [PATCH 040/145] Bump github/codeql-action from 1 to 2 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 1 to 2. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/v1...v2) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql-analysis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index f65521105..10e1bf481 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -26,7 +26,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v1 + uses: github/codeql-action/init@v2 with: languages: java @@ -35,4 +35,4 @@ jobs: ./gradlew jar - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + uses: github/codeql-action/analyze@v2 From 56f2ef5362a883a00602892f61bd11381fb76274 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Wed, 8 Jun 2022 16:47:12 +0200 Subject: [PATCH 041/145] Fix broken setup-java actions Broken in commit dd53f8499a618bcb1065823d2af5e689aed0ce48 by a new required parameter in the `setup-java` action. --- .github/workflows/build.yml | 2 ++ .github/workflows/code-formatting.yml | 2 ++ .github/workflows/codeql-analysis.yml | 1 + .github/workflows/coverage.yml | 1 + .github/workflows/release-verify-signatures.yml | 2 ++ 5 files changed, 8 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 98c7fa17f..52dacc7ad 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,6 +17,7 @@ jobs: strategy: matrix: java: [8, 11, 16] + distribution: [zulu] steps: - name: Check out code @@ -26,6 +27,7 @@ jobs: uses: actions/setup-java@v3 with: java-version: ${{ matrix.java }} + distribution: ${{ matrix.distribution }} - name: Run tests run: ./gradlew cleanTest test diff --git a/.github/workflows/code-formatting.yml b/.github/workflows/code-formatting.yml index 562791e07..53f87b879 100644 --- a/.github/workflows/code-formatting.yml +++ b/.github/workflows/code-formatting.yml @@ -17,6 +17,7 @@ jobs: strategy: matrix: java: [11] + distribution: [zulu] steps: - name: Check out code @@ -26,6 +27,7 @@ jobs: uses: actions/setup-java@v3 with: java-version: ${{ matrix.java }} + distribution: ${{ matrix.distribution }} - name: Check code formatting run: ./gradlew spotlessCheck diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 87a79ceea..d2d1348c6 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -23,6 +23,7 @@ jobs: - uses: actions/setup-java@v3 with: java-version: '11' + distribution: zulu # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index e21c9bf68..6f4c5bcf1 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -22,6 +22,7 @@ jobs: uses: actions/setup-java@v3 with: java-version: 11 + distribution: zulu - name: Run mutation test run: ./gradlew pitestMerge diff --git a/.github/workflows/release-verify-signatures.yml b/.github/workflows/release-verify-signatures.yml index b5b555c85..41384888a 100644 --- a/.github/workflows/release-verify-signatures.yml +++ b/.github/workflows/release-verify-signatures.yml @@ -12,6 +12,7 @@ jobs: strategy: matrix: java: [11] + distribution: [zulu] steps: - name: check out code @@ -21,6 +22,7 @@ jobs: uses: actions/setup-java@v3 with: java-version: ${{ matrix.java }} + distribution: ${{ matrix.distribution }} - name: Build jars run: | From 51107bb0894c2512199c6f3f612be0118cef27e2 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Wed, 8 Jun 2022 16:52:29 +0200 Subject: [PATCH 042/145] Grant security-events: write permission to CodeQL workflow --- .github/workflows/codeql-analysis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index d2d1348c6..2791329ca 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -16,6 +16,9 @@ jobs: runs-on: ubuntu-latest + permissions: + security-events: write + steps: - name: Checkout repository uses: actions/checkout@v3 From 2789c03c6a30e42a77c53ecad2dbd5a6a0f0ea4c Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Wed, 8 Jun 2022 18:06:28 +0200 Subject: [PATCH 043/145] Grant required permissions to publish-test-results job --- .github/workflows/build.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 52dacc7ad..e790c3ace 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -55,6 +55,10 @@ jobs: runs-on: ubuntu-latest if: ${{ always() && github.event_name == 'pull_request' }} + permissions: + checks: write + issues: write + steps: - name: Download artifacts uses: actions/download-artifact@v3 From a870a4b2b02796cf7bad0549a73cd5a167c2cbe6 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Wed, 8 Jun 2022 19:12:31 +0200 Subject: [PATCH 044/145] Format jq functions a bit more readably --- .github/workflows/coverage/stats-to-comment.sh | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/coverage/stats-to-comment.sh b/.github/workflows/coverage/stats-to-comment.sh index ea744fff1..2f68e594e 100755 --- a/.github/workflows/coverage/stats-to-comment.sh +++ b/.github/workflows/coverage/stats-to-comment.sh @@ -47,8 +47,16 @@ EOF | { ("**Overall**"): .overall } + .packages | to_entries | .[] - | def difficon: if . > 0 then ":green_circle:" else if . < 0 then ":small_red_triangle_down:" else ":small_blue_diamond:" end end; - def triangles: if . > 0 then ":small_red_triangle:" else if . < 0 then ":small_red_triangle_down:" else ":small_blue_diamond:" end end; + | def difficon: + if . > 0 then ":green_circle:" + else if . < 0 then ":small_red_triangle_down:" + else ":small_blue_diamond:" + end end; + def triangles: + if . > 0 then ":small_red_triangle:" + else if . < 0 then ":small_red_triangle_down:" + else ":small_blue_diamond:" + end end; "\(.key) | **\(.value.after.detected / .value.after.mutations * 100 | floor) %** \(.value.percentage_diff | difficon) | \(.value.after.detected) \(.value.after.detected - .value.before.detected | triangles) / \(.value.after.mutations) \(.value.after.mutations - .value.before.mutations | triangles)| \(.value.before.detected / .value.before.mutations * 100 | floor) % | \(.value.before.detected) / \(.value.before.mutations)" ' \ "${1}" "${2}" --raw-output From a2134e01c31f7acba9c1e3ed7d1ef9d5d3fb542a Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Wed, 8 Jun 2022 19:14:19 +0200 Subject: [PATCH 045/145] Use elif in jq --- .github/workflows/coverage/stats-to-comment.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/coverage/stats-to-comment.sh b/.github/workflows/coverage/stats-to-comment.sh index 2f68e594e..e9ff72311 100755 --- a/.github/workflows/coverage/stats-to-comment.sh +++ b/.github/workflows/coverage/stats-to-comment.sh @@ -49,14 +49,14 @@ EOF | .[] | def difficon: if . > 0 then ":green_circle:" - else if . < 0 then ":small_red_triangle_down:" + elif . < 0 then ":small_red_triangle_down:" else ":small_blue_diamond:" - end end; + end; def triangles: if . > 0 then ":small_red_triangle:" - else if . < 0 then ":small_red_triangle_down:" + elif . < 0 then ":small_red_triangle_down:" else ":small_blue_diamond:" - end end; + end; "\(.key) | **\(.value.after.detected / .value.after.mutations * 100 | floor) %** \(.value.percentage_diff | difficon) | \(.value.after.detected) \(.value.after.detected - .value.before.detected | triangles) / \(.value.after.mutations) \(.value.after.mutations - .value.before.mutations | triangles)| \(.value.before.detected / .value.before.mutations * 100 | floor) % | \(.value.before.detected) / \(.value.before.mutations)" ' \ "${1}" "${2}" --raw-output From d0b2f1ab8bab4b679e8c50f618de81af3db2fc68 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Wed, 8 Jun 2022 19:17:32 +0200 Subject: [PATCH 046/145] Show trophy emoji for full mutation coverage --- .github/workflows/coverage/stats-to-comment.sh | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/coverage/stats-to-comment.sh b/.github/workflows/coverage/stats-to-comment.sh index e9ff72311..387fdffdf 100755 --- a/.github/workflows/coverage/stats-to-comment.sh +++ b/.github/workflows/coverage/stats-to-comment.sh @@ -48,8 +48,9 @@ EOF | to_entries | .[] | def difficon: - if . > 0 then ":green_circle:" - elif . < 0 then ":small_red_triangle_down:" + if .after.detected == .after.mutations then ":trophy:" + elif .percentage_diff > 0 then ":green_circle:" + elif .percentage_diff < 0 then ":small_red_triangle_down:" else ":small_blue_diamond:" end; def triangles: @@ -57,7 +58,7 @@ EOF elif . < 0 then ":small_red_triangle_down:" else ":small_blue_diamond:" end; - "\(.key) | **\(.value.after.detected / .value.after.mutations * 100 | floor) %** \(.value.percentage_diff | difficon) | \(.value.after.detected) \(.value.after.detected - .value.before.detected | triangles) / \(.value.after.mutations) \(.value.after.mutations - .value.before.mutations | triangles)| \(.value.before.detected / .value.before.mutations * 100 | floor) % | \(.value.before.detected) / \(.value.before.mutations)" + "\(.key) | **\(.value.after.detected / .value.after.mutations * 100 | floor) %** \(.value | difficon) | \(.value.after.detected) \(.value.after.detected - .value.before.detected | triangles) / \(.value.after.mutations) \(.value.after.mutations - .value.before.mutations | triangles)| \(.value.before.detected / .value.before.mutations * 100 | floor) % | \(.value.before.detected) / \(.value.before.mutations)" ' \ "${1}" "${2}" --raw-output From 5e535f2032797b1dd6886fffcbfa2cb449190ac6 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Wed, 8 Jun 2022 20:10:21 +0200 Subject: [PATCH 047/145] Remove unnecessary step from mutation test results comment step --- .github/workflows/coverage.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 6f4c5bcf1..28cd97bc7 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -83,7 +83,6 @@ jobs: curl -X POST \ -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \ ${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/commits/${GITHUB_SHA}/comments -d @build/results-comment.json - mv new-stats.json prev-stats.json - name: Push to GitHub Pages run: | From 4763126d8a798c51e6aad00a230f4173bf147722 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Fri, 10 Jun 2022 21:24:16 +0200 Subject: [PATCH 048/145] Bump spotless-plugin-gradle to version 6.7.1 Required for compatibility with JDK 17+. See: https://github.com/diffplug/spotless/pull/1228 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 0ce490269..8b19af100 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ buildscript { } dependencies { classpath 'com.cinnober.gradle:semver-git:2.5.0' - classpath 'com.diffplug.spotless:spotless-plugin-gradle:6.7.0' + classpath 'com.diffplug.spotless:spotless-plugin-gradle:6.7.1' classpath 'io.github.cosmicsilence:gradle-scalafix:0.1.13' } } From 91c8fc44c71f852bbe78cdee30019ce068e0d54e Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Fri, 10 Jun 2022 01:06:55 +0200 Subject: [PATCH 049/145] Bump workflows and releases to JDK 17 (LTS) --- .github/workflows/build.yml | 2 +- .github/workflows/code-formatting.yml | 2 +- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/coverage.yml | 2 +- .github/workflows/release-verify-signatures.yml | 2 +- doc/releasing.md | 4 ++-- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e790c3ace..0e69d6933 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - java: [8, 11, 16] + java: [8, 11, 17, 18] distribution: [zulu] steps: diff --git a/.github/workflows/code-formatting.yml b/.github/workflows/code-formatting.yml index 53f87b879..2f893a511 100644 --- a/.github/workflows/code-formatting.yml +++ b/.github/workflows/code-formatting.yml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - java: [11] + java: [17] distribution: [zulu] steps: diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 2791329ca..830f82b0f 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -25,7 +25,7 @@ jobs: - uses: actions/setup-java@v3 with: - java-version: '11' + java-version: 17 distribution: zulu # Initializes the CodeQL tools for scanning. diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 28cd97bc7..4c0c7d5a1 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -21,7 +21,7 @@ jobs: - name: Set up JDK 11 uses: actions/setup-java@v3 with: - java-version: 11 + java-version: 17 distribution: zulu - name: Run mutation test diff --git a/.github/workflows/release-verify-signatures.yml b/.github/workflows/release-verify-signatures.yml index 41384888a..e20fec93b 100644 --- a/.github/workflows/release-verify-signatures.yml +++ b/.github/workflows/release-verify-signatures.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - java: [11] + java: [17] distribution: [zulu] steps: diff --git a/doc/releasing.md b/doc/releasing.md index 1aff7522a..44c00c9c0 100644 --- a/doc/releasing.md +++ b/doc/releasing.md @@ -6,7 +6,7 @@ Release candidate versions 1. Make sure release notes in `NEWS` are up to date. - 2. Make sure you're running Gradle in JDK 11. + 2. Make sure you're running Gradle in JDK 17. 3. Run the tests one more time: @@ -71,7 +71,7 @@ Release versions 1. Make sure release notes in `NEWS` are up to date. - 2. Make sure you're running Gradle in JDK 11. + 2. Make sure you're running Gradle in JDK 17. 3. Make a no-fast-forward merge from the last (non release candidate) release to the commit to be released: From eed123f215a151acc792b489fb491a9e68048db9 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Fri, 10 Jun 2022 21:40:13 +0200 Subject: [PATCH 050/145] Build with more JDK distributions, and Eclipse temurin by default --- .github/workflows/build.yml | 13 +++++++++---- .github/workflows/code-formatting.yml | 2 +- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/coverage.yml | 2 +- .github/workflows/release-verify-signatures.yml | 2 +- 5 files changed, 13 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0e69d6933..186be6ab9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,13 +11,18 @@ on: jobs: test: - name: JDK ${{matrix.java}} + name: JDK ${{ matrix.java }} ${{ matrix.distribution }} runs-on: ubuntu-latest strategy: matrix: java: [8, 11, 17, 18] - distribution: [zulu] + distribution: [temurin] + include: + - java: 17 + distribution: zulu + - java: 17 + distribution: microsoft steps: - name: Check out code @@ -36,14 +41,14 @@ jobs: if: ${{ always() }} uses: actions/upload-artifact@v3 with: - name: test-reports-java${{ matrix.java }}-html + name: test-reports-java${{ matrix.java }}-${{ matrix.distribution }}-html path: "*/build/reports/**" - name: Archive JUnit test report if: ${{ always() }} uses: actions/upload-artifact@v3 with: - name: test-reports-java${{ matrix.java }}-xml + name: test-reports-java${{ matrix.java }}-${{ matrix.distribution }}-xml path: "*/build/test-results/**/*.xml" - name: Build JavaDoc diff --git a/.github/workflows/code-formatting.yml b/.github/workflows/code-formatting.yml index 2f893a511..749569623 100644 --- a/.github/workflows/code-formatting.yml +++ b/.github/workflows/code-formatting.yml @@ -17,7 +17,7 @@ jobs: strategy: matrix: java: [17] - distribution: [zulu] + distribution: [temurin] steps: - name: Check out code diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 830f82b0f..a48696100 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -26,7 +26,7 @@ jobs: - uses: actions/setup-java@v3 with: java-version: 17 - distribution: zulu + distribution: temurin # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 4c0c7d5a1..b1201de0b 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -22,7 +22,7 @@ jobs: uses: actions/setup-java@v3 with: java-version: 17 - distribution: zulu + distribution: temurin - name: Run mutation test run: ./gradlew pitestMerge diff --git a/.github/workflows/release-verify-signatures.yml b/.github/workflows/release-verify-signatures.yml index e20fec93b..5b2058bc5 100644 --- a/.github/workflows/release-verify-signatures.yml +++ b/.github/workflows/release-verify-signatures.yml @@ -12,7 +12,7 @@ jobs: strategy: matrix: java: [17] - distribution: [zulu] + distribution: [temurin, zulu, microsoft] steps: - name: check out code From 00b7dc5f4e6487e009fb6170282489c0a431a2fb Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Fri, 10 Jun 2022 21:40:57 +0200 Subject: [PATCH 051/145] Download a specific artifact to use for test report comment --- .github/workflows/build.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 186be6ab9..cce9bcf05 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,6 +24,10 @@ jobs: - java: 17 distribution: microsoft + outputs: + report-java: 17 + report-dist: temurin + steps: - name: Check out code uses: actions/checkout@v3 @@ -67,6 +71,8 @@ jobs: steps: - name: Download artifacts uses: actions/download-artifact@v3 + with: + name: test-reports-java${{ needs.test.outputs.report-java }}-${{ needs.test.outputs.report-dist }}-xml - name: Publish test results uses: EnricoMi/publish-unit-test-result-action@v1 From 4e1604a7e2a8f96c856c83c9b50ee186cc49007f Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Fri, 10 Jun 2022 21:41:28 +0200 Subject: [PATCH 052/145] Minimize permissions for publish-unit-test-result-action job --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cce9bcf05..1ed93830e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -66,7 +66,7 @@ jobs: permissions: checks: write - issues: write + pull-requests: write steps: - name: Download artifacts From 57bf67acb08d45468947c337deb61cbef4770f35 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 13 Jun 2022 21:40:16 +0200 Subject: [PATCH 053/145] Archive mutation test reports --- .github/workflows/coverage.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index b1201de0b..1bf5ed35a 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -27,6 +27,12 @@ jobs: - name: Run mutation test run: ./gradlew pitestMerge + - name: Archive test reports + uses: actions/upload-artifact@v3 + with: + name: pitest-reports-${{ github.sha }} + path: "*/build/reports/pitest/**" + - name: Install yq (and xq) run: | pip install yq From 3327511eba83e75b7c4f29a044df6489501ad678 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Wed, 8 Jun 2022 20:09:27 +0200 Subject: [PATCH 054/145] Extract PIT results comment step as reusable action --- .../actions/pit-results-comment/action.yml | 56 +++++++++++++++++++ .../pit-results-comment}/compute-stats.sh | 0 .../pit-results-comment}/stats-to-comment.sh | 0 .github/workflows/coverage.yml | 25 +++------ 4 files changed, 64 insertions(+), 17 deletions(-) create mode 100644 .github/actions/pit-results-comment/action.yml rename .github/{workflows/coverage => actions/pit-results-comment}/compute-stats.sh (100%) rename .github/{workflows/coverage => actions/pit-results-comment}/stats-to-comment.sh (100%) diff --git a/.github/actions/pit-results-comment/action.yml b/.github/actions/pit-results-comment/action.yml new file mode 100644 index 000000000..4f7b25364 --- /dev/null +++ b/.github/actions/pit-results-comment/action.yml @@ -0,0 +1,56 @@ +name: Post PIT mutation test results comment +author: Emil Lundberg +description: | + Parses a [PIT][pitest] report file, compares it to a previous report, + and posts a summary as a commit comment to the commit that triggered the workflow. + + [pitest]: https://pitest.org/ + +inputs: + mutations-file: + default: build/reports/pitest/mutations.xml + description: Path to the PIT report XML file. + + prev-commit: + default: '' + description: | + The full commit SHA of the previous run of this action. + If set, the comment will include a link to the previous commit. + + prev-mutations-file: + required: true + description: Path to the PIT report XML file from the previous run of this action. + + token: + default: ${{ github.token }} + description: GITHUB_TOKEN or a PAT with permission to write commit comments. + +runs: + using: "composite" + + steps: + - name: Install yq (and xq) + shell: bash + run: pip install yq + + - name: Post results comment + shell: bash + run: | + RESULTS_COMMENT_FILE=$(mktemp) + NEW_STATS_FILE=$(mktemp) + PREV_STATS_FILE=$(mktemp) + + ./.github/actions/pit-results-comment/compute-stats.sh "${{ inputs.mutations-file }}" > "${NEW_STATS_FILE}" + + if [[ -f "${{ inputs.prev-mutations-file }}" ]]; then + ./.github/actions/pit-results-comment/compute-stats.sh "${{ inputs.prev-mutations-file }}" > "${PREV_STATS_FILE}" + else + echo 'Previous mutations file not found, using current as placeholder.' + cp "${NEW_STATS_FILE}" "${PREV_STATS_FILE}" + fi + + ./.github/actions/pit-results-comment/stats-to-comment.sh "${PREV_STATS_FILE}" "${NEW_STATS_FILE}" "${{ inputs.prev-commit }}" > "${RESULTS_COMMENT_FILE}" + + curl -X POST \ + -H "Authorization: Bearer ${{ inputs.token }}" \ + ${{ github.api_url }}/repos/${{ github.repository }}/commits/${{ github.sha }}/comments -d @"${RESULTS_COMMENT_FILE}" diff --git a/.github/workflows/coverage/compute-stats.sh b/.github/actions/pit-results-comment/compute-stats.sh similarity index 100% rename from .github/workflows/coverage/compute-stats.sh rename to .github/actions/pit-results-comment/compute-stats.sh diff --git a/.github/workflows/coverage/stats-to-comment.sh b/.github/actions/pit-results-comment/stats-to-comment.sh similarity index 100% rename from .github/workflows/coverage/stats-to-comment.sh rename to .github/actions/pit-results-comment/stats-to-comment.sh diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 1bf5ed35a..47e2840d6 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -70,25 +70,16 @@ jobs: ref: gh-pages clean: false - - name: Post mutation test results as commit comment + - name: Prepare metadata for pit-results-comment action run: | - git checkout "${GITHUB_SHA}" -- .github/workflows/coverage - - ./.github/workflows/coverage/compute-stats.sh build/reports/pitest/mutations.xml > new-stats.json - - if [[ -f prev-mutations.xml ]]; then - ./.github/workflows/coverage/compute-stats.sh prev-mutations.xml > prev-stats.json - else - cp new-stats.json prev-stats.json - fi - - touch prev-commit.txt + git checkout "${GITHUB_SHA}" -- .github/workflows/coverage .github/actions + echo PREV_COMMIT=$(cat prev-commit.txt) >> "${GITHUB_ENV}" - ./.github/workflows/coverage/stats-to-comment.sh prev-stats.json new-stats.json $(cat prev-commit.txt) > build/results-comment.json - - curl -X POST \ - -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \ - ${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/commits/${GITHUB_SHA}/comments -d @build/results-comment.json + - name: Post mutation test results as commit comment + uses: ./.github/actions/pit-results-comment + with: + prev-commit: ${{ env.PREV_COMMIT }} + prev-mutations-file: prev-mutations.xml - name: Push to GitHub Pages run: | From 8caf9674b13272ca7070eb36fc4343976e4965d6 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Wed, 8 Jun 2022 20:52:37 +0200 Subject: [PATCH 055/145] Extract mutation coverage badge step as reusable GitHub Action --- .github/actions/pit-results-badge/action.yml | 49 ++++++++++++++++++++ .github/workflows/coverage.yml | 19 ++------ 2 files changed, 52 insertions(+), 16 deletions(-) create mode 100644 .github/actions/pit-results-badge/action.yml diff --git a/.github/actions/pit-results-badge/action.yml b/.github/actions/pit-results-badge/action.yml new file mode 100644 index 000000000..5eaa858ad --- /dev/null +++ b/.github/actions/pit-results-badge/action.yml @@ -0,0 +1,49 @@ +name: Create Shields.io badge from PIT mutation test results +author: Emil Lundberg +description: | + Parses a [PIT][pitest] report file and outputs a [Shields.io][shields] + [endpoint badge][endpoint] definition file. + + [endpoint]: https://shields.io/endpoint + [pitest]: https://pitest.org/ + [shields]: https://shields.io/ + +inputs: + cache-seconds: + default: 3600 + description: Passed through as cacheSeconds to Shields.io. + + label: + default: "mutation coverage" + description: Label for the left side of the badge. + + mutations-file: + default: build/reports/pitest/mutations.xml + description: Path to the PIT report XML file. + + output-file: + required: true + description: Path to write output file to. + +runs: + using: "composite" + + steps: + - name: Install yq (and xq) + shell: bash + run: pip install yq + + - name: Create coverage badge + shell: bash + run: | + cat ${{ inputs.mutations-file }} \ + | xq '.mutations.mutation + | (map(select(.["@detected"] == "true")) | length) / length + | { + schemaVersion: 1, + label: "${{ inputs.label }}", + message: "\(. * 100 | floor | tostring) %", + color: "hsl(\(. * 120 | floor | tostring), 100%, 40%", + cacheSeconds: ${{ inputs.cache-seconds }}, + }' \ + > ${{ inputs.output-file }} diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 47e2840d6..fc6a5b1b1 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -33,10 +33,6 @@ jobs: name: pitest-reports-${{ github.sha }} path: "*/build/reports/pitest/**" - - name: Install yq (and xq) - run: | - pip install yq - - name: Create output directory run: mkdir -p build/gh-pages @@ -51,18 +47,9 @@ jobs: - name: Create coverage badge # This creates a file that defines a [Shields.io endpoint badge](https://shields.io/endpoint) # which we can then include in the project README. - run: | - cat build/reports/pitest/mutations.xml \ - | xq '.mutations.mutation - | (map(select(.["@detected"] == "true")) | length) / length - | { - schemaVersion: 1, - label: "mutation coverage", - message: "\(. * 100 | floor | tostring) %", - color: "hsl(\(. * 120 | floor | tostring), 100%, 40%", - cacheSeconds: 3600, - }' \ - > build/gh-pages/coverage-badge.json + uses: ./.github/actions/pit-results-badge + with: + output-file: build/gh-pages/coverage-badge.json - name: Check out GitHub Pages branch uses: actions/checkout@v3 From 9b3258d2899043b712776c40a9eb7551170a2762 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Fri, 17 Jun 2022 15:37:48 +0200 Subject: [PATCH 056/145] Add missing parenthesis in pit-results-badge action --- .github/actions/pit-results-badge/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/pit-results-badge/action.yml b/.github/actions/pit-results-badge/action.yml index 5eaa858ad..57adc2288 100644 --- a/.github/actions/pit-results-badge/action.yml +++ b/.github/actions/pit-results-badge/action.yml @@ -43,7 +43,7 @@ runs: schemaVersion: 1, label: "${{ inputs.label }}", message: "\(. * 100 | floor | tostring) %", - color: "hsl(\(. * 120 | floor | tostring), 100%, 40%", + color: "hsl(\(. * 120 | floor | tostring), 100%, 40%)", cacheSeconds: ${{ inputs.cache-seconds }}, }' \ > ${{ inputs.output-file }} From 283e7ebfe84d52bb2623ed265bc9a6fd0dafa78c Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 27 Jun 2022 17:40:36 +0200 Subject: [PATCH 057/145] Remove U2F AppId config environment variable The setting fell back to a default value when not set, which could cause domain mismatch issues. The features that used this were removed in commit 1f823bc0d6748114e4dcfa719194920e883ea86a, so there's little reason to keep this configuration setting. --- .../src/main/java/demo/webauthn/Config.java | 26 ++----------------- .../java/demo/webauthn/WebAuthnServer.java | 8 ++---- .../demo/webauthn/WebAuthnServerSpec.scala | 6 ----- 3 files changed, 4 insertions(+), 36 deletions(-) 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 94f69722a..bcb884be2 100644 --- a/webauthn-server-demo/src/main/java/demo/webauthn/Config.java +++ b/webauthn-server-demo/src/main/java/demo/webauthn/Config.java @@ -26,13 +26,10 @@ import com.yubico.internal.util.CollectionUtil; import com.yubico.webauthn.data.RelyingPartyIdentity; -import com.yubico.webauthn.extension.appid.AppId; -import com.yubico.webauthn.extension.appid.InvalidAppIdException; import java.net.MalformedURLException; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; -import java.util.Optional; import java.util.Set; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -49,14 +46,11 @@ public class Config { private final Set origins; private final int port; private final RelyingPartyIdentity rpIdentity; - private final Optional appId; - private Config( - Set origins, int port, RelyingPartyIdentity rpIdentity, Optional appId) { + private Config(Set origins, int port, RelyingPartyIdentity rpIdentity) { this.origins = CollectionUtil.immutableSet(origins); this.port = port; this.rpIdentity = rpIdentity; - this.appId = appId; } private static Config instance; @@ -64,11 +58,9 @@ private Config( private static Config getInstance() { if (instance == null) { try { - instance = new Config(computeOrigins(), computePort(), computeRpIdentity(), computeAppId()); + instance = new Config(computeOrigins(), computePort(), computeRpIdentity()); } catch (MalformedURLException e) { throw new RuntimeException(e); - } catch (InvalidAppIdException e) { - throw new RuntimeException(e); } } return instance; @@ -86,10 +78,6 @@ public static RelyingPartyIdentity getRpIdentity() { return getInstance().rpIdentity; } - public static Optional getAppId() { - return getInstance().appId; - } - private static Set computeOrigins() { final String origins = System.getenv("YUBICO_WEBAUTHN_ALLOWED_ORIGINS"); @@ -143,14 +131,4 @@ private static RelyingPartyIdentity computeRpIdentity() throws MalformedURLExcep logger.info("RP identity: {}", result); return result; } - - private static Optional computeAppId() throws InvalidAppIdException { - final String appId = System.getenv("YUBICO_WEBAUTHN_U2F_APPID"); - logger.debug("YUBICO_WEBAUTHN_U2F_APPID: {}", appId); - - AppId result = appId == null ? new AppId("https://localhost:8443") : new AppId(appId); - - logger.debug("U2F AppId: {}", result.getId()); - return Optional.of(result); - } } diff --git a/webauthn-server-demo/src/main/java/demo/webauthn/WebAuthnServer.java b/webauthn-server-demo/src/main/java/demo/webauthn/WebAuthnServer.java index 85e0eb634..6fed76f0c 100644 --- a/webauthn-server-demo/src/main/java/demo/webauthn/WebAuthnServer.java +++ b/webauthn-server-demo/src/main/java/demo/webauthn/WebAuthnServer.java @@ -60,7 +60,6 @@ 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.AppId; import com.yubico.webauthn.extension.appid.InvalidAppIdException; import demo.webauthn.data.AssertionRequestWrapper; import demo.webauthn.data.AssertionResponse; @@ -123,8 +122,7 @@ public WebAuthnServer() newCache(), newCache(), Config.getRpIdentity(), - Config.getOrigins(), - Config.getAppId()); + Config.getOrigins()); } public WebAuthnServer( @@ -132,8 +130,7 @@ public WebAuthnServer( Cache registerRequestStorage, Cache assertRequestStorage, RelyingPartyIdentity rpIdentity, - Set origins, - Optional appId) + Set origins) throws InvalidAppIdException, CertificateException, CertPathValidatorException, InvalidAlgorithmParameterException, Base64UrlException, DigestException, FidoMetadataDownloaderException, UnexpectedLegalHeader, IOException, @@ -153,7 +150,6 @@ public WebAuthnServer( .allowOriginSubdomain(false) .allowUntrustedAttestation(true) .validateSignatureCounter(true) - .appId(appId) .build(); } 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 247bdae7e..95632fc47 100644 --- a/webauthn-server-demo/src/test/scala/demo/webauthn/WebAuthnServerSpec.scala +++ b/webauthn-server-demo/src/test/scala/demo/webauthn/WebAuthnServerSpec.scala @@ -38,7 +38,6 @@ 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.extension.appid.AppId import demo.webauthn.data.AssertionRequestWrapper import demo.webauthn.data.CredentialRegistration import demo.webauthn.data.RegistrationRequest @@ -72,7 +71,6 @@ class WebAuthnServerSpec private val rpId = RelyingPartyIdentity.builder().id("localhost").name("Test party").build() private val origins = Set("localhost").asJava - private val appId = Optional.empty[AppId] describe("WebAuthnServer") { @@ -176,7 +174,6 @@ class WebAuthnServerSpec newCache(), rpId, Set("https://localhost").asJava, - appId, ) val (cred, keypair) = { @@ -292,7 +289,6 @@ class WebAuthnServerSpec assertionRequests, rpId, origins, - appId, ) } } @@ -340,7 +336,6 @@ class WebAuthnServerSpec newCache(), rpId, origins, - appId, ) } @@ -400,7 +395,6 @@ class WebAuthnServerSpec newCache(), rpId, origins, - appId, ) } From 5e5a2eb7f65d7835727c5fc20b7f140a0cc98f8e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 Jul 2022 13:02:29 +0000 Subject: [PATCH 058/145] Bump spotless-plugin-gradle from 6.7.1 to 6.8.0 Bumps [spotless-plugin-gradle](https://github.com/diffplug/spotless) from 6.7.1 to 6.8.0. - [Release notes](https://github.com/diffplug/spotless/releases) - [Changelog](https://github.com/diffplug/spotless/blob/main/CHANGES.md) - [Commits](https://github.com/diffplug/spotless/compare/gradle/6.7.1...gradle/6.8.0) --- updated-dependencies: - dependency-name: com.diffplug.spotless:spotless-plugin-gradle dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 8b19af100..e6fb9cd8f 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ buildscript { } dependencies { classpath 'com.cinnober.gradle:semver-git:2.5.0' - classpath 'com.diffplug.spotless:spotless-plugin-gradle:6.7.1' + classpath 'com.diffplug.spotless:spotless-plugin-gradle:6.8.0' classpath 'io.github.cosmicsilence:gradle-scalafix:0.1.13' } } From 426b313923f12d13abf5d226ff371bb010e89869 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Jul 2022 13:31:07 +0000 Subject: [PATCH 059/145] Bump EnricoMi/publish-unit-test-result-action from 1 to 2 Bumps [EnricoMi/publish-unit-test-result-action](https://github.com/EnricoMi/publish-unit-test-result-action) from 1 to 2. - [Release notes](https://github.com/EnricoMi/publish-unit-test-result-action/releases) - [Commits](https://github.com/EnricoMi/publish-unit-test-result-action/compare/v1...v2) --- updated-dependencies: - dependency-name: EnricoMi/publish-unit-test-result-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1ed93830e..ae491df61 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -75,6 +75,6 @@ jobs: name: test-reports-java${{ needs.test.outputs.report-java }}-${{ needs.test.outputs.report-dist }}-xml - name: Publish test results - uses: EnricoMi/publish-unit-test-result-action@v1 + uses: EnricoMi/publish-unit-test-result-action@v2 with: files: "**/*.xml" From 879bb3a623f163ab8caee03ec9d340c3c0a1f8c9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 28 Jul 2022 13:10:26 +0000 Subject: [PATCH 060/145] Bump spotless-plugin-gradle from 6.8.0 to 6.9.0 Bumps [spotless-plugin-gradle](https://github.com/diffplug/spotless) from 6.8.0 to 6.9.0. - [Release notes](https://github.com/diffplug/spotless/releases) - [Changelog](https://github.com/diffplug/spotless/blob/main/CHANGES.md) - [Commits](https://github.com/diffplug/spotless/compare/gradle/6.8.0...gradle/6.9.0) --- updated-dependencies: - dependency-name: com.diffplug.spotless:spotless-plugin-gradle dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index e6fb9cd8f..3405b27c2 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ buildscript { } dependencies { classpath 'com.cinnober.gradle:semver-git:2.5.0' - classpath 'com.diffplug.spotless:spotless-plugin-gradle:6.8.0' + classpath 'com.diffplug.spotless:spotless-plugin-gradle:6.9.0' classpath 'io.github.cosmicsilence:gradle-scalafix:0.1.13' } } From b3cc7fc0c3260eb3ecd48cd64e8e136dab264d3e Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 27 Jun 2022 19:56:01 +0200 Subject: [PATCH 061/145] Link to workflow run for detailed coverage reports --- .github/actions/pit-results-comment/stats-to-comment.sh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/actions/pit-results-comment/stats-to-comment.sh b/.github/actions/pit-results-comment/stats-to-comment.sh index 387fdffdf..d96ab0a33 100755 --- a/.github/actions/pit-results-comment/stats-to-comment.sh +++ b/.github/actions/pit-results-comment/stats-to-comment.sh @@ -66,6 +66,11 @@ EOF cat << EOF Previous run: ${3} +EOF + + cat << EOF + +Detailed reports: [workflow run #${GITHUB_RUN_NUMBER}](/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}) EOF fi From 76119b96fc728b0011461b822bc82a8c1f298e2b Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Wed, 13 Jul 2022 18:24:48 +0200 Subject: [PATCH 062/145] Test that 1023 bytes long credential IDs are supported --- .../RelyingPartyRegistrationSpec.scala | 51 ++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) 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 d1bf10d9b..a54c733e0 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 @@ -3553,8 +3553,57 @@ class RelyingPartyRegistrationSpec } describe("RelyingParty.finishRegistration") { - it("throws RegistrationFailedException in case of errors.") { + it("supports 1023 bytes long credential IDs.") { + val rp = RelyingParty + .builder() + .identity( + RelyingPartyIdentity + .builder() + .id("localhost") + .name("Test party") + .build() + ) + .credentialRepository(Helpers.CredentialRepository.empty) + .build() + val pkcco = rp.startRegistration( + StartRegistrationOptions + .builder() + .user( + UserIdentity + .builder() + .name("test") + .displayName("Test Testsson") + .id(new ByteArray(Array())) + .build() + ) + .build() + ) + + forAll(byteArray(1023)) { credId => + val credential = TestAuthenticator + .createUnattestedCredential(challenge = pkcco.getChallenge) + ._1 + .toBuilder() + .id(credId) + .build() + + val result = Try( + rp.finishRegistration( + FinishRegistrationOptions + .builder() + .request(pkcco) + .response(credential) + .build() + ) + ) + result shouldBe a[Success[_]] + result.get.getKeyId.getId should equal(credId) + result.get.getKeyId.getId.size should be(1023) + } + } + + it("throws RegistrationFailedException in case of errors.") { val rp = RelyingParty .builder() .identity( From 8ff292fb11ca2d4496bedaecdcdc955f8c54e044 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 Aug 2022 13:01:39 +0000 Subject: [PATCH 063/145] Bump spotless-plugin-gradle from 6.9.0 to 6.10.0 Bumps [spotless-plugin-gradle](https://github.com/diffplug/spotless) from 6.9.0 to 6.10.0. - [Release notes](https://github.com/diffplug/spotless/releases) - [Changelog](https://github.com/diffplug/spotless/blob/main/CHANGES.md) - [Commits](https://github.com/diffplug/spotless/compare/gradle/6.9.0...gradle/6.10.0) --- updated-dependencies: - dependency-name: com.diffplug.spotless:spotless-plugin-gradle dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 3405b27c2..a7fa01009 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ buildscript { } dependencies { classpath 'com.cinnober.gradle:semver-git:2.5.0' - classpath 'com.diffplug.spotless:spotless-plugin-gradle:6.9.0' + classpath 'com.diffplug.spotless:spotless-plugin-gradle:6.10.0' classpath 'io.github.cosmicsilence:gradle-scalafix:0.1.13' } } From 5cf36479682b8224f25da4772442c174e1cec91e Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Wed, 31 Aug 2022 14:59:23 +0200 Subject: [PATCH 064/145] Fix typo --- .../com/yubico/webauthn/attestation/AttestationTrustSource.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/attestation/AttestationTrustSource.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/attestation/AttestationTrustSource.java index cc9fc17e9..66639a161 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/attestation/AttestationTrustSource.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/attestation/AttestationTrustSource.java @@ -44,7 +44,7 @@ public interface AttestationTrustSource { *

    Note that it is possible for the same trust root to be used for different certificate * chains. For example, an authenticator vendor may make two different authenticator models, each * with its own attestation leaf certificate but both signed by the same attestation root - * certificate. If a Relying Party trusts one of those authenticators models but not the other, + * certificate. If a Relying Party trusts one of those authenticator models but not the other, * then its implementation of this method MUST return an empty set for the untrusted certificate * chain. * From a6a7aefe785f9fc6ac68d28497400d0446cd2688 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Wed, 31 Aug 2022 17:09:04 +0200 Subject: [PATCH 065/145] Fix mistake in FidoMetadataDownloader JavaDoc --- .../java/com/yubico/fido/metadata/FidoMetadataDownloader.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 d6e3a4913..da51266b0 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 @@ -411,7 +411,7 @@ public Step5 useDefaultBlob() { * nextUpdate property of the cached BLOB is the current date or earlier. * *

    If the BLOB is downloaded, it is also written to the cache {@link File} or {@link - * Consumer} configured in the previous step. + * Consumer} configured in the next step. * * @param url the HTTP URL to download. It MUST use the https: scheme. */ From 2e7f3f9c3fc7dec74e4af07f12bb82f4fdb4157c Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Wed, 31 Aug 2022 20:30:21 +0200 Subject: [PATCH 066/145] Fix see also reference in FidoMetadataService JavaDoc --- .../main/java/com/yubico/fido/metadata/FidoMetadataService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 388c515d7..bc75c6535 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 @@ -261,7 +261,7 @@ public FidoMetadataServiceBuilder useBlob(@NonNull MetadataBLOBPayload blobPaylo * * @param prefilter a {@link Predicate} which returns true for metadata entries to * include in the data source. - * @see #filter + * @see #filter(Predicate) * @see Filters#allOf(Predicate[]) */ public FidoMetadataServiceBuilder prefilter( From 4d93d4efa5850f3ad45b8ff86f444ab4a5ac3969 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Wed, 31 Aug 2022 20:46:38 +0200 Subject: [PATCH 067/145] Fix references to filter settings in FidoMetadataService JavaDoc --- NEWS | 7 +++++++ .../java/com/yubico/fido/metadata/FidoMetadataService.java | 4 +++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/NEWS b/NEWS index 57dea19b5..b98e5f024 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,10 @@ +== Version 2.1.0 (unreleased) == + +Fixes: + +* Fixed various typos and mistakes in JavaDocs. + + == Version 2.0.0 == This release removes deprecated APIs and changes some defaults to better align 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 bc75c6535..7f627ac85 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 @@ -328,9 +328,11 @@ public FidoMetadataService build() /** * Preconfigured filters and utilities for combining filters. See the {@link - * FidoMetadataServiceBuilder#prefilter(Predicate) filter} setting. + * FidoMetadataServiceBuilder#prefilter(Predicate) prefilter} and {@link + * FidoMetadataServiceBuilder#filter(Predicate) filter} settings. * * @see FidoMetadataServiceBuilder#prefilter(Predicate) + * @see FidoMetadataServiceBuilder#filter(Predicate) */ public static class Filters { From 9de5b723f7461d8a2f880a1cc75e34c2fe365539 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Wed, 31 Aug 2022 16:36:55 +0200 Subject: [PATCH 068/145] Include attestation object in cert path validation failure logs --- NEWS | 5 +++++ .../java/com/yubico/webauthn/FinishRegistrationSteps.java | 8 ++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/NEWS b/NEWS index b98e5f024..cde4dc621 100644 --- a/NEWS +++ b/NEWS @@ -1,5 +1,10 @@ == Version 2.1.0 (unreleased) == +Changes: + +* Log messages on attestation certificate path validation failure now include + the attestation object. + Fixes: * Fixed various typos and mistakes in JavaDocs. diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishRegistrationSteps.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishRegistrationSteps.java index 66bf3bd77..868bb9b12 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishRegistrationSteps.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishRegistrationSteps.java @@ -543,14 +543,18 @@ public boolean attestationTrusted() { } catch (CertPathValidatorException e) { log.info( - "Failed to derive trust in attestation statement: {} at cert index {}: {}", + "Failed to derive trust in attestation statement: {} at cert index {}: {}. Attestation object: {}", + response.getResponse().getAttestationObject(), e.getReason(), e.getIndex(), e.getMessage()); return false; } catch (CertificateException e) { - log.warn("Failed to build attestation certificate path.", e); + log.warn( + "Failed to build attestation certificate path. Attestation object: {}", + response.getResponse().getAttestationObject(), + e); return false; } catch (NoSuchAlgorithmException e) { From e2778f6f9352e592257ce7dc0d44cd25ce35937f Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Thu, 1 Sep 2022 20:00:21 +0200 Subject: [PATCH 069/145] Go back to JDK 11 for mutation tests When running the mutation tests in JDK 17, they seem to incorrectly report lower coverage than what is actually the case - including whole classes plummeting to 0% coverage. I have manually confirmed that some of the mutations reported as uncovered are in fact covered by the tests. Perhaps there's some test discovery mechanism that breaks in JDK 17; I'll have to test newer versions of the test frameworks and see if that helps. --- .github/workflows/coverage.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index fc6a5b1b1..5a4cef7e6 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -18,10 +18,10 @@ jobs: - name: Check out code uses: actions/checkout@v3 - - name: Set up JDK 11 + - name: Set up JDK uses: actions/setup-java@v3 with: - java-version: 17 + java-version: 11 distribution: temurin - name: Run mutation test From d5c56a96072b63bd4c850339b1a89a48db1d560a Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Sun, 4 Sep 2022 13:55:07 +0200 Subject: [PATCH 070/145] Pin Logback dependency to version 1.3.0 Version 1.4.0 is not compatible with JDK 8. --- .../build.gradle.kts | 2 -- webauthn-server-demo/build.gradle | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/test-dependent-projects/java-dep-webauthn-server-core-and-bouncycastle/build.gradle.kts b/test-dependent-projects/java-dep-webauthn-server-core-and-bouncycastle/build.gradle.kts index f558ba389..af9cd1e75 100644 --- a/test-dependent-projects/java-dep-webauthn-server-core-and-bouncycastle/build.gradle.kts +++ b/test-dependent-projects/java-dep-webauthn-server-core-and-bouncycastle/build.gradle.kts @@ -15,8 +15,6 @@ dependencies { // Runtime-only internal dependency of webauthn-server-core testImplementation("com.augustcellars.cose:cose-java:[1.0.0,2)") - testRuntimeOnly("ch.qos.logback:logback-classic:[1.2.3,2)") - // Transitive dependencies from coreTestOutput testImplementation("org.scala-lang:scala-library:[2.13.1,3)") } diff --git a/webauthn-server-demo/build.gradle b/webauthn-server-demo/build.gradle index ca28ec94a..f610cbf08 100644 --- a/webauthn-server-demo/build.gradle +++ b/webauthn-server-demo/build.gradle @@ -28,7 +28,7 @@ dependencies { ) runtimeOnly( - 'ch.qos.logback:logback-classic:[1.2.3,2)', + 'ch.qos.logback:logback-classic:1.3.0', 'org.glassfish.jersey.containers:jersey-container-servlet', 'org.glassfish.jersey.inject:jersey-hk2', ) From 97b6af7f63d3a916db5a9e8fe6cab980e06332af Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Sun, 4 Sep 2022 14:38:21 +0200 Subject: [PATCH 071/145] Separate test and demo dependency versions from main constraints --- build.gradle | 12 ------------ settings.gradle | 1 + test-platform/build.gradle | 16 ++++++++++++++++ webauthn-server-attestation/build.gradle | 1 + webauthn-server-core/build.gradle | 1 + webauthn-server-demo/build.gradle | 15 +++++++++------ yubico-util-scala/build.gradle | 1 + yubico-util/build.gradle | 1 + 8 files changed, 30 insertions(+), 18 deletions(-) create mode 100644 test-platform/build.gradle diff --git a/build.gradle b/build.gradle index a7fa01009..6c1b2fc51 100644 --- a/build.gradle +++ b/build.gradle @@ -58,21 +58,9 @@ dependencies { } api('com.google.guava:guava:[24.1.1,31)') api('com.upokecenter:cbor:[4.5.1,5)') - api('javax.ws.rs:javax.ws.rs-api:[2.1,3)') - api('javax.xml.bind:jaxb-api:[2.3.0,3)') - api('junit:junit:[4.12,5)') - api('org.apache.httpcomponents:httpclient:[4.5.2,5)') api('org.bouncycastle:bcpkix-jdk15on:[1.62,2)') api('org.bouncycastle:bcprov-jdk15on:[1.62,2)') - api('org.eclipse.jetty:jetty-servlet:[9.4.9.v20180320,10)') - api('org.glassfish.jersey.containers:jersey-container-servlet-core:[2.33,3)') - api('org.glassfish.jersey.containers:jersey-container-servlet:[2.33,3)') - api('org.glassfish.jersey.inject:jersey-hk2:[2.33,3)') - api('org.mockito:mockito-core:[2.27.0,3)') - api('org.scalacheck:scalacheck_2.13:[1.14.0,2)') - api('org.scalatest:scalatest_2.13:[3.0.8,3.1)') api('org.slf4j:slf4j-api:[1.7.25,2)') - api('uk.org.lidalia:slf4j-test:[1.1.0,2)') } } diff --git a/settings.gradle b/settings.gradle index efdcc3775..65af1138a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -9,3 +9,4 @@ include ':test-dependent-projects:java-dep-webauthn-server-attestation' include ':test-dependent-projects:java-dep-webauthn-server-core' include ':test-dependent-projects:java-dep-webauthn-server-core-and-bouncycastle' include ':test-dependent-projects:java-dep-yubico-util' +include ':test-platform' diff --git a/test-platform/build.gradle b/test-platform/build.gradle new file mode 100644 index 000000000..f865959c7 --- /dev/null +++ b/test-platform/build.gradle @@ -0,0 +1,16 @@ +plugins { + id 'java-platform' +} + +description = 'Dependency constraints for tests' + +dependencies { + constraints { + api('junit:junit:[4.12,5)') + api('org.apache.httpcomponents:httpclient:[4.5.2,5)') + api('org.mockito:mockito-core:[2.27.0,3)') + api('org.scalacheck:scalacheck_2.13:[1.14.0,2)') + api('org.scalatest:scalatest_2.13:[3.0.8,3.1)') + api('uk.org.lidalia:slf4j-test:[1.1.0,2)') + } +} diff --git a/webauthn-server-attestation/build.gradle b/webauthn-server-attestation/build.gradle index 7fe728b33..a8dfd227f 100644 --- a/webauthn-server-attestation/build.gradle +++ b/webauthn-server-attestation/build.gradle @@ -44,6 +44,7 @@ dependencies { ) testImplementation( + platform(project(":test-platform")), project(':webauthn-server-core').sourceSets.test.output, project(':yubico-util-scala'), 'com.fasterxml.jackson.datatype:jackson-datatype-jdk8', diff --git a/webauthn-server-core/build.gradle b/webauthn-server-core/build.gradle index f593e94e7..059ad2375 100644 --- a/webauthn-server-core/build.gradle +++ b/webauthn-server-core/build.gradle @@ -31,6 +31,7 @@ dependencies { ) testImplementation( + platform(project(":test-platform")), project(':yubico-util-scala'), 'com.fasterxml.jackson.datatype:jackson-datatype-jdk8', 'junit:junit', diff --git a/webauthn-server-demo/build.gradle b/webauthn-server-demo/build.gradle index f610cbf08..b4b302971 100644 --- a/webauthn-server-demo/build.gradle +++ b/webauthn-server-demo/build.gradle @@ -19,21 +19,24 @@ dependencies { 'com.fasterxml.jackson.core:jackson-databind', 'com.google.guava:guava', 'com.upokecenter:cbor', - 'javax.ws.rs:javax.ws.rs-api', 'org.bouncycastle:bcprov-jdk15on', - 'org.eclipse.jetty:jetty-server', - 'org.eclipse.jetty:jetty-servlet', - 'org.glassfish.jersey.containers:jersey-container-servlet-core', 'org.slf4j:slf4j-api', ) + implementation( + 'org.eclipse.jetty:jetty-servlet:9.4.9.v20180320', + 'org.glassfish.jersey.containers:jersey-container-servlet-core:2.36', + 'javax.ws.rs:javax.ws.rs-api:2.1.1', + ) + runtimeOnly( 'ch.qos.logback:logback-classic:1.3.0', - 'org.glassfish.jersey.containers:jersey-container-servlet', - 'org.glassfish.jersey.inject:jersey-hk2', + 'org.glassfish.jersey.containers:jersey-container-servlet:2.36', + 'org.glassfish.jersey.inject:jersey-hk2:2.36', ) testImplementation( + platform(project(":test-platform")), project(':webauthn-server-core').sourceSets.test.output, project(':yubico-util-scala'), diff --git a/yubico-util-scala/build.gradle b/yubico-util-scala/build.gradle index 5ff4568f4..0eb6d6adc 100644 --- a/yubico-util-scala/build.gradle +++ b/yubico-util-scala/build.gradle @@ -10,6 +10,7 @@ targetCompatibility = 1.8 dependencies { implementation(platform(rootProject)) + implementation(platform(project(":test-platform"))) implementation( 'org.scala-lang:scala-library', diff --git a/yubico-util/build.gradle b/yubico-util/build.gradle index 726602ddf..9edd49a86 100644 --- a/yubico-util/build.gradle +++ b/yubico-util/build.gradle @@ -16,6 +16,7 @@ targetCompatibility = 1.8 dependencies { api(platform(rootProject)) + api(platform(project(":test-platform"))) api( 'com.fasterxml.jackson.core:jackson-databind', From 2359971455b557a6f8e1a2abd9f29dcb82eaa13b Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Sun, 4 Sep 2022 14:59:38 +0200 Subject: [PATCH 072/145] Delete unnecessary version constraint This version no longer fails to resolve. --- build.gradle | 7 ------- 1 file changed, 7 deletions(-) diff --git a/build.gradle b/build.gradle index 6c1b2fc51..8e981d768 100644 --- a/build.gradle +++ b/build.gradle @@ -49,13 +49,6 @@ dependencies { api('com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:[2.13.2,3)') api('com.fasterxml.jackson.datatype:jackson-datatype-jdk8:[2.13.2,3)') api('com.fasterxml.jackson.datatype:jackson-datatype-jsr310:[2.13.2,3)') - api('com.fasterxml.jackson:jackson-bom') { - version { - strictly '[2.13.2.1,3)' - reject '2.13.2.1' - } - because 'jackson-databind 2.13.2.1 references nonexistent BOM' - } api('com.google.guava:guava:[24.1.1,31)') api('com.upokecenter:cbor:[4.5.1,5)') api('org.bouncycastle:bcpkix-jdk15on:[1.62,2)') From 9fe13c4537f2574ebb0e507385f5bdade7319c37 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 5 Sep 2022 12:42:56 +0200 Subject: [PATCH 073/145] Don't apply spotless plugin to test-platform subproject --- build.gradle | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/build.gradle b/build.gradle index 8e981d768..7ed2fe32f 100644 --- a/build.gradle +++ b/build.gradle @@ -62,7 +62,6 @@ allprojects { ext.dirtyMarker = "-DIRTY" apply plugin: 'com.cinnober.gradle.semver-git' - apply plugin: 'com.diffplug.spotless' apply plugin: 'idea' group = 'com.yubico' @@ -91,12 +90,15 @@ subprojects { mavenCentral() } - spotless { - java { - googleJavaFormat() - } - scala { - scalafmt('2.6.3').configFile(rootProject.file('scalafmt.conf')) + if (project !== project(':test-platform')) { + apply plugin: 'com.diffplug.spotless' + spotless { + java { + googleJavaFormat() + } + scala { + scalafmt('2.6.3').configFile(rootProject.file('scalafmt.conf')) + } } } } From 82bea3faf34c27d17aaf2c942a3e73f0a0697ad3 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 16 Aug 2022 19:01:35 +0200 Subject: [PATCH 074/145] Modularize createCredential methods in TestAuthenticator --- .../yubico/webauthn/TestAuthenticator.scala | 125 +++++++++++------- 1 file changed, 80 insertions(+), 45 deletions(-) diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/TestAuthenticator.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/TestAuthenticator.scala index 88f06de19..00314e378 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/TestAuthenticator.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/TestAuthenticator.scala @@ -302,28 +302,50 @@ object TestAuthenticator { } } - private def createCredential( + private def createAuthenticatorData( aaguid: ByteArray = Defaults.aaguid, - attestationMaker: AttestationMaker, authenticatorExtensions: Option[JsonNode] = None, - challenge: ByteArray = Defaults.challenge, - clientData: Option[JsonNode] = None, - clientExtensions: ClientRegistrationExtensionOutputs = - ClientRegistrationExtensionOutputs.builder().build(), credentialKeypair: Option[KeyPair] = None, keyAlgorithm: COSEAlgorithmIdentifier = Defaults.keyAlgorithm, - origin: String = Defaults.origin, - tokenBindingStatus: String = Defaults.TokenBinding.status, - tokenBindingId: Option[String] = Defaults.TokenBinding.id, ): ( - data.PublicKeyCredential[ - data.AuthenticatorAttestationResponse, - ClientRegistrationExtensionOutputs, - ], + ByteArray, KeyPair, - List[(X509Certificate, PrivateKey)], ) = { + val keypair = + credentialKeypair.getOrElse(generateKeypair(algorithm = keyAlgorithm)) + val publicKeyCose = keypair.getPublic match { + case pub: ECPublicKey => WebAuthnTestCodecs.ecPublicKeyToCose(pub) + case pub: BCEdDSAPublicKey => WebAuthnTestCodecs.eddsaPublicKeyToCose(pub) + case pub: RSAPublicKey => + WebAuthnTestCodecs.rsaPublicKeyToCose(pub, keyAlgorithm) + } + val authDataBytes: ByteArray = makeAuthDataBytes( + rpId = Defaults.rpId, + attestedCredentialDataBytes = Some( + makeAttestedCredentialDataBytes( + aaguid = aaguid, + publicKeyCose = publicKeyCose, + ) + ), + extensionsCborBytes = authenticatorExtensions map (ext => + new ByteArray(JacksonCodecs.cbor().writeValueAsBytes(ext)) + ), + ) + + ( + authDataBytes, + keypair, + ) + } + + private def createClientData( + challenge: ByteArray = Defaults.challenge, + clientData: Option[JsonNode] = None, + origin: String = Defaults.origin, + tokenBindingStatus: String = Defaults.TokenBinding.status, + tokenBindingId: Option[String] = Defaults.TokenBinding.id, + ): String = { val clientDataJson: String = JacksonCodecs.json.writeValueAsString(clientData getOrElse { val json: ObjectNode = jsonFactory.objectNode() @@ -349,29 +371,27 @@ object TestAuthenticator { json }) - val clientDataJsonBytes = toBytes(clientDataJson) - val keypair = - credentialKeypair.getOrElse(generateKeypair(algorithm = keyAlgorithm)) - val publicKeyCose = keypair.getPublic match { - case pub: ECPublicKey => WebAuthnTestCodecs.ecPublicKeyToCose(pub) - case pub: BCEdDSAPublicKey => WebAuthnTestCodecs.eddsaPublicKeyToCose(pub) - case pub: RSAPublicKey => - WebAuthnTestCodecs.rsaPublicKeyToCose(pub, keyAlgorithm) - } + clientDataJson + } - val authDataBytes: ByteArray = makeAuthDataBytes( - rpId = Defaults.rpId, - attestedCredentialDataBytes = Some( - makeAttestedCredentialDataBytes( - aaguid = aaguid, - publicKeyCose = publicKeyCose, - ) - ), - extensionsCborBytes = authenticatorExtensions map (ext => - new ByteArray(JacksonCodecs.cbor().writeValueAsBytes(ext)) - ), - ) + private def createCredential( + authDataBytes: ByteArray, + clientDataJson: String, + credentialKeypair: KeyPair, + attestationMaker: AttestationMaker, + clientExtensions: ClientRegistrationExtensionOutputs = + ClientRegistrationExtensionOutputs.builder().build(), + ): ( + data.PublicKeyCredential[ + data.AuthenticatorAttestationResponse, + ClientRegistrationExtensionOutputs, + ], + KeyPair, + List[(X509Certificate, PrivateKey)], + ) = { + + val clientDataJsonBytes = toBytes(clientDataJson) val attestationObjectBytes = attestationMaker.makeAttestationObjectBytes(authDataBytes, clientDataJson) @@ -391,7 +411,7 @@ object TestAuthenticator { .response(response) .clientExtensionResults(clientExtensions) .build(), - keypair, + credentialKeypair, attestationMaker.certChain, ) } @@ -407,13 +427,20 @@ object TestAuthenticator { ], KeyPair, List[(X509Certificate, PrivateKey)], - ) = - createCredential( + ) = { + val (authData, credentialKeypair) = createAuthenticatorData( aaguid = aaguid, - attestationMaker = attestationMaker, keyAlgorithm = keyAlgorithm, ) + createCredential( + authDataBytes = authData, + credentialKeypair = credentialKeypair, + clientDataJson = createClientData(), + attestationMaker = attestationMaker, + ) + } + def createSelfAttestedCredential( attestationMaker: SelfAttestation => AttestationMaker, keyAlgorithm: COSEAlgorithmIdentifier = Defaults.keyAlgorithm, @@ -425,12 +452,15 @@ object TestAuthenticator { KeyPair, List[(X509Certificate, PrivateKey)], ) = { - val keypair = generateKeypair(keyAlgorithm) + val (authData, keypair) = createAuthenticatorData(credentialKeypair = + Some(generateKeypair(keyAlgorithm)) + ) val signer = SelfAttestation(keypair, keyAlgorithm) createCredential( + authDataBytes = authData, + clientDataJson = createClientData(), + credentialKeypair = keypair, attestationMaker = attestationMaker(signer), - credentialKeypair = Some(keypair), - keyAlgorithm = keyAlgorithm, ) } @@ -444,12 +474,17 @@ object TestAuthenticator { ], KeyPair, List[(X509Certificate, PrivateKey)], - ) = + ) = { + val (authData, keypair) = createAuthenticatorData( + authenticatorExtensions = authenticatorExtensions + ) createCredential( + authDataBytes = authData, + clientDataJson = createClientData(challenge = challenge), + credentialKeypair = keypair, attestationMaker = AttestationMaker.none(), - authenticatorExtensions = authenticatorExtensions, - challenge = challenge, ) + } def createAssertionFromTestData( testData: RegistrationTestData, From 24a7bbd27aa3b089182bfee638c43c9bc200cc27 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 16 May 2022 17:12:33 +0200 Subject: [PATCH 075/145] Add support for tpm attestation --- .../main/java/com/yubico/webauthn/Crypto.java | 9 + .../webauthn/FinishRegistrationSteps.java | 5 +- .../TpmAttestationStatementVerifier.java | 680 ++++++++++++++++++ .../webauthn/RegistrationTestData.scala | 31 +- .../RelyingPartyRegistrationSpec.scala | 130 ++-- 5 files changed, 804 insertions(+), 51 deletions(-) create mode 100644 webauthn-server-core/src/main/java/com/yubico/webauthn/TpmAttestationStatementVerifier.java diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/Crypto.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/Crypto.java index 5893f0dd6..60ddff1bc 100755 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/Crypto.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/Crypto.java @@ -96,9 +96,18 @@ public static boolean verifySignature( public static ByteArray sha256(ByteArray bytes) { //noinspection UnstableApiUsage + // TODO remove noinspection return new ByteArray(Hashing.sha256().hashBytes(bytes.getBytes()).asBytes()); } + public static ByteArray sha384(ByteArray bytes) { + return new ByteArray(Hashing.sha384().hashBytes(bytes.getBytes()).asBytes()); + } + + public static ByteArray sha512(ByteArray bytes) { + return new ByteArray(Hashing.sha512().hashBytes(bytes.getBytes()).asBytes()); + } + public static ByteArray sha256(String str) { return sha256(new ByteArray(str.getBytes(StandardCharsets.UTF_8))); } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishRegistrationSteps.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishRegistrationSteps.java index 868bb9b12..3c6cda6b2 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishRegistrationSteps.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishRegistrationSteps.java @@ -373,6 +373,8 @@ public Optional attestationStatementVerifier() { return Optional.of(new AndroidSafetynetAttestationStatementVerifier()); case "apple": return Optional.of(new AppleAttestationStatementVerifier()); + case "tpm": + return Optional.of(new TpmAttestationStatementVerifier()); default: return Optional.empty(); } @@ -411,9 +413,6 @@ public AttestationType attestationType() { case "android-key": // TODO delete this once android-key attestation verification is implemented return AttestationType.BASIC; - case "tpm": - // TODO delete this once tpm attestation verification is implemented - return AttestationType.ATTESTATION_CA; default: return AttestationType.UNKNOWN; } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/TpmAttestationStatementVerifier.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/TpmAttestationStatementVerifier.java new file mode 100644 index 000000000..b48386cfd --- /dev/null +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/TpmAttestationStatementVerifier.java @@ -0,0 +1,680 @@ +// Copyright (c) 2018, Yubico AB +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package com.yubico.webauthn; + +import COSE.CoseException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.upokecenter.cbor.CBORObject; +import com.yubico.internal.util.BinaryUtil; +import com.yubico.internal.util.ByteInputStream; +import com.yubico.internal.util.CertificateParser; +import com.yubico.internal.util.ExceptionUtil; +import com.yubico.webauthn.data.AttestationObject; +import com.yubico.webauthn.data.AttestationType; +import com.yubico.webauthn.data.ByteArray; +import com.yubico.webauthn.data.COSEAlgorithmIdentifier; +import java.io.IOException; +import java.math.BigInteger; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.cert.CertificateException; +import java.security.cert.CertificateParsingException; +import java.security.cert.X509Certificate; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.RSAPublicKeySpec; +import java.util.Arrays; +import java.util.List; +import javax.naming.InvalidNameException; +import javax.naming.directory.Attributes; +import javax.naming.ldap.LdapName; +import lombok.Value; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +final class TpmAttestationStatementVerifier + implements AttestationStatementVerifier, X5cAttestationStatementVerifier { + + private static final String TPM_VER = "2.0"; + private static final ByteArray TPM_GENERATED_VALUE = ByteArray.fromBase64("/1RDRw=="); + private static final ByteArray TPM_ST_ATTEST_CERTIFY = ByteArray.fromBase64("gBc="); + + private static final int TPM_ALG_NULL = 0x0010; + + private static final String OID_TCG_AT_TPM_MANUFACTURER = "2.23.133.2.1"; + private static final String OID_TCG_AT_TPM_MODEL = "2.23.133.2.2"; + private static final String OID_TCG_AT_TPM_VERSION = "2.23.133.2.3"; + + /** + * Object attributes + * + *

    see section 8.3 of + * https://www.trustedcomputinggroup.org/wp-content/uploads/TPM-Rev-2.0-Part-2-Structures-01.38.pdf + */ + private static final class Attributes { + public static final int SIGN_ENCRYPT = 1 << 18; + + private static final int SHALL_BE_ZERO = + (1 << 0) // 0 Reserved + | (1 << 3) // 3 Reserved + | (0x3 << 8) // 9:8 Reserved + | (0xF << 12) // 15:12 Reserved + | ((0xFFFFFFFF << 19) & ((1 << 32) - 1)) // 31:19 Reserved + ; + } + + @Override + public AttestationType getAttestationType(AttestationObject attestation) { + if (attestation.getAttestationStatement().hasNonNull("x5c")) { + return AttestationType.BASIC; + } else { + return AttestationType.SELF_ATTESTATION; + } + } + + @Override + public boolean verifyAttestationSignature( + AttestationObject attestationObject, ByteArray clientDataJsonHash) { + + // Step 1: Verify that attStmt is valid CBOR conforming to the syntax defined above and perform + // CBOR decoding on it to extract the contained fields. + + ObjectNode attStmt = attestationObject.getAttestationStatement(); + + JsonNode verNode = attStmt.get("ver"); + ExceptionUtil.assure( + verNode != null && verNode.isTextual() && verNode.textValue().equals(TPM_VER), + "attStmt.ver must equal \"%s\", was: %s", + TPM_VER, + verNode); + + JsonNode algNode = attStmt.get("alg"); + ExceptionUtil.assure( + algNode != null && algNode.canConvertToLong(), + "attStmt.alg must be set to an integer value, was: %s", + algNode); + final COSEAlgorithmIdentifier alg = + COSEAlgorithmIdentifier.fromId(algNode.longValue()) + .orElseThrow( + () -> + new IllegalArgumentException("Unknown COSE algorithm identifier: " + algNode)); + + JsonNode x5cNode = attStmt.get("x5c"); + ExceptionUtil.assure( + x5cNode != null && x5cNode.isArray(), + "attStmt.x5c must be set to an array value, was: %s", + x5cNode); + final List x5c; + try { + x5c = + getAttestationTrustPath(attestationObject) + .orElseThrow( + () -> + new IllegalArgumentException( + "Failed to parse \"x5c\" attestation certificate chain in \"tpm\" attestation statement.")); + } catch (CertificateException e) { + throw new RuntimeException(e); + } + final X509Certificate aikCert = x5c.get(0); + + JsonNode sigNode = attStmt.get("sig"); + ExceptionUtil.assure( + sigNode != null && sigNode.isBinary(), + "attStmt.sig must be set to a binary value, was: %s", + sigNode); + final ByteArray sig; + try { + sig = new ByteArray(sigNode.binaryValue()); + } catch (IOException e) { + throw new RuntimeException(e); + } + + JsonNode certInfoNode = attStmt.get("certInfo"); + ExceptionUtil.assure( + certInfoNode != null && certInfoNode.isBinary(), + "attStmt.certInfo must be set to a binary value, was: %s", + certInfoNode); + + JsonNode pubAreaNode = attStmt.get("pubArea"); + ExceptionUtil.assure( + pubAreaNode != null && pubAreaNode.isBinary(), + "attStmt.pubArea must be set to a binary value, was: %s", + pubAreaNode); + + final TpmtPublic pubArea; + try { + pubArea = TpmtPublic.parse(pubAreaNode.binaryValue()); + } catch (IOException e) { + throw new RuntimeException("Failed to parse TPMT_PUBLIC data structure.", e); + } + + final TpmsAttest certInfo; + try { + certInfo = TpmsAttest.parse(certInfoNode.binaryValue()); + } catch (IOException e) { + throw new RuntimeException("Failed to parse TPMS_ATTEST data structure.", e); + } + + // Step 2: Verify that the public key specified by the parameters and unique fields of pubArea + // is identical to the credentialPublicKey in the attestedCredentialData in authenticatorData. + try { + verifyPublicKeysMatch(attestationObject, pubArea); + } catch (CoseException | IOException | InvalidKeySpecException | NoSuchAlgorithmException e) { + throw new RuntimeException( + "Failed to verify that public key in TPM attestation matches public key in authData.", e); + } + + // Step 3: Concatenate authenticatorData and clientDataHash to form attToBeSigned. + final ByteArray attToBeSigned = + attestationObject.getAuthenticatorData().getBytes().concat(clientDataJsonHash); + + // Step 4: Validate that certInfo is valid: + try { + validateCertInfo(alg, aikCert, sig, pubArea, certInfo, attToBeSigned, attestationObject); + } catch (CertificateParsingException e) { + throw new RuntimeException("Failed to verify TPM attestation.", e); + } + + return true; + } + + private void validateCertInfo( + COSEAlgorithmIdentifier alg, + X509Certificate aikCert, + ByteArray sig, + TpmtPublic pubArea, + TpmsAttest certInfo, + ByteArray attToBeSigned, + AttestationObject attestationObject) + throws CertificateParsingException { + // Sub-steps 1-2 handled in TpmsAttest.parse() + // Sub-step 3: Verify that extraData is set to the hash of attToBeSigned using the hash + // algorithm employed in "alg". + final ByteArray expectedExtraData; + switch (alg) { + case ES256: + case RS256: + expectedExtraData = Crypto.sha256(attToBeSigned); + break; + + case RS1: + try { + expectedExtraData = Crypto.sha1(attToBeSigned); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("Failed to hash attToBeSigned to verify TPM attestation.", e); + } + break; + + default: + throw new UnsupportedOperationException("Signing algorithm not implemented: " + alg); + } + ExceptionUtil.assure( + certInfo.extraData.equals(expectedExtraData), "Incorrect certInfo.extraData."); + + // Sub-step 4: Verify that attested contains a TPMS_CERTIFY_INFO structure as specified in + // [TPMv2-Part2] section 10.12.3, whose name field contains a valid Name for pubArea, as + // computed using the algorithm in the nameAlg field of pubArea using the procedure specified in + // [TPMv2-Part1] section 16. + ExceptionUtil.assure( + certInfo.attestedName.equals(pubArea.name()), "Incorrect certInfo.attestedName."); + + // Sub-step 5 handled by parsing above + // Sub-step 6: Nothing to do + + // Sub-step 7: Verify the sig is a valid signature over certInfo using the attestation public + // key in aikCert with the algorithm specified in alg. + ExceptionUtil.assure( + Crypto.verifySignature(aikCert, certInfo.getRawBytes(), sig, alg), + "Incorrect TPM attestation signature."); + + // Sub-step 8: Verify that aikCert meets the requirements in § 8.3.1 TPM Attestation Statement + // Certificate Requirements. + // Sub-step 9: If aikCert contains an extension with OID 1.3.6.1.4.1.45724.1.1.4 + // (id-fido-gen-ce-aaguid) verify that the value of this extension matches the aaguid in + // authenticatorData. + verifyX5cRequirements( + aikCert, + attestationObject.getAuthenticatorData().getAttestedCredentialData().get().getAaguid()); + } + + private void verifyPublicKeysMatch(AttestationObject attestationObject, TpmtPublic pubArea) + throws CoseException, IOException, InvalidKeySpecException, NoSuchAlgorithmException { + final PublicKey credentialPubKey = + WebAuthnCodecs.importCosePublicKey( + attestationObject + .getAuthenticatorData() + .getAttestedCredentialData() + .get() + .getCredentialPublicKey()); + + final PublicKey signedCredentialPublicKey; + switch (pubArea.signAlg) { + case TpmAlgAsym.RSA: + { + TpmsRsaParms params = (TpmsRsaParms) pubArea.parameters; + Tpm2bPublicKeyRsa unique = (Tpm2bPublicKeyRsa) pubArea.unique; + RSAPublicKeySpec spec = + new RSAPublicKeySpec( + new BigInteger(1, unique.bytes.getBytes()), BigInteger.valueOf(params.exponent)); + KeyFactory kf = KeyFactory.getInstance("RSA"); + signedCredentialPublicKey = kf.generatePublic(spec); + } + + ExceptionUtil.assure( + Arrays.equals(credentialPubKey.getEncoded(), signedCredentialPublicKey.getEncoded()), + "Signed public key in TPM attestation is not identical to credential public key in authData."); + break; + + case TpmAlgAsym.ECC: + { + TpmsEccParms params = (TpmsEccParms) pubArea.parameters; + TpmsEccPoint unique = (TpmsEccPoint) pubArea.unique; + + final COSEAlgorithmIdentifier algId = + WebAuthnCodecs.getCoseKeyAlg( + attestationObject + .getAuthenticatorData() + .getAttestedCredentialData() + .get() + .getCredentialPublicKey()) + .get(); + final COSEAlgorithmIdentifier tpmAlgId; + final CBORObject cosePubkey = + CBORObject.DecodeFromBytes( + attestationObject + .getAuthenticatorData() + .getAttestedCredentialData() + .get() + .getCredentialPublicKey() + .getBytes()); + + switch (params.curve_id) { + case TpmEccCurve.NIST_P256: + tpmAlgId = COSEAlgorithmIdentifier.ES256; + break; + + default: + throw new UnsupportedOperationException( + "Unsupported elliptic curve: " + params.curve_id); + } + + ExceptionUtil.assure( + algId.equals(tpmAlgId), + "Signed public key in TPM attestation is not identical to credential public key in authData; elliptic curve differs: %s != %s", + tpmAlgId, + algId); + byte[] cosePubkeyX = cosePubkey.get(CBORObject.FromObject(-2)).GetByteString(); + byte[] cosePubkeyY = cosePubkey.get(CBORObject.FromObject(-3)).GetByteString(); + ExceptionUtil.assure( + new BigInteger(1, unique.x.getBytes()).equals(new BigInteger(1, cosePubkeyX)), + "Signed public key in TPM attestation is not identical to credential public key in authData; EC X coordinate differs: %s != %s", + unique.x, + new ByteArray(cosePubkeyX)); + ExceptionUtil.assure( + new BigInteger(1, unique.y.getBytes()).equals(new BigInteger(1, cosePubkeyY)), + "Signed public key in TPM attestation is not identical to credential public key in authData; EC Y coordinate differs: %s != %s", + unique.y, + new ByteArray(cosePubkeyY)); + } + break; + + default: + throw new UnsupportedOperationException( + "Unsupported algorithm for credential public key: " + pubArea.signAlg); + } + } + + private static final class TpmAlgAsym { + public static final int RSA = 0x0001; + public static final int ECC = 0x0023; + } + + private interface Parameters {} + + private interface Unique {} + + @Value + private static class TpmtPublic { + int signAlg; + int nameAlg; + Parameters parameters; + Unique unique; + ByteArray rawBytes; + + public static TpmtPublic parse(byte[] pubArea) throws IOException { + try (ByteInputStream reader = new ByteInputStream(pubArea)) { + final int signAlg = reader.readUnsignedShort(); + final int nameAlg = reader.readUnsignedShort(); + + final int attributes = reader.readInt(); + ExceptionUtil.assure( + (attributes & Attributes.SHALL_BE_ZERO) == 0, + "Attributes contains 1 bits in reserved position(s): 0x%08x", + attributes); + + // authPolicy is not used by this implementation + reader.skipBytes(reader.readUnsignedShort()); + + final Parameters parameters; + final Unique unique; + + ExceptionUtil.assure( + (attributes & Attributes.SIGN_ENCRYPT) == Attributes.SIGN_ENCRYPT, + "Public key is expected to have the SIGN_ENCRYPT attribute set, attributes were: 0x%08x", + attributes); + + if (signAlg == TpmAlgAsym.RSA) { + parameters = TpmsRsaParms.parse(reader); + unique = Tpm2bPublicKeyRsa.parse(reader); + } else if (signAlg == TpmAlgAsym.ECC) { + parameters = TpmsEccParms.parse(reader); + unique = TpmsEccPoint.parse(reader); + } else { + throw new UnsupportedOperationException("Signing algorithm not implemented: " + signAlg); + } + + ExceptionUtil.assure( + reader.available() == 0, + "%d remaining bytes in TPMT_PUBLIC buffer", + reader.available()); + + return new TpmtPublic(signAlg, nameAlg, parameters, unique, new ByteArray(pubArea)); + } + } + + /** + * Computing Entity Names + * + *

    see: + * https://www.trustedcomputinggroup.org/wp-content/uploads/TPM-Rev-2.0-Part-1-Architecture-01.38.pdf + * section 16 Names + * + *

    +     * Name ≔ nameAlg || HnameAlg (handle→nvPublicArea)
    +     * where
    +     * nameAlg algorithm used to compute Name
    +     * HnameAlg hash using the nameAlg parameter in the NV Index location
    +     * associated with handle
    +     * nvPublicArea contents of the TPMS_NV_PUBLIC associated with handle
    +     * 
    + */ + public ByteArray name() { + final ByteArray hash; + switch (this.nameAlg) { + case TpmAlgHash.SHA1: + try { + hash = Crypto.sha1(this.rawBytes); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("Failed to hash TPMU_ATTEST name.", e); + } + break; + + case TpmAlgHash.SHA256: + hash = Crypto.sha256(this.rawBytes); + break; + + case TpmAlgHash.SHA384: + hash = Crypto.sha384(this.rawBytes); + break; + + case TpmAlgHash.SHA512: + hash = Crypto.sha512(this.rawBytes); + break; + + default: + throw new IllegalArgumentException("Unknown hash algorithm identifier: " + this.nameAlg); + } + return new ByteArray(BinaryUtil.encodeUint16(this.nameAlg)).concat(hash); + } + } + + private static class TpmAlgHash { + public static final int SHA1 = 0x0004; + public static final int SHA256 = 0x000B; + public static final int SHA384 = 0x000C; + public static final int SHA512 = 0x000D; + } + + public void verifyX5cRequirements(X509Certificate cert, ByteArray aaguid) + throws CertificateParsingException { + ExceptionUtil.assure( + cert.getVersion() == 3, + "Invalid TPM attestation certificate: Version MUST be 3, but was: %s", + cert.getVersion()); + + ExceptionUtil.assure( + cert.getSubjectX500Principal().getName().isEmpty(), + "Invalid TPM attestation certificate: subject MUST be empty, but was: %s", + cert.getSubjectX500Principal()); + + boolean foundManufacturer = false; + boolean foundModel = false; + boolean foundVersion = false; + for (List n : cert.getSubjectAlternativeNames()) { + if ((Integer) n.get(0) == 4) { // GeneralNames CHOICE 4: directoryName + if (n.get(1) instanceof String) { + try { + javax.naming.directory.Attributes attrs = + new LdapName((String) n.get(1)).getRdns().get(0).toAttributes(); + foundManufacturer = foundManufacturer || attrs.get(OID_TCG_AT_TPM_MANUFACTURER) != null; + foundModel = foundModel || attrs.get(OID_TCG_AT_TPM_MODEL) != null; + foundVersion = foundVersion || attrs.get(OID_TCG_AT_TPM_VERSION) != null; + } catch (InvalidNameException e) { + throw new RuntimeException( + "Failed to decode subject alternative name in TPM attestation cert", e); + } + } else { + log.debug("Unknown type of SubjectAlternativeNames entry: {}", n.get(1)); + } + } + } + ExceptionUtil.assure( + foundManufacturer && foundModel && foundVersion, + "Invalid TPM attestation certificate: The Subject Alternative Name extension MUST be set as defined in [TPMv2-EK-Profile] section 3.2.9.%s%s%s", + foundManufacturer ? "" : " Missing TPM manufacturer.", + foundModel ? "" : " Missing TPM model.", + foundVersion ? "" : " Missing TPM version."); + + ExceptionUtil.assure( + cert.getExtendedKeyUsage() != null && cert.getExtendedKeyUsage().contains("2.23.133.8.3"), + "Invalid TPM attestation certificate: extended key usage extension MUST contain the OID 2.23.133.8.3, but was: %s", + cert.getExtendedKeyUsage()); + + ExceptionUtil.assure( + cert.getBasicConstraints() == -1, + "Invalid TPM attestation certificate: MUST NOT be a CA certificate, but was."); + + CertificateParser.parseFidoAaguidExtension(cert) + .ifPresent( + extensionAaguid -> { + ExceptionUtil.assure( + Arrays.equals(aaguid.getBytes(), extensionAaguid), + "Invalid TPM attestation certificate: X.509 extension \"id-fido-gen-ce-aaguid\" is present but does not match the authenticator AAGUID."); + }); + } + + private static final class TpmRsaScheme { + public static final int RSASSA = 0x0014; + } + + /** + * See: + * https://www.trustedcomputinggroup.org/wp-content/uploads/TPM-Rev-2.0-Part-2-Structures-01.38.pdf + * section 12.2.3.5 + */ + @Value + private static class TpmsRsaParms implements Parameters { + + long exponent; + + public static TpmsRsaParms parse(ByteInputStream reader) throws IOException { + final int symmetric = reader.readUnsignedShort(); + ExceptionUtil.assure( + symmetric == TPM_ALG_NULL, + "RSA key is expected to have \"symmetric\" set to TPM_ALG_NULL, was: 0x%04x", + symmetric); + + final int scheme = reader.readUnsignedShort(); + ExceptionUtil.assure( + scheme == TpmRsaScheme.RSASSA || scheme == TPM_ALG_NULL, + "RSA key is expected to have \"scheme\" set to TPM_ALG_RSASSA or TPM_ALG_NULL, was: 0x%04x", + scheme); + + reader.skipBytes(2); // key_bits is not used by this implementation + + int exponent = reader.readInt(); + ExceptionUtil.assure( + exponent >= 0, "Exponent is too large and wrapped around to negative: %d", exponent); + if (exponent == 0) { + // When zero, indicates that the exponent is the default of 2^16 + 1 + exponent = (1 << 16) + 1; + } + + return new TpmsRsaParms(exponent); + } + } + + @Value + private static class Tpm2bPublicKeyRsa implements Unique { + ByteArray bytes; + + public static Tpm2bPublicKeyRsa parse(ByteInputStream reader) throws IOException { + return new Tpm2bPublicKeyRsa(new ByteArray(reader.read(reader.readUnsignedShort()))); + } + } + + @Value + private static class TpmsEccParms implements Parameters { + int curve_id; + + public static TpmsEccParms parse(ByteInputStream reader) throws IOException { + final int symmetric = reader.readUnsignedShort(); + final int scheme = reader.readUnsignedShort(); + ExceptionUtil.assure( + symmetric == TPM_ALG_NULL, + "ECC key is expected to have \"symmetric\" set to TPM_ALG_NULL, was: 0x%04x", + symmetric); + ExceptionUtil.assure( + scheme == TPM_ALG_NULL, + "ECC key is expected to have \"scheme\" set to TPM_ALG_NULL, was: 0x%04x", + scheme); + + final int curve_id = reader.readUnsignedShort(); + reader.skipBytes(2); // kdf_scheme is not used by this implementation + + return new TpmsEccParms(curve_id); + } + } + + /** + * TPMS_ECC_POINT + * + *

    See + * https://www.trustedcomputinggroup.org/wp-content/uploads/TPM-Rev-2.0-Part-2-Structures-01.38.pdf + * Section 11.2.5.2 + */ + @Value + private static class TpmsEccPoint implements Unique { + + ByteArray x; + ByteArray y; + + public static TpmsEccPoint parse(ByteInputStream reader) throws IOException { + final ByteArray x = new ByteArray(reader.read(reader.readUnsignedShort())); + final ByteArray y = new ByteArray(reader.read(reader.readUnsignedShort())); + + return new TpmsEccPoint(x, y); + } + } + + /** + * TPM_ECC_CURVE + * + *

    https://www.trustedcomputinggroup.org/wp-content/uploads/TPM-Rev-2.0-Part-2-Structures-01.38.pdf + * section 6.4 + */ + private static class TpmEccCurve { + + public static final int NONE = 0x0000; + public static final int NIST_P256 = 0x0003; + public static final int NIST_P384 = 0x0004; + public static final int NIST_P521 = 0x0005; + } + + /** + * the signature data is defined by [TPMv2-Part2] Section 10.12.8 (TPMS_ATTEST) as: + * TPM_GENERATED_VALUE (0xff544347 aka "\xffTCG") TPMI_ST_ATTEST - always TPM_ST_ATTEST_CERTIFY + * (0x8017) because signing procedure defines it should call TPM_Certify [TPMv2-Part3] Section + * 18.2 TPM2B_NAME size (uint16) name (size long) TPM2B_DATA size (uint16) name (size long) + * TPMS_CLOCK_INFO clock (uint64) resetCount (uint32) restartCount (uint32) safe (byte) 1 yes, 0 + * no firmwareVersion uint64 attested TPMS_CERTIFY_INFO (because TPM_ST_ATTEST_CERTIFY) name + * TPM2B_NAME qualified_name TPM2B_NAME See: + * https://www.trustedcomputinggroup.org/wp-content/uploads/TPM-Rev-2.0-Part-2-Structures-01.38.pdf + * https://www.trustedcomputinggroup.org/wp-content/uploads/TPM-Rev-2.0-Part-3-Commands-01.38.pdf + */ + @Value + private static class TpmsAttest { + + ByteArray rawBytes; + ByteArray extraData; + ByteArray attestedName; + + private static TpmsAttest parse(byte[] certInfo) throws IOException { + try (ByteInputStream reader = new ByteInputStream(certInfo)) { + final ByteArray magic = new ByteArray(reader.read(4)); + + // Verify that magic is set to TPM_GENERATED_VALUE. + // see https://w3c.github.io/webauthn/#sctn-tpm-attestation + // verification procedure + ExceptionUtil.assure( + magic.equals(TPM_GENERATED_VALUE), "magic field is invalid: %s", magic); + + // Verify that type is set to TPM_ST_ATTEST_CERTIFY. + // see https://w3c.github.io/webauthn/#sctn-tpm-attestation + // verification procedure + final ByteArray type = new ByteArray(reader.read(2)); + ExceptionUtil.assure(type.equals(TPM_ST_ATTEST_CERTIFY), "type field is invalid: %s", type); + + // qualifiedSigner is not used by this implementation + reader.skipBytes(reader.readUnsignedShort()); + + final ByteArray extraData = new ByteArray(reader.read(reader.readUnsignedShort())); + + // clockInfo is not used by this implementation + reader.skipBytes(8 + 4 + 4 + 1); + + // firmwareVersion is not used by this implementation + reader.skipBytes(8); + + final ByteArray attestedName = new ByteArray(reader.read(reader.readUnsignedShort())); + + // attestedQualifiedName is not used by this implementation + + return new TpmsAttest(new ByteArray(certInfo), extraData, attestedName); + } + } + } +} diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RegistrationTestData.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RegistrationTestData.scala index f343fcd0b..cda19a39a 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RegistrationTestData.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RegistrationTestData.scala @@ -585,9 +585,36 @@ object RegistrationTestData { ) } } + object Tpm { - val PrivacyCa: RegistrationTestData = - Packed.BasicAttestation.setAttestationStatementFormat("tpm") + val RealExample: RegistrationTestData = + new RegistrationTestData( + alg = COSEAlgorithmIdentifier.RS256, + // Real attestation object from Windows + attestationObject = + ByteArray.fromBase64Url("o2NmbXRjdHBtZ2F0dFN0bXSmY2FsZzn__mNzaWdZAQBFEaTe-uZvbZBNsIMtJa26eigMUxEM1mBtddR7gdEBH5Hyeo9hFCqiJwYVKUq_iP9hvFaiLzoGbAWDgiG-fa3F-S71c8w83756dyRBMXNHYEvYjfv0TqGyky73V4xyKpf1iHiO_g4t31UjQiyTfypdP_rRcm42KVKgVyRPZzx_AKweN9XKEFfT2Ym3fmqD_scaIeKSyGs9qwH1MbILLUVnRK6fKK6sAA4ZaDVz4gUiSUoK9ZycCC2hfLBq5GjiTLgQF_Q2O3gRTqmU8VfwVsmtN5OMaGOyaFrUk97-RvZVrARXhNzrUAJT7KjTLDZeIA96F3pB_F_q3xd_dgvwVpWHY3ZlcmMyLjBjeDVjglkFuzCCBbcwggOfoAMCAQICEHHcna7VCE3QpRyKgi2uvXYwDQYJKoZIhvcNAQELBQAwQTE_MD0GA1UEAxM2RVVTLU5UQy1LRVlJRC04ODJGMDQ3Qjg3MTIxQ0Y5ODg1RjMxMTYwQkM3QkI1NTg2QUY0NzFCMB4XDTIyMDEyMDE5NTQxNloXDTI3MDYwMzE3NTE0OFowADCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALtT5frDB-WUq6N0VYnlYEalJzSut0JQx3vP29_ub-kZ7csJrm8uGXQGUkPlf4EFehFTnQ1jX_oZ8jNPw1m3rV5ijcuCe3r5GICFD6gpbuErmGS2mDVfe3fl_p0gPvhtulqatb1uYkWfW5SIKix1XWRvm92s3lRQvd-6vX_ExPIP-pEf0tkeINpBNNWgdtx3VdW4KVFTcv-q2FKhqfqXiAdOMHmwmWyXYulppYqW2XC7Pw9QmHZR_C5Urpc5UMmABz4zWSAYOyBMkKsX8koAsk8RgLtus07wW3FhJqi-BYczIe0IxG0q9UL295lkaxreTkfWYZHMMcU4M-Tm1w7QvKsCAwEAAaOCAeowggHmMA4GA1UdDwEB_wQEAwIHgDAMBgNVHRMBAf8EAjAAMG0GA1UdIAEB_wRjMGEwXwYJKwYBBAGCNxUfMFIwUAYIKwYBBQUHAgIwRB5CAFQAQwBQAEEAIAAgAFQAcgB1AHMAdABlAGQAIAAgAFAAbABhAHQAZgBvAHIAbQAgACAASQBkAGUAbgB0AGkAdAB5MBAGA1UdJQQJMAcGBWeBBQgDMFAGA1UdEQEB_wRGMESkQjBAMT4wEAYFZ4EFAgIMB05QQ1Q3NXgwFAYFZ4EFAgEMC2lkOjRFNTQ0MzAwMBQGBWeBBQIDDAtpZDowMDA3MDAwMjAfBgNVHSMEGDAWgBSMmnF_AA0xD8rW7i0pqjSXJYwSHjAdBgNVHQ4EFgQUFmUMIda76eb7Whi8CweaWhe7yNMwgbIGCCsGAQUFBwEBBIGlMIGiMIGfBggrBgEFBQcwAoaBkmh0dHA6Ly9hemNzcHJvZGV1c2Fpa3B1Ymxpc2guYmxvYi5jb3JlLndpbmRvd3MubmV0L2V1cy1udGMta2V5aWQtODgyZjA0N2I4NzEyMWNmOTg4NWYzMTE2MGJjN2JiNTU4NmFmNDcxYi84ODIzMGNhMi0yN2U1LTQxNTEtOWJhMi01OWI1ODJjMzlhYWEuY2VyMA0GCSqGSIb3DQEBCwUAA4ICAQCxsTbR5V8qnw6H6HEWJvrqcRy8fkY_vFUSjUq27hRl0t9D6LuS20l65FFm48yLwCkQbIf-aOBjwWafAbSVnEMig3KP-2Ml8IFtH63Msq9lwDlnXx2PNi7ISOemHNzBNeOG7pd_Zs69XUTq9zCriw9gAILCVCYllBluycdT7wZdjf0Bb5QJtTMuhwNXnOWmjv0VBOfsclWo-SEnnufaIDi0Vcf_TzbgmNn408Ej7R4Njy4qLnhPk64ruuWNJt3xlLMjbJXe_VKdO3lhM7JVFWSNAn8zfvEIwrrgCPhp1k2mFUGxJEvpSTnuZtNF35z4_54K6cEqZiqO-qd4FKt4KYs1GYJDyxttuUySGtnYyZg2aYB6hamg3asRDjBMPqoURsdVJcWQh3dFnD88cbs7Qt4_ytqAY61qfPE7bJ6E33o0X7OtxmECPd3aBJk6nsyXEXNF2vIww1UCrRC0OEr1HsTqA4bQU8KCWV6kduUnvkUWPT8CF0d2ER4wnszb053Tlcf2ebcytTMf_Nd95g520Hhqb2FZALCErijBi04Bu6SNeND1NQ3nxDSKC-CamOYW0ODch05Xzi1V0_sq0zmdKTxMSpg1jOZ1Q9924D4lJkruCB3zcsIBTUxV0EgAM1zGuoqwWjwYXr_8tO4_kEO1Lw8DckZIrk1s3ySsMVC89TRrIVkG7zCCBuswggTToAMCAQICEzMAAAQI5W53M7IUDf4AAAAABAgwDQYJKoZIhvcNAQELBQAwgYwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xNjA0BgNVBAMTLU1pY3Jvc29mdCBUUE0gUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgMjAxNDAeFw0yMTA2MDMxNzUxNDhaFw0yNzA2MDMxNzUxNDhaMEExPzA9BgNVBAMTNkVVUy1OVEMtS0VZSUQtODgyRjA0N0I4NzEyMUNGOTg4NUYzMTE2MEJDN0JCNTU4NkFGNDcxQjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMEye3QdCUOGeseHj_QjLMJVbHBEJKkRbXFsmZi6Mob_3IsVfxO-mQQC-xfY9tDyB1jhxfFAG-rjUfRVBwYYPfaVo2W58Q09Kcpf-43Iw9SRxx2ThP-KPFJPFBofZroloNaTNz3DRaWZ2ha-_PUG2nwTXR7LoIpqMVW1PDzGxb47SNpRmKJxVZhQ2_wRhZZvHRHJpZmrCmHRpTRqWSzQT1jn7Zo9VuMYvp_OFj7-LFpkqi4BYyhi0kTBPDQTpYrBi7RtmF1MhZBmm1HGDhoXHcPSZkN5vq5at4g03R15KWyRDgBcckCAgtewd6Dtd_Zwaejlm57xyGqP6T-AE-N8udh1NPv_PZVlSc4CnCayUTPORuaJ7N-v7Y4wpNSIdipq29hw19WVuO_z7q6GpQbn17arYf6LSoDZfwO8GHXPrtBOYYSZCNKuZ_IK8nomBLJPtN5AzwEZNyLCZIkg0U0sJ-oVr2UEYxlwwZQm5RSDxProaKU-OXq4f_j_0pEu5_DbJx9syR3Nsv6Lt9Zkf3JSJTVtWXoM0-R_82vAJ669PX0LLr603PKWBZbW7zQvtGojT_Pc1FDGfwhcdckxd3MGpEjZwh_1D8elYcxj3Ndw5jClWosZKr33pUcjqeFtSZSur0lbm6vyCfS16XzSMn8IkHmbbXcpgGKHumUCFD8CHJIBAgMBAAGjggGOMIIBijAOBgNVHQ8BAf8EBAMCAoQwGwYDVR0lBBQwEgYJKwYBBAGCNxUkBgVngQUIAzAWBgNVHSAEDzANMAsGCSsGAQQBgjcVHzASBgNVHRMBAf8ECDAGAQH_AgEAMB0GA1UdDgQWBBSMmnF_AA0xD8rW7i0pqjSXJYwSHjAfBgNVHSMEGDAWgBR6jArOL0hiF-KU0a5VwVLscXSkVjBwBgNVHR8EaTBnMGWgY6Bhhl9odHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NybC9NaWNyb3NvZnQlMjBUUE0lMjBSb290JTIwQ2VydGlmaWNhdGUlMjBBdXRob3JpdHklMjAyMDE0LmNybDB9BggrBgEFBQcBAQRxMG8wbQYIKwYBBQUHMAKGYWh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY2VydHMvTWljcm9zb2Z0JTIwVFBNJTIwUm9vdCUyMENlcnRpZmljYXRlJTIwQXV0aG9yaXR5JTIwMjAxNC5jcnQwDQYJKoZIhvcNAQELBQADggIBAHG-1grb-6xpObMtxfFScl8PRLd_GjLFaeAd0kVPls0jzKplG2Am4O87Qg0OUY0VhQ-uD39590gGrWEWnOmdrVJ-R1lJc1yrIZFEASBEedJSvxw9YTNknD59uXtznIP_4Glk-4NpqpcYov2OkBdV59V4dTL5oWFH0vzkZQfvGFxEHwtB9O6Bh0Lk142zXAh5_vf_-hSw3t9adloBAnA0AtPUVkzmgNRGeTpfPm-Iud-MAoUXaccFn2EChjKb9ApbS1ww8ZvX4x2kFU6qctu32g7Vf6CgACc1i-UYDT_E-h6c1O4n7JK2OxVS-DVwybps-cALU0gj-ZMauNMej0_x_NerzvDuQ77eFMTDMY4ZTzYOzlg4Nj0K8y1Bx_KqeTBO0N9CdEG3dBxWCUUFQzAx-i38xJL-dtYTCCFATVhc9FFJ0CQgU07JAGAeuNm_GL8kN46bMXd_ApQYFzDQUWYYXvRIt9mCw0Zd45lpAMuiDKT9TgjUVDNu8LQ8FPK0KeiQVrGHFMhHgg2pbVH9Pvc1jNEeRpCo0BLpZQwuIgEt90mepkt6Va-C9krHsU4y2oalG2LUu-jOC3NWNK8LssYVUCFWtaKh5d-xdTQmjx4uO-1sq9GFntVJ94QnEDhldz0XMopQ8srTGLMqR3MT-GkSNb5X1UFC-X1udXI8YvB3ADr_Z3B1YkFyZWFZATYAAQALAAYEcgAgnf_L82w4OuaZ-5ho3G3LidcVOIS-KAOSLBJBWL-tIq4AEAAQCAAAAAAAAQC6zY02JuN_qf28iVCISa6d6aUM5sS2PsKvZMO0P_-28lLX_9-xbmbEcTPvCj5aIEqVUfCb07U4qCBBUdaWygAbhAckvzYrACm5I8hjMoGkxMkZLw5kX0SjtHx6VfgksElxnu4DcC2g9pqL_McgddZV0zNGrYNj1iamzoxTyOcYyvZjQv_4gU-t9mEc0uqHHv-H6k23p4mensyaGAhkGTV8odyBxsNpdnR8IPnXWPO7tBDTbk4mg3VtclqLhS0-TCh_QnZ6lcEl27wTE8FjwqVdQG9F1Ouyn-eNAAq0EufbzwjSNpuDrlj-Kj5xY_lS5BMEjQUXqP-U5nzW22TehX8VaGNlcnRJbmZvWKH_VENHgBcAIgALt-OVET9fzihC1dzyG5xG70MVdRkPpSEkWQ0nns4zaYkAFGo6XjXu3JY-wup-EHJYWEuLpb45AAAACMgZxo6-OwT1-cIZwQGjXsp0dUws1gAiAAvzCsernlTAewF0QwNOA-iWpyhGxC-k_xBRjTtGNz9m6gAiAAs54tyczU1aCAh_N3Vy3qcAnC9zGeOxtQQKCe8CAanbbGhhdXRoRGF0YVkBZ-MWwK8fdtoeGn4DEn0TAUu4IUP_PMiBiJd4lDRznbBDRQAAAAAImHBYytxLgbbhMN5Q3L6WACBxLUIzn9ngKAM11_UwWG7kCiAvVyO1mYGSsEhfWeyhDaQBAwM5AQAgWQEAus2NNibjf6n9vIlQiEmunemlDObEtj7Cr2TDtD__tvJS1__fsW5mxHEz7wo-WiBKlVHwm9O1OKggQVHWlsoAG4QHJL82KwApuSPIYzKBpMTJGS8OZF9Eo7R8elX4JLBJcZ7uA3AtoPaai_zHIHXWVdMzRq2DY9Ymps6MU8jnGMr2Y0L_-IFPrfZhHNLqhx7_h-pNt6eJnp7MmhgIZBk1fKHcgcbDaXZ0fCD511jzu7QQ025OJoN1bXJai4UtPkwof0J2epXBJdu8ExPBY8KlXUBvRdTrsp_njQAKtBLn288I0jabg65Y_io-cWP5UuQTBI0FF6j_lOZ81ttk3oV_FSFDAQAB"), + clientDataJson = new String( + ByteArray + .fromBase64Url( + "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoia0lnZElRbWFBbXM1NlVOencwREg4dU96M0JERjJVSllhSlA2eklRWDFhOCIsIm9yaWdpbiI6Imh0dHBzOi8vZGV2LmQydXJweXB2cmhiMDV4LmFtcGxpZnlhcHAuY29tIiwiY3Jvc3NPcmlnaW4iOmZhbHNlfQ" + ) + .getBytes, + StandardCharsets.UTF_8, + ), + rpId = RelyingPartyIdentity + .builder() + .id("d2urpypvrhb05x.amplifyapp.com") + .name("") + .build(), + userId = UserIdentity + .builder() + .name("foo") + .displayName("Foo Bar") + .id( + ByteArray.fromBase64Url("AAAA") + ) + .build(), + ) } } 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 a54c733e0..0fe103e15 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 @@ -1108,9 +1108,9 @@ class RelyingPartyRegistrationSpec checkKnown("fido-u2f") checkKnown("none") checkKnown("packed") + checkKnown("tpm") checkUnknown("android-key") - checkUnknown("tpm") checkUnknown("FIDO-U2F") checkUnknown("Fido-U2F") @@ -2053,9 +2053,15 @@ class RelyingPartyRegistrationSpec } } - ignore("The tpm statement format is supported.") { + it("The tpm statement format is supported.") { + val testData = RegistrationTestData.Tpm.RealExample val steps = - finishRegistration(testData = RegistrationTestData.Tpm.PrivacyCa) + finishRegistration( + testData = testData, + origins = + Some(Set("https://dev.d2urpypvrhb05x.amplifyapp.com")), + credentialRepository = Helpers.CredentialRepository.empty, + ) val step: FinishRegistrationSteps#Step19 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next @@ -2883,7 +2889,7 @@ class RelyingPartyRegistrationSpec testUntrusted(RegistrationTestData.AndroidSafetynet.BasicAttestation) testUntrusted(RegistrationTestData.FidoU2f.BasicAttestation) testUntrusted(RegistrationTestData.NoneAttestation.Default) - testUntrusted(RegistrationTestData.Tpm.PrivacyCa) + testUntrusted(RegistrationTestData.Tpm.RealExample) } } } @@ -2974,17 +2980,24 @@ class RelyingPartyRegistrationSpec } it("accept TPM attestations but report they're untrusted.") { - val result = rp.finishRegistration( - FinishRegistrationOptions - .builder() - .request(request) - .response(RegistrationTestData.Tpm.PrivacyCa.response) - .build() - ) + val testData = RegistrationTestData.Tpm.RealExample + val result = rp.toBuilder + .identity(testData.rpId) + .origins(Set("https://dev.d2urpypvrhb05x.amplifyapp.com").asJava) + .build() + .finishRegistration( + FinishRegistrationOptions + .builder() + .request( + request.toBuilder.challenge(testData.responseChallenge).build() + ) + .response(testData.response) + .build() + ) result.isAttestationTrusted should be(false) result.getKeyId.getId should equal( - RegistrationTestData.Tpm.PrivacyCa.response.getId + RegistrationTestData.Tpm.RealExample.response.getId ) } @@ -3500,40 +3513,65 @@ class RelyingPartyRegistrationSpec } } - it("exposes getAttestationTrustPath() with the attestation trust path, if any.") { - val testData = RegistrationTestData.FidoU2f.BasicAttestation - val steps = finishRegistration( - testData = testData, - attestationTrustSource = Some( - trustSourceWith( - testData.attestationCertChain.last._1, - crls = Some( - testData.attestationCertChain - .map({ - case (cert, key) => - TestAuthenticator.buildCrl( - JcaX500NameUtil.getSubject(cert), - key, - "SHA256withECDSA", - TestAuthenticator.Defaults.certValidFrom, - TestAuthenticator.Defaults.certValidTo, - ) - }) - .toSet - ), - ) - ), - credentialRepository = Helpers.CredentialRepository.empty, - clock = Clock.fixed( - TestAuthenticator.Defaults.certValidFrom, - ZoneOffset.UTC, - ), - ) - val result = steps.run() - result.isAttestationTrusted should be(true) - result.getAttestationTrustPath.toScala.map(_.asScala) should equal( - Some(testData.attestationCertChain.init.map(_._1)) - ) + describe( + "exposes getAttestationTrustPath() with the attestation trust path" + ) { + it("for a fido-u2f attestation.") { + val testData = RegistrationTestData.FidoU2f.BasicAttestation + val steps = finishRegistration( + testData = testData, + attestationTrustSource = Some( + trustSourceWith( + testData.attestationCertChain.last._1, + crls = Some( + testData.attestationCertChain + .map({ + case (cert, key) => + TestAuthenticator.buildCrl( + JcaX500NameUtil.getSubject(cert), + key, + "SHA256withECDSA", + TestAuthenticator.Defaults.certValidFrom, + TestAuthenticator.Defaults.certValidTo, + ) + }) + .toSet + ), + ) + ), + credentialRepository = Helpers.CredentialRepository.empty, + clock = Clock.fixed( + TestAuthenticator.Defaults.certValidFrom, + ZoneOffset.UTC, + ), + ) + val result = steps.run() + result.isAttestationTrusted should be(true) + result.getAttestationTrustPath.toScala.map(_.asScala) should equal( + Some(testData.attestationCertChain.init.map(_._1)) + ) + } + + it("for a tpm attestation.") { + val testData = RegistrationTestData.Tpm.RealExample + val steps = finishRegistration( + testData = testData, + origins = Some(Set("https://dev.d2urpypvrhb05x.amplifyapp.com")), + attestationTrustSource = Some( + trustSourceWith( + CertificateParser.parsePem("MIIF9TCCA92gAwIBAgIQXbYwTgy/J79JuMhpUB5dyzANBgkqhkiG9w0BAQsFADCBjDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjE2MDQGA1UEAxMtTWljcm9zb2Z0IFRQTSBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAyMDE0MB4XDTE0MTIxMDIxMzExOVoXDTM5MTIxMDIxMzkyOFowgYwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xNjA0BgNVBAMTLU1pY3Jvc29mdCBUUE0gUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgMjAxNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAJ+n+bnKt/JHIRC/oI/xgkgsYdPzP0gpvduDA2GbRtth+L4WUyoZKGBw7uz5bjjP8Aql4YExyjR3EZQ4LqnZChMpoCofbeDR4MjCE1TGwWghGpS0mM3GtWD9XiME4rE2K0VW3pdN0CLzkYbvZbs2wQTFfE62yNQiDjyHFWAZ4BQH4eWa8wrDMUxIAneUCpU6zCwM+l6Qh4ohX063BHzXlTSTc1fDsiPaKuMMjWjK9vp5UHFPa+dMAWr6OljQZPFIg3aZ4cUfzS9y+n77Hs1NXPBn6E4Db679z4DThIXyoKeZTv1aaWOWl/exsDLGt2mTMTyykVV8uD1eRjYriFpmoRDwJKAEMOfaURarzp7hka9TOElGyD2gOV4Fscr2MxAYCywLmOLzA4VDSYLuKAhPSp7yawET30AvY1HRfMwBxetSqWP2+yZRNYJlHpor5QTuRDgzR+Zej+aWx6rWNYx43kLthozeVJ3QCsD5iEI/OZlmWn5WYf7O8LB/1A7scrYv44FD8ck3Z+hxXpkklAsjJMsHZa9mBqh+VR1AicX4uZG8m16x65ZU2uUpBa3rn8CTNmw17ZHOiuSWJtS9+PrZVA8ljgf4QgA1g6NPOEiLG2fn8Gm+r5Ak+9tqv72KDd2FPBJ7Xx4stYj/WjNPtEUhW4rcLK3ktLfcy6ea7Rocw5y5AgMBAAGjUTBPMAsGA1UdDwQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR6jArOL0hiF+KU0a5VwVLscXSkVjAQBgkrBgEEAYI3FQEEAwIBADANBgkqhkiG9w0BAQsFAAOCAgEAW4ioo1+J9VWC0UntSBXcXRm1ePTVamtsxVy/GpP4EmJd3Ub53JzNBfYdgfUL51CppS3ZY6BoagB+DqoA2GbSL+7sFGHBl5ka6FNelrwsH6VVw4xV/8klIjmqOyfatPYsz0sUdZev+reeiGpKVoXrK6BDnUU27/mgPtem5YKWvHB/soofUrLKzZV3WfGdx9zBr8V0xW6vO3CKaqkqU9y6EsQw34n7eJCbEVVQ8VdFd9iV1pmXwaBAfBwkviPTKEP9Cm+zbFIOLr3V3CL9hJj+gkTUuXWlJJ6wVXEG5i4rIbLAV59UrW4LonP+seqvWMJYUFxu/niF0R3fSGM+NU11DtBVkhRZt1u0kFhZqjDz1dWyfT/N7Hke3WsDqUFsBi+8SEw90rWx2aUkLvKo83oU4Mx4na+2I3l9F2a2VNGk4K7l3a00g51miPiq0Da0jqw30PaLluTMTGY5+RnZVh50JD6nk+Ea3wRkU8aiYFnpIxfKBZ72whmYYa/egj9IKeqpR0vuLebbU0fJBf880K1jWD3Z5SFyJXo057Mv0OPw5mttytE585ZIy5JsaRXlsOoWGRXE3kUT/MKR1UoAgR54c8Bsh+9Dq2wqIK9mRn15zvBDeyHG6+czurLopziOUeWokxZN1syrEdKlhFoPYavm6t+PzIcpdxZwHA+V3jLJPfI="), + enableRevocationChecking = false, + ) + ), + credentialRepository = Helpers.CredentialRepository.empty, + clock = Clock.fixed( + Instant.parse("2022-05-11T12:34:50Z"), + ZoneOffset.UTC, + ), + ) + val result = steps.run() + result.isAttestationTrusted should be(true) + } } it("exposes getAaguid() with the authenticator AAGUID.") { From 4a0b4298403a4456911f0f1e41cde73757acc862 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Fri, 8 Jul 2022 18:45:09 +0200 Subject: [PATCH 076/145] Set setPolicyQualifiersRejected to false for testing The default Java certificate path validator rejects certificates with critical policy extensions, and Windows Hello uses such an attestation cert. The solution for this is to set the `setPolicyQualifiersRejected(boolean)` setting and for the application to validate the policy tree. For now, we'll just set the parameter to `false` and add a validator setting later. --- .../main/java/com/yubico/webauthn/FinishRegistrationSteps.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishRegistrationSteps.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishRegistrationSteps.java index 3c6cda6b2..6934ed3c9 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishRegistrationSteps.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishRegistrationSteps.java @@ -535,6 +535,8 @@ public boolean attestationTrusted() { .collect(Collectors.toSet())); pathParams.setDate(Date.from(clock.instant())); pathParams.setRevocationEnabled(trustRoots.get().isEnableRevocationChecking()); + pathParams.setPolicyQualifiersRejected( + false); // TODO: Add parameter to configure policy qualifier processor trustRoots.get().getCertStore().ifPresent(pathParams::addCertStore); cpv.validate(certPath, pathParams); return true; From 47c98413e5e7a986de34afac8a034e4c57b76d7a Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 11 Jul 2022 22:33:17 +0200 Subject: [PATCH 077/145] Fix attestation type of TPM attestation --- .../webauthn/TpmAttestationStatementVerifier.java | 6 +----- .../com/yubico/webauthn/RegistrationTestData.scala | 4 ++++ .../webauthn/RelyingPartyRegistrationSpec.scala | 11 ++++++++++- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/TpmAttestationStatementVerifier.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/TpmAttestationStatementVerifier.java index b48386cfd..b2048ca24 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/TpmAttestationStatementVerifier.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/TpmAttestationStatementVerifier.java @@ -88,11 +88,7 @@ private static final class Attributes { @Override public AttestationType getAttestationType(AttestationObject attestation) { - if (attestation.getAttestationStatement().hasNonNull("x5c")) { - return AttestationType.BASIC; - } else { - return AttestationType.SELF_ATTESTATION; - } + return AttestationType.ATTESTATION_CA; } @Override diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RegistrationTestData.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RegistrationTestData.scala index cda19a39a..86ff851bc 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RegistrationTestData.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RegistrationTestData.scala @@ -614,6 +614,9 @@ object RegistrationTestData { ByteArray.fromBase64Url("AAAA") ) .build(), + attestationRootCertificate = Some( + CertificateParser.parsePem("MIIF9TCCA92gAwIBAgIQXbYwTgy/J79JuMhpUB5dyzANBgkqhkiG9w0BAQsFADCBjDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjE2MDQGA1UEAxMtTWljcm9zb2Z0IFRQTSBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAyMDE0MB4XDTE0MTIxMDIxMzExOVoXDTM5MTIxMDIxMzkyOFowgYwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xNjA0BgNVBAMTLU1pY3Jvc29mdCBUUE0gUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgMjAxNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAJ+n+bnKt/JHIRC/oI/xgkgsYdPzP0gpvduDA2GbRtth+L4WUyoZKGBw7uz5bjjP8Aql4YExyjR3EZQ4LqnZChMpoCofbeDR4MjCE1TGwWghGpS0mM3GtWD9XiME4rE2K0VW3pdN0CLzkYbvZbs2wQTFfE62yNQiDjyHFWAZ4BQH4eWa8wrDMUxIAneUCpU6zCwM+l6Qh4ohX063BHzXlTSTc1fDsiPaKuMMjWjK9vp5UHFPa+dMAWr6OljQZPFIg3aZ4cUfzS9y+n77Hs1NXPBn6E4Db679z4DThIXyoKeZTv1aaWOWl/exsDLGt2mTMTyykVV8uD1eRjYriFpmoRDwJKAEMOfaURarzp7hka9TOElGyD2gOV4Fscr2MxAYCywLmOLzA4VDSYLuKAhPSp7yawET30AvY1HRfMwBxetSqWP2+yZRNYJlHpor5QTuRDgzR+Zej+aWx6rWNYx43kLthozeVJ3QCsD5iEI/OZlmWn5WYf7O8LB/1A7scrYv44FD8ck3Z+hxXpkklAsjJMsHZa9mBqh+VR1AicX4uZG8m16x65ZU2uUpBa3rn8CTNmw17ZHOiuSWJtS9+PrZVA8ljgf4QgA1g6NPOEiLG2fn8Gm+r5Ak+9tqv72KDd2FPBJ7Xx4stYj/WjNPtEUhW4rcLK3ktLfcy6ea7Rocw5y5AgMBAAGjUTBPMAsGA1UdDwQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR6jArOL0hiF+KU0a5VwVLscXSkVjAQBgkrBgEEAYI3FQEEAwIBADANBgkqhkiG9w0BAQsFAAOCAgEAW4ioo1+J9VWC0UntSBXcXRm1ePTVamtsxVy/GpP4EmJd3Ub53JzNBfYdgfUL51CppS3ZY6BoagB+DqoA2GbSL+7sFGHBl5ka6FNelrwsH6VVw4xV/8klIjmqOyfatPYsz0sUdZev+reeiGpKVoXrK6BDnUU27/mgPtem5YKWvHB/soofUrLKzZV3WfGdx9zBr8V0xW6vO3CKaqkqU9y6EsQw34n7eJCbEVVQ8VdFd9iV1pmXwaBAfBwkviPTKEP9Cm+zbFIOLr3V3CL9hJj+gkTUuXWlJJ6wVXEG5i4rIbLAV59UrW4LonP+seqvWMJYUFxu/niF0R3fSGM+NU11DtBVkhRZt1u0kFhZqjDz1dWyfT/N7Hke3WsDqUFsBi+8SEw90rWx2aUkLvKo83oU4Mx4na+2I3l9F2a2VNGk4K7l3a00g51miPiq0Da0jqw30PaLluTMTGY5+RnZVh50JD6nk+Ea3wRkU8aiYFnpIxfKBZ72whmYYa/egj9IKeqpR0vuLebbU0fJBf880K1jWD3Z5SFyJXo057Mv0OPw5mttytE585ZIy5JsaRXlsOoWGRXE3kUT/MKR1UoAgR54c8Bsh+9Dq2wqIK9mRn15zvBDeyHG6+czurLopziOUeWokxZN1syrEdKlhFoPYavm6t+PzIcpdxZwHA+V3jLJPfI=") + ), ) } } @@ -623,6 +626,7 @@ case class RegistrationTestData( assertion: Option[AssertionTestData] = None, attestationObject: ByteArray, attestationCertChain: List[(X509Certificate, PrivateKey)] = Nil, + attestationRootCertificate: Option[X509Certificate] = None, clientDataJson: String, authenticatorSelection: Option[AuthenticatorSelectionCriteria] = None, clientExtensionResults: ClientRegistrationExtensionOutputs = 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 0fe103e15..141b927ed 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 @@ -2061,12 +2061,21 @@ class RelyingPartyRegistrationSpec origins = Some(Set("https://dev.d2urpypvrhb05x.amplifyapp.com")), credentialRepository = Helpers.CredentialRepository.empty, + attestationTrustSource = Some( + trustSourceWith( + testData.attestationRootCertificate.get, + enableRevocationChecking = false, + ) + ), ) val step: FinishRegistrationSteps#Step19 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next step.validations shouldBe a[Success[_]] step.tryNext shouldBe a[Success[_]] + step.run.getAttestationType should be( + AttestationType.ATTESTATION_CA + ) } ignore("The android-key statement format is supported.") { @@ -3559,7 +3568,7 @@ class RelyingPartyRegistrationSpec origins = Some(Set("https://dev.d2urpypvrhb05x.amplifyapp.com")), attestationTrustSource = Some( trustSourceWith( - CertificateParser.parsePem("MIIF9TCCA92gAwIBAgIQXbYwTgy/J79JuMhpUB5dyzANBgkqhkiG9w0BAQsFADCBjDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjE2MDQGA1UEAxMtTWljcm9zb2Z0IFRQTSBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAyMDE0MB4XDTE0MTIxMDIxMzExOVoXDTM5MTIxMDIxMzkyOFowgYwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xNjA0BgNVBAMTLU1pY3Jvc29mdCBUUE0gUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgMjAxNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAJ+n+bnKt/JHIRC/oI/xgkgsYdPzP0gpvduDA2GbRtth+L4WUyoZKGBw7uz5bjjP8Aql4YExyjR3EZQ4LqnZChMpoCofbeDR4MjCE1TGwWghGpS0mM3GtWD9XiME4rE2K0VW3pdN0CLzkYbvZbs2wQTFfE62yNQiDjyHFWAZ4BQH4eWa8wrDMUxIAneUCpU6zCwM+l6Qh4ohX063BHzXlTSTc1fDsiPaKuMMjWjK9vp5UHFPa+dMAWr6OljQZPFIg3aZ4cUfzS9y+n77Hs1NXPBn6E4Db679z4DThIXyoKeZTv1aaWOWl/exsDLGt2mTMTyykVV8uD1eRjYriFpmoRDwJKAEMOfaURarzp7hka9TOElGyD2gOV4Fscr2MxAYCywLmOLzA4VDSYLuKAhPSp7yawET30AvY1HRfMwBxetSqWP2+yZRNYJlHpor5QTuRDgzR+Zej+aWx6rWNYx43kLthozeVJ3QCsD5iEI/OZlmWn5WYf7O8LB/1A7scrYv44FD8ck3Z+hxXpkklAsjJMsHZa9mBqh+VR1AicX4uZG8m16x65ZU2uUpBa3rn8CTNmw17ZHOiuSWJtS9+PrZVA8ljgf4QgA1g6NPOEiLG2fn8Gm+r5Ak+9tqv72KDd2FPBJ7Xx4stYj/WjNPtEUhW4rcLK3ktLfcy6ea7Rocw5y5AgMBAAGjUTBPMAsGA1UdDwQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR6jArOL0hiF+KU0a5VwVLscXSkVjAQBgkrBgEEAYI3FQEEAwIBADANBgkqhkiG9w0BAQsFAAOCAgEAW4ioo1+J9VWC0UntSBXcXRm1ePTVamtsxVy/GpP4EmJd3Ub53JzNBfYdgfUL51CppS3ZY6BoagB+DqoA2GbSL+7sFGHBl5ka6FNelrwsH6VVw4xV/8klIjmqOyfatPYsz0sUdZev+reeiGpKVoXrK6BDnUU27/mgPtem5YKWvHB/soofUrLKzZV3WfGdx9zBr8V0xW6vO3CKaqkqU9y6EsQw34n7eJCbEVVQ8VdFd9iV1pmXwaBAfBwkviPTKEP9Cm+zbFIOLr3V3CL9hJj+gkTUuXWlJJ6wVXEG5i4rIbLAV59UrW4LonP+seqvWMJYUFxu/niF0R3fSGM+NU11DtBVkhRZt1u0kFhZqjDz1dWyfT/N7Hke3WsDqUFsBi+8SEw90rWx2aUkLvKo83oU4Mx4na+2I3l9F2a2VNGk4K7l3a00g51miPiq0Da0jqw30PaLluTMTGY5+RnZVh50JD6nk+Ea3wRkU8aiYFnpIxfKBZ72whmYYa/egj9IKeqpR0vuLebbU0fJBf880K1jWD3Z5SFyJXo057Mv0OPw5mttytE585ZIy5JsaRXlsOoWGRXE3kUT/MKR1UoAgR54c8Bsh+9Dq2wqIK9mRn15zvBDeyHG6+czurLopziOUeWokxZN1syrEdKlhFoPYavm6t+PzIcpdxZwHA+V3jLJPfI="), + testData.attestationRootCertificate.get, enableRevocationChecking = false, ) ), From e573f14cdd35ecd602df29fd8db299af40c7526d Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Thu, 18 Aug 2022 14:52:14 +0200 Subject: [PATCH 078/145] Add support for ES384 and ES512 algorithms --- .../com/yubico/webauthn/RelyingParty.java | 8 ++- .../TpmAttestationStatementVerifier.java | 16 +++++ .../com/yubico/webauthn/WebAuthnCodecs.java | 60 +++++++++++++++---- .../data/COSEAlgorithmIdentifier.java | 2 + .../PublicKeyCredentialCreationOptions.java | 10 ++++ .../data/PublicKeyCredentialParameters.java | 14 +++++ .../webauthn/RegistrationTestData.scala | 2 + .../yubico/webauthn/TestAuthenticator.scala | 4 +- 8 files changed, 103 insertions(+), 13 deletions(-) diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/RelyingParty.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/RelyingParty.java index 58ce3de51..b2378f523 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/RelyingParty.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/RelyingParty.java @@ -210,11 +210,13 @@ public class RelyingParty { *

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

    The default is the following list: + *

    The default is the following list, in order: * *

      *
    1. {@link com.yubico.webauthn.data.PublicKeyCredentialParameters#ES256 ES256} *
    2. {@link com.yubico.webauthn.data.PublicKeyCredentialParameters#EdDSA EdDSA} + *
    3. {@link com.yubico.webauthn.data.PublicKeyCredentialParameters#ES256 ES384} + *
    4. {@link com.yubico.webauthn.data.PublicKeyCredentialParameters#ES256 ES512} *
    5. {@link com.yubico.webauthn.data.PublicKeyCredentialParameters#RS256 RS256} *
    * @@ -228,6 +230,8 @@ public class RelyingParty { Arrays.asList( PublicKeyCredentialParameters.ES256, PublicKeyCredentialParameters.EdDSA, + PublicKeyCredentialParameters.ES384, + PublicKeyCredentialParameters.ES512, PublicKeyCredentialParameters.RS256)); /** @@ -417,6 +421,8 @@ private static List filterAvailableAlgorithms( break; case ES256: + case ES384: + case ES512: KeyFactory.getInstance("EC"); break; diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/TpmAttestationStatementVerifier.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/TpmAttestationStatementVerifier.java index b2048ca24..6a734b29a 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/TpmAttestationStatementVerifier.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/TpmAttestationStatementVerifier.java @@ -216,6 +216,14 @@ private void validateCertInfo( expectedExtraData = Crypto.sha256(attToBeSigned); break; + case ES384: + expectedExtraData = Crypto.sha384(attToBeSigned); + break; + + case ES512: + expectedExtraData = Crypto.sha512(attToBeSigned); + break; + case RS1: try { expectedExtraData = Crypto.sha1(attToBeSigned); @@ -312,6 +320,14 @@ private void verifyPublicKeysMatch(AttestationObject attestationObject, TpmtPubl tpmAlgId = COSEAlgorithmIdentifier.ES256; break; + case TpmEccCurve.NIST_P384: + tpmAlgId = COSEAlgorithmIdentifier.ES384; + break; + + case TpmEccCurve.NIST_P521: + tpmAlgId = COSEAlgorithmIdentifier.ES512; + break; + default: throw new UnsupportedOperationException( "Unsupported elliptic curve: " + params.curve_id); 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 34d961bae..b617dbf89 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 @@ -50,10 +50,14 @@ final class WebAuthnCodecs { new ByteArray(new byte[] {0x30, 0x05, 0x06, 0x03, 0x2B, 0x65, 0x70}); static ByteArray ecPublicKeyToRaw(ECPublicKey key) { + + final int fieldSizeBytes = + Math.toIntExact( + Math.round(Math.ceil(key.getParams().getCurve().getField().getFieldSize() / 8.0))); byte[] x = key.getW().getAffineX().toByteArray(); byte[] y = key.getW().getAffineY().toByteArray(); - byte[] xPadding = new byte[Math.max(0, 32 - x.length)]; - byte[] yPadding = new byte[Math.max(0, 32 - y.length)]; + byte[] xPadding = new byte[Math.max(0, fieldSizeBytes - x.length)]; + byte[] yPadding = new byte[Math.max(0, fieldSizeBytes - y.length)]; Arrays.fill(xPadding, (byte) 0); Arrays.fill(yPadding, (byte) 0); @@ -61,28 +65,58 @@ static ByteArray ecPublicKeyToRaw(ECPublicKey key) { return new ByteArray( Bytes.concat( new byte[] {0x04}, - Bytes.concat(xPadding, Arrays.copyOfRange(x, Math.max(0, x.length - 32), x.length)), - Bytes.concat(yPadding, Arrays.copyOfRange(y, Math.max(0, y.length - 32), y.length)))); + Bytes.concat( + xPadding, Arrays.copyOfRange(x, Math.max(0, x.length - fieldSizeBytes), x.length)), + Bytes.concat( + yPadding, + Arrays.copyOfRange(y, Math.max(0, y.length - fieldSizeBytes), y.length)))); } static ByteArray rawEcKeyToCose(ByteArray key) { final byte[] keyBytes = key.getBytes(); - if (!(keyBytes.length == 64 || (keyBytes.length == 65 && keyBytes[0] == 0x04))) { + final int len = keyBytes.length; + final int lenSub1 = keyBytes.length - 1; + if (!(len == 64 + || len == 96 + || len == 132 + || (keyBytes[0] == 0x04 && (lenSub1 == 64 || lenSub1 == 96 || lenSub1 == 132)))) { 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", + "Raw key must be 64, 96 or 132 bytes long, or start with 0x04 and be 65, 97 or 133 bytes long; was %d bytes starting with %02x", keyBytes.length, keyBytes[0])); } - final int start = (keyBytes.length == 64) ? 0 : 1; + final int start = (len == 64 || len == 96 || len == 132) ? 0 : 1; + final int coordinateLength = (len - start) / 2; final Map coseKey = new HashMap<>(); coseKey.put(1L, 2L); // Key type: EC - coseKey.put(3L, COSEAlgorithmIdentifier.ES256.getId()); - coseKey.put(-1L, 1L); // Curve: P-256 + final COSEAlgorithmIdentifier coseAlg; + final int coseCrv; + switch (len - start) { + case 64: + coseAlg = COSEAlgorithmIdentifier.ES256; + coseCrv = 1; + break; + case 96: + coseAlg = COSEAlgorithmIdentifier.ES384; + coseCrv = 2; + break; + case 132: + coseAlg = COSEAlgorithmIdentifier.ES512; + coseCrv = 3; + break; + default: + throw new RuntimeException( + "Failed to determine COSE EC algorithm. This should not be possible, please file a bug report."); + } + coseKey.put(3L, coseAlg.getId()); + coseKey.put(-1L, coseCrv); - coseKey.put(-2L, Arrays.copyOfRange(keyBytes, start, start + 32)); // x - coseKey.put(-3L, Arrays.copyOfRange(keyBytes, start + 32, start + 64)); // y + coseKey.put(-2L, Arrays.copyOfRange(keyBytes, start, start + coordinateLength)); // x + coseKey.put( + -3L, + Arrays.copyOfRange(keyBytes, start + coordinateLength, start + 2 * coordinateLength)); // y return new ByteArray(CBORObject.FromObject(coseKey).EncodeToBytes()); } @@ -152,6 +186,10 @@ static String getJavaAlgorithmName(COSEAlgorithmIdentifier alg) { return "EDDSA"; case ES256: return "SHA256withECDSA"; + case ES384: + return "SHA384withECDSA"; + case ES512: + return "SHA512withECDSA"; case RS256: return "SHA256withRSA"; case RS1: diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/COSEAlgorithmIdentifier.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/COSEAlgorithmIdentifier.java index 1ba31d5ca..563ba5235 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/COSEAlgorithmIdentifier.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/COSEAlgorithmIdentifier.java @@ -42,6 +42,8 @@ public enum COSEAlgorithmIdentifier { EdDSA(-8), ES256(-7), + ES384(-35), + ES512(-36), RS256(-257), RS1(-65535); diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialCreationOptions.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialCreationOptions.java index 7da6ea2c4..40baa5af2 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialCreationOptions.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialCreationOptions.java @@ -376,6 +376,8 @@ private static List filterAvailableAlgorithms( break; case ES256: + case ES384: + case ES512: KeyFactory.getInstance("EC"); break; @@ -405,6 +407,14 @@ private static List filterAvailableAlgorithms( Signature.getInstance("SHA256withECDSA"); break; + case ES384: + Signature.getInstance("SHA384withECDSA"); + break; + + case ES512: + Signature.getInstance("SHA512withECDSA"); + break; + case RS256: Signature.getInstance("SHA256withRSA"); break; diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialParameters.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialParameters.java index fed7c04b6..5e58aa3ed 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialParameters.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialParameters.java @@ -72,6 +72,20 @@ private PublicKeyCredentialParameters( public static final PublicKeyCredentialParameters ES256 = builder().alg(COSEAlgorithmIdentifier.ES256).build(); + /** + * Algorithm {@link COSEAlgorithmIdentifier#ES384} and type {@link + * PublicKeyCredentialType#PUBLIC_KEY}. + */ + public static final PublicKeyCredentialParameters ES384 = + builder().alg(COSEAlgorithmIdentifier.ES384).build(); + + /** + * Algorithm {@link COSEAlgorithmIdentifier#ES512} and type {@link + * PublicKeyCredentialType#PUBLIC_KEY}. + */ + public static final PublicKeyCredentialParameters ES512 = + builder().alg(COSEAlgorithmIdentifier.ES512).build(); + /** * Algorithm {@link COSEAlgorithmIdentifier#RS1} and type {@link * PublicKeyCredentialType#PUBLIC_KEY}. diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RegistrationTestData.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RegistrationTestData.scala index 86ff851bc..382e2fa55 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RegistrationTestData.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RegistrationTestData.scala @@ -779,6 +779,8 @@ case class RegistrationTestData( List( PublicKeyCredentialParameters.ES256, PublicKeyCredentialParameters.EdDSA, + PublicKeyCredentialParameters.ES384, + PublicKeyCredentialParameters.ES512, PublicKeyCredentialParameters.RS256, ).asJava ) diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/TestAuthenticator.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/TestAuthenticator.scala index 00314e378..870ed148a 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/TestAuthenticator.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/TestAuthenticator.scala @@ -874,7 +874,9 @@ object TestAuthenticator { def generateKeypair(algorithm: COSEAlgorithmIdentifier): KeyPair = algorithm match { case COSEAlgorithmIdentifier.EdDSA => generateEddsaKeypair() - case COSEAlgorithmIdentifier.ES256 => generateEcKeypair() + case COSEAlgorithmIdentifier.ES256 => generateEcKeypair("secp256r1") + case COSEAlgorithmIdentifier.ES384 => generateEcKeypair("secp384r1") + case COSEAlgorithmIdentifier.ES512 => generateEcKeypair("secp521r1") case COSEAlgorithmIdentifier.RS256 => generateRsaKeypair() case COSEAlgorithmIdentifier.RS1 => generateRsaKeypair() } From 84746447188b64bb06245aacb04b2cb86a8cc14e Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 11 Jul 2022 22:26:22 +0200 Subject: [PATCH 079/145] Add generated TPM attestation test cases --- NEWS | 5 + .../yubico/fido/metadata/FidoMds3Spec.scala | 2 +- .../TpmAttestationStatementVerifier.java | 14 +- .../webauthn/RegistrationTestData.scala | 261 ++++- .../RelyingPartyRegistrationSpec.scala | 893 +++++++++++++++++- .../yubico/webauthn/TestAuthenticator.scala | 294 +++++- .../yubico/webauthn/WebAuthnTestCodecs.scala | 6 + .../com/yubico/webauthn/data/Generators.scala | 17 +- 8 files changed, 1415 insertions(+), 77 deletions(-) diff --git a/NEWS b/NEWS index cde4dc621..bf5a4d6cd 100644 --- a/NEWS +++ b/NEWS @@ -5,6 +5,11 @@ Changes: * Log messages on attestation certificate path validation failure now include the attestation object. +New features: + +* Added support for the `"tpm"` attestation statement format. +* Added support for ES384 and ES512 signature algorithms. + Fixes: * Fixed various typos and mistakes in JavaDocs. diff --git a/webauthn-server-attestation/src/test/scala/com/yubico/fido/metadata/FidoMds3Spec.scala b/webauthn-server-attestation/src/test/scala/com/yubico/fido/metadata/FidoMds3Spec.scala index 9b8417814..adb065977 100644 --- a/webauthn-server-attestation/src/test/scala/com/yubico/fido/metadata/FidoMds3Spec.scala +++ b/webauthn-server-attestation/src/test/scala/com/yubico/fido/metadata/FidoMds3Spec.scala @@ -312,7 +312,7 @@ class FidoMds3Spec extends FunSpec with Matchers { attestationMaker = AttestationMaker.packed( AttestationSigner.ca( COSEAlgorithmIdentifier.ES256, - aaguid = aaguidA.asBytes, + aaguid = Some(aaguidA.asBytes), validFrom = CertValidFrom, validTo = CertValidTo, ) diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/TpmAttestationStatementVerifier.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/TpmAttestationStatementVerifier.java index 6a734b29a..50ef27d10 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/TpmAttestationStatementVerifier.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/TpmAttestationStatementVerifier.java @@ -59,10 +59,10 @@ final class TpmAttestationStatementVerifier implements AttestationStatementVerifier, X5cAttestationStatementVerifier { private static final String TPM_VER = "2.0"; - private static final ByteArray TPM_GENERATED_VALUE = ByteArray.fromBase64("/1RDRw=="); - private static final ByteArray TPM_ST_ATTEST_CERTIFY = ByteArray.fromBase64("gBc="); + static final ByteArray TPM_GENERATED_VALUE = ByteArray.fromBase64("/1RDRw=="); + static final ByteArray TPM_ST_ATTEST_CERTIFY = ByteArray.fromBase64("gBc="); - private static final int TPM_ALG_NULL = 0x0010; + static final int TPM_ALG_NULL = 0x0010; private static final String OID_TCG_AT_TPM_MANUFACTURER = "2.23.133.2.1"; private static final String OID_TCG_AT_TPM_MODEL = "2.23.133.2.2"; @@ -74,7 +74,7 @@ final class TpmAttestationStatementVerifier *

    see section 8.3 of * https://www.trustedcomputinggroup.org/wp-content/uploads/TPM-Rev-2.0-Part-2-Structures-01.38.pdf */ - private static final class Attributes { + static final class Attributes { public static final int SIGN_ENCRYPT = 1 << 18; private static final int SHALL_BE_ZERO = @@ -359,7 +359,7 @@ private void verifyPublicKeysMatch(AttestationObject attestationObject, TpmtPubl } } - private static final class TpmAlgAsym { + static final class TpmAlgAsym { public static final int RSA = 0x0001; public static final int ECC = 0x0023; } @@ -463,7 +463,7 @@ public ByteArray name() { } } - private static class TpmAlgHash { + static class TpmAlgHash { public static final int SHA1 = 0x0004; public static final int SHA256 = 0x000B; public static final int SHA384 = 0x000C; @@ -528,7 +528,7 @@ public void verifyX5cRequirements(X509Certificate cert, ByteArray aaguid) }); } - private static final class TpmRsaScheme { + static final class TpmRsaScheme { public static final int RSASSA = 0x0014; } diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RegistrationTestData.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RegistrationTestData.scala index 382e2fa55..89c67f4f1 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RegistrationTestData.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RegistrationTestData.scala @@ -48,7 +48,15 @@ import com.yubico.webauthn.data.PublicKeyCredentialParameters import com.yubico.webauthn.data.RegistrationExtensionInputs import com.yubico.webauthn.data.RelyingPartyIdentity import com.yubico.webauthn.data.UserIdentity +import org.bouncycastle.asn1.ASN1ObjectIdentifier +import org.bouncycastle.asn1.DERSequence +import org.bouncycastle.asn1.DERUTF8String +import org.bouncycastle.asn1.x500.AttributeTypeAndValue +import org.bouncycastle.asn1.x500.RDN import org.bouncycastle.asn1.x500.X500Name +import org.bouncycastle.asn1.x509.Extension +import org.bouncycastle.asn1.x509.GeneralName +import org.bouncycastle.asn1.x509.GeneralNamesBuilder import java.nio.charset.StandardCharsets import java.security.KeyFactory @@ -136,6 +144,11 @@ object RegistrationTestDataGenerator extends App { td.Packed.BasicAttestationWithWrongAaguidExtension, td.Packed.SelfAttestation, td.Packed.SelfAttestationRs1, + td.Tpm.ValidEs256, + td.Tpm.ValidEs384, + td.Tpm.ValidEs512, + td.Tpm.ValidRs256, + td.Tpm.ValidRs1, ).zipWithIndex } { testData.regenerateFull() match { @@ -165,6 +178,11 @@ object RegistrationTestData { Packed.BasicAttestationRsa, Packed.BasicAttestationRsaReal, Packed.SelfAttestation, + Tpm.RealExample, + Tpm.ValidEs256, + Tpm.ValidEs384, + Tpm.ValidEs512, + Tpm.ValidRs256, ) object AndroidKey { @@ -587,6 +605,47 @@ object RegistrationTestData { } object Tpm { + + private val tpmCertExtensions = List( + ( + Extension.subjectAlternativeName.getId, + true, + new GeneralNamesBuilder() + .addName( + new GeneralName( + new X500Name( + Array( + new RDN( + Array( + new AttributeTypeAndValue( + new ASN1ObjectIdentifier("2.23.133.2.1"), + new DERUTF8String("id:00000000"), + ), // tcg-at-tpmManufacturer + new AttributeTypeAndValue( + new ASN1ObjectIdentifier("2.23.133.2.2"), + new DERUTF8String( + "TEST_Yubico_java-webauthn-server" + ), + ), // tcg-at-tpmModel + new AttributeTypeAndValue( + new ASN1ObjectIdentifier("2.23.133.2.3"), + new DERUTF8String("id:00000000"), + ), // tcg-at-tpmVersion + ) + ) + ) + ) + ) + ) + .build(), + ), + ( + Extension.extendedKeyUsage.getId, + true, + new DERSequence(new ASN1ObjectIdentifier("2.23.133.8.3")), + ), + ) + val RealExample: RegistrationTestData = new RegistrationTestData( alg = COSEAlgorithmIdentifier.RS256, @@ -618,7 +677,198 @@ object RegistrationTestData { CertificateParser.parsePem("MIIF9TCCA92gAwIBAgIQXbYwTgy/J79JuMhpUB5dyzANBgkqhkiG9w0BAQsFADCBjDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjE2MDQGA1UEAxMtTWljcm9zb2Z0IFRQTSBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAyMDE0MB4XDTE0MTIxMDIxMzExOVoXDTM5MTIxMDIxMzkyOFowgYwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xNjA0BgNVBAMTLU1pY3Jvc29mdCBUUE0gUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgMjAxNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAJ+n+bnKt/JHIRC/oI/xgkgsYdPzP0gpvduDA2GbRtth+L4WUyoZKGBw7uz5bjjP8Aql4YExyjR3EZQ4LqnZChMpoCofbeDR4MjCE1TGwWghGpS0mM3GtWD9XiME4rE2K0VW3pdN0CLzkYbvZbs2wQTFfE62yNQiDjyHFWAZ4BQH4eWa8wrDMUxIAneUCpU6zCwM+l6Qh4ohX063BHzXlTSTc1fDsiPaKuMMjWjK9vp5UHFPa+dMAWr6OljQZPFIg3aZ4cUfzS9y+n77Hs1NXPBn6E4Db679z4DThIXyoKeZTv1aaWOWl/exsDLGt2mTMTyykVV8uD1eRjYriFpmoRDwJKAEMOfaURarzp7hka9TOElGyD2gOV4Fscr2MxAYCywLmOLzA4VDSYLuKAhPSp7yawET30AvY1HRfMwBxetSqWP2+yZRNYJlHpor5QTuRDgzR+Zej+aWx6rWNYx43kLthozeVJ3QCsD5iEI/OZlmWn5WYf7O8LB/1A7scrYv44FD8ck3Z+hxXpkklAsjJMsHZa9mBqh+VR1AicX4uZG8m16x65ZU2uUpBa3rn8CTNmw17ZHOiuSWJtS9+PrZVA8ljgf4QgA1g6NPOEiLG2fn8Gm+r5Ak+9tqv72KDd2FPBJ7Xx4stYj/WjNPtEUhW4rcLK3ktLfcy6ea7Rocw5y5AgMBAAGjUTBPMAsGA1UdDwQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR6jArOL0hiF+KU0a5VwVLscXSkVjAQBgkrBgEEAYI3FQEEAwIBADANBgkqhkiG9w0BAQsFAAOCAgEAW4ioo1+J9VWC0UntSBXcXRm1ePTVamtsxVy/GpP4EmJd3Ub53JzNBfYdgfUL51CppS3ZY6BoagB+DqoA2GbSL+7sFGHBl5ka6FNelrwsH6VVw4xV/8klIjmqOyfatPYsz0sUdZev+reeiGpKVoXrK6BDnUU27/mgPtem5YKWvHB/soofUrLKzZV3WfGdx9zBr8V0xW6vO3CKaqkqU9y6EsQw34n7eJCbEVVQ8VdFd9iV1pmXwaBAfBwkviPTKEP9Cm+zbFIOLr3V3CL9hJj+gkTUuXWlJJ6wVXEG5i4rIbLAV59UrW4LonP+seqvWMJYUFxu/niF0R3fSGM+NU11DtBVkhRZt1u0kFhZqjDz1dWyfT/N7Hke3WsDqUFsBi+8SEw90rWx2aUkLvKo83oU4Mx4na+2I3l9F2a2VNGk4K7l3a00g51miPiq0Da0jqw30PaLluTMTGY5+RnZVh50JD6nk+Ea3wRkU8aiYFnpIxfKBZ72whmYYa/egj9IKeqpR0vuLebbU0fJBf880K1jWD3Z5SFyJXo057Mv0OPw5mttytE585ZIy5JsaRXlsOoWGRXE3kUT/MKR1UoAgR54c8Bsh+9Dq2wqIK9mRn15zvBDeyHG6+czurLopziOUeWokxZN1syrEdKlhFoPYavm6t+PzIcpdxZwHA+V3jLJPfI=") ), ) + + val ValidEs256: RegistrationTestData = new RegistrationTestData( + alg = COSEAlgorithmIdentifier.ES256, + attestationObject = + ByteArray.fromHex("bf68617574684461746158a449960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97634100000539000102030405060708090a0b0c0d0e0f00204ba50777bcdb0749e472452d00783d09e3946fa23f2cb6036b851c9cfa1f4e54a50102032620012158203b20d5b76242f25632daf1eee349c2f5ea2aefbcedd5a422e645ce95d891ec71225820ed953dc6b66c59bc027c82e19f45d5af2969596632ce28fba9bc02072f6fcdbe63666d746374706d6761747453746d74bf63616c67266373696758473045022100bbce3f5c82af4e2e4bce9fe75bea7a153cc884765762da7ba7fdc354c00841110220164dfc8eb26e85b0f9c5015b921653be62853ebd701c9f51b200e15ae4a19065637835638259020630820202308201a7a003020102020212b2300a06082a8648ce3d040302306a3126302406035504030c1d59756269636f20576562417574686e20756e6974207465737473204341310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b3009060355040613025345301e170d3138303930363137343230305a170d3138303931333137343230305a30003059301306072a8648ce3d020106082a8648ce3d03010703420004fee4104d866caf972a45fe3c9e50458980bde7c443777237b0e318195a1e20cc128a05114d5f66da1873b2d648aae3f0b81c586300a25fee6439dec4531d5a72a381a63081a33021060b2b0601040182e51c01010404120410000102030405060708090a0b0c0d0e0f30690603551d110101ff045f305da45b305931573014060567810502010c0b69643a30303030303030303014060567810502030c0b69643a30303030303030303029060567810502020c20544553545f59756269636f5f6a6176612d776562617574686e2d73657276657230130603551d250101ff0409300706056781050803300a06082a8648ce3d0403020349003046022100937b55680c54f4e310b877d7d4920c0ab5cc595d09051c296c9f3d340fc1f552022100d91f3d5e31148826e1136e176189a905467fe99c6dce329e39b332170fedcd1b5901db308201d73082017da00302010202021a1a300a06082a8648ce3d040302306a3126302406035504030c1d59756269636f20576562417574686e20756e6974207465737473204341310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b3009060355040613025345301e170d3138303930363137343230305a170d3138303931333137343230305a306a3126302406035504030c1d59756269636f20576562417574686e20756e6974207465737473204341310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b30090603550406130253453059301306072a8648ce3d020106082a8648ce3d030107034200049d957c950587a67f7560d3216a47f6c52c40f01c7b2de5d8250809d73aab814eccd0d77e4288189d905980a1072191925f82649965f2e5e2c5b1803fb5347328a3133011300f0603551d130101ff040530030101ff300a06082a8648ce3d0403020348003045022100ac1ff60cbc9e80c46ae62b51a47cdbbedce65d0aadc11d320543347d3099aab6022024914461e3b580eb737026ffb32969ce0463cf75edda2b96bc6645da8ab843c86863657274496e666f5869ff544347801700000020c18552e578be4f9c30427a963c5fca784031f97126de0c7e560ffd65b20bce65000000000000000011111111222222223300000000000000000022000bb418e5cf50f689f828bdb3fee73d6e8d478674c711c2971f588fb38dbaad537700006376657263322e30677075624172656158570023000b000400000000001000100003001000203b20d5b76242f25632daf1eee349c2f5ea2aefbcedd5a422e645ce95d891ec71002100ed953dc6b66c59bc027c82e19f45d5af2969596632ce28fba9bc02072f6fcdbeffff"), + clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","type":"webauthn.create","tokenBinding":{"status":"supported"}}""", + privateKey = Some( + ByteArray.fromHex("308193020100301306072a8648ce3d020106082a8648ce3d0301070479307702010104207660425e23b16c9b8618674c1c6279d67dc01f038f12a954a8d0b0b41d761b3da00a06082a8648ce3d030107a144034200043b20d5b76242f25632daf1eee349c2f5ea2aefbcedd5a422e645ce95d891ec71ed953dc6b66c59bc027c82e19f45d5af2969596632ce28fba9bc02072f6fcdbe") + ), + attestationCertChain = List( + RegistrationTestDataGenerator.importAttestationCa( + "MIICAjCCAaegAwIBAgICErIwCgYIKoZIzj0EAwIwajEmMCQGA1UEAwwdWXViaWNvIFdlYkF1dGhuIHVuaXQgdGVzdHMgQ0ExDzANBgNVBAoMBll1YmljbzEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjELMAkGA1UEBhMCU0UwHhcNMTgwOTA2MTc0MjAwWhcNMTgwOTEzMTc0MjAwWjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE/uQQTYZsr5cqRf48nlBFiYC958RDd3I3sOMYGVoeIMwSigURTV9m2hhzstZIquPwuBxYYwCiX+5kOd7EUx1acqOBpjCBozAhBgsrBgEEAYLlHAEBBAQSBBAAAQIDBAUGBwgJCgsMDQ4PMGkGA1UdEQEB/wRfMF2kWzBZMVcwFAYFZ4EFAgEMC2lkOjAwMDAwMDAwMBQGBWeBBQIDDAtpZDowMDAwMDAwMDApBgVngQUCAgwgVEVTVF9ZdWJpY29famF2YS13ZWJhdXRobi1zZXJ2ZXIwEwYDVR0lAQH/BAkwBwYFZ4EFCAMwCgYIKoZIzj0EAwIDSQAwRgIhAJN7VWgMVPTjELh319SSDAq1zFldCQUcKWyfPTQPwfVSAiEA2R89XjEUiCbhE24XYYmpBUZ/6ZxtzjKeObMyFw/tzRs=", + "EC", + "MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgzP/U0pW2a4diOrv5m9K2V8+FBO/YMt6y2P5gsqwgGLigCgYIKoZIzj0DAQehRANCAAT+5BBNhmyvlypF/jyeUEWJgL3nxEN3cjew4xgZWh4gzBKKBRFNX2baGHOy1kiq4/C4HFhjAKJf7mQ53sRTHVpy", + ), + RegistrationTestDataGenerator.importAttestationCa( + "MIIB1zCCAX2gAwIBAgICGhowCgYIKoZIzj0EAwIwajEmMCQGA1UEAwwdWXViaWNvIFdlYkF1dGhuIHVuaXQgdGVzdHMgQ0ExDzANBgNVBAoMBll1YmljbzEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjELMAkGA1UEBhMCU0UwHhcNMTgwOTA2MTc0MjAwWhcNMTgwOTEzMTc0MjAwWjBqMSYwJAYDVQQDDB1ZdWJpY28gV2ViQXV0aG4gdW5pdCB0ZXN0cyBDQTEPMA0GA1UECgwGWXViaWNvMSIwIAYDVQQLDBlBdXRoZW50aWNhdG9yIEF0dGVzdGF0aW9uMQswCQYDVQQGEwJTRTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJ2VfJUFh6Z/dWDTIWpH9sUsQPAcey3l2CUICdc6q4FOzNDXfkKIGJ2QWYChByGRkl+CZJll8uXixbGAP7U0cyijEzARMA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0EAwIDSAAwRQIhAKwf9gy8noDEauYrUaR8277c5l0KrcEdMgVDNH0wmaq2AiAkkURh47WA63NwJv+zKWnOBGPPde3aK5a8ZkXairhDyA==", + "EC", + "MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgM23lz14jimoztDLaGkIUwU1M0nBptdn8U+/f7Lqc5l6gCgYIKoZIzj0DAQehRANCAASdlXyVBYemf3Vg0yFqR/bFLEDwHHst5dglCAnXOquBTszQ135CiBidkFmAoQchkZJfgmSZZfLl4sWxgD+1NHMo", + ), + ), + ) { + override def regenerate() = + TestAuthenticator.createBasicAttestedCredential( + keyAlgorithm = COSEAlgorithmIdentifier.ES256, + attestationMaker = AttestationMaker.tpm( + AttestationSigner.ca( + alg = COSEAlgorithmIdentifier.ES256, + certSubject = new X500Name(Array.empty[RDN]), + certExtensions = tpmCertExtensions, + ) + ), + ) + } + val ValidEs384: RegistrationTestData = new RegistrationTestData( + alg = COSEAlgorithmIdentifier.ES384, + attestationObject = + ByteArray.fromHex("bf68617574684461746158c549960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97634100000539000102030405060708090a0b0c0d0e0f0020ba64a3d23cef24334cc7d4c07745fa5a83cfed05c656ddd83a5a64b61715b33ea5010203382220022158305b5d607978a6a706069df8fb997b2fabc049220d675965ca8effe285aaa9a97643c89cc4a595165663f9a958ba5a722b2258303af880a2b7668261c92c070394e517b00ac7991b52848feb8a9bc936d5e9d06471d2c4edea5937324aba9e9047b8654063666d746374706d6761747453746d74bf63616c6738226373696758673065023100c25740fc43cc97687b4c450456885ce21df6ec45fa41682bb64b7f8196f66ef554e6f62e4ddfc084fbc7d83876a8c624023042c8a0a2146aedb0ab493e7f9ef50d93ddae2eac4895dd4f59de16b787cf5747b42bdae5c39cb8b3f698d7fd48bca95263783563825902413082023d308201c4a00302010202020469300a06082a8648ce3d040303306a3126302406035504030c1d59756269636f20576562417574686e20756e6974207465737473204341310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b3009060355040613025345301e170d3138303930363137343230305a170d3138303931333137343230305a30003076301006072a8648ce3d020106052b81040022036200047b6946084762e55c3d921dfc3dcb466e8b49c00df8ee0a794c0005c24c37fb64ac82cdbb1263b7a9b644f9d1d03f4e568af99d245c61ed317e559950686210fcd01b596f4dfb249e0664a96a73884190f3ffb47710b88728386fb84800f51a97a381a63081a33021060b2b0601040182e51c01010404120410000102030405060708090a0b0c0d0e0f30690603551d110101ff045f305da45b305931573014060567810502010c0b69643a30303030303030303014060567810502030c0b69643a30303030303030303029060567810502020c20544553545f59756269636f5f6a6176612d776562617574686e2d73657276657230130603551d250101ff0409300706056781050803300a06082a8648ce3d0403030367003064023046958ad5fc19260b2a62fec767b6669054fd7d2a392953c0593a72924be0d70cdc168071f750fd5a68037fe11da8507d0230162a11b4e3605de4f4bac6c9cfba2f51c28280859788d6a92583c1a8f5a72a4092cd8e523298550105643ad4a1dab5bd590219308202153082019aa0030201020202099c300a06082a8648ce3d040303306a3126302406035504030c1d59756269636f20576562417574686e20756e6974207465737473204341310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b3009060355040613025345301e170d3138303930363137343230305a170d3138303931333137343230305a306a3126302406035504030c1d59756269636f20576562417574686e20756e6974207465737473204341310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b30090603550406130253453076301006072a8648ce3d020106052b810400220362000402ef421a687384c2cc1719570d2d11d8be506e67841a8e893706cbb700f2312a1b4900fda578a927de59a7b6cd0a7fb540a336a03734307e2b8aed380826754a170575eb705e466c704e50a52eabaa1815761b85c995479e8371ed554c6a549aa3133011300f0603551d130101ff040530030101ff300a06082a8648ce3d0403030369003066023100e4c8f4f7ad6d1c74cbbbdc1da11061ba46fcf49aef01f465f324db8e2fe0e32de69c1d5de20eeb3f3d1e366c98b113bc023100ee69b5cf545a56ba343fb2a9f22a3cdc934f2f38574091a8b28db96e0bcb8f0413259347097c6f37870780ab1ad7d2156863657274496e666f5889ff544347801700000030195a7269268dc231b13139b0116973364984bc22956b5a77d3505bdbd04946a77492fdc30198599beefc9ab73234cf31000000000000000011111111222222223300000000000000000032000c68b9c0508642ec9bd644425a23f6ce911fa6bf3a1af3f473cf90a38038ad7824b7ae0e6a653a475d349affd9e1be5a9200006376657263322e30677075624172656158760023000c000400000000001000100004001000305b5d607978a6a706069df8fb997b2fabc049220d675965ca8effe285aaa9a97643c89cc4a595165663f9a958ba5a722b00303af880a2b7668261c92c070394e517b00ac7991b52848feb8a9bc936d5e9d06471d2c4edea5937324aba9e9047b86540ffff"), + clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","type":"webauthn.create","tokenBinding":{"status":"supported"}}""", + privateKey = Some( + ByteArray.fromHex("3081bf020100301006072a8648ce3d020106052b810400220481a73081a4020101043031b5ed68c23d09ef553b053a723f654b08069a1bc38f6e061860dd07197ebf6fb7df1711ee2d8954d2921519e2f2220ba00706052b81040022a164036200045b5d607978a6a706069df8fb997b2fabc049220d675965ca8effe285aaa9a97643c89cc4a595165663f9a958ba5a722b3af880a2b7668261c92c070394e517b00ac7991b52848feb8a9bc936d5e9d06471d2c4edea5937324aba9e9047b86540") + ), + attestationCertChain = List( + RegistrationTestDataGenerator.importAttestationCa( + "MIICPTCCAcSgAwIBAgICBGkwCgYIKoZIzj0EAwMwajEmMCQGA1UEAwwdWXViaWNvIFdlYkF1dGhuIHVuaXQgdGVzdHMgQ0ExDzANBgNVBAoMBll1YmljbzEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjELMAkGA1UEBhMCU0UwHhcNMTgwOTA2MTc0MjAwWhcNMTgwOTEzMTc0MjAwWjAAMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEe2lGCEdi5Vw9kh38PctGbotJwA347gp5TAAFwkw3+2Ssgs27EmO3qbZE+dHQP05WivmdJFxh7TF+VZlQaGIQ/NAbWW9N+ySeBmSpanOIQZDz/7R3ELiHKDhvuEgA9RqXo4GmMIGjMCEGCysGAQQBguUcAQEEBBIEEAABAgMEBQYHCAkKCwwNDg8waQYDVR0RAQH/BF8wXaRbMFkxVzAUBgVngQUCAQwLaWQ6MDAwMDAwMDAwFAYFZ4EFAgMMC2lkOjAwMDAwMDAwMCkGBWeBBQICDCBURVNUX1l1Ymljb19qYXZhLXdlYmF1dGhuLXNlcnZlcjATBgNVHSUBAf8ECTAHBgVngQUIAzAKBggqhkjOPQQDAwNnADBkAjBGlYrV/BkmCypi/sdntmaQVP19KjkpU8BZOnKSS+DXDNwWgHH3UP1aaAN/4R2oUH0CMBYqEbTjYF3k9LrGyc+6L1HCgoCFl4jWqSWDwaj1pypAks2OUjKYVQEFZDrUodq1vQ==", + "EC", + "MIG/AgEAMBAGByqGSM49AgEGBSuBBAAiBIGnMIGkAgEBBDAZb9xZRaxukSkUc9DxDx9bEzIt3MVPbHKNw1Q3A8icRIcwZAV48zOeoWSEIiO7bmugBwYFK4EEACKhZANiAAR7aUYIR2LlXD2SHfw9y0Zui0nADfjuCnlMAAXCTDf7ZKyCzbsSY7eptkT50dA/TlaK+Z0kXGHtMX5VmVBoYhD80BtZb037JJ4GZKlqc4hBkPP/tHcQuIcoOG+4SAD1Gpc=", + ), + RegistrationTestDataGenerator.importAttestationCa( + "MIICFTCCAZqgAwIBAgICCZwwCgYIKoZIzj0EAwMwajEmMCQGA1UEAwwdWXViaWNvIFdlYkF1dGhuIHVuaXQgdGVzdHMgQ0ExDzANBgNVBAoMBll1YmljbzEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjELMAkGA1UEBhMCU0UwHhcNMTgwOTA2MTc0MjAwWhcNMTgwOTEzMTc0MjAwWjBqMSYwJAYDVQQDDB1ZdWJpY28gV2ViQXV0aG4gdW5pdCB0ZXN0cyBDQTEPMA0GA1UECgwGWXViaWNvMSIwIAYDVQQLDBlBdXRoZW50aWNhdG9yIEF0dGVzdGF0aW9uMQswCQYDVQQGEwJTRTB2MBAGByqGSM49AgEGBSuBBAAiA2IABALvQhpoc4TCzBcZVw0tEdi+UG5nhBqOiTcGy7cA8jEqG0kA/aV4qSfeWae2zQp/tUCjNqA3NDB+K4rtOAgmdUoXBXXrcF5GbHBOUKUuq6oYFXYbhcmVR56Dce1VTGpUmqMTMBEwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNpADBmAjEA5Mj0961tHHTLu9wdoRBhukb89JrvAfRl8yTbji/g4y3mnB1d4g7rPz0eNmyYsRO8AjEA7mm1z1RaVro0P7Kp8io83JNPLzhXQJGoso25bgvLjwQTJZNHCXxvN4cHgKsa19IV", + "EC", + "MIG/AgEAMBAGByqGSM49AgEGBSuBBAAiBIGnMIGkAgEBBDC0yTOXQkL9UVCkqhnQnrcDz0Lhq7MyG9JcBiy2CI+82SXQJmRWUlj8kAoyQ2J5qvagBwYFK4EEACKhZANiAAQC70IaaHOEwswXGVcNLRHYvlBuZ4Qajok3Bsu3APIxKhtJAP2leKkn3lmnts0Kf7VAozagNzQwfiuK7TgIJnVKFwV163BeRmxwTlClLquqGBV2G4XJlUeeg3HtVUxqVJo=", + ), + ), + ) { + override def regenerate() = + TestAuthenticator.createBasicAttestedCredential( + keyAlgorithm = COSEAlgorithmIdentifier.ES384, + attestationMaker = AttestationMaker.tpm( + AttestationSigner.ca( + alg = COSEAlgorithmIdentifier.ES384, + certSubject = new X500Name(Array.empty[RDN]), + certExtensions = tpmCertExtensions, + ) + ), + ) + } + val ValidEs512: RegistrationTestData = new RegistrationTestData( + alg = COSEAlgorithmIdentifier.ES512, + attestationObject = + ByteArray.fromHex("bf68617574684461746158e949960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97634100000539000102030405060708090a0b0c0d0e0f0020f24b6c18dad993c043e8bca98c466d602b011332e3497c5293ab00f11607a96da50102033823200321584201f3895d6d36dbd789e9836d069a91e94876bf2f50b3537eef21229fe9b278172be2fd693e8a619d75ba6a0e9428149eda716eb68c7fdcdfaa9dc450ac342c0902f422584200d952ce99778ceb86ef5b26185789d17df0562595c2424cedefb24352e0ba09ba9fd1e52a0b99b25bf80cd509bc2f0fa7f3324507058032e8d6eb0494972addb44063666d746374706d6761747453746d74bf63616c67382363736967588a308187024151efde3ed5d6fefb16e51d076b365a90ea302585faf92e0083f6505fdc46fccbce414676440733da36df515303ef76580125d0da69d757e7f4036e506167d6b84b0242018c8d65f68f7e1c5a38239cfee8fd88e9abf20628f349bb62f12f5f362dc9039c077139a5f083ac34fae9315f0456497b1af7328f9c138ca94cb4df813417cb545c637835638259028b30820287308201eaa00302010202021528300a06082a8648ce3d040304306a3126302406035504030c1d59756269636f20576562417574686e20756e6974207465737473204341310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b3009060355040613025345301e170d3138303930363137343230305a170d3138303931333137343230305a300030819b301006072a8648ce3d020106052b8104002303818600040016b1ebf22c294a4a62092508a19cdbd4ca58e66e7a0125b9c59b10da194b69e939922b63b1903f9eb2a744fb82582ed6f17b2c4f9036aaf8ca3a5d6d3fd60c100301cf35805517cd1b5f98849c90a4ce4e5a0fc4dd61fedef2fb99c8cdea04ab5bf7f6d01877364d9ae8046096c71271fdd928f826b24b510d81333f5f64ad0a8d6629a381a63081a33021060b2b0601040182e51c01010404120410000102030405060708090a0b0c0d0e0f30690603551d110101ff045f305da45b305931573014060567810502010c0b69643a30303030303030303014060567810502030c0b69643a30303030303030303029060567810502020c20544553545f59756269636f5f6a6176612d776562617574686e2d73657276657230130603551d250101ff0409300706056781050803300a06082a8648ce3d04030403818a0030818602416db6495537bbe31113ce6fc707477bd4a7b9207e099b68e14f03f549def3d171a33c37d2281b046e8009354f4e267b60f5c5eb4ef4c2adeeb526d6aa00283d37d50241245df418b75d4c31dc026585ff53a0fc039e387978d996a81f51afb1697d92d9e7e8a2431ea9e063f8c4af35c3660d478cd9e8285057b6c98e9c5ec47e7fd0a70a5902623082025e308201c0a003020102020221bb300a06082a8648ce3d040304306a3126302406035504030c1d59756269636f20576562417574686e20756e6974207465737473204341310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b3009060355040613025345301e170d3138303930363137343230305a170d3138303931333137343230305a306a3126302406035504030c1d59756269636f20576562417574686e20756e6974207465737473204341310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b300906035504061302534530819b301006072a8648ce3d020106052b8104002303818600040052d9b121c847b6ac9432ced565d863e4efed1d9ef7e95903ecc4f5dde0bd2ebe5ec54bee60d54de75f457f241c6d92ea39b42f6f188365bd606c07c66f102cb4f60186c816e16b72481b7ad370069f82765c042cb3f9ba503affd376a2a99a6ce114e3bd2b8fd09c649eda1311c0c40baa6d25d97a13eb1e041ca03c4ce498131a7976a3133011300f0603551d130101ff040530030101ff300a06082a8648ce3d04030403818b00308187024130f07431cfcdcafd7fc357d0d7689a0f030c859c2d9c86a196476a4d7360d668b3fcdc26453c6cb963d54a9bee3bbd62358753f0c88c9504749b19a4b846a92fcf024201373abfa5c71c47b4b5f4fbd64d44273ed85a192447c6eca0e3f506868c5b317141b3182975353c65c45ee90dfc65aaa09e210190d35eb56ecd4f5593fc1b36c2886863657274496e666f58a9ff544347801700000040302269d22180e0485672fd616d14833f68575c4f9821e5f88c96866bc2d12219001a809c3fed3636c08b454a56703275289b912a25c647da8e18826e60267733000000000000000011111111222222223300000000000000000042000d2b46abf13d16a2e40d5dd76ccaa6f9ac4f051bf2c040cc3effb86ffed66556903fde2e70162c5aa89abade483b2a1843f2a7b7cae4969a242880b7d46a2b41e200006376657263322e306770756241726561589a0023000d0004000000000010001000050010004201f3895d6d36dbd789e9836d069a91e94876bf2f50b3537eef21229fe9b278172be2fd693e8a619d75ba6a0e9428149eda716eb68c7fdcdfaa9dc450ac342c0902f4004200d952ce99778ceb86ef5b26185789d17df0562595c2424cedefb24352e0ba09ba9fd1e52a0b99b25bf80cd509bc2f0fa7f3324507058032e8d6eb0494972addb440ffff"), + clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","type":"webauthn.create","tokenBinding":{"status":"supported"}}""", + privateKey = Some( + ByteArray.fromHex("3081f7020100301006072a8648ce3d020106052b810400230481df3081dc020101044201a3c355e4920f3fb144732a5d93322ff10777467f64a4fff60507225c4576bf67174c69e0d443e982b7b0c0726d248b8511a071548be473e6e3b237116cea537ccda00706052b81040023a18189038186000401f3895d6d36dbd789e9836d069a91e94876bf2f50b3537eef21229fe9b278172be2fd693e8a619d75ba6a0e9428149eda716eb68c7fdcdfaa9dc450ac342c0902f400d952ce99778ceb86ef5b26185789d17df0562595c2424cedefb24352e0ba09ba9fd1e52a0b99b25bf80cd509bc2f0fa7f3324507058032e8d6eb0494972addb440") + ), + attestationCertChain = List( + RegistrationTestDataGenerator.importAttestationCa( + "MIIChzCCAeqgAwIBAgICFSgwCgYIKoZIzj0EAwQwajEmMCQGA1UEAwwdWXViaWNvIFdlYkF1dGhuIHVuaXQgdGVzdHMgQ0ExDzANBgNVBAoMBll1YmljbzEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjELMAkGA1UEBhMCU0UwHhcNMTgwOTA2MTc0MjAwWhcNMTgwOTEzMTc0MjAwWjAAMIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQAFrHr8iwpSkpiCSUIoZzb1MpY5m56ASW5xZsQ2hlLaek5kitjsZA/nrKnRPuCWC7W8XssT5A2qvjKOl1tP9YMEAMBzzWAVRfNG1+YhJyQpM5OWg/E3WH+3vL7mcjN6gSrW/f20Bh3Nk2a6ARglscScf3ZKPgmsktRDYEzP19krQqNZimjgaYwgaMwIQYLKwYBBAGC5RwBAQQEEgQQAAECAwQFBgcICQoLDA0ODzBpBgNVHREBAf8EXzBdpFswWTFXMBQGBWeBBQIBDAtpZDowMDAwMDAwMDAUBgVngQUCAwwLaWQ6MDAwMDAwMDAwKQYFZ4EFAgIMIFRFU1RfWXViaWNvX2phdmEtd2ViYXV0aG4tc2VydmVyMBMGA1UdJQEB/wQJMAcGBWeBBQgDMAoGCCqGSM49BAMEA4GKADCBhgJBbbZJVTe74xETzm/HB0d71Ke5IH4Jm2jhTwP1Sd7z0XGjPDfSKBsEboAJNU9OJntg9cXrTvTCre61JtaqACg9N9UCQSRd9Bi3XUwx3AJlhf9ToPwDnjh5eNmWqB9Rr7FpfZLZ5+iiQx6p4GP4xK81w2YNR4zZ6ChQV7bJjpxexH5/0KcK", + "EC", + "MIH3AgEAMBAGByqGSM49AgEGBSuBBAAjBIHfMIHcAgEBBEIA2IorORYB75tWAa1vGmUrIDa73QfrZCKwAqO9sFKYCj+UE0sSX5Yop8sYau7U7TLCPch7dsuiRRSYefh6Y9PA3u2gBwYFK4EEACOhgYkDgYYABAAWsevyLClKSmIJJQihnNvUyljmbnoBJbnFmxDaGUtp6TmSK2OxkD+esqdE+4JYLtbxeyxPkDaq+Mo6XW0/1gwQAwHPNYBVF80bX5iEnJCkzk5aD8TdYf7e8vuZyM3qBKtb9/bQGHc2TZroBGCWxxJx/dko+CayS1ENgTM/X2StCo1mKQ==", + ), + RegistrationTestDataGenerator.importAttestationCa( + "MIICXjCCAcCgAwIBAgICIbswCgYIKoZIzj0EAwQwajEmMCQGA1UEAwwdWXViaWNvIFdlYkF1dGhuIHVuaXQgdGVzdHMgQ0ExDzANBgNVBAoMBll1YmljbzEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjELMAkGA1UEBhMCU0UwHhcNMTgwOTA2MTc0MjAwWhcNMTgwOTEzMTc0MjAwWjBqMSYwJAYDVQQDDB1ZdWJpY28gV2ViQXV0aG4gdW5pdCB0ZXN0cyBDQTEPMA0GA1UECgwGWXViaWNvMSIwIAYDVQQLDBlBdXRoZW50aWNhdG9yIEF0dGVzdGF0aW9uMQswCQYDVQQGEwJTRTCBmzAQBgcqhkjOPQIBBgUrgQQAIwOBhgAEAFLZsSHIR7aslDLO1WXYY+Tv7R2e9+lZA+zE9d3gvS6+XsVL7mDVTedfRX8kHG2S6jm0L28Yg2W9YGwHxm8QLLT2AYbIFuFrckgbetNwBp+CdlwELLP5ulA6/9N2oqmabOEU470rj9CcZJ7aExHAxAuqbSXZehPrHgQcoDxM5JgTGnl2oxMwETAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49BAMEA4GLADCBhwJBMPB0Mc/Nyv1/w1fQ12iaDwMMhZwtnIahlkdqTXNg1miz/NwmRTxsuWPVSpvuO71iNYdT8MiMlQR0mxmkuEapL88CQgE3Or+lxxxHtLX0+9ZNRCc+2FoZJEfG7KDj9QaGjFsxcUGzGCl1NTxlxF7pDfxlqqCeIQGQ0161bs1PVZP8GzbCiA==", + "EC", + "MIH3AgEAMBAGByqGSM49AgEGBSuBBAAjBIHfMIHcAgEBBEIBH6QXnTBYT/m2fGSf89i9z1PvzG80y8JSVZi9BXBor5V9lsvrhTQGp5YNc6HtkUpmqfu30RllBppic6jAzwnBclSgBwYFK4EEACOhgYkDgYYABABS2bEhyEe2rJQyztVl2GPk7+0dnvfpWQPsxPXd4L0uvl7FS+5g1U3nX0V/JBxtkuo5tC9vGINlvWBsB8ZvECy09gGGyBbha3JIG3rTcAafgnZcBCyz+bpQOv/TdqKpmmzhFOO9K4/QnGSe2hMRwMQLqm0l2XoT6x4EHKA8TOSYExp5dg==", + ), + ), + ) { + override def regenerate() = + TestAuthenticator.createBasicAttestedCredential( + keyAlgorithm = COSEAlgorithmIdentifier.ES512, + attestationMaker = AttestationMaker.tpm( + AttestationSigner.ca( + alg = COSEAlgorithmIdentifier.ES512, + certSubject = new X500Name(Array.empty[RDN]), + certExtensions = tpmCertExtensions, + ) + ), + ) + } + + val ValidRs256: RegistrationTestData = new RegistrationTestData( + alg = COSEAlgorithmIdentifier.RS256, + attestationObject = + ByteArray.fromHex("bf68617574684461746159016849960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97634100000539000102030405060708090a0b0c0d0e0f002007bdca7ffc3f53dc8b02deffaa4798ba2c7a21eeb2f47f186829ef825bd06c48a4010303390100205901010087ffcd54a9763897fa951c30e00fd39aca161f9d5bee8cc6b5cb410603adb6ccda7ac447acf5cd8a40dc3c3d8f463e1ef5dd1522d2f57350fb9a4cfe5aa977e63abafa480e3d8f1f357284882e91dd7ec0ff5e22fe79703c0121682a5015df589a0c2af526c4a8ee700cc98142c437b0105dc226b672aab44b09bcecf273fae4173f75ac6c5b8c9b417446e9a339977d8f54cdb3e39cdf5cb0cc78439c55bbd3601e61ed99ae2d6a5707d40d10661aecc765142b006bf6fd0d0a214433f84f66b50397f306a541f71c340cc0b80d71771a6c3ccbdc27dd9a90c7681f972771e3cdeea08193d98a7c25ab53e92d91c465074700b09088a68127c424a2f9dc1027214301000163666d746374706d6761747453746d74bf63616c67390100637369675901004cff0e000761d22cdc6ae0aec28fb4e0727b45cc5cd7d162c7f1008f919ca8df2d437829130acda6a55c96d9fe2edbb4669ad09302c40a979bb1d8c51ea9a4339cbab82cf981039433bea04096ce3ba637426f68e0de5a2df1229ababa935043ac2cfdc5a8c74018bf3c84c073497403bff8c7c654087b690944df98c79e86be66c012a9af01440ae78b044f90bacd33841aa06426b3a159eaa90882d8e471ddf42bdafe7ce4349d04bec8cfbc451f6b62721e82fc1dec09337f9342aca3adb3ce6ceb7265647a712390319faa80e5130a28622b923ede9b0d9862cc3d8a46d000f7b416ccd7a1f67607d3d2aa730604cb4e827a981ce7246bc07f4339e87d8763783563825903913082038d30820275a00302010202020810300d06092a864886f70d01010b0500306a3126302406035504030c1d59756269636f20576562417574686e20756e6974207465737473204341310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b3009060355040613025345301e170d3138303930363137343230305a170d3138303931333137343230305a300030820122300d06092a864886f70d01010105000382010f003082010a02820101008830d398bc44b894cf3ec4e4ff835284906e9af9df1b343888cbcc18916853a54d49b9424e91aa108dee96e61fafc1392fe4fefe005905aba406b1ccb6f32bb9cd932c3baa56568ab38a5555c3229b08a6e6406fdf18e34692b9bd8d54adad6c04d2b375a46cb5cb1502c0a92515787f5c69b4db92ebe5402260845575546788a278248bcde3cc70bf7c0cba0da8b6e1ca48ceb98333e27beac93b68ee17d11a9ade8b3153bb9ee57c9ab893503bfbfb765ce1ac05a64a9bf8b3280bf739869b3ca10fb7b4e52ceeae2a3a15bbddfc372ca4e82d1874f50815f8f940a09bf733df7c9c0e4887e7c4fe871eb45cef94d9544d78594eaace95c245660321806f5b0203010001a381a63081a33021060b2b0601040182e51c01010404120410000102030405060708090a0b0c0d0e0f30690603551d110101ff045f305da45b305931573014060567810502010c0b69643a30303030303030303014060567810502030c0b69643a30303030303030303029060567810502020c20544553545f59756269636f5f6a6176612d776562617574686e2d73657276657230130603551d250101ff0409300706056781050803300d06092a864886f70d01010b050003820101000dc5b21c713a09e9eeba1e0ad56d839976c8468ac0759cf2891b1bc439fe6857a6ec4ee327b717ab67bb1085de4f433f30d6b4a664975eb509c981ec2105ce9f6dbef1f9b392e9fd1d2a1bee185803f9ca5fa0fbbfddf41d0df369b39532c2465f53b12c4ec00ec180604b1b61982a913a72b2099e09fc71b4b395b1c1432b8b9f540761eae7b15b7d0c65cc0641d32207ac6e6f7f33073b5e62efecf6ed07487c8cb000640c018df760ff72588efcead5328506b5ea270b90666fdc3bf077cd5492a61d10389d44c5d31850c024dc886349e9a7e998058956f877fc162c5389086629f434cabd7b57f2e1bd3d1637676dceb8e66c1d0fca46d9e9b02e964463590367308203633082024ba00302010202020514300d06092a864886f70d01010b0500306a3126302406035504030c1d59756269636f20576562417574686e20756e6974207465737473204341310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b3009060355040613025345301e170d3138303930363137343230305a170d3138303931333137343230305a306a3126302406035504030c1d59756269636f20576562417574686e20756e6974207465737473204341310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b300906035504061302534530820122300d06092a864886f70d01010105000382010f003082010a028201010098aff2d0259ef6f996989af530238a6ab15f6536168050bbd08e28bdb2fd7dba7fefb79e733255f8e7b6c32b7dbacac7a1611bd0c705f53a463f55ef4e0f0bf4153f46ae2c63acb1f74d2389159b9020ea8fb344d5e2af18f33fe4d6d6be84d775130773d836e25706113c99e604bd45e8a1710674e943326d52053b6372e753eddfef380dab1ce440fa1c77fa02aa4b5dbbcfd2a4679497a8c6c450b468b168725345e81dcf37f3c41c7d3a0c20e1f8e806725fbe7a3af4fd135c8f90cd30d81c3bdda15b8c20e007ae48ddb8aece5f8526c9a78d44d607f81f8f214f84226be8400050d277305eac2a088a27f8382a415e77c13e6aba9ca79494aca45ad29f0203010001a3133011300f0603551d130101ff040530030101ff300d06092a864886f70d01010b05000382010100817e70d6eda767e160b7883531bcf5dc09ec405f8a1ab222673b9631898dd249fec7ecd616bddf812315c8bfcb361cb88931a6a38041da7c2e88be1cfee341cacd079101de6ccb9cf1ddca88e82cba89bd29591313ba3681195d850e06922f6691c3a74acfde6de30bf1a63faa10d4a5b5b97bc537cf34abdd2e66f295fc1e93d987a5d0b38b1ebf1668f494e46054e89ebe0b420c6238fe645eb87779cdede7470c5fb7111a75cc143391f528bd49878eb86e0618c3c3b89856d2cbaf4745c1bd598625b0fb94902ece3516b52604a5fad04e79210790dd69acf08855ce4f6f2392f24d1484bd7d194a158c8d608acd06cb0f056fbaa6caacafd3e6b0ef7fcd6863657274496e666f5869ff5443478017000000208c4aae34ac9284f203ee6bf5f5e8900ffa5d9389ea351bb8234ffffbb894c09f000000000000000011111111222222223300000000000000000022000b9cf8d6842ad9d92c115b71017e2ca26f4c7b878ad5bd1efd7bdf342060e77bfa00006376657263322e3067707562417265615901170001000b0004000000000010001408000001000101010087ffcd54a9763897fa951c30e00fd39aca161f9d5bee8cc6b5cb410603adb6ccda7ac447acf5cd8a40dc3c3d8f463e1ef5dd1522d2f57350fb9a4cfe5aa977e63abafa480e3d8f1f357284882e91dd7ec0ff5e22fe79703c0121682a5015df589a0c2af526c4a8ee700cc98142c437b0105dc226b672aab44b09bcecf273fae4173f75ac6c5b8c9b417446e9a339977d8f54cdb3e39cdf5cb0cc78439c55bbd3601e61ed99ae2d6a5707d40d10661aecc765142b006bf6fd0d0a214433f84f66b50397f306a541f71c340cc0b80d71771a6c3ccbdc27dd9a90c7681f972771e3cdeea08193d98a7c25ab53e92d91c465074700b09088a68127c424a2f9dc1027ffff"), + clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","type":"webauthn.create","tokenBinding":{"status":"supported"}}""", + privateKey = Some( + ByteArray.fromHex("308204bd020100300d06092a864886f70d0101010500048204a7308204a3020100028201010087ffcd54a9763897fa951c30e00fd39aca161f9d5bee8cc6b5cb410603adb6ccda7ac447acf5cd8a40dc3c3d8f463e1ef5dd1522d2f57350fb9a4cfe5aa977e63abafa480e3d8f1f357284882e91dd7ec0ff5e22fe79703c0121682a5015df589a0c2af526c4a8ee700cc98142c437b0105dc226b672aab44b09bcecf273fae4173f75ac6c5b8c9b417446e9a339977d8f54cdb3e39cdf5cb0cc78439c55bbd3601e61ed99ae2d6a5707d40d10661aecc765142b006bf6fd0d0a214433f84f66b50397f306a541f71c340cc0b80d71771a6c3ccbdc27dd9a90c7681f972771e3cdeea08193d98a7c25ab53e92d91c465074700b09088a68127c424a2f9dc102702030100010282010062af108c8566fe8bf14dafa61b8000790a78e139eb127f5e555e0671d9cb7ca0cb4c580ef6876a3d0ef18058df150650aaf160bbe33e2e0e2e73f9b87b8b0b30a99f31ab8581cfcfc295b56ba7f73a3516d076bb87d210c9c9bc36fcb51e19f20dde1471cd4ed8922406735573603454729bc61d1738bf7b92139fde83d3fad1e2284b75bb613abe34e7b640eccbe2b9fe59a818b02c9b6fa50a4aa2f125626200746810995a02fe3d05a785d11c612314ea47343a02dcde420e34501911d97c3c9da41084d9d865ab719b44ca70e596e41ab8254a73dd0adadfaa70b76f0648cff3017cecbb16e7a289e859e34b2a7c04852c567ba84d1fc5e3d398f72cc40102818100e110ce79365dc443d43d7bd76babe21788b68cebbfc8afcf9c41ffbf8ce7976318b1d2dcc8a9dc5659241621dadac4ed5c9381289a9b49c0935b6d89f7ab182814b5b7a5a3b3b3d823d6d7440f6390a0c4ff1ff3856d1c247d187f8c33f5836c26332cd3790bbb767c48e1d83f90a96dfe472b10551c3b5359032967985fb227028181009ab1175270a0347e6cb97f5a3a74c5463cc6846cfd4e06d777faff95e2a693cf749ffb6e277c0c840b3816c4420c69f7917686e0d23fd07969fce0207773f4bbf6b091a01855d53432782490b68590117861b70ae51c29abe8e4f9e01aced95f11957883be28860c636907a55d1bd595b4c5ab59088d94a286e7e7ab9e0172010281803a01b5e581c09b040c60a8597633bfbae70e7db589217546a1f454b10ee4e59cb1d1ab122259bd23382857d7f3eb2c942ca70bc3e64d1dae178c99e7d44071a26aec06e017180ac32b41850bd2978bc013e5d95b4f4936d6a4b33ab46cf3db227599fcf4a81f00fae1bf7b0ddc1c31bedaa9870cd792c62b8e26857660cc51430281802f8d54b000f31e6fe698372fd35c65f02b6a92f6b5ff30573808ae5cb2e9a5f255d58002e29c5d7491c652294e6c667eb5f68b8bbcd5e50e0da8b0750a8358ae172d3bf6ccc445dfdfcbd2e1b159e9699569e44cb3152f322b4b880c7df12c1cef58d54d1a3d76c7841f9b3c181d2050fedaeccb57b7be03201955bc09bc4401028181008052d596595fcb6ecd5f29f86874bcb4830566485a9a059c2ecec1d085775b33ed96da7d32a8d8edc1bebf820d9fc56d308ec91b5433331f3dde784172912c4520c695ea741693cdf1f365c43b06b0cbc508d9b9b93ed96d4c49fef32618e445ae0f98cd599c15e1871e4d62e44708118433aadcbb76e55bf053f3dbfb3c1479") + ), + attestationCertChain = List( + RegistrationTestDataGenerator.importAttestationCa( + "MIIDjTCCAnWgAwIBAgICCBAwDQYJKoZIhvcNAQELBQAwajEmMCQGA1UEAwwdWXViaWNvIFdlYkF1dGhuIHVuaXQgdGVzdHMgQ0ExDzANBgNVBAoMBll1YmljbzEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjELMAkGA1UEBhMCU0UwHhcNMTgwOTA2MTc0MjAwWhcNMTgwOTEzMTc0MjAwWjAAMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAiDDTmLxEuJTPPsTk/4NShJBumvnfGzQ4iMvMGJFoU6VNSblCTpGqEI3uluYfr8E5L+T+/gBZBaukBrHMtvMruc2TLDuqVlaKs4pVVcMimwim5kBv3xjjRpK5vY1Ura1sBNKzdaRstcsVAsCpJRV4f1xptNuS6+VAImCEVXVUZ4iieCSLzePMcL98DLoNqLbhykjOuYMz4nvqyTto7hfRGpreizFTu57lfJq4k1A7+/t2XOGsBaZKm/izKAv3OYabPKEPt7TlLO6uKjoVu938Nyyk6C0YdPUIFfj5QKCb9zPffJwOSIfnxP6HHrRc75TZVE14WU6qzpXCRWYDIYBvWwIDAQABo4GmMIGjMCEGCysGAQQBguUcAQEEBBIEEAABAgMEBQYHCAkKCwwNDg8waQYDVR0RAQH/BF8wXaRbMFkxVzAUBgVngQUCAQwLaWQ6MDAwMDAwMDAwFAYFZ4EFAgMMC2lkOjAwMDAwMDAwMCkGBWeBBQICDCBURVNUX1l1Ymljb19qYXZhLXdlYmF1dGhuLXNlcnZlcjATBgNVHSUBAf8ECTAHBgVngQUIAzANBgkqhkiG9w0BAQsFAAOCAQEADcWyHHE6Cenuuh4K1W2DmXbIRorAdZzyiRsbxDn+aFem7E7jJ7cXq2e7EIXeT0M/MNa0pmSXXrUJyYHsIQXOn22+8fmzkun9HSob7hhYA/nKX6D7v930HQ3zabOVMsJGX1OxLE7ADsGAYEsbYZgqkTpysgmeCfxxtLOVscFDK4ufVAdh6uexW30MZcwGQdMiB6xub38zBzteYu/s9u0HSHyMsABkDAGN92D/cliO/OrVMoUGteonC5Bmb9w78HfNVJKmHRA4nUTF0xhQwCTciGNJ6afpmAWJVvh3/BYsU4kIZin0NMq9e1fy4b09Fjdnbc645mwdD8pG2emwLpZEYw==", + "RSA", + "MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCIMNOYvES4lM8+xOT/g1KEkG6a+d8bNDiIy8wYkWhTpU1JuUJOkaoQje6W5h+vwTkv5P7+AFkFq6QGscy28yu5zZMsO6pWVoqzilVVwyKbCKbmQG/fGONGkrm9jVStrWwE0rN1pGy1yxUCwKklFXh/XGm025Lr5UAiYIRVdVRniKJ4JIvN48xwv3wMug2otuHKSM65gzPie+rJO2juF9Eamt6LMVO7nuV8mriTUDv7+3Zc4awFpkqb+LMoC/c5hps8oQ+3tOUs7q4qOhW73fw3LKToLRh09QgV+PlAoJv3M998nA5Ih+fE/ocetFzvlNlUTXhZTqrOlcJFZgMhgG9bAgMBAAECggEAHx8JO13KVmOq+C0GJ11a/fADVmbDFPRZ9uibOwH/VR3xC2fKPyoKMr48Rz9O/lqpNsidfG2X6jPOx7jZjvUCiPLxLqpzwkcEawKxfWeaANN5UCRpbBHD3dyplSc2snlutatwVvG75c8Cfh6IiHDfmDsF7M5ARKeegDyOAPlO0FmSo2Om+lI4avvTGfp6zVxGXTboI/lE0HqP5uDf5ZNC6Wksm5PMvjWVnUSEwLql/0dXC/DRTsxbuXJBXtp4QXOcHCIE3BVwWEmCDDTUGg8zwmaZLI7wf/cs0owivmM36enoVEiuMbRA3EWPLyS+AHn+N2Vj+HPEx8/+5qFWEOXkAQKBgQDWHt+wmAfE+JbQnY0IaifvFQIRJUO+/mlnd1hfgD63nSGass0HD6uKhTyWPMsGQ4aCUHzLYDnmgNfbMEzXhP4wMTk0xBOES3ddtxvnRuhzERNKkCWnkY4aYdHiA3vi3aAxD3OAxlVdG+mp9e758IalG3x93QWU2l6XFG1/XsG/PQKBgQCi0/cA/RY9H76VLmWN3MvgOJrjaQKWo4k1ErGwtBWsJTLfHU7S7Y1F25IQvnZTXODhHtugGxH0asCooe/1p7+1KyoGvqsDG43WpIEvS+CDYLmzuc4OLAXjsP2zgN7fKoDFboI4RXAJSLLA6G4wCQnZqxRW94DtjQXSvHQx8dtSdwKBgClQOa7UFqOtp0PHMmAOQ3hA4G44d3LRmbrJ7zY2A2PgIIy9tQuIvXtzq7X9MtsZikl4iCuhfGp6L6vuDNWEpprb1ILW1kEvYm+lle+w4cbZ45P+bhV/4yA6AYoPTAcA5hixN4MAQZY+fX46oop9Gy2eOQ376EjJPXj/CwWJXe6tAoGAI5NqYWXqqPo5msCjYaZ/SQM1HEDCVwVuIhFuj2wZXB5YihUONtm+RygdNtlWYwpk++rRE582gg+c/ns7QZIgOcYvjX+1P52SlPYmX54VdL76dAFBuyj1NHVkSQb8KwhPUFO/0emh+/VNUQa3pHklFNDjRckX+08XmZ6hSJROVisCgYAVWimW0WLbg1FQ04ub1dwmOLbUTIdiEpP74ZJQYJAUn1upinvEy/+grB5VHlX5zoLGSL5OkL/VRcOJFKlb6TDkHRg6u4KJv4XVBLFPq0HOFQeRWxRglFFdfD+P6r6YpTRzvgwLRNVD2r9NteCc/KB5yQqG5kHFDX6ECG/s6u+yDg==", + ), + RegistrationTestDataGenerator.importAttestationCa( + "MIIDYzCCAkugAwIBAgICBRQwDQYJKoZIhvcNAQELBQAwajEmMCQGA1UEAwwdWXViaWNvIFdlYkF1dGhuIHVuaXQgdGVzdHMgQ0ExDzANBgNVBAoMBll1YmljbzEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjELMAkGA1UEBhMCU0UwHhcNMTgwOTA2MTc0MjAwWhcNMTgwOTEzMTc0MjAwWjBqMSYwJAYDVQQDDB1ZdWJpY28gV2ViQXV0aG4gdW5pdCB0ZXN0cyBDQTEPMA0GA1UECgwGWXViaWNvMSIwIAYDVQQLDBlBdXRoZW50aWNhdG9yIEF0dGVzdGF0aW9uMQswCQYDVQQGEwJTRTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJiv8tAlnvb5lpia9TAjimqxX2U2FoBQu9COKL2y/X26f++3nnMyVfjntsMrfbrKx6FhG9DHBfU6Rj9V704PC/QVP0auLGOssfdNI4kVm5Ag6o+zRNXirxjzP+TW1r6E13UTB3PYNuJXBhE8meYEvUXooXEGdOlDMm1SBTtjcudT7d/vOA2rHORA+hx3+gKqS127z9KkZ5SXqMbEULRosWhyU0XoHc8388QcfToMIOH46AZyX756OvT9E1yPkM0w2Bw73aFbjCDgB65I3biuzl+FJsmnjUTWB/gfjyFPhCJr6EAAUNJ3MF6sKgiKJ/g4KkFed8E+arqcp5SUrKRa0p8CAwEAAaMTMBEwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAgX5w1u2nZ+Fgt4g1Mbz13AnsQF+KGrIiZzuWMYmN0kn+x+zWFr3fgSMVyL/LNhy4iTGmo4BB2nwuiL4c/uNBys0HkQHebMuc8d3KiOgsuom9KVkTE7o2gRldhQ4Gki9mkcOnSs/ebeML8aY/qhDUpbW5e8U3zzSr3S5m8pX8HpPZh6XQs4sevxZo9JTkYFTonr4LQgxiOP5kXrh3ec3t50cMX7cRGnXMFDOR9Si9SYeOuG4GGMPDuJhW0suvR0XBvVmGJbD7lJAuzjUWtSYEpfrQTnkhB5DdaazwiFXOT28jkvJNFIS9fRlKFYyNYIrNBssPBW+6psqsr9PmsO9/zQ==", + "RSA", + "MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCYr/LQJZ72+ZaYmvUwI4pqsV9lNhaAULvQjii9sv19un/vt55zMlX457bDK326ysehYRvQxwX1OkY/Ve9ODwv0FT9GrixjrLH3TSOJFZuQIOqPs0TV4q8Y8z/k1ta+hNd1Ewdz2DbiVwYRPJnmBL1F6KFxBnTpQzJtUgU7Y3LnU+3f7zgNqxzkQPocd/oCqktdu8/SpGeUl6jGxFC0aLFoclNF6B3PN/PEHH06DCDh+OgGcl++ejr0/RNcj5DNMNgcO92hW4wg4AeuSN24rs5fhSbJp41E1gf4H48hT4Qia+hAAFDSdzBerCoIiif4OCpBXnfBPmq6nKeUlKykWtKfAgMBAAECggEAKi7iDHN4WY9W9c5J0wTept9eFZ491TF40gOUaeRbeDLgSaAXHIhZjCyoJ3+KeuKvCHzFrIZvtPJmmfTp6kzp5oNAOgzAosEBYetj1+mqUsVlSFIkwFqiOWhqzJQ2O+iDhgq90ll3wEx+lqCBfDTu/bNpdspr3k38TouMen3dLt9p3btvpfLJmwTfJJ6/gEFcUjS0pOkFAICEkaNVaeyxFFlGrpEhdehkizFSUqLAo58rzXZMrjvSsA6zf7pZ2eD5S7Uuh3Kn00wwoV6SXHJggBpBH4Bs9BE584MdKd2ZJCYBD18N2Q1D4zvZLAjw6qaQKHH99lCWJS9F7oUybMfgMQKBgQDLP2BBfQGo2jOedtl0Yym+eg1nNbbCOT7MNR4fWJ3BAZJSijamTORA6iHN/0eKHOhZmBt5J5D4bgC7SbkjyA0N0Cj638wLyVAQmxvTQKX80Qvp91x/AOGlP9tqq6twalf3B6qtKwB+f6S1lKvwRmDAQV4ZiyNkYyroroajP2ihLQKBgQDAUSK5WiLM1ay65Oggr1qX3goaFzTvkJnp+HDYhRO2VSPFPRh7n3vKU/jacFbhgMaz3K2vBA6LoJ05MCK5w1qMRigs+gsHkVV8x3V+I7MCji4dNc8/obi/So+iiXP2SQrnjs6J1b0mbPzuBWJ1vgIhbLMbYuPZGaMTK3YOy7QqewKBgAnCelHKueisyavDUz/WfyupWrlpB+SdsRlHN7ITpEefVrJl9qfXq2I+m+7zYjEMoE+lETSpJLn5NknICX7hXVcbdsxNMNQkD5csi5KCWTYhp6vNeACVP0CbJ2Mg6TOVt7GiCZ0VIonwgS1C/VqlVoIE4YridomchXP05XwzUEflAoGAQzSXQ9qByr7oy67uh21/5Q5MzW1KrGUFxENze9aVWuRJycVd5uWGpt/NWNhlJAySY4w8jaqHQrfv+Woe2HeyDs79fyop7I0XKLGzF092YPA6oS6KrBvhqcduhkguY+SGkQDQoE42+VSg1rS/AZJSwEdyF6HpKZbR7AMGEImS/j0CgYBhxtpqycW2jkL1/z+/AyCxEPclliw5dzPDHwi5c8OgVoSo4WtZKUJzdF+KpKrhkZIZWP40p5yMAl50p50gfbfIN1KEK7AcxoGn9xQtASONrAwdkKzJTwq5hG/vEUG9yBmaM9ErS49n52S6lJgB+wLnNetYbu5cz6Nz6NkoZkiaQg==", + ), + ), + ) { + override def regenerate() = + TestAuthenticator.createBasicAttestedCredential( + keyAlgorithm = COSEAlgorithmIdentifier.RS256, + attestationMaker = AttestationMaker.tpm( + AttestationSigner.ca( + alg = COSEAlgorithmIdentifier.RS256, + certSubject = new X500Name(Array.empty[RDN]), + certExtensions = tpmCertExtensions, + ) + ), + ) + } + + val ValidRs1: RegistrationTestData = new RegistrationTestData( + alg = COSEAlgorithmIdentifier.RS1, + attestationObject = + ByteArray.fromHex("bf68617574684461746159016849960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97634100000539000102030405060708090a0b0c0d0e0f00201bc746ca3c93b3bbe5b5feda72c0ea5ec308b8137fd541920cc65d9ecc4cd3b1a401030339fffe2059010100a2d13b2f968f779c12ecc59c29816128da728e7e17ea099483153b08fd8078ee7bd638430bd8cef561c6a6fdddcb02ca1bb54d0afc1a47e9416e92374c062fa1229376937493bae8091cfc3c6b3fe9f92440aa597bd1af96a3c72c58ef020038c73e3a9ac476856fb6a07fc5606ed4eab4c7d1567f8ed7aede16f9f86f7d1b3f8de8544aa14a26eac934fe083844a1457c60a610f8d9bc57a5dd7c197de48eaa44a64457358bab6ac1f14a63c9f662d520c2d0424c8b78de3810bb091b75786b92839db10776e2ad9572f7d222565a2b5ff6ba769f0a3c3f28a12311123531e672f55748a3772002a43935d5b1b0c1da4267466dafaead60ae3db4fd1052e37f214301000163666d746374706d6761747453746d74bf63616c6739fffe637369675901001da18c3e41a175b65fea5474f99853693d49dd9fcafbf0e6402f74ca4971babb3aed4fdda21964e18fd689ccd9e17dc0794a5a761454fe1383061e3bd898918316f571855b923087385a037f3857e839a14bcc9ff3be3e5c9adfaebad48fe649f16d6098ac9036329184c74dd45c52891de982d2c0b69e80ec10391fe321dca47af772495c69abef0d1d0b7e4749574391370373aa3afeabb2879bf98ffb0dead8119fe0e00a0e0c485f805823f97225e8c377884814ba24bd2628f54b82f73ef343d9a5fb91c02cb299afc4d45ac8888d3465794f05e9a49b8209d8907cea78e569e736185fb08847104f7bb7d6212c1b9866b6aff4e46039916419e8e1eaab63783563825903913082038d30820275a00302010202020620300d06092a864886f70d0101050500306a3126302406035504030c1d59756269636f20576562417574686e20756e6974207465737473204341310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b3009060355040613025345301e170d3138303930363137343230305a170d3138303931333137343230305a300030820122300d06092a864886f70d01010105000382010f003082010a0282010100912405950730aac20ef5dabc286c463b2212619bea17d96a6ae11780c8241b97c61ca6ccf15b4a7befbbb9ee263e980e49627517905fffd07d9eb1c91e84aa3e663a4e1bb683846961f7e1a6d5f7ac27edbc76beacc699fa3825bc44ed1667b306f1b745d3ada92c5587fde0e1a5659654bf3489455d4a604bbfbf661f462451196c2dd15db2f5e583d1936901b6a8d9a46041971705fb07eed1388ce117b423dfd29b60e20ac0ab205c081b6d0477cd8c1203a30d565c1a74623ab310d16d3e4cf486596ce103f92df8414e4835f51e96ac4ecfcd422c964cb97fd20193fd57aee06d33e48d48250189c0036aa80e3cfece7c85cd0e44ff981daf4adc69ae810203010001a381a63081a33021060b2b0601040182e51c01010404120410000102030405060708090a0b0c0d0e0f30690603551d110101ff045f305da45b305931573014060567810502010c0b69643a30303030303030303014060567810502030c0b69643a30303030303030303029060567810502020c20544553545f59756269636f5f6a6176612d776562617574686e2d73657276657230130603551d250101ff0409300706056781050803300d06092a864886f70d0101050500038201010045408e411fd40f2e7af4d26d13bd38b699a86a40c352d4a965fb6bf66c3c3e832aa01a01534925581f3ccf2cddc681b15c867c1811ee2009a10952a2f82da5ea9db2eed1791c037a6352a9d36dbc2081111b59648df3bc15e6c4e7f14e1c6635fd158d012fba273feb35ca37b7cceda750784c1e28973ef4f64bf77ca2d54d598dd86b2f4795a8e56a4ccd48b899d870685254baff6419bc45602b548ef18308baca33a23c09789eb42fb5b58edaecf1aed80ce37021cc6b4b7d047dc686a982f3ca257e91db8790a5ded4c97551528793e29f98049dad1f93dd3191ae0c3119cefc8d9a5aa61b28eb44efbc2510e1f35fafbe1ed3e39a8263cc656905df80eb590367308203633082024ba00302010202022343300d06092a864886f70d0101050500306a3126302406035504030c1d59756269636f20576562417574686e20756e6974207465737473204341310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b3009060355040613025345301e170d3138303930363137343230305a170d3138303931333137343230305a306a3126302406035504030c1d59756269636f20576562417574686e20756e6974207465737473204341310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b300906035504061302534530820122300d06092a864886f70d01010105000382010f003082010a02820101008284d456f685cedd23b2e2120de3383ef98ef86c24a50f5c94fe6930124e0b66666347a51653362bf40914750e32ed02db0b28a2c1e69dd1cc5bcff7f76cb867d56b2b4e031683efd5477b30b2ee809b121a6cbb44471d2a8a9a1dc4f32c3cb439d574360184f830157eb4f84c39b96e4fa43361810b7b89d96f368a757f3e3460117976b737719409eb1be4293f9b2735d9eb54b6ce8bd3cd2fdcef8e587c44848375b4a2c9a6915a18ed642e7b127c665ee1cc036050a5b349d3e5360ad11395471a68cde6398d7afbb124476932b2103bbe079998b8f186f01c0d2abce264dd317f99815eb5e0bbaf6f1d58ed50e77dd037b9ea4b0d171c214b57e5d2478b0203010001a3133011300f0603551d130101ff040530030101ff300d06092a864886f70d0101050500038201010048ff4b2f45d9e319578314774fd5bda5426318791ed992a8491a4f4e648767d697ff022d8115b2f949c2e823f8575f4a4e9abffa6cef86791b24dd342543e21e9c08671da1193fd949cfa52cb429bceb07bc08bf723e17c5618a71399896aedda98e7e1ed179a3b9f2e894b10d2934e8936bd5def9e64a296b597832d3350ba338dd9b40ceb987d6ad1b154cc3118a44010a343b027772601d52855f0c8df9d6fad8e5fad29153c6b1ee7098c16c8ef3dd25d68d4ee1c4a4492b5e4c3a8d206acbbc94d79cb32a2c9238ff8db2da85e2416f533e21baf62c323d8f93b487dee6e200283ca8d0874ff8ba5787317bf4fb5e523c75b8ecb42711f99efdb0f49b266863657274496e666f5851ff544347801700000014d5424d6b571459a67182bfe1d1d0df65be8446150000000000000000111111112222222233000000000000000000160004a07cb80887fd98656dcd946cb85185ce2e700d9600006376657263322e3067707562417265615901170001000400040000000000100014080000010001010100a2d13b2f968f779c12ecc59c29816128da728e7e17ea099483153b08fd8078ee7bd638430bd8cef561c6a6fdddcb02ca1bb54d0afc1a47e9416e92374c062fa1229376937493bae8091cfc3c6b3fe9f92440aa597bd1af96a3c72c58ef020038c73e3a9ac476856fb6a07fc5606ed4eab4c7d1567f8ed7aede16f9f86f7d1b3f8de8544aa14a26eac934fe083844a1457c60a610f8d9bc57a5dd7c197de48eaa44a64457358bab6ac1f14a63c9f662d520c2d0424c8b78de3810bb091b75786b92839db10776e2ad9572f7d222565a2b5ff6ba769f0a3c3f28a12311123531e672f55748a3772002a43935d5b1b0c1da4267466dafaead60ae3db4fd1052e37fffff"), + clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","type":"webauthn.create","tokenBinding":{"status":"supported"}}""", + privateKey = Some( + ByteArray.fromHex("308204c0020100300d06092a864886f70d0101010500048204aa308204a60201000282010100a2d13b2f968f779c12ecc59c29816128da728e7e17ea099483153b08fd8078ee7bd638430bd8cef561c6a6fdddcb02ca1bb54d0afc1a47e9416e92374c062fa1229376937493bae8091cfc3c6b3fe9f92440aa597bd1af96a3c72c58ef020038c73e3a9ac476856fb6a07fc5606ed4eab4c7d1567f8ed7aede16f9f86f7d1b3f8de8544aa14a26eac934fe083844a1457c60a610f8d9bc57a5dd7c197de48eaa44a64457358bab6ac1f14a63c9f662d520c2d0424c8b78de3810bb091b75786b92839db10776e2ad9572f7d222565a2b5ff6ba769f0a3c3f28a12311123531e672f55748a3772002a43935d5b1b0c1da4267466dafaead60ae3db4fd1052e37f0203010001028201010087eaac42c4a80d4c5fcc0206a3eb5a65553e6e4f3abd67b3ef5d68c3cf8350f09cb62e8f61b362c91b0f4f55fbb1be2963ca0c7f90068c635ef8e3dc7f7d6683582ecbbcba839c729930f62ba5c85c145c3c1338d21130484b7e383a21838515e0d5c4ec6ff714db361473b51c14496f88ec898770c298b064bbbf7eb1eb393396c9992b24f1d159008756565530c3bd2bf8e4d3352855890887d2eb0a1fbabeed57661bae2d159b72b4f9909ea4c34dff696788b1b71974d906bd7702663abc32ab3e2b1cd8e53a892c72d3b99621cf672f351896fee74de44c3e8c69ceabc85e4977a806e6c7a7ba50bb6fc66dfd214d1e8080cc6129e7608dcae41cbeb8e902818100d268e5a0b58bf122d06a35d94734c01816f51b659ab3fcff3f5bf272cfcd6d9ed6b8add3ede5363c0b8503a4c60826a8764d9de0b756cdfb2b672764ae8947cee902a3daa1f2dfbaf2717d27c6e66cfc9a4b468138f41ef7114ce15f042b3e2d2ad9a1b13053b7d598659881157cc0368b19b88565a9cb85238ed8635af9dff302818100c618747ed089a7cc4237d908fb0d486d2bf7a07e42658ab4c3ceab52f19722ff5da21a9f8aa2b386ea7e04dd0bd03dafdff585b34d0faadf84ddc472d6ebf82644e9281d37bcee21da8c9715018405b1f00e8333350f1ba9dcc32825ec0d7dd41a659aec04256968af1e3c909843a7f3302a6421104ccfa973cbb8b982b91d4502818100969ea49270a366d0a72500bb332fddbae0e440e270e61b5b94bd7b4718de53747afce4e26acfc40d23a9ea3bcfcf11ed5212a9cbad32a46d025aeb663552ec667f82764d11d54cb704ca9cef1680e8cfc29bd432b8d4783e20d24a1abc5f4039110d8da3cb9682689299579c4007778913f62b92c27dd3c4d0f97689591cba6502818100844adaa9c22cdc11adfb4c071259f98f66f875873c6241b29cbd8d6ed406a209b687468e5b7072c25c2192afe86ec67388f697b67975482103c372a95adcb59921163082eab152baeb104ee9695cb8ccef4b51d545cef423895a0f9adbbcdad666568a92a9e62e320a19004b7454627a2725783f187aa3883fdbc25ea96d649d02818100c8bfd89a4ba39cadbd4f08ed6ec27336d361c64f3d672cc950f4d787ea84d80461fcd3c4e8f810331e01be30bd8bb825cd29f9ebecf6a0405dbbda8643573146cff6eae9e0cb71c752d7e3b9e5de609742ac9d1ebe8942002e35a560637c2cd0fd5f6fae62c2b502fd657b71e23b5b4dcb9568e1a6ef32cbb8c351379ff656cd") + ), + attestationCertChain = List( + RegistrationTestDataGenerator.importAttestationCa( + "MIIDjTCCAnWgAwIBAgICBiAwDQYJKoZIhvcNAQEFBQAwajEmMCQGA1UEAwwdWXViaWNvIFdlYkF1dGhuIHVuaXQgdGVzdHMgQ0ExDzANBgNVBAoMBll1YmljbzEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjELMAkGA1UEBhMCU0UwHhcNMTgwOTA2MTc0MjAwWhcNMTgwOTEzMTc0MjAwWjAAMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkSQFlQcwqsIO9dq8KGxGOyISYZvqF9lqauEXgMgkG5fGHKbM8VtKe++7ue4mPpgOSWJ1F5Bf/9B9nrHJHoSqPmY6Thu2g4RpYffhptX3rCftvHa+rMaZ+jglvETtFmezBvG3RdOtqSxVh/3g4aVlllS/NIlFXUpgS7+/Zh9GJFEZbC3RXbL15YPRk2kBtqjZpGBBlxcF+wfu0TiM4Re0I9/Sm2DiCsCrIFwIG20Ed82MEgOjDVZcGnRiOrMQ0W0+TPSGWWzhA/kt+EFOSDX1HpasTs/NQiyWTLl/0gGT/Veu4G0z5I1IJQGJwANqqA48/s58hc0ORP+YHa9K3GmugQIDAQABo4GmMIGjMCEGCysGAQQBguUcAQEEBBIEEAABAgMEBQYHCAkKCwwNDg8waQYDVR0RAQH/BF8wXaRbMFkxVzAUBgVngQUCAQwLaWQ6MDAwMDAwMDAwFAYFZ4EFAgMMC2lkOjAwMDAwMDAwMCkGBWeBBQICDCBURVNUX1l1Ymljb19qYXZhLXdlYmF1dGhuLXNlcnZlcjATBgNVHSUBAf8ECTAHBgVngQUIAzANBgkqhkiG9w0BAQUFAAOCAQEARUCOQR/UDy569NJtE704tpmoakDDUtSpZftr9mw8PoMqoBoBU0klWB88zyzdxoGxXIZ8GBHuIAmhCVKi+C2l6p2y7tF5HAN6Y1Kp0228IIERG1lkjfO8FebE5/FOHGY1/RWNAS+6Jz/rNco3t8ztp1B4TB4olz709kv3fKLVTVmN2GsvR5Wo5WpMzUi4mdhwaFJUuv9kGbxFYCtUjvGDCLrKM6I8CXietC+1tY7a7PGu2AzjcCHMa0t9BH3GhqmC88olfpHbh5Cl3tTJdVFSh5Pin5gEna0fk90xka4MMRnO/I2aWqYbKOtE77wlEOHzX6++HtPjmoJjzGVpBd+A6w==", + "RSA", + "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCRJAWVBzCqwg712rwobEY7IhJhm+oX2Wpq4ReAyCQbl8YcpszxW0p777u57iY+mA5JYnUXkF//0H2esckehKo+ZjpOG7aDhGlh9+Gm1fesJ+28dr6sxpn6OCW8RO0WZ7MG8bdF062pLFWH/eDhpWWWVL80iUVdSmBLv79mH0YkURlsLdFdsvXlg9GTaQG2qNmkYEGXFwX7B+7ROIzhF7Qj39KbYOIKwKsgXAgbbQR3zYwSA6MNVlwadGI6sxDRbT5M9IZZbOED+S34QU5INfUelqxOz81CLJZMuX/SAZP9V67gbTPkjUglAYnAA2qoDjz+znyFzQ5E/5gdr0rcaa6BAgMBAAECggEAfPqIuAA9/vwlh7z3gtNhUnAPZe+tDyZPRYNYCrPMq9nwZSGYnkhfBgO0IfGZCxNCUhyu+UB/+bcdRLaQmW/hbOP4VuP0MKGnYQ3jSBc9Mwga5ctWe050rosEq26qvT1EYrlneIBDLMaZTAXoTEVxCZcmImYFzcRK0U9mz9gkPQYteuLAzOpRyYAc4UPLK7iGNx0flp3UD/FRRDsAL6gH23YSA6iDG/wKlS9TdcBByTX29YEilwsmO64yplg4Wgr6aJNuFULxOUWUzgDDhY9j76AMD9xF2jppPUllLQaPl5/8MXA/bKtKXcXRdthRiIGKyz+oGUJu72nbEzf2lDLvxQKBgQDdT5Ly/fSJh7bKUXT7qJBsxkzo4YXJ4qzy2U3ScFSUyZm1MsI9SS5x5GwVohdE6FlI2VBANBMxgay8sTjtwq5WkU/q5yMxsOoAijNYNZc1YoywAHGKiC/l1yiMhGiLVIvpe7pjnuC5zTCWQ+iJyaLefITToSz3+lKo/RkuRpo2zwKBgQCn5AIj4IDNht5zZLwjaqJEZVIj231Fjxs4Z11kS/QH9gHYW4ep2SQyQGyVxxysirb8rdewrG0eCVH+rIQ31TkoahEF5wWNGeptmTchesjEVr8yNkdV0VfzE7NaHPSm2H48jo//Z75MK8Rzz4eN2GURGyko5CRe614hgffUhRwZrwKBgGsJfog53Zja48SMiyjgSSHi8vW7haq0EHPQN/xsyevAabAioaFkkKsTEFeSMvDn867xNAgpZ5MNJc+JY4BTJWDHHUD+k54H89VZAiZKnRx70pGZVVDsN0ZRvtHfhHTG6nh9mBNwlz4mCLbUl1Z1CGnVDaURkh9JmcsTxqcEDLgvAoGBAIhL3kDp/SbdGrJrUSEfbGRCLRDXGzfhGaQMphDKaG4eFRlkFRqaIXx6OKzPXEPmyO8Q4k2XbW44+svZme0JuMFKek9kYWlPZLVc8RjI6TwbgFRvJDJTJSc9ExlQ8HySvMjEo7ogqqiDz5SFIfLRfhsJBb0gmTZFtcFWFa/97/YZAoGBALzrMvru5IO6IYAjtl7XLqpWqIKtDpvG25S+FpzsK32VXbQNVQGNqcRHkusvYqnT80J6S/JAqkzUOf+vtHv91duX0IAjKb12VLPNGnZYXtiJc4TYHQa8jN6CGg66z0qgNuQ3KO74SgD/KZVUv9rlKUxd7W8zUPNMsaEDUERlEWxv", + ), + RegistrationTestDataGenerator.importAttestationCa( + "MIIDYzCCAkugAwIBAgICI0MwDQYJKoZIhvcNAQEFBQAwajEmMCQGA1UEAwwdWXViaWNvIFdlYkF1dGhuIHVuaXQgdGVzdHMgQ0ExDzANBgNVBAoMBll1YmljbzEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjELMAkGA1UEBhMCU0UwHhcNMTgwOTA2MTc0MjAwWhcNMTgwOTEzMTc0MjAwWjBqMSYwJAYDVQQDDB1ZdWJpY28gV2ViQXV0aG4gdW5pdCB0ZXN0cyBDQTEPMA0GA1UECgwGWXViaWNvMSIwIAYDVQQLDBlBdXRoZW50aWNhdG9yIEF0dGVzdGF0aW9uMQswCQYDVQQGEwJTRTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAIKE1Fb2hc7dI7LiEg3jOD75jvhsJKUPXJT+aTASTgtmZmNHpRZTNiv0CRR1DjLtAtsLKKLB5p3RzFvP9/dsuGfVaytOAxaD79VHezCy7oCbEhpsu0RHHSqKmh3E8yw8tDnVdDYBhPgwFX60+Ew5uW5PpDNhgQt7idlvNop1fz40YBF5drc3cZQJ6xvkKT+bJzXZ61S2zovTzS/c745YfESEg3W0osmmkVoY7WQuexJ8Zl7hzANgUKWzSdPlNgrRE5VHGmjN5jmNevuxJEdpMrIQO74HmZi48YbwHA0qvOJk3TF/mYFeteC7r28dWO1Q533QN7nqSw0XHCFLV+XSR4sCAwEAAaMTMBEwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEASP9LL0XZ4xlXgxR3T9W9pUJjGHke2ZKoSRpPTmSHZ9aX/wItgRWy+UnC6CP4V19KTpq/+mzvhnkbJN00JUPiHpwIZx2hGT/ZSc+lLLQpvOsHvAi/cj4XxWGKcTmYlq7dqY5+HtF5o7ny6JSxDSk06JNr1d755kopa1l4MtM1C6M43ZtAzrmH1q0bFUzDEYpEAQo0OwJ3cmAdUoVfDI351vrY5frSkVPGse5wmMFsjvPdJdaNTuHEpEkrXkw6jSBqy7yU15yzKiySOP+NstqF4kFvUz4huvYsMj2Pk7SH3ubiACg8qNCHT/i6V4cxe/T7XlI8dbjstCcR+Z79sPSbJg==", + "RSA", + "MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCChNRW9oXO3SOy4hIN4zg++Y74bCSlD1yU/mkwEk4LZmZjR6UWUzYr9AkUdQ4y7QLbCyiiwead0cxbz/f3bLhn1WsrTgMWg+/VR3swsu6AmxIabLtERx0qipodxPMsPLQ51XQ2AYT4MBV+tPhMObluT6QzYYELe4nZbzaKdX8+NGAReXa3N3GUCesb5Ck/myc12etUts6L080v3O+OWHxEhIN1tKLJppFaGO1kLnsSfGZe4cwDYFCls0nT5TYK0ROVRxpozeY5jXr7sSRHaTKyEDu+B5mYuPGG8BwNKrziZN0xf5mBXrXgu69vHVjtUOd90De56ksNFxwhS1fl0keLAgMBAAECggEAe2nAIo6uTcF6rP3pFmqg16NAFhSjvdO9tkCuE79rPopQDFZFesup8HurTkW07GCCD78IaIWyW85yTupiTPnnkH8T+/mjH9oXoHMbwBuhO8floUjo9hHMOVqficCeM1kfDYSRgzOCmO9Wk93o3qLCfNUfrVnoHIRu/0OSre+WJqkax889ugpGnj/8JplsvaPb648VX8zhgArLSxolNYtVbpR8FCDpD46PxJAK95hGoV1LT+hfw7XWRBqszoX0vf8KMrJZxJQb9WqND1Z+E2llGBkkTj2TyJTu92GLH1sXCZfShWLqOK0qXTvONYrKbUrvFvs2yL+w5wfgcyFYzHqkIQKBgQDmAr62Q6t+nDr098wvcB0+AbO3kgup9+GXToDJ1HKob1fhuFBSnbb6rmYaNcefU6j36I+S3TnyJMK77fINAZvTxUEShTi58hlXaGECshEpoA/sDUNKhEgnZL/d0iOPUDGrWl28Xd4nggnIWM2ay0faYqhHBk/hFFgCf34blHExUQKBgQCRRDK9zVpECvIBAes45GFc3U0XtfOzQyDVKMsHZI4oTCiXzl3X1LBMfgBI8q1w/sV/pp6ATX3xw+FV8ASv/Jrcb50mCbzvyyWWmze4BIfK/6AHvu/d+l3DfD16H6eNyjB3urqrrcGK0dPzCXfJGtRIeXSQl3wmpffs7vvdFZvUGwKBgGzQB6D6SntPNaUGRZZ7l5np/Ddv5Ay4tAR8ovjYhJWidxTVuUocSqA1OSBVKOb7EQiXALUd63feZDG707LLfAinXK2CUN/G5K4xNxOrYesrSd0GOTtbcTcRtqMd8qyt55GE4qtmTN6r6izZtgrw+EEcQze2iLuWgDxOTD6H7zTBAoGAGF1H5W275g2v6VtZCIHFkr3LYF60MSzhJN9iriq/bSLY7B1AwCJ8L1gy1Alf7cPNfEuF6h8VMKeZ87/+CUZk8vQFd4vKlK2N8GS4Q7T+0Z1uSd5MfP6xG0iLRyBoCfAPkQVQqdrKy23GDs1XufM2B7HXSykdOZ48pxCuTbaCHGECgYBk7LdRO6MAK48JqUW3TDHRg3cLnC+uDRBFJLcC5VpmISJtli9UE26mSGDH1C/KbYfLfyAXe/6KRn8Rr9tazqrc28u6sb2vliyUg4fQley1NqbQ1cmP3wBve/US+NOYt9UqOK59YX6E+5ZVgsy3hOBrrctT0mYshd6df6Cl8crfAw==", + ), + ), + ) { + override def regenerate() = + TestAuthenticator.createBasicAttestedCredential( + keyAlgorithm = COSEAlgorithmIdentifier.RS1, + attestationMaker = AttestationMaker.tpm( + AttestationSigner.ca( + alg = COSEAlgorithmIdentifier.RS1, + certSubject = new X500Name(Array.empty[RDN]), + certExtensions = tpmCertExtensions, + ) + ), + ) + } } + + def from( + credential: PublicKeyCredential[ + AuthenticatorAttestationResponse, + ClientRegistrationExtensionOutputs, + ], + keypair: KeyPair, + attestationCertChain: List[(X509Certificate, PrivateKey)], + ): RegistrationTestData = + RegistrationTestData( + alg = WebAuthnCodecs + .getCoseKeyAlg( + credential.getResponse.getParsedAuthenticatorData.getAttestedCredentialData.get.getCredentialPublicKey + ) + .get, + attestationObject = credential.getResponse.getAttestationObject, + clientDataJson = new String( + credential.getResponse.getClientDataJSON.getBytes, + StandardCharsets.UTF_8, + ), + privateKey = Some(new ByteArray(keypair.getPrivate.getEncoded)), + attestationCertChain = attestationCertChain, + ) } case class RegistrationTestData( @@ -658,15 +908,8 @@ case class RegistrationTestData( def regenerateFull(): Try[RegistrationTestData] = Try({ val (credential, keypair, attestationCertChain) = regenerate() - val newValue = copy( - attestationObject = credential.getResponse.getAttestationObject, - clientDataJson = new String( - credential.getResponse.getClientDataJSON.getBytes, - StandardCharsets.UTF_8, - ), - privateKey = Some(new ByteArray(keypair.getPrivate.getEncoded)), - attestationCertChain = attestationCertChain, - ) + val newValue = + RegistrationTestData.from(credential, keypair, attestationCertChain) newValue.copy( assertion = newValue.assertion.map(_.regenerate(newValue)) ) 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 141b927ed..79f12ea96 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 @@ -25,6 +25,7 @@ package com.yubico.webauthn import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.node.ArrayNode import com.fasterxml.jackson.databind.node.JsonNodeFactory import com.fasterxml.jackson.databind.node.ObjectNode import com.upokecenter.cbor.CBORObject @@ -33,10 +34,15 @@ import com.yubico.internal.util.CertificateParser import com.yubico.internal.util.JacksonCodecs import com.yubico.webauthn.TestAuthenticator.AttestationCert import com.yubico.webauthn.TestAuthenticator.AttestationMaker +import com.yubico.webauthn.TestAuthenticator.AttestationSigner +import com.yubico.webauthn.TpmAttestationStatementVerifier.Attributes +import com.yubico.webauthn.TpmAttestationStatementVerifier.TPM_ALG_NULL +import com.yubico.webauthn.TpmAttestationStatementVerifier.TpmRsaScheme import com.yubico.webauthn.attestation.AttestationTrustSource import com.yubico.webauthn.attestation.AttestationTrustSource.TrustRootsResult import com.yubico.webauthn.data.AttestationObject import com.yubico.webauthn.data.AttestationType +import com.yubico.webauthn.data.AuthenticatorAttestationResponse import com.yubico.webauthn.data.AuthenticatorData import com.yubico.webauthn.data.AuthenticatorSelectionCriteria import com.yubico.webauthn.data.AuthenticatorTransport @@ -63,11 +69,21 @@ import com.yubico.webauthn.extension.uvm.UserVerificationMethod import com.yubico.webauthn.test.Helpers import com.yubico.webauthn.test.RealExamples import com.yubico.webauthn.test.Util.toStepWithUtilities +import org.bouncycastle.asn1.ASN1Encodable +import org.bouncycastle.asn1.ASN1ObjectIdentifier import org.bouncycastle.asn1.DEROctetString +import org.bouncycastle.asn1.DERSequence +import org.bouncycastle.asn1.DERUTF8String +import org.bouncycastle.asn1.x500.AttributeTypeAndValue +import org.bouncycastle.asn1.x500.RDN import org.bouncycastle.asn1.x500.X500Name +import org.bouncycastle.asn1.x509.Extension +import org.bouncycastle.asn1.x509.GeneralName +import org.bouncycastle.asn1.x509.GeneralNamesBuilder import org.bouncycastle.cert.jcajce.JcaX500NameUtil import org.junit.runner.RunWith import org.mockito.Mockito +import org.scalacheck.Arbitrary.arbitrary import org.scalacheck.Gen import org.scalatest.FunSpec import org.scalatest.Matchers @@ -75,6 +91,7 @@ import org.scalatestplus.junit.JUnitRunner import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks import java.io.IOException +import java.math.BigInteger import java.nio.charset.Charset import java.nio.charset.StandardCharsets import java.security.KeyFactory @@ -86,6 +103,7 @@ import java.security.cert.CRL import java.security.cert.CertStore import java.security.cert.CollectionCertStoreParameters import java.security.cert.X509Certificate +import java.security.interfaces.ECPublicKey import java.security.interfaces.RSAPublicKey import java.time.Clock import java.time.Instant @@ -132,7 +150,7 @@ class RelyingPartyRegistrationSpec Helpers.CredentialRepository.unimplemented, attestationTrustSource: Option[AttestationTrustSource] = None, origins: Option[Set[String]] = None, - preferredPubkeyParams: List[PublicKeyCredentialParameters] = Nil, + pubkeyCredParams: Option[List[PublicKeyCredentialParameters]] = None, testData: RegistrationTestData, clock: Clock = Clock.systemUTC(), ): FinishRegistrationSteps = { @@ -140,7 +158,6 @@ class RelyingPartyRegistrationSpec .builder() .identity(testData.rpId) .credentialRepository(credentialRepository) - .preferredPubkeyParams(preferredPubkeyParams.asJava) .allowOriginPort(allowOriginPort) .allowOriginSubdomain(allowOriginSubdomain) .allowUntrustedAttestation(allowUntrustedAttestation) @@ -155,7 +172,11 @@ class RelyingPartyRegistrationSpec builder .build() ._finishRegistration( - testData.request, + pubkeyCredParams + .map(pkcp => + testData.request.toBuilder.pubKeyCredParams(pkcp.asJava).build() + ) + .getOrElse(testData.request), testData.response, callerTokenBindingId.toJava, ) @@ -2053,29 +2074,832 @@ class RelyingPartyRegistrationSpec } } - it("The tpm statement format is supported.") { - val testData = RegistrationTestData.Tpm.RealExample - val steps = - finishRegistration( - testData = testData, - origins = - Some(Set("https://dev.d2urpypvrhb05x.amplifyapp.com")), - credentialRepository = Helpers.CredentialRepository.empty, - attestationTrustSource = Some( - trustSourceWith( - testData.attestationRootCertificate.get, - enableRevocationChecking = false, - ) - ), + describe("The tpm statement format") { + + it("is supported.") { + val testData = RegistrationTestData.Tpm.RealExample + val steps = + finishRegistration( + testData = testData, + origins = + Some(Set("https://dev.d2urpypvrhb05x.amplifyapp.com")), + credentialRepository = Helpers.CredentialRepository.empty, + attestationTrustSource = Some( + trustSourceWith( + testData.attestationRootCertificate.get, + enableRevocationChecking = false, + ) + ), + ) + val step: FinishRegistrationSteps#Step19 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next + + step.validations shouldBe a[Success[_]] + step.tryNext shouldBe a[Success[_]] + step.run.getAttestationType should be( + AttestationType.ATTESTATION_CA ) - val step: FinishRegistrationSteps#Step19 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next + } - step.validations shouldBe a[Success[_]] - step.tryNext shouldBe a[Success[_]] - step.run.getAttestationType should be( - AttestationType.ATTESTATION_CA - ) + describe("is supported and accepts test-generated values:") { + + val emptySubject = new X500Name(Array.empty[RDN]) + val tcgAtTpmManufacturer = new AttributeTypeAndValue( + new ASN1ObjectIdentifier("2.23.133.2.1"), + new DERUTF8String("id:00000000"), + ) + val tcgAtTpmModel = new AttributeTypeAndValue( + new ASN1ObjectIdentifier("2.23.133.2.2"), + new DERUTF8String("TEST_Yubico_java-webauthn-server"), + ) + val tcgAtTpmVersion = new AttributeTypeAndValue( + new ASN1ObjectIdentifier("2.23.133.2.3"), + new DERUTF8String("id:00000000"), + ) + val tcgKpAikCertificate = new ASN1ObjectIdentifier("2.23.133.8.3") + + def makeCred( + authDataAndKeypair: Option[(ByteArray, KeyPair)] = None, + clientDataJson: Option[String] = None, + subject: X500Name = emptySubject, + rdn: Array[AttributeTypeAndValue] = + Array(tcgAtTpmManufacturer, tcgAtTpmModel, tcgAtTpmVersion), + extendedKeyUsage: Array[ASN1Encodable] = + Array(tcgKpAikCertificate), + ver: Option[String] = Some("2.0"), + magic: ByteArray = + TpmAttestationStatementVerifier.TPM_GENERATED_VALUE, + `type`: ByteArray = + TpmAttestationStatementVerifier.TPM_ST_ATTEST_CERTIFY, + modifyAttestedName: ByteArray => ByteArray = an => an, + overrideCosePubkey: Option[ByteArray] = None, + aaguidInCert: Option[ByteArray] = None, + attributes: Option[Long] = None, + symmetric: Option[Int] = None, + scheme: Option[Int] = None, + ): ( + PublicKeyCredential[ + AuthenticatorAttestationResponse, + ClientRegistrationExtensionOutputs, + ], + KeyPair, + List[(X509Certificate, PrivateKey)], + ) = { + val (authData, credentialKeypair) = + authDataAndKeypair.getOrElse( + TestAuthenticator.createAuthenticatorData(keyAlgorithm = + COSEAlgorithmIdentifier.ES256 + ) + ) + + TestAuthenticator.createCredential( + authDataBytes = authData, + credentialKeypair = credentialKeypair, + clientDataJson = clientDataJson.getOrElse( + TestAuthenticator.createClientData() + ), + attestationMaker = AttestationMaker.tpm( + cert = AttestationSigner.ca( + alg = COSEAlgorithmIdentifier.ES256, + certSubject = subject, + aaguid = aaguidInCert, + certExtensions = List( + ( + Extension.subjectAlternativeName.getId, + true, + new GeneralNamesBuilder() + .addName( + new GeneralName(new X500Name(Array(new RDN(rdn)))) + ) + .build(), + ), + ( + Extension.extendedKeyUsage.getId, + true, + new DERSequence(extendedKeyUsage), + ), + ), + validFrom = Instant.now(), + validTo = Instant.now().plusSeconds(600), + ), + ver = ver, + magic = magic, + `type` = `type`, + modifyAttestedName = modifyAttestedName, + overrideCosePubkey = overrideCosePubkey, + attributes = attributes, + symmetric = symmetric, + scheme = scheme, + ), + ) + } + + def init( + testData: RegistrationTestData + ): FinishRegistrationSteps#Step19 = { + val steps = + finishRegistration( + credentialRepository = Helpers.CredentialRepository.empty, + testData = testData, + attestationTrustSource = Some( + trustSourceWith( + testData.attestationCertChain.last._1, + enableRevocationChecking = false, + ) + ), + ) + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next + } + + def check( + testData: RegistrationTestData, + pubKeyCredParams: Option[ + List[PublicKeyCredentialParameters] + ] = None, + ) = { + val steps = + finishRegistration( + testData = testData, + credentialRepository = Helpers.CredentialRepository.empty, + attestationTrustSource = Some( + trustSourceWith( + testData.attestationRootCertificate.getOrElse( + testData.attestationCertChain.last._1 + ), + enableRevocationChecking = false, + ) + ), + pubkeyCredParams = pubKeyCredParams, + clock = Clock.fixed( + TestAuthenticator.Defaults.certValidFrom, + ZoneOffset.UTC, + ), + ) + val step: FinishRegistrationSteps#Step19 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next + + step.validations shouldBe a[Success[_]] + step.tryNext shouldBe a[Success[_]] + step.run.getAttestationType should be( + AttestationType.ATTESTATION_CA + ) + } + + it("ES256.") { + check(RegistrationTestData.Tpm.ValidEs256) + } + it("ES384.") { + check(RegistrationTestData.Tpm.ValidEs384) + } + it("ES512.") { + check(RegistrationTestData.Tpm.ValidEs512) + } + it("RS256.") { + check(RegistrationTestData.Tpm.ValidRs256) + } + it("RS1.") { + check( + RegistrationTestData.Tpm.ValidRs1, + pubKeyCredParams = + Some(List(PublicKeyCredentialParameters.RS1)), + ) + } + + it("Default cert generator settings.") { + val testData = (RegistrationTestData.from _).tupled(makeCred()) + val step = init(testData) + + step.validations shouldBe a[Success[_]] + step.tryNext shouldBe a[Success[_]] + step.run.getAttestationType should be( + AttestationType.ATTESTATION_CA + ) + } + + describe("Verify that the public key specified by the parameters and unique fields of pubArea is identical to the credentialPublicKey in the attestedCredentialData in authenticatorData.") { + it("Fails when EC key is unrelated but on the same curve.") { + val testData = (RegistrationTestData.from _).tupled( + makeCred( + overrideCosePubkey = Some( + WebAuthnTestCodecs.ecPublicKeyToCose( + TestAuthenticator + .generateEcKeypair() + .getPublic + .asInstanceOf[ECPublicKey] + ) + ) + ) + ) + val step = init(testData) + + step.validations shouldBe a[Failure[_]] + step.tryNext shouldBe a[Failure[_]] + step.validations.failed.get.getMessage should include( + "EC X coordinate differs" + ) + } + + it("Fails when EC key is on a different curve.") { + val testData = (RegistrationTestData.from _).tupled( + makeCred( + overrideCosePubkey = Some( + WebAuthnTestCodecs.ecPublicKeyToCose( + TestAuthenticator + .generateEcKeypair("secp384r1") + .getPublic + .asInstanceOf[ECPublicKey] + ) + ) + ) + ) + val step = init(testData) + + step.validations shouldBe a[Failure[_]] + step.tryNext shouldBe a[Failure[_]] + step.validations.failed.get.getMessage should include( + "elliptic curve differs" + ) + } + + it("Fails when EC key has an inverted Y coordinate.") { + val (authData, keypair) = + TestAuthenticator.createAuthenticatorData(keyAlgorithm = + COSEAlgorithmIdentifier.ES256 + ) + + val cose = CBORObject.DecodeFromBytes( + WebAuthnTestCodecs + .ecPublicKeyToCose( + keypair.getPublic.asInstanceOf[ECPublicKey] + ) + .getBytes + ) + cose.Set( + -3, + TestAuthenticator.Es256PrimeModulus + .subtract( + new BigInteger(1, cose.get(-3).GetByteString()) + ), // Setting to BigInteger seems to work, but Array[Byte] does not + ) + val testData = (RegistrationTestData.from _).tupled( + makeCred( + authDataAndKeypair = Some((authData, keypair)), + overrideCosePubkey = + Some(new ByteArray(cose.EncodeToBytes())), + ) + ) + val step = init(testData) + + step.validations shouldBe a[Failure[_]] + step.tryNext shouldBe a[Failure[_]] + step.validations.failed.get.getMessage should include( + "EC Y coordinate differs" + ) + } + + it("Fails when RSA key is unrelated.") { + val (authData, keypair) = + TestAuthenticator.createAuthenticatorData(keyAlgorithm = + COSEAlgorithmIdentifier.RS256 + ) + val testData = (RegistrationTestData.from _).tupled( + makeCred( + authDataAndKeypair = Some((authData, keypair)), + overrideCosePubkey = Some( + WebAuthnTestCodecs.rsaPublicKeyToCose( + TestAuthenticator + .generateRsaKeypair() + .getPublic + .asInstanceOf[RSAPublicKey], + COSEAlgorithmIdentifier.RS256, + ) + ), + ) + ) + val step = init(testData) + + step.validations shouldBe a[Failure[_]] + step.tryNext shouldBe a[Failure[_]] + } + } + + it("""The "ver" property must equal "2.0".""") { + forAll( + Gen.option( + Gen.oneOf( + Gen.numStr, + for { + major <- arbitrary[Int] + minor <- arbitrary[Int] + } yield s"${major}.${minor}", + arbitrary[String], + ) + ) + ) { ver: Option[String] => + whenever(!ver.contains("2.0")) { + val testData = + (RegistrationTestData.from _).tupled(makeCred(ver = ver)) + val step = init(testData) + + step.validations shouldBe a[Failure[_]] + step.tryNext shouldBe a[Failure[_]] + } + } + } + + it("""Verify that magic is set to TPM_GENERATED_VALUE.""") { + forAll(byteArray(4)) { magic => + whenever( + magic != TpmAttestationStatementVerifier.TPM_GENERATED_VALUE + ) { + val testData = (RegistrationTestData.from _).tupled( + makeCred(magic = magic) + ) + val step = init(testData) + + step.validations shouldBe a[Failure[_]] + step.tryNext shouldBe a[Failure[_]] + } + } + } + + it("""Verify that type is set to TPM_ST_ATTEST_CERTIFY.""") { + forAll( + Gen.oneOf( + byteArray(2), + flipOneBit( + TpmAttestationStatementVerifier.TPM_ST_ATTEST_CERTIFY + ), + ) + ) { `type` => + whenever( + `type` != TpmAttestationStatementVerifier.TPM_ST_ATTEST_CERTIFY + ) { + val testData = (RegistrationTestData.from _).tupled( + makeCred(`type` = `type`) + ) + val step = init(testData) + + step.validations shouldBe a[Failure[_]] + step.tryNext shouldBe a[Failure[_]] + } + } + } + + it("""Verify that extraData is set to the hash of attToBeSigned using the hash algorithm employed in "alg".""") { + val testData = (RegistrationTestData.from _).tupled(makeCred()) + val json = JacksonCodecs.json() + val clientData = json + .readTree(testData.clientDataJson) + .asInstanceOf[ObjectNode] + clientData.set( + "challenge", + jsonFactory.textNode( + Crypto + .sha256( + ByteArray.fromBase64Url( + clientData.get("challenge").textValue + ) + ) + .getBase64Url + ), + ) + val mutatedTestData = testData.copy(clientDataJson = + json.writeValueAsString(clientData) + ) + val step = init(mutatedTestData) + + step.validations shouldBe a[Failure[_]] + step.tryNext shouldBe a[Failure[_]] + } + + it("Verify that attested contains a TPMS_CERTIFY_INFO structure as specified in [TPMv2-Part2] section 10.12.3, whose name field contains a valid Name for pubArea, as computed using the algorithm in the nameAlg field of pubArea using the procedure specified in [TPMv2-Part1] section 16.") { + forAll( + Gen.oneOf( + for { + flipBitIndex: Int <- + Gen.oneOf(Gen.const(0), Gen.posNum[Int]) + } yield (an: ByteArray) => + flipBit(flipBitIndex % (8 * an.size()))(an), + for { + attestedName <- arbitrary[ByteArray] + } yield (_: ByteArray) => attestedName, + ) + ) { (modifyAttestedName: ByteArray => ByteArray) => + val testData = (RegistrationTestData.from _).tupled( + makeCred(modifyAttestedName = modifyAttestedName) + ) + val step = init(testData) + + step.validations shouldBe a[Failure[_]] + step.tryNext shouldBe a[Failure[_]] + } + } + + it("Verify the sig is a valid signature over certInfo using the attestation public key in aikCert with the algorithm specified in alg.") { + val testData = (RegistrationTestData.from _).tupled(makeCred()) + forAll( + flipOneBit( + new ByteArray( + new AttestationObject( + testData.attestationObject + ).getAttestationStatement.get("sig").binaryValue() + ) + ) + ) { sig => + val mutatedTestData = testData.updateAttestationObject( + "attStmt", + attStmt => + attStmt + .asInstanceOf[ObjectNode] + .set[ObjectNode]( + "sig", + jsonFactory.binaryNode(sig.getBytes), + ), + ) + val step = init(mutatedTestData) + + step.validations shouldBe a[Failure[_]] + step.tryNext shouldBe a[Failure[_]] + } + } + + describe("Verify that aikCert meets the requirements in §8.3.1 TPM Attestation Statement Certificate Requirements.") { + it("Version MUST be set to 3.") { + val testData = + (RegistrationTestData.from _).tupled(makeCred()) + forAll(arbitrary[Byte] suchThat { _ != 2 }) { version => + val mutatedTestData = testData.updateAttestationObject( + "attStmt", + attStmt => { + val origAikCert = attStmt + .get("x5c") + .get(0) + .binaryValue + + val x509VerOffset = 12 + attStmt + .get("x5c") + .asInstanceOf[ArrayNode] + .set(0, origAikCert.updated(x509VerOffset, version)) + attStmt + }, + ) + val step = init(mutatedTestData) + + step.validations shouldBe a[Failure[_]] + step.tryNext shouldBe a[Failure[_]] + } + } + + describe("Subject field MUST be set to empty.") { + it("Fails if a subject is set.") { + val testData = (RegistrationTestData.from _).tupled( + makeCred(subject = + new X500Name( + Array( + new RDN( + Array( + tcgAtTpmManufacturer, + tcgAtTpmModel, + tcgAtTpmVersion, + ) + ) + ) + ) + ) + ) + val step = init(testData) + + step.validations shouldBe a[Failure[_]] + step.tryNext shouldBe a[Failure[_]] + } + } + + describe("The Subject Alternative Name extension MUST be set as defined in [TPMv2-EK-Profile] section 3.2.9.") { + it("Fails when manufacturer is absent.") { + val testData = (RegistrationTestData.from _).tupled( + makeCred(rdn = Array(tcgAtTpmModel, tcgAtTpmVersion)) + ) + val step = init(testData) + + step.validations shouldBe a[Failure[_]] + step.tryNext shouldBe a[Failure[_]] + } + + it("Fails when model is absent.") { + val testData = (RegistrationTestData.from _).tupled( + makeCred(rdn = + Array(tcgAtTpmManufacturer, tcgAtTpmVersion) + ) + ) + val step = init(testData) + + step.validations shouldBe a[Failure[_]] + step.tryNext shouldBe a[Failure[_]] + } + + it("Fails when version is absent.") { + val testData = (RegistrationTestData.from _).tupled( + makeCred(rdn = Array(tcgAtTpmManufacturer, tcgAtTpmModel)) + ) + val step = init(testData) + + step.validations shouldBe a[Failure[_]] + step.tryNext shouldBe a[Failure[_]] + } + } + + describe("The Extended Key Usage extension MUST contain the OID 2.23.133.8.3 (\"joint-iso-itu-t(2) internationalorganizations(23) 133 tcg-kp(8) tcg-kp-AIKCertificate(3)\").") { + it("Fails when extended key usage is empty.") { + val testData = (RegistrationTestData.from _).tupled( + makeCred(extendedKeyUsage = Array.empty) + ) + val step = init(testData) + + step.validations shouldBe a[Failure[_]] + step.tryNext shouldBe a[Failure[_]] + } + + it("""Fails when extended key usage contains only "serverAuth".""") { + val testData = (RegistrationTestData.from _).tupled( + makeCred(extendedKeyUsage = + Array(new ASN1ObjectIdentifier("1.3.6.1.5.5.7.3.1")) + ) + ) + val step = init(testData) + + step.validations shouldBe a[Failure[_]] + step.tryNext shouldBe a[Failure[_]] + } + } + + describe("The Basic Constraints extension MUST have the CA component set to false.") { + it( + "Fails when the attestation cert is a self-signed CA cert." + ) { + val testData = (RegistrationTestData.from _).tupled( + TestAuthenticator.createBasicAttestedCredential( + keyAlgorithm = COSEAlgorithmIdentifier.ES256, + attestationMaker = AttestationMaker.tpm( + AttestationSigner.selfsigned( + alg = COSEAlgorithmIdentifier.ES256, + certSubject = emptySubject, + issuerSubject = + Some(TestAuthenticator.Defaults.caCertSubject), + certExtensions = List( + ( + Extension.subjectAlternativeName.getId, + true, + new GeneralNamesBuilder() + .addName( + new GeneralName( + new X500Name( + Array( + new RDN( + Array( + tcgAtTpmManufacturer, + tcgAtTpmModel, + tcgAtTpmVersion, + ) + ) + ) + ) + ) + ) + .build(), + ), + ( + Extension.extendedKeyUsage.getId, + true, + new DERSequence(tcgKpAikCertificate), + ), + ), + validFrom = Instant.now(), + validTo = Instant.now().plusSeconds(600), + isCa = true, + ) + ), + ) + ) + val step = init(testData) + testData.attestationCertChain.head._1.getBasicConstraints should not be (-1) + + step.validations shouldBe a[Failure[_]] + step.tryNext shouldBe a[Failure[_]] + } + } + + describe("An Authority Information Access (AIA) extension with entry id-ad-ocsp and a CRL Distribution Point extension [RFC5280] are both OPTIONAL as the status of many attestation certificates is available through metadata services. See, for example, the FIDO Metadata Service [FIDOMetadataService].") { + it("Nothing to test.") {} + } + } + + describe("If aikCert contains an extension with OID 1.3.6.1.4.1.45724.1.1.4 (id-fido-gen-ce-aaguid) verify that the value of this extension matches the aaguid in authenticatorData.") { + it("Succeeds if the cert does not have the extension.") { + val testData = (RegistrationTestData.from _).tupled( + makeCred(aaguidInCert = None) + ) + val step = init(testData) + + step.validations shouldBe a[Success[_]] + step.tryNext shouldBe a[Success[_]] + } + + it( + "Succeeds if the cert has the extension with the right value." + ) { + forAll(byteArray(16)) { aaguid => + val (authData, keypair) = + TestAuthenticator.createAuthenticatorData( + aaguid = aaguid, + keyAlgorithm = COSEAlgorithmIdentifier.ES256, + ) + val testData = (RegistrationTestData.from _).tupled( + makeCred( + authDataAndKeypair = Some((authData, keypair)), + aaguidInCert = Some(aaguid), + ) + ) + val step = init(testData) + + step.validations shouldBe a[Success[_]] + step.tryNext shouldBe a[Success[_]] + } + } + + it( + "Fails if the cert has the extension with the wrong value." + ) { + forAll(byteArray(16), byteArray(16)) { + (aaguidInCred, aaguidInCert) => + whenever(aaguidInCred != aaguidInCert) { + val (authData, keypair) = + TestAuthenticator.createAuthenticatorData( + aaguid = aaguidInCred, + keyAlgorithm = COSEAlgorithmIdentifier.ES256, + ) + val testData = (RegistrationTestData.from _).tupled( + makeCred( + authDataAndKeypair = Some((authData, keypair)), + aaguidInCert = Some(aaguidInCert), + ) + ) + val step = init(testData) + + step.validations shouldBe a[Failure[_]] + step.tryNext shouldBe a[Failure[_]] + } + } + } + } + + describe("Other requirements:") { + it("RSA keys must have the SIGN_ENCRYPT attribute.") { + forAll(Gen.chooseNum(0, Int.MaxValue.toLong * 2 + 1)) { + attributes: Long => + val testData = (RegistrationTestData.from _).tupled( + makeCred( + authDataAndKeypair = Some( + TestAuthenticator + .createAuthenticatorData(keyAlgorithm = + COSEAlgorithmIdentifier.RS256 + ) + ), + attributes = + Some(attributes & ~Attributes.SIGN_ENCRYPT), + ) + ) + val step = init(testData) + testData.alg should be(COSEAlgorithmIdentifier.RS256) + + step.validations shouldBe a[Failure[_]] + step.tryNext shouldBe a[Failure[_]] + } + } + + it("""RSA keys must have "symmetric" set to TPM_ALG_NULL""") { + forAll(Gen.chooseNum(0, Short.MaxValue * 2 + 1)) { + symmetric: Int => + whenever(symmetric != TPM_ALG_NULL) { + val testData = (RegistrationTestData.from _).tupled( + makeCred( + authDataAndKeypair = Some( + TestAuthenticator + .createAuthenticatorData(keyAlgorithm = + COSEAlgorithmIdentifier.RS256 + ) + ), + symmetric = Some(symmetric), + ) + ) + val step = init(testData) + testData.alg should be(COSEAlgorithmIdentifier.RS256) + + step.validations shouldBe a[Failure[_]] + step.tryNext shouldBe a[Failure[_]] + } + } + } + + it("""RSA keys must have "scheme" set to TPM_ALG_RSASSA or TPM_ALG_NULL""") { + forAll(Gen.chooseNum(0, Short.MaxValue * 2 + 1)) { + scheme: Int => + whenever( + scheme != TpmRsaScheme.RSASSA && scheme != TPM_ALG_NULL + ) { + val testData = (RegistrationTestData.from _).tupled( + makeCred( + authDataAndKeypair = Some( + TestAuthenticator + .createAuthenticatorData(keyAlgorithm = + COSEAlgorithmIdentifier.RS256 + ) + ), + scheme = Some(scheme), + ) + ) + val step = init(testData) + testData.alg should be(COSEAlgorithmIdentifier.RS256) + + step.validations shouldBe a[Failure[_]] + step.tryNext shouldBe a[Failure[_]] + } + } + } + + it("ECC keys must have the SIGN_ENCRYPT attribute.") { + forAll(Gen.chooseNum(0, Int.MaxValue.toLong * 2 + 1)) { + attributes: Long => + val testData = (RegistrationTestData.from _).tupled( + makeCred( + authDataAndKeypair = Some( + TestAuthenticator + .createAuthenticatorData(keyAlgorithm = + COSEAlgorithmIdentifier.ES256 + ) + ), + attributes = + Some(attributes & ~Attributes.SIGN_ENCRYPT), + ) + ) + val step = init(testData) + testData.alg should be(COSEAlgorithmIdentifier.ES256) + + step.validations shouldBe a[Failure[_]] + step.tryNext shouldBe a[Failure[_]] + } + } + + it("""ECC keys must have "symmetric" set to TPM_ALG_NULL""") { + forAll(Gen.chooseNum(0, Short.MaxValue * 2 + 1)) { + symmetric: Int => + whenever(symmetric != TPM_ALG_NULL) { + val testData = (RegistrationTestData.from _).tupled( + makeCred( + authDataAndKeypair = Some( + TestAuthenticator + .createAuthenticatorData(keyAlgorithm = + COSEAlgorithmIdentifier.ES256 + ) + ), + symmetric = Some(symmetric), + ) + ) + val step = init(testData) + testData.alg should be(COSEAlgorithmIdentifier.ES256) + + step.validations shouldBe a[Failure[_]] + step.tryNext shouldBe a[Failure[_]] + } + } + } + + it("""ECC keys must have "scheme" set to TPM_ALG_NULL""") { + forAll(Gen.chooseNum(0, Short.MaxValue * 2 + 1)) { + scheme: Int => + whenever(scheme != TPM_ALG_NULL) { + val testData = (RegistrationTestData.from _).tupled( + makeCred( + authDataAndKeypair = Some( + TestAuthenticator + .createAuthenticatorData(keyAlgorithm = + COSEAlgorithmIdentifier.ES256 + ) + ), + scheme = Some(scheme), + ) + ) + val step = init(testData) + testData.alg should be(COSEAlgorithmIdentifier.ES256) + + step.validations shouldBe a[Failure[_]] + step.tryNext shouldBe a[Failure[_]] + } + } + } + } + } } ignore("The android-key statement format is supported.") { @@ -2539,6 +3363,7 @@ class RelyingPartyRegistrationSpec clock: Clock, trustedRootCert: Option[X509Certificate] = None, enableRevocationChecking: Boolean = true, + origins: Option[Set[String]] = None, ): Unit = { it("is rejected if untrusted attestation is not allowed and the trust source does not trust it.") { val steps = finishRegistration( @@ -2546,6 +3371,7 @@ class RelyingPartyRegistrationSpec testData = testData, attestationTrustSource = Some(emptyTrustSource), clock = clock, + origins = origins, ) val step: FinishRegistrationSteps#Step21 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next @@ -2561,6 +3387,7 @@ class RelyingPartyRegistrationSpec testData = testData, attestationTrustSource = Some(emptyTrustSource), clock = clock, + origins = origins, ) val step: FinishRegistrationSteps#Step21 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next @@ -2597,6 +3424,7 @@ class RelyingPartyRegistrationSpec testData = testData, attestationTrustSource = attestationTrustSource, clock = clock, + origins = origins, ) val step: FinishRegistrationSteps#Step21 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next @@ -2647,6 +3475,7 @@ class RelyingPartyRegistrationSpec testData = testData, attestationTrustSource = Some(attestationTrustSource), clock = clock, + origins = origins, ) val step: FinishRegistrationSteps#Step21 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next @@ -2674,6 +3503,7 @@ class RelyingPartyRegistrationSpec testData = testData, attestationTrustSource = Some(attestationTrustSource), clock = clock, + origins = origins, ) val step: FinishRegistrationSteps#Step21 = steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next @@ -2731,8 +3561,21 @@ class RelyingPartyRegistrationSpec ), ) } - } + describe("A tpm attestation") { + val testData = RegistrationTestData.Tpm.RealExample + generateTests( + testData = testData, + clock = Clock.fixed( + Instant.parse("2022-08-25T16:00:00Z"), + ZoneOffset.UTC, + ), + origins = Some(Set(testData.clientData.getOrigin)), + trustedRootCert = Some(testData.attestationRootCertificate.get), + enableRevocationChecking = false, + ) + } + } } describe("22. Check that the credentialId is not yet registered to any other user. If registration is requested for a credential that is already registered to a different user, the Relying Party SHOULD fail this registration ceremony, or it MAY decide to accept the registration, e.g. while deleting the older registration.") { diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/TestAuthenticator.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/TestAuthenticator.scala index 870ed148a..313739061 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/TestAuthenticator.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/TestAuthenticator.scala @@ -27,9 +27,14 @@ package com.yubico.webauthn import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.node.JsonNodeFactory import com.fasterxml.jackson.databind.node.ObjectNode +import com.upokecenter.cbor.CBORObject import com.yubico.internal.util.BinaryUtil import com.yubico.internal.util.CertificateParser import com.yubico.internal.util.JacksonCodecs +import com.yubico.webauthn.TpmAttestationStatementVerifier.Attributes +import com.yubico.webauthn.TpmAttestationStatementVerifier.TpmAlgAsym +import com.yubico.webauthn.TpmAttestationStatementVerifier.TpmAlgHash +import com.yubico.webauthn.TpmAttestationStatementVerifier.TpmRsaScheme import com.yubico.webauthn.data.AuthenticatorAssertionResponse import com.yubico.webauthn.data.AuthenticatorAttestationResponse import com.yubico.webauthn.data.AuthenticatorData @@ -39,6 +44,7 @@ import com.yubico.webauthn.data.ClientAssertionExtensionOutputs import com.yubico.webauthn.data.ClientRegistrationExtensionOutputs import com.yubico.webauthn.data.PublicKeyCredential import com.yubico.webauthn.data.PublicKeyCredentialRequestOptions +import org.bouncycastle.asn1.ASN1Encodable import org.bouncycastle.asn1.ASN1ObjectIdentifier import org.bouncycastle.asn1.ASN1Primitive import org.bouncycastle.asn1.DEROctetString @@ -110,9 +116,24 @@ object TestAuthenticator { val credentialKey: KeyPair = generateEcKeypair() + val leafCertSubject: X500Name = new X500Name( + "CN=Yubico WebAuthn unit tests, O=Yubico, OU=Authenticator Attestation, C=SE" + ) + val caCertSubject: X500Name = new X500Name( + "CN=Yubico WebAuthn unit tests CA, O=Yubico, OU=Authenticator Attestation, C=SE" + ) val certValidFrom: Instant = Instant.parse("2018-09-06T17:42:00Z") val certValidTo: Instant = certValidFrom.plusSeconds(7 * 24 * 3600) } + val RsaKeySizeBits = 2048 + val Es256PrimeModulus: BigInteger = new BigInteger( + 1, + ByteArray + .fromHex( + "FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF" + ) + .getBytes, + ) private def jsonFactory: JsonNodeFactory = JsonNodeFactory.instance private def toBytes(s: String): ByteArray = new ByteArray(s.getBytes("UTF-8")) @@ -221,6 +242,39 @@ object TestAuthenticator { caKey, ) } + def tpm( + cert: AttestationCert, + ver: Option[String] = Some("2.0"), + magic: ByteArray = TpmAttestationStatementVerifier.TPM_GENERATED_VALUE, + `type`: ByteArray = + TpmAttestationStatementVerifier.TPM_ST_ATTEST_CERTIFY, + modifyAttestedName: ByteArray => ByteArray = an => an, + overrideCosePubkey: Option[ByteArray] = None, + attributes: Option[Long] = None, + symmetric: Option[Int] = None, + scheme: Option[Int] = None, + ): AttestationMaker = + new AttestationMaker { + override val format = "tpm" + override def certChain = cert.certChain + override def makeAttestationStatement( + authDataBytes: ByteArray, + clientDataJson: String, + ): JsonNode = + makeTpmAttestationStatement( + authDataBytes, + clientDataJson, + cert, + ver = ver, + magic = magic, + `type` = `type`, + modifyAttestedName = modifyAttestedName, + overrideCosePubkey = overrideCosePubkey, + attributes = attributes, + symmetric = symmetric, + scheme = scheme, + ) + } def none(): AttestationMaker = new AttestationMaker { @@ -261,10 +315,9 @@ object TestAuthenticator { object AttestationSigner { def ca( alg: COSEAlgorithmIdentifier, - aaguid: ByteArray = Defaults.aaguid, - certSubject: X500Name = new X500Name( - "CN=Yubico WebAuthn unit tests, O=Yubico, OU=Authenticator Attestation, C=SE" - ), + aaguid: Option[ByteArray] = Some(Defaults.aaguid), + certSubject: X500Name = Defaults.leafCertSubject, + certExtensions: List[(String, Boolean, ASN1Encodable)] = Nil, validFrom: Instant = Defaults.certValidFrom, validTo: Instant = Defaults.certValidTo, ): AttestationCert = { @@ -278,13 +331,15 @@ object TestAuthenticator { alg, caCertAndKey = Some((caCert, caKey)), name = certSubject, - extensions = List( - ( - "1.3.6.1.4.1.45724.1.1.4", - false, - new DEROctetString(aaguid.getBytes), + extensions = aaguid + .map(aaguid => + ( + "1.3.6.1.4.1.45724.1.1.4", + false, + new DEROctetString(aaguid.getBytes), + ) ) - ), + .toList ++ certExtensions, validFrom = validFrom, validTo = validTo, ) @@ -296,13 +351,29 @@ object TestAuthenticator { ) } - def selfsigned(alg: COSEAlgorithmIdentifier): AttestationCert = { - val (cert, key) = generateAttestationCertificate(alg = alg) + def selfsigned( + alg: COSEAlgorithmIdentifier, + certSubject: X500Name = Defaults.leafCertSubject, + issuerSubject: Option[X500Name] = None, + certExtensions: List[(String, Boolean, ASN1Encodable)] = Nil, + isCa: Boolean = false, + validFrom: Instant = Defaults.certValidFrom, + validTo: Instant = Defaults.certValidTo, + ): AttestationCert = { + val (cert, key) = generateAttestationCertificate( + alg = alg, + name = certSubject, + issuerName = issuerSubject, + extensions = certExtensions, + isCa = isCa, + validFrom = validFrom, + validTo = validTo, + ) AttestationCert(cert, key, alg, certChain = List((cert, key))) } } - private def createAuthenticatorData( + def createAuthenticatorData( aaguid: ByteArray = Defaults.aaguid, authenticatorExtensions: Option[JsonNode] = None, credentialKeypair: Option[KeyPair] = None, @@ -339,7 +410,7 @@ object TestAuthenticator { ) } - private def createClientData( + def createClientData( challenge: ByteArray = Defaults.challenge, clientData: Option[JsonNode] = None, origin: String = Defaults.origin, @@ -375,7 +446,7 @@ object TestAuthenticator { clientDataJson } - private def createCredential( + def createCredential( authDataBytes: ByteArray, clientDataJson: String, credentialKeypair: KeyPair, @@ -808,6 +879,171 @@ object TestAuthenticator { ) } + def makeTpmAttestationStatement( + authDataBytes: ByteArray, + clientDataJson: String, + cert: AttestationCert, + ver: Option[String] = Some("2.0"), + magic: ByteArray = TpmAttestationStatementVerifier.TPM_GENERATED_VALUE, + `type`: ByteArray = TpmAttestationStatementVerifier.TPM_ST_ATTEST_CERTIFY, + modifyAttestedName: ByteArray => ByteArray = an => an, + overrideCosePubkey: Option[ByteArray] = None, + attributes: Option[Long] = None, + symmetric: Option[Int] = None, + scheme: Option[Int] = None, + ): JsonNode = { + assert(magic.size() == 4) + assert(`type`.size() == 2) + + val authData = new AuthenticatorData(authDataBytes) + val cosePubkey = overrideCosePubkey.getOrElse( + authData.getAttestedCredentialData.get.getCredentialPublicKey + ) + + val coseKeyAlg = WebAuthnCodecs.getCoseKeyAlg(cosePubkey).get + val (hashId, signAlg) = coseKeyAlg match { + case COSEAlgorithmIdentifier.ES256 => + (TpmAlgHash.SHA256, TpmAlgAsym.ECC) + case COSEAlgorithmIdentifier.ES384 => + (TpmAlgHash.SHA384, TpmAlgAsym.ECC) + case COSEAlgorithmIdentifier.ES512 => + (TpmAlgHash.SHA512, TpmAlgAsym.ECC) + case COSEAlgorithmIdentifier.RS256 => + (TpmAlgHash.SHA256, TpmAlgAsym.RSA) + case COSEAlgorithmIdentifier.RS1 => (TpmAlgHash.SHA1, TpmAlgAsym.RSA) + } + val hashFunc = hashId match { + case TpmAlgHash.SHA256 => Crypto.sha256(_: ByteArray) + case TpmAlgHash.SHA384 => Crypto.sha384 _ + case TpmAlgHash.SHA512 => Crypto.sha512 _ + case TpmAlgHash.SHA1 => Crypto.sha1 _ + } + val extraData = { + hashFunc( + authDataBytes concat Crypto.sha256( + new ByteArray(clientDataJson.getBytes(StandardCharsets.UTF_8)) + ) + ) + } + val (parameters, unique) = WebAuthnTestCodecs.getCoseKty(cosePubkey) match { + case 3 => { // RSA + val cose = CBORObject.DecodeFromBytes(cosePubkey.getBytes) + ( + new ByteArray(BinaryUtil.encodeUint16(symmetric getOrElse 0x0010)) + .concat( + new ByteArray( + BinaryUtil.encodeUint16(scheme getOrElse TpmRsaScheme.RSASSA) + ) + ) + .concat( + new ByteArray(BinaryUtil.encodeUint16(RsaKeySizeBits)) + ) // key_bits + .concat( + new ByteArray( + BinaryUtil.encodeUint32( + new BigInteger(1, cose.get(-2).GetByteString()).longValue() + ) + ) + ) // exponent + , + new ByteArray( + BinaryUtil.encodeUint16(cose.get(-1).GetByteString().length) + ).concat(new ByteArray(cose.get(-1).GetByteString())), // modulus + ) + } + case 2 => { // EC + val pubkey = WebAuthnCodecs + .importCosePublicKey(cosePubkey) + .asInstanceOf[ECPublicKey] + ( + new ByteArray(BinaryUtil.encodeUint16(symmetric getOrElse 0x0010)) + .concat( + new ByteArray(BinaryUtil.encodeUint16(scheme getOrElse 0x0010)) + ) + .concat( + new ByteArray(BinaryUtil.encodeUint16(coseKeyAlg match { + case COSEAlgorithmIdentifier.ES256 => 0x0003 + case COSEAlgorithmIdentifier.ES384 => 0x0004 + case COSEAlgorithmIdentifier.ES512 => 0x0005 + })) + ) + .concat( + new ByteArray(BinaryUtil.encodeUint16(0x0010)) + ) // kdf_scheme: ??? (unused?) + , + new ByteArray( + BinaryUtil.encodeUint16(pubkey.getW.getAffineX.toByteArray.length) + ) + .concat(new ByteArray(pubkey.getW.getAffineX.toByteArray)) + .concat( + new ByteArray( + BinaryUtil.encodeUint16( + pubkey.getW.getAffineY.toByteArray.length + ) + ) + ) + .concat(new ByteArray(pubkey.getW.getAffineY.toByteArray)), + ) + } + } + val pubArea = new ByteArray(BinaryUtil.encodeUint16(signAlg)) + .concat(new ByteArray(BinaryUtil.encodeUint16(hashId))) + .concat( + new ByteArray( + BinaryUtil.encodeUint32(attributes getOrElse Attributes.SIGN_ENCRYPT) + ) + ) + .concat( + new ByteArray(BinaryUtil.encodeUint16(0)) + ) // authPolicy is ignored by TpmAttestationStatementVerifier + .concat(parameters) + .concat(unique) + + val qualifiedSigner = ByteArray.fromHex("") + val clockInfo = ByteArray.fromHex("0000000000000000111111112222222233") + val firmwareVersion = ByteArray.fromHex("0000000000000000") + val attestedName = + modifyAttestedName( + new ByteArray(BinaryUtil.encodeUint16(hashId)).concat(hashFunc(pubArea)) + ) + val attestedQualifiedName = ByteArray.fromHex("") + + val certInfo = magic + .concat(`type`) + .concat(new ByteArray(BinaryUtil.encodeUint16(qualifiedSigner.size))) + .concat(qualifiedSigner) + .concat(new ByteArray(BinaryUtil.encodeUint16(extraData.size))) + .concat(extraData) + .concat(clockInfo) + .concat(firmwareVersion) + .concat(new ByteArray(BinaryUtil.encodeUint16(attestedName.size))) + .concat(attestedName) + .concat( + new ByteArray(BinaryUtil.encodeUint16(attestedQualifiedName.size)) + ) + .concat(attestedQualifiedName) + + val sig = sign(certInfo, cert.key, cert.alg) + + val f = JsonNodeFactory.instance + f + .objectNode() + .setAll[ObjectNode]( + Map( + "ver" -> ver.map(f.textNode).getOrElse(f.nullNode()), + "alg" -> f.numberNode(cert.alg.getId), + "x5c" -> f + .arrayNode() + .addAll( + cert.certChain.map(_._1.getEncoded).map(f.binaryNode).asJava + ), + "sig" -> f.binaryNode(sig.getBytes), + "certInfo" -> f.binaryNode(certInfo.getBytes), + "pubArea" -> f.binaryNode(pubArea.getBytes), + ).asJava + ) + } + def makeAuthDataBytes( rpId: String = Defaults.rpId, signatureCount: Option[Int] = None, @@ -915,7 +1151,7 @@ object TestAuthenticator { def generateRsaKeypair(): KeyPair = { val g: KeyPairGenerator = KeyPairGenerator.getInstance("RSA") - g.initialize(2048, random) + g.initialize(RsaKeySizeBits, random) g.generateKeyPair() } @@ -975,9 +1211,7 @@ object TestAuthenticator { def generateAttestationCaCertificate( keypair: Option[KeyPair] = None, signingAlg: COSEAlgorithmIdentifier = COSEAlgorithmIdentifier.ES256, - name: X500Name = new X500Name( - "CN=Yubico WebAuthn unit tests CA, O=Yubico, OU=Authenticator Attestation, C=SE" - ), + name: X500Name = Defaults.caCertSubject, superCa: Option[(X509Certificate, PrivateKey)] = None, extensions: Iterable[(String, Boolean, ASN1Primitive)] = Nil, validFrom: Instant = Defaults.certValidFrom, @@ -1004,10 +1238,9 @@ object TestAuthenticator { def generateAttestationCertificate( alg: COSEAlgorithmIdentifier = COSEAlgorithmIdentifier.ES256, keypair: Option[KeyPair] = None, - name: X500Name = new X500Name( - "CN=Yubico WebAuthn unit tests, O=Yubico, OU=Authenticator Attestation, C=SE" - ), - extensions: Iterable[(String, Boolean, ASN1Primitive)] = List( + name: X500Name = Defaults.leafCertSubject, + issuerName: Option[X500Name] = None, + extensions: Iterable[(String, Boolean, ASN1Encodable)] = List( ( "1.3.6.1.4.1.45724.1.1.4", false, @@ -1017,20 +1250,23 @@ object TestAuthenticator { caCertAndKey: Option[(X509Certificate, PrivateKey)] = None, validFrom: Instant = Defaults.certValidFrom, validTo: Instant = Defaults.certValidTo, + isCa: Boolean = false, ): (X509Certificate, PrivateKey) = { val actualKeypair = keypair.getOrElse(generateKeypair(alg)) ( buildCertificate( publicKey = actualKeypair.getPublic, - issuerName = caCertAndKey - .map(_._1) - .map(JcaX500NameUtil.getSubject) - .getOrElse(name), + issuerName = issuerName.getOrElse( + caCertAndKey + .map(_._1) + .map(JcaX500NameUtil.getSubject) + .getOrElse(name) + ), subjectName = name, signingKey = caCertAndKey.map(_._2).getOrElse(actualKeypair.getPrivate), signingAlg = alg, - isCa = false, + isCa = isCa, extensions = extensions, validFrom = validFrom, validTo = validTo, @@ -1046,7 +1282,7 @@ object TestAuthenticator { signingKey: PrivateKey, signingAlg: COSEAlgorithmIdentifier, isCa: Boolean = false, - extensions: Iterable[(String, Boolean, ASN1Primitive)] = None, + extensions: Iterable[(String, Boolean, ASN1Encodable)] = None, validFrom: Instant = Defaults.certValidFrom, validTo: Instant = Defaults.certValidTo, ): X509Certificate = { diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/WebAuthnTestCodecs.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/WebAuthnTestCodecs.scala index 92a68dd67..2773747ee 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/WebAuthnTestCodecs.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/WebAuthnTestCodecs.scala @@ -96,4 +96,10 @@ object WebAuthnTestCodecs { } } + def getCoseKty(encodedPublicKey: ByteArray): Int = { + val cose = CBORObject.DecodeFromBytes(encodedPublicKey.getBytes) + val kty = cose.get(CBORObject.FromObject(1)).AsInt32 + kty + } + } 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 d83d942fc..493ef1a95 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 @@ -317,15 +317,20 @@ object Generators { len <- Gen.chooseNum(minSize, maxSize) } yield new ByteArray(nums.take(len).toArray) - def flipOneBit(bytes: ByteArray): Gen[ByteArray] = - for { - byteIndex: Int <- Gen.choose(0, bytes.size() - 1) - bitIndex: Int <- Gen.choose(0, 7) - flipMask: Byte = (1 << bitIndex).toByte - } yield new ByteArray( + def flipBit(bitIndex: Int)(bytes: ByteArray): ByteArray = { + val byteIndex: Int = bitIndex / 8 + val bitIndexInByte: Int = bitIndex % 8 + val flipMask: Byte = (1 << bitIndexInByte).toByte + new ByteArray( bytes.getBytes .updated(byteIndex, (bytes.getBytes()(byteIndex) ^ flipMask).toByte) ) + } + + def flipOneBit(bytes: ByteArray): Gen[ByteArray] = + for { + bitIndex <- Gen.choose(0, 8 * bytes.size() - 1) + } yield flipBit(bitIndex)(bytes) object Extensions { private val RegistrationExtensionIds: Set[String] = From 083a5184be60b9758a6a4b6de29eeab699a71f75 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Thu, 1 Sep 2022 19:52:43 +0200 Subject: [PATCH 080/145] Add policy tree validator setting --- NEWS | 5 ++ .../webauthn/FinishRegistrationSteps.java | 22 +++++- .../attestation/AttestationTrustSource.java | 52 ++++++++++++-- .../RelyingPartyRegistrationSpec.scala | 71 +++++++++++++++++++ 4 files changed, 141 insertions(+), 9 deletions(-) diff --git a/NEWS b/NEWS index bf5a4d6cd..fd850e3ff 100644 --- a/NEWS +++ b/NEWS @@ -9,6 +9,11 @@ New features: * Added support for the `"tpm"` attestation statement format. * Added support for ES384 and ES512 signature algorithms. +* Added property `policyTreeValidator` to `TrustRootsResult`. If set, the given + predicate function will be used to validate the certificate policy tree after + successful attestation certificate path validation. This may be required for + some JCA providers to accept attestation certificates with critical + certificate policy extensions. Fixes: diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishRegistrationSteps.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishRegistrationSteps.java index 6934ed3c9..fdc305bb4 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishRegistrationSteps.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishRegistrationSteps.java @@ -50,6 +50,7 @@ import java.security.cert.CertPathValidatorException; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; +import java.security.cert.PKIXCertPathValidatorResult; import java.security.cert.PKIXParameters; import java.security.cert.TrustAnchor; import java.security.cert.X509Certificate; @@ -536,10 +537,25 @@ public boolean attestationTrusted() { pathParams.setDate(Date.from(clock.instant())); pathParams.setRevocationEnabled(trustRoots.get().isEnableRevocationChecking()); pathParams.setPolicyQualifiersRejected( - false); // TODO: Add parameter to configure policy qualifier processor + !trustRoots.get().getPolicyTreeValidator().isPresent()); trustRoots.get().getCertStore().ifPresent(pathParams::addCertStore); - cpv.validate(certPath, pathParams); - return true; + final PKIXCertPathValidatorResult result = + (PKIXCertPathValidatorResult) cpv.validate(certPath, pathParams); + return trustRoots + .get() + .getPolicyTreeValidator() + .map( + policyNodePredicate -> { + if (policyNodePredicate.test(result.getPolicyTree())) { + return true; + } else { + log.info( + "Failed to derive trust in attestation statement: Certificate path policy tree does not satisfy policy tree validator. Attestation object: {}", + response.getResponse().getAttestationObject()); + return false; + } + }) + .orElse(true); } } catch (CertPathValidatorException e) { diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/attestation/AttestationTrustSource.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/attestation/AttestationTrustSource.java index 66639a161..5e731cb59 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/attestation/AttestationTrustSource.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/attestation/AttestationTrustSource.java @@ -27,10 +27,12 @@ import com.yubico.internal.util.CollectionUtil; import com.yubico.webauthn.data.ByteArray; import java.security.cert.CertStore; +import java.security.cert.PolicyNode; import java.security.cert.X509Certificate; import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.function.Predicate; import lombok.Builder; import lombok.NonNull; import lombok.Value; @@ -60,11 +62,21 @@ TrustRootsResult findTrustRoots( List attestationCertificateChain, Optional aaguid); /** - * A result of looking up attestation trust roots for a particular attestation statement. This - * primarily consists of a set of trust root certificates, but may also include a {@link - * CertStore} of additional CRLs and/or intermediate certificate to use during certificate path - * validation, and may also disable certificate revocation checking for the relevant attestation - * statement. + * A result of looking up attestation trust roots for a particular attestation statement. + * + *

    This primarily consists of a set of trust root certificates - see {@link + * TrustRootsResultBuilder#trustRoots(Set) trustRoots(Set)} - but may also: + * + *

      + *
    • include a {@link CertStore} of additional CRLs and/or intermediate certificates to use + * during certificate path validation - see {@link + * TrustRootsResultBuilder#certStore(CertStore) certStore(CertStore)}; + *
    • disable certificate revocation checking for the relevant attestation statement - see + * {@link TrustRootsResultBuilder#enableRevocationChecking(boolean) + * enableRevocationChecking(boolean)}; and/or + *
    • define a policy tree validator for the PKIX policy tree result - see {@link + * TrustRootsResultBuilder#policyTreeValidator(Predicate) policyTreeValidator(Predicate)}. + *
    */ @Value @Builder(toBuilder = true) @@ -97,19 +109,47 @@ class TrustRootsResult { */ @Builder.Default private final boolean enableRevocationChecking = true; + /** + * If non-null, the PolicyQualifiersRejected flag will be set to false during certificate path + * validation. See {@link + * java.security.cert.PKIXParameters#setPolicyQualifiersRejected(boolean)}. + * + *

    The given {@link Predicate} will be used to validate the policy tree. The {@link + * Predicate} should return true if the policy tree is acceptable, and false + * otherwise. + * + *

    This may be required if any certificate in the certificate path contains a certificate + * policies extension marked critical. If this is not set, then such a certificate will be + * rejected by the certificate path validator. + * + *

    Consult the Java + * PKI Programmer's Guide for how to use the {@link PolicyNode} argument of the {@link + * Predicate}. + * + *

    The default is null. + */ + @Builder.Default private final Predicate policyTreeValidator = null; + private TrustRootsResult( @NonNull Set trustRoots, CertStore certStore, - boolean enableRevocationChecking) { + boolean enableRevocationChecking, + Predicate policyTreeValidator) { this.trustRoots = CollectionUtil.immutableSet(trustRoots); this.certStore = certStore; this.enableRevocationChecking = enableRevocationChecking; + this.policyTreeValidator = policyTreeValidator; } public Optional getCertStore() { return Optional.ofNullable(certStore); } + public Optional> getPolicyTreeValidator() { + return Optional.ofNullable(policyTreeValidator); + } + public static TrustRootsResultBuilder.Step1 builder() { return new TrustRootsResultBuilder.Step1(); } 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 79f12ea96..c6afa80fe 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 @@ -81,6 +81,7 @@ import org.bouncycastle.asn1.x509.Extension import org.bouncycastle.asn1.x509.GeneralName import org.bouncycastle.asn1.x509.GeneralNamesBuilder import org.bouncycastle.cert.jcajce.JcaX500NameUtil +import org.bouncycastle.jce.provider.BouncyCastleProvider import org.junit.runner.RunWith import org.mockito.Mockito import org.scalacheck.Arbitrary.arbitrary @@ -98,10 +99,12 @@ import java.security.KeyFactory import java.security.KeyPair import java.security.MessageDigest import java.security.PrivateKey +import java.security.Security import java.security.SignatureException import java.security.cert.CRL import java.security.cert.CertStore import java.security.cert.CollectionCertStoreParameters +import java.security.cert.PolicyNode import java.security.cert.X509Certificate import java.security.interfaces.ECPublicKey import java.security.interfaces.RSAPublicKey @@ -111,6 +114,7 @@ import java.time.ZoneOffset import java.util import java.util.Collections import java.util.Optional +import java.util.function.Predicate import javax.security.auth.x500.X500Principal import scala.jdk.CollectionConverters._ import scala.jdk.OptionConverters.RichOption @@ -193,6 +197,7 @@ class RelyingPartyRegistrationSpec trustedCert: X509Certificate, crls: Option[Set[CRL]] = None, enableRevocationChecking: Boolean = true, + policyTreeValidator: Option[Predicate[PolicyNode]] = None, ): AttestationTrustSource = (_: util.List[X509Certificate], _: Optional[ByteArray]) => { TrustRootsResult @@ -209,6 +214,7 @@ class RelyingPartyRegistrationSpec .orNull ) .enableRevocationChecking(enableRevocationChecking) + .policyTreeValidator(policyTreeValidator.orNull) .build() } @@ -2088,6 +2094,7 @@ class RelyingPartyRegistrationSpec trustSourceWith( testData.attestationRootCertificate.get, enableRevocationChecking = false, + policyTreeValidator = Some(_ => true), ) ), ) @@ -3364,6 +3371,7 @@ class RelyingPartyRegistrationSpec trustedRootCert: Option[X509Certificate] = None, enableRevocationChecking: Boolean = true, origins: Option[Set[String]] = None, + policyTreeValidator: Option[Predicate[PolicyNode]] = None, ): Unit = { it("is rejected if untrusted attestation is not allowed and the trust source does not trust it.") { val steps = finishRegistration( @@ -3418,6 +3426,7 @@ class RelyingPartyRegistrationSpec ) }), enableRevocationChecking = enableRevocationChecking, + policyTreeValidator = policyTreeValidator, ) ) val steps = finishRegistration( @@ -3469,6 +3478,7 @@ class RelyingPartyRegistrationSpec .trustRoots(Collections.emptySet()) .certStore(certStore) .enableRevocationChecking(enableRevocationChecking) + .policyTreeValidator(policyTreeValidator.orNull) .build() } val steps = finishRegistration( @@ -3497,6 +3507,7 @@ class RelyingPartyRegistrationSpec .trustRoots(Collections.singleton(rootCert)) .certStore(certStore) .enableRevocationChecking(enableRevocationChecking) + .policyTreeValidator(policyTreeValidator.orNull) .build() } val steps = finishRegistration( @@ -3573,8 +3584,67 @@ class RelyingPartyRegistrationSpec origins = Some(Set(testData.clientData.getOrigin)), trustedRootCert = Some(testData.attestationRootCertificate.get), enableRevocationChecking = false, + policyTreeValidator = Some(_ => true), ) } + + describe("Critical certificate policy extensions") { + def init( + policyTreeValidator: Option[Predicate[PolicyNode]] + ): FinishRegistrationSteps#Step21 = { + val testData = RegistrationTestData.Tpm.RealExample + val clock = Clock.fixed( + Instant.parse("2022-08-25T16:00:00Z"), + ZoneOffset.UTC, + ) + val steps = finishRegistration( + allowUntrustedAttestation = false, + origins = Some(Set(testData.clientData.getOrigin)), + testData = testData, + attestationTrustSource = Some( + trustSourceWith( + testData.attestationRootCertificate.get, + enableRevocationChecking = false, + policyTreeValidator = policyTreeValidator, + ) + ), + clock = clock, + ) + + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next + } + + it("are rejected if no policy tree validator is set.") { + // BouncyCastle provider does not reject critical policy extensions + // TODO Mark test as ignored instead of just skipping (assume() and cancel() currently break pitest) + if ( + !Security.getProviders + .exists(p => p.isInstanceOf[BouncyCastleProvider]) + ) { + val step = init(policyTreeValidator = None) + + step.validations shouldBe a[Failure[_]] + step.attestationTrusted should be(false) + step.tryNext shouldBe a[Failure[_]] + } + } + + it("are accepted if a policy tree validator is set and accepts the policy tree.") { + val step = init(policyTreeValidator = Some(_ => true)) + + step.validations shouldBe a[Success[_]] + step.attestationTrusted should be(true) + step.tryNext shouldBe a[Success[_]] + } + + it("are rejected if a policy tree validator is set and does not accept the policy tree.") { + val step = init(policyTreeValidator = Some(_ => false)) + + step.validations shouldBe a[Failure[_]] + step.attestationTrusted should be(false) + step.tryNext shouldBe a[Failure[_]] + } + } } } @@ -4413,6 +4483,7 @@ class RelyingPartyRegistrationSpec trustSourceWith( testData.attestationRootCertificate.get, enableRevocationChecking = false, + policyTreeValidator = Some(_ => true), ) ), credentialRepository = Helpers.CredentialRepository.empty, From 86679127501fa6c2aacb9c2c1cc17f7c6978b785 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 5 Sep 2022 13:01:18 +0200 Subject: [PATCH 081/145] Expand policyTreeValidator JavaDoc with that it depends on JCA provider --- .../webauthn/attestation/AttestationTrustSource.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/attestation/AttestationTrustSource.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/attestation/AttestationTrustSource.java index 5e731cb59..a11d57dcf 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/attestation/AttestationTrustSource.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/attestation/AttestationTrustSource.java @@ -118,9 +118,10 @@ class TrustRootsResult { * Predicate} should return true if the policy tree is acceptable, and false * otherwise. * - *

    This may be required if any certificate in the certificate path contains a certificate - * policies extension marked critical. If this is not set, then such a certificate will be - * rejected by the certificate path validator. + *

    Depending on your "PKIX" JCA provider configuration, this may be required if + * any certificate in the certificate path contains a certificate policies extension marked + * critical. If this is not set, then such a certificate will be rejected by the certificate + * path validator from the default provider. * *

    Consult the Java From 3b8e505d4cb15e5a53a668edeaa7cb077391376d Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 5 Sep 2022 13:20:43 +0200 Subject: [PATCH 082/145] Port test-platform/build.gradle to build.gradle.kts --- test-platform/build.gradle | 16 ---------------- test-platform/build.gradle.kts | 16 ++++++++++++++++ 2 files changed, 16 insertions(+), 16 deletions(-) delete mode 100644 test-platform/build.gradle create mode 100644 test-platform/build.gradle.kts diff --git a/test-platform/build.gradle b/test-platform/build.gradle deleted file mode 100644 index f865959c7..000000000 --- a/test-platform/build.gradle +++ /dev/null @@ -1,16 +0,0 @@ -plugins { - id 'java-platform' -} - -description = 'Dependency constraints for tests' - -dependencies { - constraints { - api('junit:junit:[4.12,5)') - api('org.apache.httpcomponents:httpclient:[4.5.2,5)') - api('org.mockito:mockito-core:[2.27.0,3)') - api('org.scalacheck:scalacheck_2.13:[1.14.0,2)') - api('org.scalatest:scalatest_2.13:[3.0.8,3.1)') - api('uk.org.lidalia:slf4j-test:[1.1.0,2)') - } -} diff --git a/test-platform/build.gradle.kts b/test-platform/build.gradle.kts new file mode 100644 index 000000000..b2bfd9a56 --- /dev/null +++ b/test-platform/build.gradle.kts @@ -0,0 +1,16 @@ +plugins { + `java-platform` +} + +description = "Dependency constraints for tests" + +dependencies { + constraints { + api("junit:junit:[4.12,5)") + api("org.apache.httpcomponents:httpclient:[4.5.2,5)") + api("org.mockito:mockito-core:[2.27.0,3)") + api("org.scalacheck:scalacheck_2.13:[1.14.0,2)") + api("org.scalatest:scalatest_2.13:[3.0.8,3.1)") + api("uk.org.lidalia:slf4j-test:[1.1.0,2)") + } +} From 4404e29fe09af75a08c9f70161ef7b1ec441b849 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 5 Sep 2022 16:30:31 +0200 Subject: [PATCH 083/145] Port settings.gradle to settings.gradle.kts --- settings.gradle | 12 ------------ settings.gradle.kts | 12 ++++++++++++ 2 files changed, 12 insertions(+), 12 deletions(-) delete mode 100644 settings.gradle create mode 100644 settings.gradle.kts diff --git a/settings.gradle b/settings.gradle deleted file mode 100644 index 65af1138a..000000000 --- a/settings.gradle +++ /dev/null @@ -1,12 +0,0 @@ -rootProject.name = 'webauthn-server-parent' -include ':webauthn-server-attestation' -include ':webauthn-server-core' -include ':webauthn-server-demo' -include ':yubico-util' -include ':yubico-util-scala' - -include ':test-dependent-projects:java-dep-webauthn-server-attestation' -include ':test-dependent-projects:java-dep-webauthn-server-core' -include ':test-dependent-projects:java-dep-webauthn-server-core-and-bouncycastle' -include ':test-dependent-projects:java-dep-yubico-util' -include ':test-platform' diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 000000000..9c076aa98 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,12 @@ +rootProject.name = "webauthn-server-parent" +include(":webauthn-server-attestation") +include(":webauthn-server-core") +include(":webauthn-server-demo") +include(":yubico-util") +include(":yubico-util-scala") + +include(":test-dependent-projects:java-dep-webauthn-server-attestation") +include(":test-dependent-projects:java-dep-webauthn-server-core") +include(":test-dependent-projects:java-dep-webauthn-server-core-and-bouncycastle") +include(":test-dependent-projects:java-dep-yubico-util") +include(":test-platform") From 003c660136480287412fd01a56463441bdc2d348 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 5 Sep 2022 16:30:50 +0200 Subject: [PATCH 084/145] Port yubico-util-scala/build.gradle to build.gradle.kts --- yubico-util-scala/build.gradle | 23 ----------------------- yubico-util-scala/build.gradle.kts | 20 ++++++++++++++++++++ 2 files changed, 20 insertions(+), 23 deletions(-) delete mode 100644 yubico-util-scala/build.gradle create mode 100644 yubico-util-scala/build.gradle.kts diff --git a/yubico-util-scala/build.gradle b/yubico-util-scala/build.gradle deleted file mode 100644 index 0eb6d6adc..000000000 --- a/yubico-util-scala/build.gradle +++ /dev/null @@ -1,23 +0,0 @@ -plugins { - id 'scala' - id 'io.github.cosmicsilence.scalafix' -} - -description = 'Yubico internal Scala utilities' - -sourceCompatibility = 1.8 -targetCompatibility = 1.8 - -dependencies { - implementation(platform(rootProject)) - implementation(platform(project(":test-platform"))) - - implementation( - 'org.scala-lang:scala-library', - 'org.scalacheck:scalacheck_2.13', - ) - - testImplementation( - 'org.scalatest:scalatest_2.13', - ) -} diff --git a/yubico-util-scala/build.gradle.kts b/yubico-util-scala/build.gradle.kts new file mode 100644 index 000000000..7ffbc14c7 --- /dev/null +++ b/yubico-util-scala/build.gradle.kts @@ -0,0 +1,20 @@ +plugins { + scala + id("io.github.cosmicsilence.scalafix") +} + +description = "Yubico internal Scala utilities" + +java { + targetCompatibility = JavaVersion.VERSION_1_8 +} + +dependencies { + implementation(platform(rootProject)) + implementation(platform(project(":test-platform"))) + + implementation("org.scala-lang:scala-library") + implementation("org.scalacheck:scalacheck_2.13") + + testImplementation( "org.scalatest:scalatest_2.13") +} From 87529b1705d38d6da327cff6a20f32b51632de57 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 5 Sep 2022 16:36:13 +0200 Subject: [PATCH 085/145] Move httpclient dependency version constraint from tests to main --- build.gradle | 1 + test-platform/build.gradle.kts | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 7ed2fe32f..f81a0908b 100644 --- a/build.gradle +++ b/build.gradle @@ -51,6 +51,7 @@ dependencies { api('com.fasterxml.jackson.datatype:jackson-datatype-jsr310:[2.13.2,3)') api('com.google.guava:guava:[24.1.1,31)') api('com.upokecenter:cbor:[4.5.1,5)') + api('org.apache.httpcomponents:httpclient:[4.5.2,5)') api('org.bouncycastle:bcpkix-jdk15on:[1.62,2)') api('org.bouncycastle:bcprov-jdk15on:[1.62,2)') api('org.slf4j:slf4j-api:[1.7.25,2)') diff --git a/test-platform/build.gradle.kts b/test-platform/build.gradle.kts index b2bfd9a56..308f7afe6 100644 --- a/test-platform/build.gradle.kts +++ b/test-platform/build.gradle.kts @@ -7,7 +7,6 @@ description = "Dependency constraints for tests" dependencies { constraints { api("junit:junit:[4.12,5)") - api("org.apache.httpcomponents:httpclient:[4.5.2,5)") api("org.mockito:mockito-core:[2.27.0,3)") api("org.scalacheck:scalacheck_2.13:[1.14.0,2)") api("org.scalatest:scalatest_2.13:[3.0.8,3.1)") From 14634834d5e103091d59acc509335dd6aa53c5db Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 5 Sep 2022 16:42:19 +0200 Subject: [PATCH 086/145] Don't depend on test platform in yubico-util API --- NEWS | 2 ++ yubico-util/build.gradle | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/NEWS b/NEWS index cde4dc621..9238b3685 100644 --- a/NEWS +++ b/NEWS @@ -8,6 +8,8 @@ Changes: Fixes: * Fixed various typos and mistakes in JavaDocs. +* Moved version constraints for test dependencies from meta-module + `webauthn-server-parent` to unpublished test meta-module. == Version 2.0.0 == diff --git a/yubico-util/build.gradle b/yubico-util/build.gradle index 9edd49a86..3a997d34a 100644 --- a/yubico-util/build.gradle +++ b/yubico-util/build.gradle @@ -16,7 +16,6 @@ targetCompatibility = 1.8 dependencies { api(platform(rootProject)) - api(platform(project(":test-platform"))) api( 'com.fasterxml.jackson.core:jackson-databind', @@ -32,6 +31,7 @@ dependencies { ) testImplementation( + platform(project(":test-platform")), project(':yubico-util-scala'), 'junit:junit', 'org.scala-lang:scala-library', From c68c57cce76f367d6bab09a5931eacb29cb64c41 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 5 Sep 2022 16:39:40 +0200 Subject: [PATCH 087/145] Port yubico-util/build.gradle to build.gradle.kts --- yubico-util/build.gradle | 68 ------------------------------------ yubico-util/build.gradle.kts | 64 +++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 68 deletions(-) delete mode 100644 yubico-util/build.gradle create mode 100644 yubico-util/build.gradle.kts diff --git a/yubico-util/build.gradle b/yubico-util/build.gradle deleted file mode 100644 index 3a997d34a..000000000 --- a/yubico-util/build.gradle +++ /dev/null @@ -1,68 +0,0 @@ -plugins { - id 'java-library' - id 'scala' - id 'maven-publish' - id 'signing' - id 'info.solidsoft.pitest' - id 'io.github.cosmicsilence.scalafix' -} - -description = 'Yubico internal utilities' - -project.ext.publishMe = true - -sourceCompatibility = 1.8 -targetCompatibility = 1.8 - -dependencies { - api(platform(rootProject)) - - api( - 'com.fasterxml.jackson.core:jackson-databind', - ) - - implementation( - 'com.fasterxml.jackson.dataformat:jackson-dataformat-cbor', - 'com.fasterxml.jackson.datatype:jackson-datatype-jdk8', - 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310', - 'com.google.guava:guava', - 'com.upokecenter:cbor', - 'org.slf4j:slf4j-api', - ) - - testImplementation( - platform(project(":test-platform")), - project(':yubico-util-scala'), - 'junit:junit', - 'org.scala-lang:scala-library', - 'org.scalacheck:scalacheck_2.13', - 'org.scalatest:scalatest_2.13', - ) -} - - -jar { - manifest { - attributes([ - 'Implementation-Id': 'yubico-util', - 'Implementation-Title': project.description, - 'Implementation-Version': project.version, - 'Implementation-Vendor': 'Yubico', - ]) - } -} - -pitest { - pitestVersion = '1.4.11' - - timestampedReports = false - outputFormats = ['XML', 'HTML'] - - avoidCallsTo = [ - 'java.util.logging', - 'org.apache.log4j', - 'org.slf4j', - 'org.apache.commons.logging', - 'com.google.common.io.Closeables', - ] -} diff --git a/yubico-util/build.gradle.kts b/yubico-util/build.gradle.kts new file mode 100644 index 000000000..fb82f96d8 --- /dev/null +++ b/yubico-util/build.gradle.kts @@ -0,0 +1,64 @@ +plugins { + `java-library` + scala + `maven-publish` + signing + id("info.solidsoft.pitest") + id("io.github.cosmicsilence.scalafix") +} + +description = "Yubico internal utilities" + +val publishMe by extra(true) + +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +dependencies { + api(platform(rootProject)) + + api("com.fasterxml.jackson.core:jackson-databind") + + implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-cbor") + implementation("com.fasterxml.jackson.datatype:jackson-datatype-jdk8") + implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310") + implementation("com.google.guava:guava") + implementation("com.upokecenter:cbor") + implementation("org.slf4j:slf4j-api") + + testImplementation(platform(project(":test-platform"))) + testImplementation(project(":yubico-util-scala")) + testImplementation("junit:junit") + testImplementation("org.scala-lang:scala-library") + testImplementation("org.scalacheck:scalacheck_2.13") + testImplementation("org.scalatest:scalatest_2.13") +} + + +tasks.jar { + manifest { + attributes(mapOf( + "Implementation-Id" to "yubico-util", + "Implementation-Title" to project.description, + "Implementation-Version" to project.version, + "Implementation-Vendor" to "Yubico", + )) + } +} + +pitest { + pitestVersion.set("1.4.11") + timestampedReports.set(false) + + outputFormats.set(listOf("XML", "HTML")) + + avoidCallsTo.set(listOf( + "java.util.logging", + "org.apache.log4j", + "org.slf4j", + "org.apache.commons.logging", + "com.google.common.io.Closeables", + )) +} From 3d639e2f00b2475d36b66e6336a2c17939d96125 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 5 Sep 2022 17:19:57 +0200 Subject: [PATCH 088/145] Port buildSrc/build.gradle to build.gradle.kts --- buildSrc/build.gradle | 12 ------------ buildSrc/build.gradle.kts | 12 ++++++++++++ 2 files changed, 12 insertions(+), 12 deletions(-) delete mode 100644 buildSrc/build.gradle create mode 100644 buildSrc/build.gradle.kts diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle deleted file mode 100644 index 24de977c2..000000000 --- a/buildSrc/build.gradle +++ /dev/null @@ -1,12 +0,0 @@ -apply plugin: 'groovy' - -repositories { - mavenCentral() -} - -dependencies { - implementation( - 'commons-io:commons-io:2.5', - 'info.solidsoft.gradle.pitest:gradle-pitest-plugin:1.5.1', - ) -} diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts new file mode 100644 index 000000000..00b97c10d --- /dev/null +++ b/buildSrc/build.gradle.kts @@ -0,0 +1,12 @@ +plugins { + groovy +} + +repositories { + mavenCentral() +} + +dependencies { + implementation("commons-io:commons-io:2.5") + implementation("info.solidsoft.gradle.pitest:gradle-pitest-plugin:1.5.1") +} From ebfeb623b795e6066164d6d22d521df0a21daa68 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 5 Sep 2022 17:32:13 +0200 Subject: [PATCH 089/145] Apply spotless plugin declaratively --- build.gradle | 5 ++--- webauthn-server-attestation/build.gradle | 1 + webauthn-server-core/build.gradle | 1 + webauthn-server-demo/build.gradle | 1 + yubico-util-scala/build.gradle.kts | 1 + yubico-util/build.gradle.kts | 1 + 6 files changed, 7 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index f81a0908b..c3832ce49 100644 --- a/build.gradle +++ b/build.gradle @@ -91,8 +91,7 @@ subprojects { mavenCentral() } - if (project !== project(':test-platform')) { - apply plugin: 'com.diffplug.spotless' + if (project.plugins.hasPlugin('com.diffplug.spotless')) { spotless { java { googleJavaFormat() @@ -130,7 +129,7 @@ String getGitCommitOrUnknown() { subprojects { project -> - if (project.plugins.hasPlugin('scala')) { + if (project.plugins.hasPlugin('scala') && project.plugins.hasPlugin('com.diffplug.spotless')) { project.scalafix { configFile = rootProject.file('scalafix.conf') diff --git a/webauthn-server-attestation/build.gradle b/webauthn-server-attestation/build.gradle index a8dfd227f..3d7429d6d 100644 --- a/webauthn-server-attestation/build.gradle +++ b/webauthn-server-attestation/build.gradle @@ -3,6 +3,7 @@ plugins { id 'scala' id 'maven-publish' id 'signing' + id 'com.diffplug.spotless' id 'info.solidsoft.pitest' id 'io.github.cosmicsilence.scalafix' } diff --git a/webauthn-server-core/build.gradle b/webauthn-server-core/build.gradle index 059ad2375..348771b1c 100644 --- a/webauthn-server-core/build.gradle +++ b/webauthn-server-core/build.gradle @@ -3,6 +3,7 @@ plugins { id 'scala' id 'maven-publish' id 'signing' + id 'com.diffplug.spotless' id 'info.solidsoft.pitest' id 'io.github.cosmicsilence.scalafix' } diff --git a/webauthn-server-demo/build.gradle b/webauthn-server-demo/build.gradle index b4b302971..32f90498b 100644 --- a/webauthn-server-demo/build.gradle +++ b/webauthn-server-demo/build.gradle @@ -3,6 +3,7 @@ plugins { id 'war' id 'application' id 'scala' + id 'com.diffplug.spotless' id 'io.github.cosmicsilence.scalafix' } diff --git a/yubico-util-scala/build.gradle.kts b/yubico-util-scala/build.gradle.kts index 7ffbc14c7..d9baf98c6 100644 --- a/yubico-util-scala/build.gradle.kts +++ b/yubico-util-scala/build.gradle.kts @@ -1,5 +1,6 @@ plugins { scala + id("com.diffplug.spotless") id("io.github.cosmicsilence.scalafix") } diff --git a/yubico-util/build.gradle.kts b/yubico-util/build.gradle.kts index fb82f96d8..e0d3a3bb9 100644 --- a/yubico-util/build.gradle.kts +++ b/yubico-util/build.gradle.kts @@ -3,6 +3,7 @@ plugins { scala `maven-publish` signing + id("com.diffplug.spotless") id("info.solidsoft.pitest") id("io.github.cosmicsilence.scalafix") } From 395329f79b182008ea3d80e6c5bcf086c9480dcb Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 5 Sep 2022 17:33:41 +0200 Subject: [PATCH 090/145] Apply publishing plugins declaratively --- build.gradle | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index c3832ce49..919624c10 100644 --- a/build.gradle +++ b/build.gradle @@ -10,6 +10,8 @@ buildscript { } plugins { id 'java-platform' + id 'maven-publish' + id 'signing' id 'io.github.gradle-nexus.publish-plugin' version '1.1.0' id 'io.franzbecker.gradle-lombok' version '5.0.0' } @@ -260,9 +262,6 @@ subprojects { project -> // The root project has no sources, but the dependency platform also needs to be published as an artifact // See https://docs.gradle.org/current/userguide/java_platform_plugin.html // See https://github.com/Yubico/java-webauthn-server/issues/93#issuecomment-822806951 -apply plugin: 'maven-publish' -apply plugin: 'signing' - publishing { publications { jars(MavenPublication) { From 5da1e907510f47521008f3026d463304bd6e356d Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 5 Sep 2022 17:54:44 +0200 Subject: [PATCH 091/145] Update name of YubiKey 5Ci in FIDO MDS integration tests --- .../fido/metadata/FidoMetadataServiceIntegrationTest.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/webauthn-server-attestation/src/integrationTest/scala/com/yubico/fido/metadata/FidoMetadataServiceIntegrationTest.scala b/webauthn-server-attestation/src/integrationTest/scala/com/yubico/fido/metadata/FidoMetadataServiceIntegrationTest.scala index 399c8ceb8..4db610b8d 100644 --- a/webauthn-server-attestation/src/integrationTest/scala/com/yubico/fido/metadata/FidoMetadataServiceIntegrationTest.scala +++ b/webauthn-server-attestation/src/integrationTest/scala/com/yubico/fido/metadata/FidoMetadataServiceIntegrationTest.scala @@ -186,7 +186,7 @@ class FidoMetadataServiceIntegrationTest } it("a YubiKey 5Ci.") { - check("YubiKey 5Ci", RealExamples.YubiKey5Ci, attachmentHintsUsb) + check("YubiKey 5 .*Lightning", RealExamples.YubiKey5Ci, attachmentHintsUsb) } ignore("a Security Key by Yubico.") { // TODO: Investigate why this fails @@ -223,7 +223,7 @@ class FidoMetadataServiceIntegrationTest it("a YubiKey 5.4 Ci FIPS.") { check( - "YubiKey 5Ci FIPS", + "YubiKey 5 .*FIPS .*Lightning", RealExamples.Yubikey5ciFips, attachmentHintsUsb, ) From d6744bdcf51ea849db5050977b432de3c99fcbd4 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 6 Sep 2022 14:51:11 +0200 Subject: [PATCH 092/145] Port webauthn-server-demo/build.gradle to build.gradle.kts --- webauthn-server-demo/build.gradle | 68 --------------------------- webauthn-server-demo/build.gradle.kts | 68 +++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 68 deletions(-) delete mode 100644 webauthn-server-demo/build.gradle create mode 100644 webauthn-server-demo/build.gradle.kts diff --git a/webauthn-server-demo/build.gradle b/webauthn-server-demo/build.gradle deleted file mode 100644 index 32f90498b..000000000 --- a/webauthn-server-demo/build.gradle +++ /dev/null @@ -1,68 +0,0 @@ -plugins { - id 'java' - id 'war' - id 'application' - id 'scala' - id 'com.diffplug.spotless' - id 'io.github.cosmicsilence.scalafix' -} - -description = 'WebAuthn demo' - -dependencies { - implementation(platform(rootProject)) - - implementation( - project(':webauthn-server-attestation'), - project(':webauthn-server-core'), - project(':yubico-util'), - - 'com.fasterxml.jackson.core:jackson-databind', - 'com.google.guava:guava', - 'com.upokecenter:cbor', - 'org.bouncycastle:bcprov-jdk15on', - 'org.slf4j:slf4j-api', - ) - - implementation( - 'org.eclipse.jetty:jetty-servlet:9.4.9.v20180320', - 'org.glassfish.jersey.containers:jersey-container-servlet-core:2.36', - 'javax.ws.rs:javax.ws.rs-api:2.1.1', - ) - - runtimeOnly( - 'ch.qos.logback:logback-classic:1.3.0', - 'org.glassfish.jersey.containers:jersey-container-servlet:2.36', - 'org.glassfish.jersey.inject:jersey-hk2:2.36', - ) - - testImplementation( - platform(project(":test-platform")), - project(':webauthn-server-core').sourceSets.test.output, - project(':yubico-util-scala'), - - 'junit:junit', - 'org.mockito:mockito-core', - 'org.scala-lang:scala-library', - 'org.scalacheck:scalacheck_2.13', - 'org.scalatest:scalatest_2.13', - ) - - modules { - module('javax.servlet:servlet-api') { - replacedBy('javax.servlet:javax.servlet-api') - } - } -} - -mainClassName = 'demo.webauthn.EmbeddedServer' - -[installDist, distZip, distTar].each { task -> - def intoDir = (task == installDist) ? "/" : "${project.name}-${project.version}" - task.into(intoDir) { - from 'keystore.jks' - from('src/main/webapp') { - into 'src/main/webapp' - } - } -} diff --git a/webauthn-server-demo/build.gradle.kts b/webauthn-server-demo/build.gradle.kts new file mode 100644 index 000000000..e9727b78e --- /dev/null +++ b/webauthn-server-demo/build.gradle.kts @@ -0,0 +1,68 @@ +plugins { + java + war + application + scala + id("com.diffplug.spotless") + id("io.github.cosmicsilence.scalafix") +} + +description = "WebAuthn demo" + +// Can't use test fixtures because they interfere with pitest: https://github.com/gradle/gradle/issues/12168 +evaluationDependsOn(":webauthn-server-core") +val coreTestsOutput = project(":webauthn-server-core").extensions.getByType(SourceSetContainer::class).test.get().output + +dependencies { + implementation(platform(rootProject)) + + implementation(project(":webauthn-server-attestation")) + implementation(project(":webauthn-server-core")) + implementation(project(":yubico-util")) + + implementation("com.fasterxml.jackson.core:jackson-databind") + implementation("com.google.guava:guava") + implementation("com.upokecenter:cbor") + implementation("org.bouncycastle:bcprov-jdk15on") + implementation("org.slf4j:slf4j-api") + + implementation("org.eclipse.jetty:jetty-servlet:9.4.9.v20180320") + implementation("org.glassfish.jersey.containers:jersey-container-servlet-core:2.36") + implementation("javax.ws.rs:javax.ws.rs-api:2.1.1") + + runtimeOnly("ch.qos.logback:logback-classic:1.3.0") + runtimeOnly("org.glassfish.jersey.containers:jersey-container-servlet:2.36") + runtimeOnly("org.glassfish.jersey.inject:jersey-hk2:2.36") + + testImplementation(platform(project(":test-platform"))) + testImplementation(coreTestsOutput) + testImplementation(project(":yubico-util-scala")) + + testImplementation("junit:junit") + testImplementation("org.mockito:mockito-core") + testImplementation("org.scala-lang:scala-library") + testImplementation("org.scalacheck:scalacheck_2.13") + testImplementation("org.scalatest:scalatest_2.13") + + modules { + module("javax.servlet:servlet-api") { + replacedBy("javax.servlet:javax.servlet-api") + } + } +} + +application { + mainClass.set("demo.webauthn.EmbeddedServer") +} + +for (task in listOf(tasks.installDist, tasks.distZip, tasks.distTar)) { + val intoDir = if (task == tasks.installDist) { "/" } else { "${project.name}-${project.version}" } + task { + into(intoDir) { + from("keystore.jks") + from("src/main/webapp") { + into("src/main/webapp") + } + } + } +} From 8e39b322abf4239a70e8c1d33a34d5afcfdecc3d Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 6 Sep 2022 16:15:46 +0200 Subject: [PATCH 093/145] Port webauthn-server-attestation/build.gradle to build.gradle.kts --- build.gradle | 16 +-- .../groovy/com/yubico/gradle/GitUtils.groovy | 18 +++ webauthn-server-attestation/build.gradle | 107 ------------------ webauthn-server-attestation/build.gradle.kts | 104 +++++++++++++++++ webauthn-server-core/build.gradle | 3 +- 5 files changed, 126 insertions(+), 122 deletions(-) create mode 100644 buildSrc/src/main/groovy/com/yubico/gradle/GitUtils.groovy delete mode 100644 webauthn-server-attestation/build.gradle create mode 100644 webauthn-server-attestation/build.gradle.kts diff --git a/build.gradle b/build.gradle index 919624c10..97aa204a5 100644 --- a/build.gradle +++ b/build.gradle @@ -18,6 +18,7 @@ plugins { import io.franzbecker.gradle.lombok.LombokPlugin import io.franzbecker.gradle.lombok.task.DelombokTask +import com.yubico.gradle.GitUtils; rootProject.description = "Metadata root for the com.yubico:webauthn-server-* module family" @@ -116,19 +117,6 @@ task assembleJavadoc(type: Sync) { destinationDir = file("${rootProject.buildDir}/javadoc") } -String getGitCommit() { - def proc = "git rev-parse HEAD".execute(null, projectDir) - proc.waitFor() - if (proc.exitValue() != 0) { - return null - } - return proc.text.trim() -} - -String getGitCommitOrUnknown() { - return getGitCommit() ?: 'UNKNOWN' -} - subprojects { project -> if (project.plugins.hasPlugin('scala') && project.plugins.hasPlugin('com.diffplug.spotless')) { @@ -207,7 +195,7 @@ subprojects { project -> if (project.hasProperty('publishMe') && project.publishMe) { - if (getGitCommit() == null) { + if (GitUtils.getGitCommit(projectDir) == null) { throw new RuntimeException("Failed to get git commit ID"); } diff --git a/buildSrc/src/main/groovy/com/yubico/gradle/GitUtils.groovy b/buildSrc/src/main/groovy/com/yubico/gradle/GitUtils.groovy new file mode 100644 index 000000000..65e63d910 --- /dev/null +++ b/buildSrc/src/main/groovy/com/yubico/gradle/GitUtils.groovy @@ -0,0 +1,18 @@ +package com.yubico.gradle; + +public class GitUtils { + + public static String getGitCommit(File projectDir) { + def proc = "git rev-parse HEAD".execute(null, projectDir) + proc.waitFor() + if (proc.exitValue() != 0) { + return null + } + return proc.text.trim() + } + + public static String getGitCommitOrUnknown(projectDir) { + return getGitCommit(projectDir) ?: 'UNKNOWN' + } + +} diff --git a/webauthn-server-attestation/build.gradle b/webauthn-server-attestation/build.gradle deleted file mode 100644 index 3d7429d6d..000000000 --- a/webauthn-server-attestation/build.gradle +++ /dev/null @@ -1,107 +0,0 @@ -plugins { - id 'java-library' - id 'scala' - id 'maven-publish' - id 'signing' - id 'com.diffplug.spotless' - id 'info.solidsoft.pitest' - id 'io.github.cosmicsilence.scalafix' -} - -description = 'Yubico WebAuthn attestation subsystem' - -project.ext.publishMe = true - -sourceCompatibility = 1.8 -targetCompatibility = 1.8 - -evaluationDependsOn(':webauthn-server-core') - -sourceSets { - integrationTest { - compileClasspath += sourceSets.main.output - runtimeClasspath += sourceSets.main.output - } -} - -configurations { - integrationTestImplementation.extendsFrom testImplementation - integrationTestRuntimeOnly.extendsFrom testRuntimeOnly -} - -dependencies { - api(platform(rootProject)) - - api( - project(':webauthn-server-core'), - ) - - implementation( - project(':yubico-util'), - 'com.google.guava:guava', - 'com.fasterxml.jackson.core:jackson-databind', - 'org.bouncycastle:bcprov-jdk15on', - 'org.slf4j:slf4j-api', - ) - - testImplementation( - platform(project(":test-platform")), - project(':webauthn-server-core').sourceSets.test.output, - project(':yubico-util-scala'), - 'com.fasterxml.jackson.datatype:jackson-datatype-jdk8', - 'junit:junit', - 'org.bouncycastle:bcpkix-jdk15on', - 'org.eclipse.jetty:jetty-server:[9.4.9.v20180320,10)', - 'org.mockito:mockito-core', - 'org.scala-lang:scala-library', - 'org.scalacheck:scalacheck_2.13', - 'org.scalatest:scalatest_2.13', - 'uk.org.lidalia:slf4j-test', - ) - - testImplementation('org.slf4j:slf4j-api') { - version { - strictly '[1.7.25,1.8-a)' // Pre-1.8 version required by slf4j-test - } - } -} - -tasks.register('integrationTest', Test) { - description = 'Runs integration tests.' - group = 'verification' - - testClassesDirs = sourceSets.integrationTest.output.classesDirs - classpath = sourceSets.integrationTest.runtimeClasspath - shouldRunAfter test - check.dependsOn it - - // Required for processing CRL distribution points extension - systemProperty 'com.sun.security.enableCRLDP', 'true' -} - -jar { - manifest { - attributes([ - 'Implementation-Id': 'java-webauthn-server-attestation', - 'Implementation-Title': project.description, - 'Implementation-Version': project.version, - 'Implementation-Vendor': 'Yubico', - 'Git-Commit': getGitCommitOrUnknown(), - ]) - } -} - -pitest { - pitestVersion = '1.4.11' - - timestampedReports = false - outputFormats = ['XML', 'HTML'] - - avoidCallsTo = [ - 'java.util.logging', - 'org.apache.log4j', - 'org.slf4j', - 'org.apache.commons.logging', - 'com.google.common.io.Closeables', - ] -} diff --git a/webauthn-server-attestation/build.gradle.kts b/webauthn-server-attestation/build.gradle.kts new file mode 100644 index 000000000..b1c5aa38d --- /dev/null +++ b/webauthn-server-attestation/build.gradle.kts @@ -0,0 +1,104 @@ +import com.yubico.gradle.GitUtils; +plugins { + `java-library` + scala + `maven-publish` + signing + id("com.diffplug.spotless") + id("info.solidsoft.pitest") + id("io.github.cosmicsilence.scalafix") +} + +description = "Yubico WebAuthn attestation subsystem" + +val publishMe by extra(true) + +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +sourceSets { + create("integrationTest") { + compileClasspath += sourceSets.main.get().output + runtimeClasspath += sourceSets.main.get().output + } +} + +configurations["integrationTestImplementation"].extendsFrom(configurations.testImplementation.get()) +configurations["integrationTestRuntimeOnly"].extendsFrom(configurations.testRuntimeOnly.get()) + +// Can't use test fixtures because they interfere with pitest: https://github.com/gradle/gradle/issues/12168 +evaluationDependsOn(":webauthn-server-core") +val coreTestsOutput = project(":webauthn-server-core").extensions.getByType(SourceSetContainer::class).test.get().output + +dependencies { + api(platform(rootProject)) + + api(project(":webauthn-server-core")) + + implementation(project(":yubico-util")) + implementation("com.google.guava:guava") + implementation("com.fasterxml.jackson.core:jackson-databind") + implementation("org.bouncycastle:bcprov-jdk15on") + implementation("org.slf4j:slf4j-api") + + testImplementation(platform(project(":test-platform"))) + testImplementation(coreTestsOutput) + testImplementation(project(":yubico-util-scala")) + testImplementation("com.fasterxml.jackson.datatype:jackson-datatype-jdk8") + testImplementation("junit:junit") + testImplementation("org.bouncycastle:bcpkix-jdk15on") + testImplementation("org.eclipse.jetty:jetty-server:[9.4.9.v20180320,10)") + testImplementation("org.mockito:mockito-core") + testImplementation("org.scala-lang:scala-library") + testImplementation("org.scalacheck:scalacheck_2.13") + testImplementation("org.scalatest:scalatest_2.13") + testImplementation("uk.org.lidalia:slf4j-test") + + testImplementation("org.slf4j:slf4j-api") { + version { + strictly("[1.7.25,1.8-a)") // Pre-1.8 version required by slf4j-test + } + } +} + +val integrationTest = task("integrationTest") { + description = "Runs integration tests." + group = "verification" + + testClassesDirs = sourceSets["integrationTest"].output.classesDirs + classpath = sourceSets["integrationTest"].runtimeClasspath + shouldRunAfter(tasks.test) + + // Required for processing CRL distribution points extension + systemProperty("com.sun.security.enableCRLDP", "true") +} +tasks["check"].dependsOn(integrationTest) + +tasks.jar { + manifest { + attributes(mapOf( + "Implementation-Id" to "java-webauthn-server-attestation", + "Implementation-Title" to project.description, + "Implementation-Version" to project.version, + "Implementation-Vendor" to "Yubico", + "Git-Commit" to GitUtils.getGitCommitOrUnknown(projectDir), + )) + } +} + +pitest { + pitestVersion.set("1.4.11") + timestampedReports.set(false) + + outputFormats.set(listOf("XML", "HTML")) + + avoidCallsTo.set(listOf( + "java.util.logging", + "org.apache.log4j", + "org.slf4j", + "org.apache.commons.logging", + "com.google.common.io.Closeables", + )) +} diff --git a/webauthn-server-core/build.gradle b/webauthn-server-core/build.gradle index 348771b1c..4b75f43e7 100644 --- a/webauthn-server-core/build.gradle +++ b/webauthn-server-core/build.gradle @@ -1,3 +1,4 @@ +import com.yubico.gradle.GitUtils; plugins { id 'java-library' id 'scala' @@ -69,7 +70,7 @@ jar { 'Implementation-Version': project.version, 'Implementation-Vendor': 'Yubico', 'Implementation-Source-Url': 'https://github.com/Yubico/java-webauthn-server', - 'Git-Commit': getGitCommitOrUnknown(), + 'Git-Commit': GitUtils.getGitCommitOrUnknown(projectDir), ]) } } From 518a5d69c0d73a79f9166b5196e2dd0c56b25c42 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 6 Sep 2022 15:38:09 +0200 Subject: [PATCH 094/145] Port webauthn-server-core/build.gradle to build.gradle.kts --- webauthn-server-core/build.gradle | 92 --------------------------- webauthn-server-core/build.gradle.kts | 88 +++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 92 deletions(-) delete mode 100644 webauthn-server-core/build.gradle create mode 100644 webauthn-server-core/build.gradle.kts diff --git a/webauthn-server-core/build.gradle b/webauthn-server-core/build.gradle deleted file mode 100644 index 4b75f43e7..000000000 --- a/webauthn-server-core/build.gradle +++ /dev/null @@ -1,92 +0,0 @@ -import com.yubico.gradle.GitUtils; -plugins { - id 'java-library' - id 'scala' - id 'maven-publish' - id 'signing' - id 'com.diffplug.spotless' - id 'info.solidsoft.pitest' - id 'io.github.cosmicsilence.scalafix' -} - -description = 'Yubico WebAuthn server core API' - -project.ext.publishMe = true - -sourceCompatibility = 1.8 -targetCompatibility = 1.8 - -dependencies { - api(platform(rootProject)) - - api( - project(':yubico-util'), - ) - - implementation( - 'com.augustcellars.cose:cose-java', - 'com.fasterxml.jackson.core:jackson-databind', - 'com.google.guava:guava', - 'com.upokecenter:cbor', - 'org.apache.httpcomponents:httpclient', - 'org.slf4j:slf4j-api', - ) - - testImplementation( - platform(project(":test-platform")), - project(':yubico-util-scala'), - 'com.fasterxml.jackson.datatype:jackson-datatype-jdk8', - 'junit:junit', - 'org.bouncycastle:bcpkix-jdk15on', - 'org.bouncycastle:bcprov-jdk15on', - 'org.mockito:mockito-core', - 'org.scala-lang:scala-library', - 'org.scalacheck:scalacheck_2.13', - 'org.scalatest:scalatest_2.13', - 'uk.org.lidalia:slf4j-test', - ) - - testImplementation('org.slf4j:slf4j-api') { - version { - strictly '[1.7.25,1.8-a)' // Pre-1.8 version required by slf4j-test - } - } -} - -jar { - manifest { - attributes([ - 'Specification-Title': 'Web Authentication: An API for accessing Public Key Credentials', - 'Specification-Version': 'Level 2 Proposed Recommendation 2021-04-08', - 'Specification-Vendor': 'World Wide Web Consortium', - - 'Specification-Url': 'https://www.w3.org/TR/2021/REC-webauthn-2-20210408/', - 'Specification-Url-Latest': 'https://www.w3.org/TR/webauthn-2/', - 'Specification-W3c-Status': 'recommendation', - 'Specification-Release-Date': '2021-04-08', - - 'Implementation-Id': 'java-webauthn-server', - 'Implementation-Title': 'Yubico Web Authentication server library', - 'Implementation-Version': project.version, - 'Implementation-Vendor': 'Yubico', - 'Implementation-Source-Url': 'https://github.com/Yubico/java-webauthn-server', - 'Git-Commit': GitUtils.getGitCommitOrUnknown(projectDir), - ]) - } -} - -pitest { - pitestVersion = '1.4.11' - - timestampedReports = false - outputFormats = ['XML', 'HTML'] - - avoidCallsTo = [ - 'java.util.logging', - 'org.apache.log4j', - 'org.slf4j', - 'org.apache.commons.logging', - 'com.google.common.io.Closeables', - ] -} - diff --git a/webauthn-server-core/build.gradle.kts b/webauthn-server-core/build.gradle.kts new file mode 100644 index 000000000..7e2ed6a3c --- /dev/null +++ b/webauthn-server-core/build.gradle.kts @@ -0,0 +1,88 @@ +import com.yubico.gradle.GitUtils; +plugins { + `java-library` + scala + `maven-publish` + signing + id("com.diffplug.spotless") + id("info.solidsoft.pitest") + id("io.github.cosmicsilence.scalafix") +} + +description = "Yubico WebAuthn server core API" + +val publishMe by extra(true) + +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +dependencies { + api(platform(rootProject)) + + api(project(":yubico-util")) + + implementation("com.augustcellars.cose:cose-java") + implementation("com.fasterxml.jackson.core:jackson-databind") + implementation("com.google.guava:guava") + implementation("com.upokecenter:cbor") + implementation("org.apache.httpcomponents:httpclient") + implementation("org.slf4j:slf4j-api") + + testImplementation(platform(project(":test-platform"))) + testImplementation(project(":yubico-util-scala")) + testImplementation("com.fasterxml.jackson.core:jackson-databind") + testImplementation("com.upokecenter:cbor") + testImplementation("junit:junit") + testImplementation("org.bouncycastle:bcpkix-jdk15on") + testImplementation("org.bouncycastle:bcprov-jdk15on") + testImplementation("org.mockito:mockito-core") + testImplementation("org.scala-lang:scala-library") + testImplementation("org.scalacheck:scalacheck_2.13") + testImplementation("org.scalatest:scalatest_2.13") + testImplementation("uk.org.lidalia:slf4j-test") + + testImplementation("org.slf4j:slf4j-api") { + version { + strictly("[1.7.25,1.8-a)") // Pre-1.8 version required by slf4j-test + } + } +} + +tasks.jar { + manifest { + attributes(mapOf( + "Specification-Title" to "Web Authentication: An API for accessing Public Key Credentials", + "Specification-Version" to "Level 2 Proposed Recommendation 2021-04-08", + "Specification-Vendor" to "World Wide Web Consortium", + + "Specification-Url" to "https://www.w3.org/TR/2021/REC-webauthn-2-20210408/", + "Specification-Url-Latest" to "https://www.w3.org/TR/webauthn-2/", + "Specification-W3c-Status" to "recommendation", + "Specification-Release-Date" to "2021-04-08", + + "Implementation-Id" to "java-webauthn-server", + "Implementation-Title" to "Yubico Web Authentication server library", + "Implementation-Version" to project.version, + "Implementation-Vendor" to "Yubico", + "Implementation-Source-Url" to "https://github.com/Yubico/java-webauthn-server", + "Git-Commit" to com.yubico.gradle.GitUtils.getGitCommitOrUnknown(projectDir), + )) + } +} + +pitest { + pitestVersion.set("1.4.11") + timestampedReports.set(false) + + outputFormats.set(listOf("XML", "HTML")) + + avoidCallsTo.set(listOf( + "java.util.logging", + "org.apache.log4j", + "org.slf4j", + "org.apache.commons.logging", + "com.google.common.io.Closeables", + )) +} From f55ef9caa9c521f6cf259b6bb3e60f1b653ce0cf Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 6 Sep 2022 16:44:19 +0200 Subject: [PATCH 095/145] Upgrade Lombok to version 1.18.24 --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 97aa204a5..df79860a8 100644 --- a/build.gradle +++ b/build.gradle @@ -80,8 +80,8 @@ subprojects { apply plugin: LombokPlugin lombok { - version '1.18.20' - sha256 = 'ce947be6c2fbe759fbbe8ef3b42b6825f814c98c8853f1013f2d9630cedf74b0' + version '1.18.24' + sha256 = 'd3584bc2db03f059f984fb0a9c119aac1fa0da578a448e69fc3f68b36584c749' } tasks.withType(AbstractCompile) { if (tasks.findByName('verifyLombok')) { From 0b9208d050a0cb087795f480aa0de9cd3c202564 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 6 Sep 2022 18:00:07 +0200 Subject: [PATCH 096/145] Upgrade Gradle wrapper to version 7.5.1 --- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.jar | Bin 59536 -> 60756 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 6 ++++++ gradlew.bat | 14 ++++++++------ 5 files changed, 16 insertions(+), 8 deletions(-) diff --git a/build.gradle b/build.gradle index df79860a8..b071b8243 100644 --- a/build.gradle +++ b/build.gradle @@ -42,7 +42,7 @@ if (publishEnabled) { } wrapper { - gradleVersion = '7.3' + gradleVersion = '7.5.1' } dependencies { diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 7454180f2ae8848c63b8b4dea2cb829da983f2fa..249e5832f090a2944b7473328c07c9755baa3196 100644 GIT binary patch delta 10158 zcmaKSbyOWsmn~e}-QC?axCPf>!2<-jxI0|j{UX8L-QC?axDz};a7}ppGBe+Nv*x{5 zy?WI?=j^WT(_Md5*V*xNP>X9&wM>xUvNiMuKDK=Xg!N%oM>Yru2rh7#yD-sW0Ov#$ zCKBSOD3>TM%&1T5t&#FK@|@1f)Ze+EE6(7`}J(Ek4})CD@I+W;L{ zO>K;wokKMA)EC6C|D@nz%D2L3U=Nm(qc>e4GM3WsHGu-T?l^PV6m-T-(igun?PZ8U z{qbiLDMcGSF1`FiKhlsV@qPMRm~h9@z3DZmWp;Suh%5BdP6jqHn}$-gu`_xNg|j{PSJ0n$ zbE;Azwq8z6IBlgKIEKc4V?*##hGW#t*rh=f<;~RFWotXS$vr;Mqz>A99PMH3N5BMi zWLNRjc57*z`2)gBV0o4rcGM(u*EG8_H5(|kThAnp|}u2xz>>X6tN zv)$|P2Nr1D*fk4wvqf(7;NmdRV3eL{!>DO-B98(s*-4$g{)EnRYAw+DP-C`=k)B!* zHU7!ejcbavGCYuz9k@$aZQaU%#K%6`D}=N_m?~^)IcmQZun+K)fSIoS>Ws zwvZ%Rfmw>%c!kCd~Pmf$E%LCj2r>+FzKGDm+%u88|hHprot{*OIVpi`Vd^^aumtx2L}h} zPu$v~zdHaWPF<`LVQX4i7bk82h#RwRyORx*z3I}o&>>eBDCif%s7&*vF6kU%1` zf(bvILch^~>cQ{=Y#?nx(8C-Uuv7!2_YeCfo?zkP;FK zX+KdjKS;HQ+7 zj>MCBI=d$~9KDJ1I2sb_3=T6D+Mu9{O&vcTnDA(I#<=L8csjEqsOe=&`=QBc7~>u2 zfdcO44PUOST%PcN+8PzKFYoR0;KJ$-Nwu#MgSM{_!?r&%rVM}acp>53if|vpH)q=O z;6uAi__am8g$EjZ33?PmCrg@(M!V_@(^+#wAWNu&e3*pGlfhF2<3NobAC zlusz>wMV--3ytd@S047g)-J@eOD;DMnC~@zvS=Gnw3=LnRzkeV`LH4#JGPklE4!Q3 zq&;|yGR0FiuE-|&1p2g{MG!Z3)oO9Jf4@0h*3!+RHv=SiEf*oGQCSRQf=LqT5~sajcJ8XjE>E*@q$n z!4|Rz%Lv8TgI23JV6%)N&`Otk6&RBdS|lCe7+#yAfdyEWNTfFb&*S6-;Q}d`de!}*3vM(z71&3 z37B%@GWjeQ_$lr%`m-8B&Zl4Gv^X{+N{GCsQGr!LLU4SHmLt3{B*z-HP{73G8u>nK zHxNQ4eduv>lARQfULUtIlLx#7ea+O;w?LH}FF28c9pg#*M`pB~{jQmPB*gA;Hik#e zZpz&X#O}}r#O_#oSr4f`zN^wedt>ST791bAZ5(=g<Oj)m9X8J^>Th}fznPY0T zsD9ayM7Hrlb6?jHXL<{kdA*Q#UPCYce0p`fHxoZ7_P`cF-$1YY9Pi;0QFt{CCf%C# zuF60A_NTstTQeFR3)O*ThlWKk08}7Nshh}J-sGY=gzE!?(_ZI4ovF6oZ$)&Zt~WZi z_0@Bk!~R4+<&b6CjI{nGj+P{*+9}6;{RwZ7^?H)xjhiRi;?A|wb0UxjPr?L@$^v|0= z@6d3+eU|&re3+G*XgFS}tih3;>2-R1x>`2hmUb5+Z~eM4P|$ zAxvE$l@sIhf_#YLnF|Wcfp(Gh@@dJ-yh|FhKqsyQp_>7j1)w|~5OKETx2P$~`}5huK;{gw_~HXP6=RsG)FKSZ=VYkt+0z&D zr?`R3bqVV?Zmqj&PQ`G3b^PIrd{_K|Hhqt zAUS#|*WpEOeZ{@h*j6%wYsrL`oHNV=z*^}yT1NCTgk1-Gl(&+TqZhODTKb9|0$3;| z;{UUq7X9Oz`*gwbi|?&USWH?Fr;6=@Be4w=8zu>DLUsrwf+7A`)lpdGykP`^SA8{ok{KE3sM$N@l}kB2GDe7MEN? zWcQ2I0fJ1ZK%s-YKk?QbEBO6`C{bg$%le0FTgfmSan-Kih0A7)rGy|2gd)_gRH7qp z*bNlP0u|S^5<)kFcd&wQg*6QP5;y(3ZgI%vUgWk#`g!sMf`02>@xz{Ie9_-fXllyw zh>P%cK+-HkQ;D$Jh=ig(ASN^zJ7|q*#m;}2M*T#s0a^nF_>jI(L(|*}#|$O&B^t!W zv-^-vP)kuu+b%(o3j)B@do)n*Y0x%YNy`sYj*-z2ncYoggD6l z6{1LndTQUh+GCX;7rCrT z@=vy&^1zyl{#7vRPv;R^PZPaIks8okq)To8!Cks0&`Y^Xy5iOWC+MmCg0Jl?1ufXO zaK8Q5IO~J&E|<;MnF_oXLc=LU#m{6yeomA^Ood;)fEqGPeD|fJiz(`OHF_f*{oWJq z1_$NF&Mo7@GKae#f4AD|KIkGVi~ubOj1C>>WCpQq>MeDTR_2xL01^+K1+ zr$}J>d=fW{65hi2bz&zqRKs8zpDln z*7+Gtfz6rkgfj~#{MB=49FRP;ge*e0=x#czw5N{@T1{EAl;G&@tpS!+&2&Stf<%<+55R18u2%+}`?PZo8xg|Y9Xli(fSQyC7 z+O5{;ZyW$!eYR~gy>;l6cA+e`oXN6a6t(&kUkWus*Kf<m$W7L)w5uXYF)->OeWMSUVXi;N#sY zvz4c?GkBU{D;FaQ)9|HU7$?BX8DFH%hC11a@6s4lI}y{XrB~jd{w1x&6bD?gemdlV z-+ZnCcldFanu`P=S0S7XzwXO(7N9KV?AkgZzm|J&f{l-Dp<)|-S7?*@HBIfRxmo1% zcB4`;Al{w-OFD08g=Qochf9=gb56_FPc{C9N5UAjTcJ(`$>)wVhW=A<8i#!bmKD#6~wMBak^2(p56d2vs&O6s4>#NB0UVr24K z%cw|-Yv}g5`_zcEqrZBaRSoBm;BuXJM^+W$yUVS9?u(`87t)IokPgC_bQ3g_#@0Yg zywb?u{Di7zd3XQ$y!m^c`6~t-7@g-hwnTppbOXckS-^N?w1`kRMpC!mfMY?K#^Ldm zYL>771%d{+iqh4a&4RdLNt3_(^^*{U2!A>u^b{7e@}Azd_PiZ>d~(@(Q@EYElLAx3LgQ5(ZUf*I%EbGiBTG!g#=t zXbmPhWH`*B;aZI)$+PWX+W)z?3kTOi{2UY9*b9bpSU!GWcVu+)!^b4MJhf=U9c?jj z%V)EOF8X3qC5~+!Pmmmd@gXzbycd5Jdn!N#i^50a$4u}8^O}DG2$w-U|8QkR-WU1mk4pF z#_imS#~c2~Z{>!oE?wfYc+T+g=eJL`{bL6=Gf_lat2s=|RxgP!e#L|6XA8w{#(Po(xk1~rNQ4UiG``U`eKy7`ot;xv4 zdv54BHMXIq;#^B%W(b8xt%JRueW5PZsB2eW=s3k^Pe1C$-NN8~UA~)=Oy->22yJ%e zu=(XD^5s{MkmWB)AF_qCFf&SDH%ytqpt-jgs35XK8Ez5FUj?uD3++@2%*9+-65LGQ zvu1eopeQoFW98@kzU{+He9$Yj#`vaQkqu%?1wCoBd%G=)TROYl2trZa{AZ@#^LARR zdzg-?EUnt9dK2;W=zCcVj18RTj-%w^#pREbgpD0aL@_v-XV2&Cd@JB^(}GRBU}9gV z6sWmVZmFZ9qrBN%4b?seOcOdOZ+6cx8-#R(+LYKJu~Y%pF5#85aF9$MnP7r^Bu%D? zT{b-KBujiy>7_*9{8u0|mTJ(atnnnS%qBDM_Gx5>3V+2~Wt=EeT4cXOdud$+weM(>wdBg+cV$}6%(ccP;`!~CzW{0O2aLY z?rQtBB6`ZztPP@_&`kzDzxc==?a{PUPUbbX31Vy?_(;c+>3q*!df!K(LQYZNrZ>$A*8<4M%e8vj1`%(x9)d~);ym4p zoo518$>9Pe| zZaFGj);h?khh*kgUI-Xvj+Dr#r&~FhU=eQ--$ZcOY9;x%&3U(&)q}eJs=)K5kUgi5 zNaI-m&4?wlwFO^`5l-B?17w4RFk(IKy5fpS0K%txp0qOj$e=+1EUJbLd-u>TYNna~ z+m?gU0~xlcnP>J>%m_y_*7hVMj3d&)2xV8>F%J;6ncm)ILGzF2sPAV|uYk5!-F%jL(53^51BKr zc3g7+v^w<4WIhk7a#{N6Ku_u{F`eo;X+u!C(lIaiY#*V5!sMed39%-AgV*`(nI)Im zemHE^2foBMPyIP<*yuD21{6I?Co?_{pqp-*#N6sZRQAzEBV4HQheOyZT5UBd)>G85 zw^xHvCEP4AJk<{v2kQQ;g;C)rCY=X!c8rNpNJ4mHETN}t1rwSe7=s8u&LzW-+6AEB z)LX0o7`EqC94HM{4p}d2wOwj2EB|O;?&^FeG9ZrT%c!J&x`Z3D2!cm(UZbFBb`+h ztfhjq75yuSn2~|Pc)p$Ul6=)}7cfXtBsvc15f&(K{jnEsw5Gh0GM^O=JC+X-~@r1kI$=FH=yBzsO#PxR1xU9+T{KuPx7sMe~GX zSP>AT3%(Xs@Ez**e@GAn{-GvB^oa6}5^2s+Mg~Gw?#$u&ZP;u~mP|FXsVtr>3k9O?%v>`Ha-3QsOG<7KdXlqKrsN25R|K<<;- z8kFY!&J&Yrqx3ptevOHiqPxKo_wwAPD)$DWMz{0>{T5qM%>rMqGZ!dJdK(&tP1#89 zVcu}I1I-&3%nMyF62m%MDpl~p)PM(%YoR zD)=W)E7kjwzAr!?^P*`?=fMHd1q4yjLGTTRUidem^Ocjrfgk2Jp|6SabEVHKC3c>RX@tNx=&Z7gC z0ztZoZx+#o36xH8mv6;^e{vU;G{JW17kn(RO&0L%q^fpWSYSkr1Cb92@bV->VO5P z;=V{hS5wcROQfbah6ND{2a$zFnj>@yuOcw}X~E20g7)5=Z#(y)RC878{_rObmGQ;9 zUy>&`YT^2R@jqR1z9Fx&x)WBstIE#*UhAa>WrMm<10={@$UN@Cog+#pxq{W@l0DOf zJGs^Jv?t8HgIXk(;NFHXun$J{{p})cJ^BWn4BeQo6dMNp%JO@$9z{(}qqEHuZOUQP zZiwo70Oa@lMYL(W*R4(!oj`)9kRggJns-A|w+XL=P07>QBMTEbG^gPS)H zu^@MFTFZtsKGFHgj|hupbK({r>PX3_kc@|4Jdqr@gyyKrHw8Tu<#0&32Hh?S zsVm_kQ2K`4+=gjw1mVhdOz7dI7V!Iu8J1LgI+_rF`Wgx5-XwU~$h>b$%#$U3wWC-ea0P(At2SjPAm57kd;!W5k{do1}X681o}`!c*(w!kCjtGTh7`=!M)$9 zWjTns{<-WX+Xi;&d!lyV&1KT9dKL??8)fu2(?Ox<^?EAzt_(#5bp4wAfgIADYgLU` z;J7f8g%-tfmTI1ZHjgufKcAT4SO(vx?xSo4pdWh`3#Yk;DqPGQE0GD?!_CfXb(E8WoJt6*Yutnkvmb?7H9B zVICAYowwxK;VM4(#~|}~Ooyzm*1ddU_Yg%Ax*_FcZm^AzYc$<+9bv;Eucr(SSF}*JsjTfb*DY>qmmkt z;dRkB#~SylP~Jcmr&Bl9TxHf^DcGUelG%rA{&s)5*$|-ww}Kwx-lWnNeghVm@z zqi3@-oJnN%r2O4t9`5I5Zfc;^ROHmY6C9 z1VRRX*1+aBlbO_p>B+50f1p&%?_A*16R0n+l}HKWI$yIH3oq2`k4O?tEVd~a4~>iI zo{d}b8tr+$q<%%K%Ett*i|RAJEMnk9hU7LtL!lxOB45xO1g)ycDBd=NbpaE3j?Gw& z0M&xx13EkCgNHu%Z8rBLo93XH-zQUfF3{Iy>65-KSPniqIzF+?x$3>`L?oBOBeEsv zs_y7@7>IbS&w2Vju^#vBpPWQuUv=dDRGm(-MH|l+8T?vfgD;{nE_*-h?@D;GN>4hA z9{!G@ANfHZOxMq5kkoh4h*p3+zE7z$13ocDJR$XA*7uKtG5Cn_-ibn%2h{ z;J0m5aCjg(@_!G>i2FDAvcn5-Aby8b;J0u%u)!`PK#%0FS-C3(cq9J{V`DJEbbE|| zYpTDd+ulcjEd5`&v!?=hVgz&S0|C^We?2|>9|2T6?~nn^_CpLn&kuI|VG7_E{Ofu9 zAqe0Reuq5Zunlx@zyTqEL+ssT15X|Z0LUfZAr-i$1_SJ{j}BHmBm}s8{OgK3lm%4F zzC%jz!y!8WUJo2FLkU(mVh7-uzC+gcbkV^bM}&Y6=HTTca{!7ZSoB!)l|v<(3ly!jq&P5A2q(U5~h)))aj-`-6&aM~LBySnAy zA0{Z{FHiUb8rW|Yo%kQwi`Kh>EEE$0g7UxeeeVkcY%~87yCmSjYyxoqq(%Jib*lH; zz`t5y094U`k_o{-*U^dFH~+1I@GsgwqmGsQC9-Vr0X94TLhlV;Kt#`9h-N?oKHqpx zzVAOxltd%gzb_Qu{NHnE8vPp=G$#S)Y%&6drobF_#NeY%VLzeod delta 9041 zcmY*t@kVBCBP!g$Qih>$!M(|j-I?-C8+=cK0w!?cVWy9LXH zd%I}(h%K_>9Qvap&`U=={XcolW-VA%#t9ljo~WmY8+Eb|zcKX3eyx7qiuU|a)zU5cYm5{k5IAa3ibZf_B&=YT!-XyLap%QRdebT+PIcg$KjM3HqA3uZ5|yBj2vv8$L{#$>P=xi+J&zLILkooDarGpiupEiuy`9uy&>yEr95d)64m+~`y*NClGrY|5MLlv!)d5$QEtqW)BeBhrd)W5g1{S@J-t8_J1 zthp@?CJY}$LmSecnf3aicXde(pXfeCei4=~ZN=7VoeU|rEEIW^!UBtxGc6W$x6;0fjRs7Nn)*b9JW5*9uVAwi) zj&N7W;i<Qy80(5gsyEIEQm>_+4@4Ol)F?0{YzD(6V~e=zXmc2+R~P~< zuz5pju;(akH2+w5w!vnpoikD5_{L<6T`uCCi@_Uorr`L(8zh~x!yEK*!LN02Q1Iri z>v*dEX<(+_;6ZAOIzxm@PbfY4a>ws4D82&_{9UHCfll!x`6o8*i0ZB+B#Ziv%RgtG z*S}<4!&COp)*ZMmXzl0A8mWA$)fCEzk$Wex*YdB}_-v|k9>jKy^Y>3me;{{|Ab~AL zQC(naNU=JtU3aP6P>Fm-!_k1XbhdS0t~?uJ$ZvLbvow10>nh*%_Kh>7AD#IflU8SL zMRF1fmMX#v8m=MGGb7y5r!Qf~Y}vBW}fsG<{1CHX7Yz z=w*V9(vOs6eO>CDuhurDTf3DVVF^j~rqP*7S-$MLSW7Ab>8H-80ly;9Q0BWoNV zz8Wr2CdK!rW0`sMD&y{Ue{`mEkXm0%S2k;J^iMe|sV5xQbt$ojzfQE+6aM9LWH`t& z8B;Ig7S<1Dwq`3W*w59L(opjq)ll4E-c?MivCh!4>$0^*=DKI&T2&j?;Z82_iZV$H zKmK7tEs7;MI-Vo(9wc1b)kc(t(Yk? z#Hgo8PG_jlF1^|6ge%;(MG~6fuKDFFd&}>BlhBTh&mmuKsn>2buYS=<5BWw^`ncCb zrCRWR5`IwKC@URU8^aOJjSrhvO>s}O&RBD8&V=Fk2@~zYY?$qO&!9%s>YecVY0zhK zBxKGTTyJ(uF`p27CqwPU1y7*)r}y;{|0FUO)-8dKT^>=LUoU_6P^^utg|* zuj}LBA*gS?4EeEdy$bn#FGex)`#y|vg77NVEjTUn8%t z@l|7T({SM!y$PZy9lb2N;BaF}MfGM%rZk10aqvUF`CDaC)&Av|eED$x_;qSoAka*2 z2rR+OTZTAPBx`vQ{;Z{B4Ad}}qOBqg>P4xf%ta|}9kJ2$od>@gyC6Bf&DUE>sqqBT zYA>(sA=Scl2C_EF8)9d8xwdBSnH5uL=I4hch6KCHj-{99IywUD{HR`d(vk@Kvl)WD zXC(v{ZTsyLy{rio*6Wi6Lck%L(7T~Is-F_`2R}q z!H1ylg_)Mv&_|b1{tVl!t{;PDa!0v6^Zqs_`RdxI%@vR)n|`i`7O<>CIMzqI00y{;` zhoMyy>1}>?kAk~ND6}`qlUR=B+a&bvA)BWf%`@N)gt@@Ji2`p1GzRGC$r1<2KBO3N z++YMLD9c|bxC;za_UVJ*r6&Ea;_YC>-Ebe-H=VAgDmx+?Q=DxCE4=yQXrn z7(0X#oIjyfZUd}fv2$;4?8y|0!L^ep_rMz|1gU-hcgVYIlI~o>o$K&)$rwo(KJO~R zDcGKo-@im7C<&2$6+q-xtxlR`I4vL|wFd<`a|T}*Nt;(~Vwx&2QG_j$r0DktR+6I4W)gUx*cDVBwGe00aa803ZYiwy;d{1p)y0?*IT8ddPS`E~MiS z1d%Vm0Hb4LN2*f8FZ|6xRQev@ZK-?(oPs+mT*{%NqhGL_0dJ$?rAxA{2 z`r3MBv&)xblcd>@hArncJpL~C(_HTo&D&CS!_J5Giz$^2EfR_)xjgPg`Bq^u%1C*+ z7W*HGp|{B?dOM}|E)Cs$61y8>&-rHBw;A8 zgkWw}r$nT%t(1^GLeAVyj1l@)6UkHdM!%LJg|0%BO74M593&LlrksrgoO{iEz$}HK z4V>WXgk|7Ya!Vgm#WO^ZLtVjxwZ&k5wT6RteViH3ds{VO+2xMJZ`hToOz~_+hRfY{ z%M;ZDKRNTsK5#h6goUF(h#VXSB|7byWWle*d0$IHP+FA`y)Q^5W!|&N$ndaHexdTn z{vf?T$(9b&tI&O`^+IqpCheAFth;KY(kSl2su_9|Y1B{o9`mm)z^E`Bqw!n+JCRO) zGbIpJ@spvz=*Jki{wufWm|m`)XmDsxvbJR5dLF=kuf_C>dl}{nGO(g4I$8 zSSW#5$?vqUDZHe_%`Zm?Amd^>I4SkBvy+i}wiQYBxj0F1a$*%T+6}Yz?lX&iQ}zaU zI@%8cwVGtF3!Ke3De$dL5^j-$Bh3+By zrSR3c2a>XtaE#TB}^#hq@!vnZ1(An#bk_eKR{?;Z&0cgh4$cMNU2HL=m=YjMTI zT$BRltXs4T=im;Ao+$Bk3Dz(3!C;rTqelJ?RF)d~dP9>$_6dbz=_8#MQFMMX0S$waWxY#mtDn}1U{4PGeRH5?a>{>TU@1UlucMAmzrd@PCwr|il)m1fooO7Z{Vyr z6wn=2A5z(9g9-OU10X_ei50@~)$}w4u)b+mt)z-sz0X32m}NKTt4>!O{^4wA(|3A8 zkr(DxtMnl$Hol>~XNUE?h9;*pGG&kl*q_pb z&*$lH70zI=D^s)fU~A7cg4^tUF6*Oa+3W0=7FFB*bf$Kbqw1&amO50YeZM)SDScqy zTw$-M$NA<_We!@4!|-?V3CEPnfN4t}AeM9W$iSWYz8f;5H)V$pRjMhRV@Z&jDz#FF zXyWh7UiIc7=0U9L35=$G54RjAupR&4j`(O3i?qjOk6gb!WjNtl1Fj-VmltDTos-Bl z*OLfOleS~o3`?l!jTYIG!V7?c<;Xu(&#~xf-f(-jwow-0Hv7JZG>}YKvB=rRbdMyv zmao*-!L?)##-S#V^}oRm7^Db zT5C2RFY4>ov~?w!3l_H}t=#X=vY-*LQy(w>u%r`zQ`_RukSqIv@WyGXa-ppbk-X=g zyn?TH(`-m*in(w=Ny$%dHNSVxsL|_+X=+kM+v_w{ZC(okof9k1RP5qDvcA-d&u{5U z?)a9LXht1f6|Tdy5FgXo;sqR|CKxDKruU9RjK~P6xN+4;0eAc|^x%UO^&NM4!nK_! z6X14Zkk=5tqpl&d6FYuMmlLGQZep0UE3`fT>xzgH>C*hQ2VzCQlO`^kThU6q%3&K^ zf^kfQm|7SeU#c%f8e?A<9mALLJ-;)p_bv6$pp~49_o;>Y=GyUQ)*prjFbkU;z%HkOW_*a#j^0b@GF|`6c}7>=W{Ef!#dz5lpkN>@IH+(sx~QMEFe4 z1GeKK67;&P%ExtO>}^JxBeHii)ykX8W@aWhJO!H(w)DH4sPatQ$F-Phiqx_clj`9m zK;z7X6gD2)8kG^aTr|oY>vmgOPQ4`_W+xj2j!$YT9x(DH6pF~ zd_C#8c>Gfb)k2Ku4~t=Xb>T^8KW;2HPN#%}@@hC1lNf~Xk)~oj=w-Y11a@DtIyYk8 z9^|_RIAA(1qUSs3rowxr&OuRVFL8(zSqU_rGlqHpkeYT4z7DGdS0q4V-b!3fsv$Yb zPq4UP^3XFd(G%JAN|0y>?&sLzNir30K(lyzNYvCtE2gDyy-nthPlrXXU75fhoS7kA zg%GYyBEFQ(xgdjtv+>?>Q!G!8& z3+F>)4|N+F1a^T?XC8 zxRRx7-{DV%uUYt&*$z2uQTbZDbUn)PozID*(i^{JDjNq`v?;&OW^&~{ZPE_e+?RMk z!7O5CUKJSnGZvjTbLX2$zwYRZs_$f{T!hvVHuTg77|O;zBHlA|GIUu_bh4`Bl?7KE zYB~a`b?O;0SfD?0EZiPYpVf=P4=|zr(u_w}oP0S`YOZziX9cuwpll&%QMv4bBC_JdP#rT3>MliqySv0& zh)r=vw?no&;5T}QVTkHKY%t`%{#*#J;aw!wPs}?q2$(e0Y#cdBG1T09ypI@#-y24+fzhJem1NSZ$TCAjU2|ebYG&&6p(0f>wQoNqVa#6J^W!3$gIWEw7d<^k!U~O5v=8goq$jC`p8CS zrox#Jw3w`k&Ty7UVbm35nZ}FYT5`fN)TO6R`tEUFotxr^BTXZGt|n(Ymqmr^pCu^^w?uX!ONbm?q{y9FehdmcJuV8V%A-ma zgl=n9+op{wkj-}N;6t;(JA1A#VF3S9AFh6EXRa0~7qop~3^~t1>hc6rdS_4!+D?Xh z5y?j}*p@*-pmlTb#7C0x{E(E@%eepK_YycNkhrYH^0m)YR&gRuQi4ZqJNv6Rih0zQ zqjMuSng>Ps;?M0YVyh<;D3~;60;>exDe)Vq3x@GRf!$wgFY5w4=Jo=g*E{76%~jqr zxTtb_L4Cz_E4RTfm@0eXfr1%ho?zP(>dsRarS>!^uAh~bd0lEhe2x7AEZQmBc%rU; z&FUrs&mIt8DL`L4JpiFp3NNyk3N>iL6;Nohp*XbZZn%BDhF_y{&{X3UtX(7aAyG63P zELC;>2L`jnFS#vC->A(hZ!tGi7N7^YtW7-LB6!SVdEM&7N?g}r4rW2wLn{Ni*I~$Y z@#;KwJIl0^?eX{JWiHQxDvccnNKBhHW0h6`j=)OH1`)7)69B$XNT@)l1s25M+~o2_ zpa&X<_vHxN_oR|B#ir2p*VNB~o6Z1OE&~a+_|AxS)(@Dgznq(b(|K8BN_nQ7+>N`= zXOx_@AhcmmcRvp6eX#4z6sn=V0%KonKFVY@+m&)Rx!Z5U@WdyHMCF4_qzJNpzc9Fw z7Bdzx54(e7>wcEqHKqH-Paiut;~ZVJpS6_q>ub)zD#TQ4j*i(I8DvS$BfyX~A%<#} z*=g2$8s;YYjEHl`7cKw!a9PFRt8tVR zM&X|bs?B1#ycjl>AzgbdRkr-@NmBc^ys)aoT75F(yweV&Y-3hNNXj-valA&=)G{NL zX?smr5sQWi3n;GGPW{%vW)xw-#D0QY%zjXxYj?($b4JzpW0sWY!fkwC5bJMkhTp$J z6CNVLd=-Ktt7D<^-f|=wjNjf0l%@iu2dR+zdQ&9NLa(B_okKdRy^!Q!F$Ro=hF$-r z!3@ocUs^7?cvdTMPbn*8S-o!PsF;>FcBkBkg&ET`W`lp?j`Z}4>DF|}9407lK9y~^No&pT7J|rVQ9Dh>qg|%=gxxg=! z>WX$!;7s~gDPmPF<--(?CvEnvV*E1KdXpr>XVv!DN~PyISE7d+K_9+W^pnR6cX&?E ziLr{0`JIs@NcA|;8L|p!3H~9y8mga2Dsm4I?rBS7$3wcT!_l*$^8U3hKUri|_I3N2 zz$xY`)IWA7P*Y1BJtyBEh?8EEvs8Oyl^{(+`gi{9hwpcN#I%Z0j$^yBp?z<;Ny!G$ zra3J_^i0(~LiKuITs%v)qE+YrJr?~w+)`Rcte^O=nwmPg@&!Q7FGTtjpTdI6wH&ZV z)2}VZY6(MbP`tgoew++(pt$jVj- zvPK)pSJ)U(XfUqBqZNo|za#Xx+IVEb?HGQ^wUVH&wTdWgP(z#ijyvXjwk>tFBUn*2 zuj5ENQjT{2&T`k;q54*Z>O~djuUBNwc6l(BzY?Ed4SIt9QA&8+>qaRIck?WdD0rh@ zh`VTZPwSNNCcLH3J}(q zdEtu@HfxDTpEqWruG=86m;QVO{}E&q8qYWhmA>(FjW`V&rg!CEL1oZCZcAX@yX(2tg8`>m1psG0ZpO+Rnph@Bhjj!~|+S=@+U{*ukwGrBj{5xfIHHP7|} z^7@g2;d%FMO8f(MS&6c##mrX2i(5uiX1o(=Vw89IQcHw)n{ZTS@``xT$Af@CQTP#w zl3kn6+MJP+l(;K-rWgjpdBU|CB4>W%cObZBH^Am~EvRO%D>uU^HVRXi$1 zb?Pr~ZlopLfT5l%03SjI7>YiGZZs=n(A!c;N9%%aByY~5(-hS4z_i2wgKYsG%OhhxH#^5i%&9ESb(@# zV_f5${Gf=$BK)1VY=NX#f+M}6f`OWmpC*OU3&+P@n>$Xvco*Nm$c<=`S|lY6S}Ut- z80}ztIpkV>W%^Ox`enpk<25_i7`RPiDugxHfUDBD8$bp9XR15>a?r^#&!1Ne6n{MI z){H`!jwrx}8b-w@@E8H0v)l!5!W8En=u67v+`iNoz<_h4{V*qQK+@)JP^JqsKAedZ zNh4toE+I7;^}7kkj|hzNVFWkZ$N9rxPl9|_@2kbW*4}&o%(L`WpQCN2M?gz>cyWHk zulMwRxpdpx+~P(({@%UY20LwM7sA&1M|`bEoq)Id zyUHt>@vfu**UOL9wiW*C75cc&qBX37qLd`<;$gS+mvL^v3Z8i4p6(@Wv`N|U6Exn< zd`@WxqU^8u^Aw+uw#vuDEIByaD)vucU2{4xRseczf_TJXUwaUK+E_IoItXJq88${0 z=K5jGehPa2)CnH&Lcxv&1jQ=T8>*vgp1^%)c&C2TL69;vSN)Q)e#Hj7!oS0 zlrEmJ=w4N9pID5KEY5qz;?2Q}0|4ESEio&cLrp221LTt~j3KjUB`LU?tP=p;B=WSXo;C?8(pnF6@?-ZD0m3DYZ* z#SzaXh|)hmTC|zQOG>aEMw%4&2XU?prlk5(M3ay-YC^QLRMN+TIB*;TB=wL_atpeD zh-!sS%A`3 z=^?niQx+^za_wQd2hRR=hsR0uzUoyOcrY!z7W)G2|C-_gqc`wrG5qCuU!Z?g*GL^H z?j^<_-A6BC^Dp`p(i0!1&?U{YlF@!|W{E@h=qQ&5*|U~V8wS;m!RK(Q6aX~oH9ToE zZYKXZoRV~!?P1ADJ74J-PFk2A{e&gh2o)@yZOZuBi^0+Hkp`dX;cZs9CRM+##;P!*BlA%M48TuR zWUgfD1DLsLs+-4XC>o>wbv-B)!t*47ON5wgoMX%llnmXG%L8209Vi;yZ`+N2v2Ox+ zMe7JHunQE$ckHHhEYRA+e`A3=XO5L%fMau71`XL7v)b{f1rkTY+WWSIkH#sG=pLqe zA(xZIp>_=4$zKq0t_G7q9@L zZ5D-0{8o%7f>0szA#c;rjL;4Y%hl}wYrx1R`Viq|Pz}c-{{LJY070ym@E~mt*pTyG z79bfcWTGGEje;PLD;N-XHw=`wS^howfzb$%oP8n)lN$o$ZWjZx|6iSsi2piI_7s7z zX#b$@z6kIJ^9{-Y^~wJ!s0V^Td5V7#4&pyU#NHw#9)N&qbpNFDR1jqC00W}91OnnS z{$J@GBz%bka`xsz;rb_iJ|rgmpUVyEZ)Xi*SO5U&|NFkTHb3y@e@%{WrvE&Jp#Lw^ zcj13CbsW+V>i@rj@SEfFf0@yjS@nbPB0)6D`lA;e%61nh`-qhydO!uS7jXGQd%i7opEnOL;| zDn!3EUm(V796;f?fA+RDF<@%qKlo)`0VtL74`!~516_aogYP%QfG#<2kQ!pijthz2 zpaFX3|D$%C7!bL242U?-e@2QZ`q$~lgZbvgfLLyVfT1OC5<8@6lLi=A{stK#zJmWd zlx+(HbgX)l$RGwH|2rV@P3o@xCrxch0$*z1ASpy(n+d4d2XWd~2AYjQm`xZU3af8F p+x$Nxf1895@0bJirXkdpJh+N7@Nb7x007(DEB&^Lm}dWn{T~m64-^0Z diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e750102e0..ae04661ee 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 1b6c78733..a69d9cb6c 100755 --- a/gradlew +++ b/gradlew @@ -205,6 +205,12 @@ set -- \ org.gradle.wrapper.GradleWrapperMain \ "$@" +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. diff --git a/gradlew.bat b/gradlew.bat index 107acd32c..f127cfd49 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -14,7 +14,7 @@ @rem limitations under the License. @rem -@if "%DEBUG%" == "" @echo off +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,7 +25,7 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -40,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute +if %ERRORLEVEL% equ 0 goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -75,13 +75,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal From c44ce29bbde6d64a77401a43005818588ff4fef0 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 6 Sep 2022 18:13:46 +0200 Subject: [PATCH 097/145] Bump and pin dependency versions for tests --- test-platform/build.gradle.kts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test-platform/build.gradle.kts b/test-platform/build.gradle.kts index 308f7afe6..855256102 100644 --- a/test-platform/build.gradle.kts +++ b/test-platform/build.gradle.kts @@ -6,10 +6,10 @@ description = "Dependency constraints for tests" dependencies { constraints { - api("junit:junit:[4.12,5)") - api("org.mockito:mockito-core:[2.27.0,3)") - api("org.scalacheck:scalacheck_2.13:[1.14.0,2)") - api("org.scalatest:scalatest_2.13:[3.0.8,3.1)") - api("uk.org.lidalia:slf4j-test:[1.1.0,2)") + api("junit:junit:4.13.2") + api("org.mockito:mockito-core:4.7.0") + api("org.scalacheck:scalacheck_2.13:1.16.0") + api("org.scalatest:scalatest_2.13:3.0.9") + api("uk.org.lidalia:slf4j-test:1.2.0") } } From 59ee2c0df7abf9739a2a6d0eb65a3734539519e4 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 6 Sep 2022 18:29:48 +0200 Subject: [PATCH 098/145] Bump ScalaTest to version 3.1 --- test-platform/build.gradle.kts | 4 +++- webauthn-server-attestation/build.gradle.kts | 2 ++ .../test/scala/com/yubico/fido/metadata/FidoMds3Spec.scala | 6 +++--- .../yubico/fido/metadata/FidoMetadataDownloaderSpec.scala | 6 +++--- .../test/scala/com/yubico/fido/metadata/JsonIoSpec.scala | 6 +++--- .../scala/com/yubico/fido/metadata/MetadataBlobSpec.scala | 6 +++--- webauthn-server-core/build.gradle.kts | 2 ++ .../webauthn/AppleAttestationStatementVerifierSpec.scala | 6 +++--- .../com/yubico/webauthn/RelyingPartyRegistrationSpec.scala | 6 +++--- .../test/scala/com/yubico/webauthn/data/JsonIoSpec.scala | 6 +++--- webauthn-server-demo/build.gradle.kts | 2 ++ .../test/scala/demo/webauthn/JsonSerializationSpec.scala | 6 +++--- .../src/test/scala/demo/webauthn/WebAuthnServerSpec.scala | 6 +++--- yubico-util/build.gradle.kts | 2 ++ .../scala/com/yubico/internal/util/BinaryUtilSpec.scala | 6 +++--- .../scala/com/yubico/internal/util/CollectionUtilSpec.scala | 6 +++--- .../scala/com/yubico/internal/util/ComparableUtilSpec.scala | 6 +++--- 17 files changed, 47 insertions(+), 37 deletions(-) diff --git a/test-platform/build.gradle.kts b/test-platform/build.gradle.kts index 855256102..3becb2348 100644 --- a/test-platform/build.gradle.kts +++ b/test-platform/build.gradle.kts @@ -9,7 +9,9 @@ dependencies { api("junit:junit:4.13.2") api("org.mockito:mockito-core:4.7.0") api("org.scalacheck:scalacheck_2.13:1.16.0") - api("org.scalatest:scalatest_2.13:3.0.9") + api("org.scalatest:scalatest_2.13:3.1.0") + api("org.scalatestplus:junit-4-13_2.13:3.2.13.0") + api("org.scalatestplus:scalacheck-1-16_2.13:3.2.13.0") api("uk.org.lidalia:slf4j-test:1.2.0") } } diff --git a/webauthn-server-attestation/build.gradle.kts b/webauthn-server-attestation/build.gradle.kts index b1c5aa38d..f4f50d20c 100644 --- a/webauthn-server-attestation/build.gradle.kts +++ b/webauthn-server-attestation/build.gradle.kts @@ -54,6 +54,8 @@ dependencies { testImplementation("org.scala-lang:scala-library") testImplementation("org.scalacheck:scalacheck_2.13") testImplementation("org.scalatest:scalatest_2.13") + testImplementation("org.scalatestplus:junit-4-13_2.13") + testImplementation("org.scalatestplus:scalacheck-1-16_2.13") testImplementation("uk.org.lidalia:slf4j-test") testImplementation("org.slf4j:slf4j-api") { diff --git a/webauthn-server-attestation/src/test/scala/com/yubico/fido/metadata/FidoMds3Spec.scala b/webauthn-server-attestation/src/test/scala/com/yubico/fido/metadata/FidoMds3Spec.scala index 9b8417814..7ac72ffd8 100644 --- a/webauthn-server-attestation/src/test/scala/com/yubico/fido/metadata/FidoMds3Spec.scala +++ b/webauthn-server-attestation/src/test/scala/com/yubico/fido/metadata/FidoMds3Spec.scala @@ -22,8 +22,8 @@ import com.yubico.webauthn.test.Helpers import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.cert.jcajce.JcaX500NameUtil import org.junit.runner.RunWith -import org.scalatest.FunSpec -import org.scalatest.Matchers +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers import org.scalatest.tags.Network import org.scalatest.tags.Slow import org.scalatestplus.junit.JUnitRunner @@ -49,7 +49,7 @@ import scala.jdk.OptionConverters.RichOptional @Slow @Network @RunWith(classOf[JUnitRunner]) -class FidoMds3Spec extends FunSpec with Matchers { +class FidoMds3Spec extends AnyFunSpec with Matchers { private val CertValidFrom = Instant.parse("2022-02-15T17:00:00Z") private val CertValidTo = Instant.parse("2022-03-15T17:00:00Z") 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 e23c2b126..ca24f5cdb 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 @@ -21,8 +21,8 @@ import org.eclipse.jetty.util.ssl.SslContextFactory import org.eclipse.jetty.util.thread.QueuedThreadPool import org.junit.runner.RunWith import org.scalatest.BeforeAndAfter -import org.scalatest.FunSpec -import org.scalatest.Matchers +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers import org.scalatest.tags.Network import org.scalatestplus.junit.JUnitRunner @@ -55,7 +55,7 @@ import scala.util.Try @Network @RunWith(classOf[JUnitRunner]) class FidoMetadataDownloaderSpec - extends FunSpec + extends AnyFunSpec with Matchers with BeforeAndAfter { diff --git a/webauthn-server-attestation/src/test/scala/com/yubico/fido/metadata/JsonIoSpec.scala b/webauthn-server-attestation/src/test/scala/com/yubico/fido/metadata/JsonIoSpec.scala index 419af153e..94458a644 100644 --- a/webauthn-server-attestation/src/test/scala/com/yubico/fido/metadata/JsonIoSpec.scala +++ b/webauthn-server-attestation/src/test/scala/com/yubico/fido/metadata/JsonIoSpec.scala @@ -29,14 +29,14 @@ import com.fasterxml.jackson.databind.ObjectMapper import com.yubico.fido.metadata.Generators._ import org.junit.runner.RunWith import org.scalacheck.Arbitrary -import org.scalatest.FunSpec -import org.scalatest.Matchers +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers import org.scalatestplus.junit.JUnitRunner import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks @RunWith(classOf[JUnitRunner]) class JsonIoSpec - extends FunSpec + extends AnyFunSpec with Matchers with ScalaCheckDrivenPropertyChecks { diff --git a/webauthn-server-attestation/src/test/scala/com/yubico/fido/metadata/MetadataBlobSpec.scala b/webauthn-server-attestation/src/test/scala/com/yubico/fido/metadata/MetadataBlobSpec.scala index 7d07a1e82..ed0d126a9 100644 --- a/webauthn-server-attestation/src/test/scala/com/yubico/fido/metadata/MetadataBlobSpec.scala +++ b/webauthn-server-attestation/src/test/scala/com/yubico/fido/metadata/MetadataBlobSpec.scala @@ -2,12 +2,12 @@ package com.yubico.fido.metadata import com.yubico.internal.util.JacksonCodecs import com.yubico.webauthn.data.ByteArray -import org.scalatest.FunSpec -import org.scalatest.Matchers +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks class MetadataBlobSpec - extends FunSpec + extends AnyFunSpec with Matchers with ScalaCheckDrivenPropertyChecks { diff --git a/webauthn-server-core/build.gradle.kts b/webauthn-server-core/build.gradle.kts index 7e2ed6a3c..73a501864 100644 --- a/webauthn-server-core/build.gradle.kts +++ b/webauthn-server-core/build.gradle.kts @@ -41,6 +41,8 @@ dependencies { testImplementation("org.scala-lang:scala-library") testImplementation("org.scalacheck:scalacheck_2.13") testImplementation("org.scalatest:scalatest_2.13") + testImplementation("org.scalatestplus:junit-4-13_2.13") + testImplementation("org.scalatestplus:scalacheck-1-16_2.13") testImplementation("uk.org.lidalia:slf4j-test") testImplementation("org.slf4j:slf4j-api") { diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/AppleAttestationStatementVerifierSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/AppleAttestationStatementVerifierSpec.scala index 726d1757a..fec9b530c 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/AppleAttestationStatementVerifierSpec.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/AppleAttestationStatementVerifierSpec.scala @@ -30,14 +30,14 @@ import com.yubico.webauthn.data.ByteArray import com.yubico.webauthn.data.Generators.arbitraryByteArray import com.yubico.webauthn.test.RealExamples import org.junit.runner.RunWith -import org.scalatest.FunSpec -import org.scalatest.Matchers +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers import org.scalatestplus.junit.JUnitRunner import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks @RunWith(classOf[JUnitRunner]) class AppleAttestationStatementVerifierSpec - extends FunSpec + extends AnyFunSpec with Matchers with TestWithEachProvider with ScalaCheckDrivenPropertyChecks { 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 a54c733e0..09b58e7f7 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 @@ -69,8 +69,8 @@ import org.bouncycastle.cert.jcajce.JcaX500NameUtil import org.junit.runner.RunWith import org.mockito.Mockito import org.scalacheck.Gen -import org.scalatest.FunSpec -import org.scalatest.Matchers +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers import org.scalatestplus.junit.JUnitRunner import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks @@ -103,7 +103,7 @@ import scala.util.Try @RunWith(classOf[JUnitRunner]) class RelyingPartyRegistrationSpec - extends FunSpec + extends AnyFunSpec with Matchers with ScalaCheckDrivenPropertyChecks with TestWithEachProvider { 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 17b4b3b20..29cfa491f 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 @@ -42,14 +42,14 @@ import org.junit.runner.RunWith import org.scalacheck.Arbitrary import org.scalacheck.Arbitrary.arbitrary import org.scalacheck.Gen -import org.scalatest.FunSpec -import org.scalatest.Matchers +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers import org.scalatestplus.junit.JUnitRunner import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks @RunWith(classOf[JUnitRunner]) class JsonIoSpec - extends FunSpec + extends AnyFunSpec with Matchers with ScalaCheckDrivenPropertyChecks { diff --git a/webauthn-server-demo/build.gradle.kts b/webauthn-server-demo/build.gradle.kts index e9727b78e..eef456a6a 100644 --- a/webauthn-server-demo/build.gradle.kts +++ b/webauthn-server-demo/build.gradle.kts @@ -43,6 +43,8 @@ dependencies { testImplementation("org.scala-lang:scala-library") testImplementation("org.scalacheck:scalacheck_2.13") testImplementation("org.scalatest:scalatest_2.13") + testImplementation("org.scalatestplus:junit-4-13_2.13") + testImplementation("org.scalatestplus:scalacheck-1-16_2.13") modules { module("javax.servlet:servlet-api") { diff --git a/webauthn-server-demo/src/test/scala/demo/webauthn/JsonSerializationSpec.scala b/webauthn-server-demo/src/test/scala/demo/webauthn/JsonSerializationSpec.scala index 6ae4d5bf4..9e4b28876 100644 --- a/webauthn-server-demo/src/test/scala/demo/webauthn/JsonSerializationSpec.scala +++ b/webauthn-server-demo/src/test/scala/demo/webauthn/JsonSerializationSpec.scala @@ -29,12 +29,12 @@ import com.yubico.webauthn.RegistrationTestData import com.yubico.webauthn.data.AuthenticatorAttestationResponse import demo.webauthn.data.RegistrationResponse import org.junit.runner.RunWith -import org.scalatest.FunSpec -import org.scalatest.Matchers +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers import org.scalatestplus.junit.JUnitRunner @RunWith(classOf[JUnitRunner]) -class JsonSerializationSpec extends FunSpec with Matchers { +class JsonSerializationSpec extends AnyFunSpec with Matchers { private val jsonMapper = JacksonCodecs.json() 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 95632fc47..a185d8c80 100644 --- a/webauthn-server-demo/src/test/scala/demo/webauthn/WebAuthnServerSpec.scala +++ b/webauthn-server-demo/src/test/scala/demo/webauthn/WebAuthnServerSpec.scala @@ -42,8 +42,8 @@ import demo.webauthn.data.AssertionRequestWrapper import demo.webauthn.data.CredentialRegistration import demo.webauthn.data.RegistrationRequest import org.junit.runner.RunWith -import org.scalatest.FunSpec -import org.scalatest.Matchers +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers import org.scalatestplus.junit.JUnitRunner import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks @@ -58,7 +58,7 @@ import scala.jdk.OptionConverters.RichOptional @RunWith(classOf[JUnitRunner]) class WebAuthnServerSpec - extends FunSpec + extends AnyFunSpec with Matchers with ScalaCheckDrivenPropertyChecks { diff --git a/yubico-util/build.gradle.kts b/yubico-util/build.gradle.kts index e0d3a3bb9..cef67bc33 100644 --- a/yubico-util/build.gradle.kts +++ b/yubico-util/build.gradle.kts @@ -35,6 +35,8 @@ dependencies { testImplementation("org.scala-lang:scala-library") testImplementation("org.scalacheck:scalacheck_2.13") testImplementation("org.scalatest:scalatest_2.13") + testImplementation("org.scalatestplus:junit-4-13_2.13") + testImplementation("org.scalatestplus:scalacheck-1-16_2.13") } diff --git a/yubico-util/src/test/scala/com/yubico/internal/util/BinaryUtilSpec.scala b/yubico-util/src/test/scala/com/yubico/internal/util/BinaryUtilSpec.scala index 34070c13d..b834f95b7 100644 --- a/yubico-util/src/test/scala/com/yubico/internal/util/BinaryUtilSpec.scala +++ b/yubico-util/src/test/scala/com/yubico/internal/util/BinaryUtilSpec.scala @@ -26,14 +26,14 @@ package com.yubico.internal.util import org.junit.runner.RunWith import org.scalacheck.Gen -import org.scalatest.FunSpec -import org.scalatest.Matchers +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers import org.scalatestplus.junit.JUnitRunner import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks @RunWith(classOf[JUnitRunner]) class BinaryUtilSpec - extends FunSpec + extends AnyFunSpec with Matchers with ScalaCheckDrivenPropertyChecks { diff --git a/yubico-util/src/test/scala/com/yubico/internal/util/CollectionUtilSpec.scala b/yubico-util/src/test/scala/com/yubico/internal/util/CollectionUtilSpec.scala index fcd173b40..520376086 100644 --- a/yubico-util/src/test/scala/com/yubico/internal/util/CollectionUtilSpec.scala +++ b/yubico-util/src/test/scala/com/yubico/internal/util/CollectionUtilSpec.scala @@ -2,14 +2,14 @@ package com.yubico.internal.util import com.yubico.scalacheck.gen.JavaGenerators._ import org.junit.runner.RunWith -import org.scalatest.FunSpec -import org.scalatest.Matchers +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers import org.scalatestplus.junit.JUnitRunner import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks @RunWith(classOf[JUnitRunner]) class CollectionUtilSpec - extends FunSpec + extends AnyFunSpec with Matchers with ScalaCheckDrivenPropertyChecks { diff --git a/yubico-util/src/test/scala/com/yubico/internal/util/ComparableUtilSpec.scala b/yubico-util/src/test/scala/com/yubico/internal/util/ComparableUtilSpec.scala index 1592f8bd7..87b561146 100644 --- a/yubico-util/src/test/scala/com/yubico/internal/util/ComparableUtilSpec.scala +++ b/yubico-util/src/test/scala/com/yubico/internal/util/ComparableUtilSpec.scala @@ -4,14 +4,14 @@ import _root_.scala.jdk.CollectionConverters._ import org.junit.runner.RunWith import org.scalacheck.Arbitrary.arbitrary import org.scalacheck.Gen -import org.scalatest.FunSpec -import org.scalatest.Matchers +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers import org.scalatestplus.junit.JUnitRunner import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks @RunWith(classOf[JUnitRunner]) class ComparableUtilSpec - extends FunSpec + extends AnyFunSpec with Matchers with ScalaCheckDrivenPropertyChecks { From 83a67badf4bb346cd39d5368887bfbd40dbb4120 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 6 Sep 2022 18:41:00 +0200 Subject: [PATCH 099/145] Bump ScalaTest to version 3.2 --- test-platform/build.gradle.kts | 2 +- .../FidoMetadataDownloaderIntegrationTest.scala | 6 +++--- .../FidoMetadataServiceIntegrationTest.scala | 6 +++--- .../com/yubico/webauthn/OriginMatcherSpec.scala | 6 +++--- .../PackedAttestationStatementVerifierSpec.scala | 6 +++--- .../webauthn/RelyingPartyAssertionSpec.scala | 6 +++--- .../webauthn/RelyingPartyCeremoniesSpec.scala | 6 +++--- .../webauthn/RelyingPartyStartOperationSpec.scala | 6 +++--- .../RelyingPartyUserIdentificationSpec.scala | 6 +++--- .../com/yubico/webauthn/TestWithEachProvider.scala | 14 +++++++------- .../com/yubico/webauthn/WebAuthnCodecsSpec.scala | 6 +++--- .../webauthn/data/AttestationObjectSpec.scala | 6 +++--- .../AuthenticatorAttestationResponseSpec.scala | 6 +++--- .../webauthn/data/AuthenticatorDataFlagsSpec.scala | 6 +++--- .../webauthn/data/AuthenticatorDataSpec.scala | 6 +++--- .../webauthn/data/AuthenticatorTransportSpec.scala | 6 +++--- .../com/yubico/webauthn/data/BuildersSpec.scala | 6 +++--- .../webauthn/data/CollectedClientDataSpec.scala | 6 +++--- .../scala/com/yubico/webauthn/data/EnumsSpec.scala | 6 +++--- .../com/yubico/webauthn/data/ExtensionsSpec.scala | 6 +++--- .../data/PublicKeyCredentialDescriptorSpec.scala | 6 +++--- 21 files changed, 65 insertions(+), 65 deletions(-) diff --git a/test-platform/build.gradle.kts b/test-platform/build.gradle.kts index 3becb2348..4be89d628 100644 --- a/test-platform/build.gradle.kts +++ b/test-platform/build.gradle.kts @@ -9,7 +9,7 @@ dependencies { api("junit:junit:4.13.2") api("org.mockito:mockito-core:4.7.0") api("org.scalacheck:scalacheck_2.13:1.16.0") - api("org.scalatest:scalatest_2.13:3.1.0") + api("org.scalatest:scalatest_2.13:3.2.13") api("org.scalatestplus:junit-4-13_2.13:3.2.13.0") api("org.scalatestplus:scalacheck-1-16_2.13:3.2.13.0") api("uk.org.lidalia:slf4j-test:1.2.0") diff --git a/webauthn-server-attestation/src/integrationTest/scala/com/yubico/fido/metadata/FidoMetadataDownloaderIntegrationTest.scala b/webauthn-server-attestation/src/integrationTest/scala/com/yubico/fido/metadata/FidoMetadataDownloaderIntegrationTest.scala index 26100a559..937a0db8c 100644 --- a/webauthn-server-attestation/src/integrationTest/scala/com/yubico/fido/metadata/FidoMetadataDownloaderIntegrationTest.scala +++ b/webauthn-server-attestation/src/integrationTest/scala/com/yubico/fido/metadata/FidoMetadataDownloaderIntegrationTest.scala @@ -2,8 +2,8 @@ package com.yubico.fido.metadata import org.junit.runner.RunWith import org.scalatest.BeforeAndAfter -import org.scalatest.FunSpec -import org.scalatest.Matchers +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers import org.scalatest.tags.Network import org.scalatest.tags.Slow import org.scalatestplus.junit.JUnitRunner @@ -16,7 +16,7 @@ import scala.util.Try @Network @RunWith(classOf[JUnitRunner]) class FidoMetadataDownloaderIntegrationTest - extends FunSpec + extends AnyFunSpec with Matchers with BeforeAndAfter { diff --git a/webauthn-server-attestation/src/integrationTest/scala/com/yubico/fido/metadata/FidoMetadataServiceIntegrationTest.scala b/webauthn-server-attestation/src/integrationTest/scala/com/yubico/fido/metadata/FidoMetadataServiceIntegrationTest.scala index 4db610b8d..4c6a5a8fc 100644 --- a/webauthn-server-attestation/src/integrationTest/scala/com/yubico/fido/metadata/FidoMetadataServiceIntegrationTest.scala +++ b/webauthn-server-attestation/src/integrationTest/scala/com/yubico/fido/metadata/FidoMetadataServiceIntegrationTest.scala @@ -10,8 +10,8 @@ import com.yubico.webauthn.data.AttestationObject import com.yubico.webauthn.test.RealExamples import org.junit.runner.RunWith import org.scalatest.BeforeAndAfter -import org.scalatest.FunSpec -import org.scalatest.Matchers +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers import org.scalatest.tags.Network import org.scalatest.tags.Slow import org.scalatestplus.junit.JUnitRunner @@ -30,7 +30,7 @@ import scala.util.Try @Network @RunWith(classOf[JUnitRunner]) class FidoMetadataServiceIntegrationTest - extends FunSpec + extends AnyFunSpec with Matchers with BeforeAndAfter { diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/OriginMatcherSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/OriginMatcherSpec.scala index 956359b77..c6614768f 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/OriginMatcherSpec.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/OriginMatcherSpec.scala @@ -28,8 +28,8 @@ import org.junit.runner.RunWith import org.scalacheck.Arbitrary import org.scalacheck.Arbitrary.arbitrary import org.scalacheck.Gen -import org.scalatest.FunSpec -import org.scalatest.Matchers +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers import org.scalatestplus.junit.JUnitRunner import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks @@ -38,7 +38,7 @@ import scala.jdk.CollectionConverters._ @RunWith(classOf[JUnitRunner]) class OriginMatcherSpec - extends FunSpec + extends AnyFunSpec with Matchers with ScalaCheckDrivenPropertyChecks { diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/PackedAttestationStatementVerifierSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/PackedAttestationStatementVerifierSpec.scala index 408166995..49a25a955 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/PackedAttestationStatementVerifierSpec.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/PackedAttestationStatementVerifierSpec.scala @@ -32,8 +32,8 @@ import com.yubico.webauthn.TestAuthenticator.AttestationMaker import com.yubico.webauthn.data.ByteArray import com.yubico.webauthn.data.COSEAlgorithmIdentifier import org.junit.runner.RunWith -import org.scalatest.FunSpec -import org.scalatest.Matchers +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers import org.scalatestplus.junit.JUnitRunner import java.nio.charset.StandardCharsets @@ -43,7 +43,7 @@ import scala.util.Try @RunWith(classOf[JUnitRunner]) class PackedAttestationStatementVerifierSpec - extends FunSpec + extends AnyFunSpec with Matchers with TestWithEachProvider { 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 da17187b5..6fda719ac 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 @@ -58,8 +58,8 @@ import com.yubico.webauthn.test.Util.toStepWithUtilities import org.junit.runner.RunWith import org.scalacheck.Arbitrary.arbitrary import org.scalacheck.Gen -import org.scalatest.FunSpec -import org.scalatest.Matchers +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers import org.scalatestplus.junit.JUnitRunner import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks @@ -78,7 +78,7 @@ import scala.util.Try @RunWith(classOf[JUnitRunner]) class RelyingPartyAssertionSpec - extends FunSpec + extends AnyFunSpec with Matchers with ScalaCheckDrivenPropertyChecks with TestWithEachProvider { diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyCeremoniesSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyCeremoniesSpec.scala index cb907a3c6..04794e601 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyCeremoniesSpec.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyCeremoniesSpec.scala @@ -29,15 +29,15 @@ import com.yubico.webauthn.data.PublicKeyCredentialRequestOptions import com.yubico.webauthn.test.Helpers import com.yubico.webauthn.test.RealExamples import org.junit.runner.RunWith -import org.scalatest.FunSpec -import org.scalatest.Matchers +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers import org.scalatestplus.junit.JUnitRunner import scala.jdk.CollectionConverters._ @RunWith(classOf[JUnitRunner]) class RelyingPartyCeremoniesSpec - extends FunSpec + extends AnyFunSpec with Matchers with TestWithEachProvider { diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyStartOperationSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyStartOperationSpec.scala index 055348a73..b0893616f 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyStartOperationSpec.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyStartOperationSpec.scala @@ -44,8 +44,8 @@ import com.yubico.webauthn.extension.appid.Generators._ import org.junit.runner.RunWith import org.scalacheck.Arbitrary.arbitrary import org.scalacheck.Gen -import org.scalatest.FunSpec -import org.scalatest.Matchers +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers import org.scalatestplus.junit.JUnitRunner import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks @@ -56,7 +56,7 @@ import scala.jdk.OptionConverters.RichOptional @RunWith(classOf[JUnitRunner]) class RelyingPartyStartOperationSpec - extends FunSpec + extends AnyFunSpec with Matchers with ScalaCheckDrivenPropertyChecks { diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyUserIdentificationSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyUserIdentificationSpec.scala index 6eadda8c8..034e2338d 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyUserIdentificationSpec.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyUserIdentificationSpec.scala @@ -33,8 +33,8 @@ import com.yubico.webauthn.data.PublicKeyCredential import com.yubico.webauthn.data.PublicKeyCredentialDescriptor import com.yubico.webauthn.data.RelyingPartyIdentity import org.junit.runner.RunWith -import org.scalatest.FunSpec -import org.scalatest.Matchers +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers import org.scalatestplus.junit.JUnitRunner import java.security.KeyPair @@ -47,7 +47,7 @@ import scala.util.Success import scala.util.Try @RunWith(classOf[JUnitRunner]) -class RelyingPartyUserIdentificationSpec extends FunSpec with Matchers { +class RelyingPartyUserIdentificationSpec extends AnyFunSpec with Matchers { private object Defaults { diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/TestWithEachProvider.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/TestWithEachProvider.scala index 85669d892..908717f0b 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/TestWithEachProvider.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/TestWithEachProvider.scala @@ -1,14 +1,14 @@ package com.yubico.webauthn import org.bouncycastle.jce.provider.BouncyCastleProvider -import org.scalatest.FunSpec -import org.scalatest.Matchers +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers import java.security.Provider import java.security.Security trait TestWithEachProvider extends Matchers { - this: FunSpec => + this: AnyFunSpec => def wrapItFunctionWithProviderContext( providerSetName: String, @@ -16,13 +16,13 @@ trait TestWithEachProvider extends Matchers { testSetupFun: (String => (=> Any) => Unit) => Any, ): Any = { - /** Wrapper around the standard [[FunSpec#it]] that sets the JCA + /** Wrapper around the standard [[AnyFunSpec#it]] that sets the JCA * [[Security]] providers before running the test, and then resets the * providers to the original state after the test. * * This is needed because ScalaTest shared tests work by taking fixture * parameters as lexical context, but JCA providers are set in the dynamic - * context. The [[FunSpec#it]] call does not immediately run the test, + * context. The [[AnyFunSpec#it]] call does not immediately run the test, * instead it registers a test to be run later. This helper ensures that * the dynamic context matches the lexical context at the time the test * runs. @@ -58,11 +58,11 @@ trait TestWithEachProvider extends Matchers { * and then reset the providers to the original state after the test. * * The caller SHOULD name the callback parameter `it`, in order to shadow the - * standard [[FunSpec#it]] from ScalaTest. + * standard [[AnyFunSpec#it]] from ScalaTest. * * This is needed because ScalaTest shared tests work by taking fixture * parameters as lexical context, but JCA providers are set in the dynamic - * context. The [[FunSpec#it]] call does not immediately run the test, + * context. The [[AnyFunSpec#it]] call does not immediately run the test, * instead it registers a test to be run later. This helper ensures that the * dynamic context matches the lexical context at the time the test runs. */ diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/WebAuthnCodecsSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/WebAuthnCodecsSpec.scala index 948109799..f70ab9ca1 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/WebAuthnCodecsSpec.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/WebAuthnCodecsSpec.scala @@ -29,8 +29,8 @@ import com.yubico.webauthn.test.Util import org.junit.runner.RunWith import org.scalacheck.Arbitrary import org.scalacheck.Gen -import org.scalatest.FunSpec -import org.scalatest.Matchers +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers import org.scalatestplus.junit.JUnitRunner import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks @@ -39,7 +39,7 @@ import scala.util.Try @RunWith(classOf[JUnitRunner]) class WebAuthnCodecsSpec - extends FunSpec + extends AnyFunSpec with Matchers with ScalaCheckDrivenPropertyChecks with TestWithEachProvider { diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/AttestationObjectSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/AttestationObjectSpec.scala index 89b57f352..c9dfff099 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/AttestationObjectSpec.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/AttestationObjectSpec.scala @@ -3,14 +3,14 @@ package com.yubico.webauthn.data import com.fasterxml.jackson.databind.node.JsonNodeFactory import com.yubico.internal.util.JacksonCodecs import org.junit.runner.RunWith -import org.scalatest.FunSpec -import org.scalatest.Matchers +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers import org.scalatestplus.junit.JUnitRunner import scala.jdk.CollectionConverters.MapHasAsJava @RunWith(classOf[JUnitRunner]) -class AttestationObjectSpec extends FunSpec with Matchers { +class AttestationObjectSpec extends AnyFunSpec with Matchers { private def jsonFactory: JsonNodeFactory = JsonNodeFactory.instance diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/AuthenticatorAttestationResponseSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/AuthenticatorAttestationResponseSpec.scala index b93b1321a..e78043867 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/AuthenticatorAttestationResponseSpec.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/AuthenticatorAttestationResponseSpec.scala @@ -25,12 +25,12 @@ package com.yubico.webauthn.data import org.junit.runner.RunWith -import org.scalatest.FunSpec -import org.scalatest.Matchers +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers import org.scalatestplus.junit.JUnitRunner @RunWith(classOf[JUnitRunner]) -class AuthenticatorAttestationResponseSpec extends FunSpec with Matchers { +class AuthenticatorAttestationResponseSpec extends AnyFunSpec with Matchers { describe("AuthenticatorAttestationResponse") { diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/AuthenticatorDataFlagsSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/AuthenticatorDataFlagsSpec.scala index ec1aaff3b..33e7b16d6 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/AuthenticatorDataFlagsSpec.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/AuthenticatorDataFlagsSpec.scala @@ -26,12 +26,12 @@ package com.yubico.webauthn.data import com.yubico.internal.util.BinaryUtil import org.junit.runner.RunWith -import org.scalatest.FunSpec -import org.scalatest.Matchers +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers import org.scalatestplus.junit.JUnitRunner @RunWith(classOf[JUnitRunner]) -class AuthenticatorDataFlagsSpec extends FunSpec with Matchers { +class AuthenticatorDataFlagsSpec extends AnyFunSpec with Matchers { describe("AuthenticatorDataFlags") { diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/AuthenticatorDataSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/AuthenticatorDataSpec.scala index 13f943ed3..d3f5186bd 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/AuthenticatorDataSpec.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/AuthenticatorDataSpec.scala @@ -31,8 +31,8 @@ import com.yubico.webauthn.data.Generators.byteArray import org.junit.runner.RunWith import org.scalacheck.Arbitrary.arbitrary import org.scalacheck.Gen -import org.scalatest.FunSpec -import org.scalatest.Matchers +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers import org.scalatestplus.junit.JUnitRunner import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks @@ -43,7 +43,7 @@ import scala.util.Try @RunWith(classOf[JUnitRunner]) class AuthenticatorDataSpec - extends FunSpec + extends AnyFunSpec with Matchers with ScalaCheckDrivenPropertyChecks { diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/AuthenticatorTransportSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/AuthenticatorTransportSpec.scala index 2e56f8539..a1fbca6a7 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/AuthenticatorTransportSpec.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/AuthenticatorTransportSpec.scala @@ -25,14 +25,14 @@ package com.yubico.webauthn.data import org.junit.runner.RunWith -import org.scalatest.FunSpec -import org.scalatest.Matchers +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers import org.scalatestplus.junit.JUnitRunner import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks @RunWith(classOf[JUnitRunner]) class AuthenticatorTransportSpec - extends FunSpec + extends AnyFunSpec with Matchers with ScalaCheckDrivenPropertyChecks { diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/BuildersSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/BuildersSpec.scala index 31eb15301..d0819af58 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/BuildersSpec.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/BuildersSpec.scala @@ -32,8 +32,8 @@ import com.yubico.webauthn.RegistrationResult import com.yubico.webauthn.data.Generators._ import org.junit.runner.RunWith import org.scalacheck.Arbitrary -import org.scalatest.FunSpec -import org.scalatest.Matchers +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers import org.scalatestplus.junit.JUnitRunner import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks @@ -41,7 +41,7 @@ import scala.language.reflectiveCalls @RunWith(classOf[JUnitRunner]) class BuildersSpec - extends FunSpec + extends AnyFunSpec with Matchers with ScalaCheckDrivenPropertyChecks { diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/CollectedClientDataSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/CollectedClientDataSpec.scala index 5117eed53..c34cfb92e 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/CollectedClientDataSpec.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/CollectedClientDataSpec.scala @@ -28,12 +28,12 @@ import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.node.ObjectNode import com.yubico.internal.util.JacksonCodecs import org.junit.runner.RunWith -import org.scalatest.FunSpec -import org.scalatest.Matchers +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers import org.scalatestplus.junit.JUnitRunner @RunWith(classOf[JUnitRunner]) -class CollectedClientDataSpec extends FunSpec with Matchers { +class CollectedClientDataSpec extends AnyFunSpec with Matchers { def parse(json: JsonNode): CollectedClientData = new CollectedClientData( diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/EnumsSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/EnumsSpec.scala index 5a0d63d12..f5f20dd16 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/EnumsSpec.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/EnumsSpec.scala @@ -5,8 +5,8 @@ import com.yubico.webauthn.extension.uvm.KeyProtectionType import com.yubico.webauthn.extension.uvm.MatcherProtectionType import com.yubico.webauthn.extension.uvm.UserVerificationMethod import org.junit.runner.RunWith -import org.scalatest.FunSpec -import org.scalatest.Matchers +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers import org.scalatestplus.junit.JUnitRunner import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks @@ -14,7 +14,7 @@ import scala.util.Try @RunWith(classOf[JUnitRunner]) class EnumsSpec - extends FunSpec + extends AnyFunSpec with Matchers with ScalaCheckDrivenPropertyChecks { diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/ExtensionsSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/ExtensionsSpec.scala index 29dbcdaa2..9abddb052 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/ExtensionsSpec.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/ExtensionsSpec.scala @@ -16,8 +16,8 @@ import com.yubico.webauthn.test.RealExamples import org.junit.runner.RunWith import org.scalacheck.Arbitrary.arbitrary import org.scalacheck.Gen -import org.scalatest.FunSpec -import org.scalatest.Matchers +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers import org.scalatestplus.junit.JUnitRunner import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks @@ -28,7 +28,7 @@ import scala.jdk.OptionConverters.RichOptional @RunWith(classOf[JUnitRunner]) class ExtensionsSpec - extends FunSpec + extends AnyFunSpec with Matchers with ScalaCheckDrivenPropertyChecks { diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/PublicKeyCredentialDescriptorSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/PublicKeyCredentialDescriptorSpec.scala index 0b7218289..2fca15d80 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/PublicKeyCredentialDescriptorSpec.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/PublicKeyCredentialDescriptorSpec.scala @@ -25,12 +25,12 @@ package com.yubico.webauthn.data import com.yubico.webauthn.data.Generators._ -import org.scalatest.FunSpec -import org.scalatest.Matchers +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks class PublicKeyCredentialDescriptorSpec - extends FunSpec + extends AnyFunSpec with Matchers with ScalaCheckDrivenPropertyChecks { From 5b1fecc03c27868a1d6f02fd26b0aa42fbbde9a7 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 6 Sep 2022 19:56:38 +0200 Subject: [PATCH 100/145] Bump pitest to version 1.9.5 --- webauthn-server-attestation/build.gradle.kts | 2 +- webauthn-server-core/build.gradle.kts | 2 +- yubico-util/build.gradle.kts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/webauthn-server-attestation/build.gradle.kts b/webauthn-server-attestation/build.gradle.kts index f4f50d20c..859012647 100644 --- a/webauthn-server-attestation/build.gradle.kts +++ b/webauthn-server-attestation/build.gradle.kts @@ -91,7 +91,7 @@ tasks.jar { } pitest { - pitestVersion.set("1.4.11") + pitestVersion.set("1.9.5") timestampedReports.set(false) outputFormats.set(listOf("XML", "HTML")) diff --git a/webauthn-server-core/build.gradle.kts b/webauthn-server-core/build.gradle.kts index 73a501864..61378da22 100644 --- a/webauthn-server-core/build.gradle.kts +++ b/webauthn-server-core/build.gradle.kts @@ -75,7 +75,7 @@ tasks.jar { } pitest { - pitestVersion.set("1.4.11") + pitestVersion.set("1.9.5") timestampedReports.set(false) outputFormats.set(listOf("XML", "HTML")) diff --git a/yubico-util/build.gradle.kts b/yubico-util/build.gradle.kts index cef67bc33..914805a33 100644 --- a/yubico-util/build.gradle.kts +++ b/yubico-util/build.gradle.kts @@ -52,7 +52,7 @@ tasks.jar { } pitest { - pitestVersion.set("1.4.11") + pitestVersion.set("1.9.5") timestampedReports.set(false) outputFormats.set(listOf("XML", "HTML")) From 4c15764bfce616b0fd2815445fd4798c10d94b96 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Wed, 7 Sep 2022 13:19:52 +0200 Subject: [PATCH 101/145] Revert "Go back to JDK 11 for mutation tests" --- .github/workflows/coverage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 5a4cef7e6..e2bbe639e 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -21,7 +21,7 @@ jobs: - name: Set up JDK uses: actions/setup-java@v3 with: - java-version: 11 + java-version: 17 distribution: temurin - name: Run mutation test From ebb533e3fd6f75eb911bcb729ca95aeb679801d5 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Wed, 7 Sep 2022 15:28:19 +0200 Subject: [PATCH 102/145] Relax Guava version constraint to allow 31.x --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index b071b8243..f32e6e8d5 100644 --- a/build.gradle +++ b/build.gradle @@ -52,7 +52,7 @@ dependencies { api('com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:[2.13.2,3)') api('com.fasterxml.jackson.datatype:jackson-datatype-jdk8:[2.13.2,3)') api('com.fasterxml.jackson.datatype:jackson-datatype-jsr310:[2.13.2,3)') - api('com.google.guava:guava:[24.1.1,31)') + api('com.google.guava:guava:[24.1.1,32)') api('com.upokecenter:cbor:[4.5.1,5)') api('org.apache.httpcomponents:httpclient:[4.5.2,5)') api('org.bouncycastle:bcpkix-jdk15on:[1.62,2)') From 9f8fad7c55fef898173c1eba5d4a504af9cd34d0 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Wed, 7 Sep 2022 15:29:14 +0200 Subject: [PATCH 103/145] Use BouncyCastle jdk18on instead of jdk15on --- build.gradle | 4 ++-- .../build.gradle.kts | 2 +- webauthn-server-attestation/build.gradle.kts | 4 ++-- webauthn-server-core/build.gradle.kts | 4 ++-- webauthn-server-demo/build.gradle.kts | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/build.gradle b/build.gradle index f32e6e8d5..1060070ae 100644 --- a/build.gradle +++ b/build.gradle @@ -55,8 +55,8 @@ dependencies { api('com.google.guava:guava:[24.1.1,32)') api('com.upokecenter:cbor:[4.5.1,5)') api('org.apache.httpcomponents:httpclient:[4.5.2,5)') - api('org.bouncycastle:bcpkix-jdk15on:[1.62,2)') - api('org.bouncycastle:bcprov-jdk15on:[1.62,2)') + api('org.bouncycastle:bcpkix-jdk18on:[1.62,2)') + api('org.bouncycastle:bcprov-jdk18on:[1.62,2)') api('org.slf4j:slf4j-api:[1.7.25,2)') } } diff --git a/test-dependent-projects/java-dep-webauthn-server-core-and-bouncycastle/build.gradle.kts b/test-dependent-projects/java-dep-webauthn-server-core-and-bouncycastle/build.gradle.kts index af9cd1e75..801446db1 100644 --- a/test-dependent-projects/java-dep-webauthn-server-core-and-bouncycastle/build.gradle.kts +++ b/test-dependent-projects/java-dep-webauthn-server-core-and-bouncycastle/build.gradle.kts @@ -6,7 +6,7 @@ val coreTestsOutput = project(":webauthn-server-core").extensions.getByType(Sour dependencies { implementation(project(":webauthn-server-core")) - implementation("org.bouncycastle:bcprov-jdk15on:[1.62,2)") + implementation("org.bouncycastle:bcprov-jdk18on:[1.62,2)") testImplementation(coreTestsOutput) testImplementation("junit:junit:4.12") diff --git a/webauthn-server-attestation/build.gradle.kts b/webauthn-server-attestation/build.gradle.kts index 859012647..6fbfd417d 100644 --- a/webauthn-server-attestation/build.gradle.kts +++ b/webauthn-server-attestation/build.gradle.kts @@ -40,7 +40,7 @@ dependencies { implementation(project(":yubico-util")) implementation("com.google.guava:guava") implementation("com.fasterxml.jackson.core:jackson-databind") - implementation("org.bouncycastle:bcprov-jdk15on") + implementation("org.bouncycastle:bcprov-jdk18on") implementation("org.slf4j:slf4j-api") testImplementation(platform(project(":test-platform"))) @@ -48,7 +48,7 @@ dependencies { testImplementation(project(":yubico-util-scala")) testImplementation("com.fasterxml.jackson.datatype:jackson-datatype-jdk8") testImplementation("junit:junit") - testImplementation("org.bouncycastle:bcpkix-jdk15on") + testImplementation("org.bouncycastle:bcpkix-jdk18on") testImplementation("org.eclipse.jetty:jetty-server:[9.4.9.v20180320,10)") testImplementation("org.mockito:mockito-core") testImplementation("org.scala-lang:scala-library") diff --git a/webauthn-server-core/build.gradle.kts b/webauthn-server-core/build.gradle.kts index 61378da22..800d3aef5 100644 --- a/webauthn-server-core/build.gradle.kts +++ b/webauthn-server-core/build.gradle.kts @@ -35,8 +35,8 @@ dependencies { testImplementation("com.fasterxml.jackson.core:jackson-databind") testImplementation("com.upokecenter:cbor") testImplementation("junit:junit") - testImplementation("org.bouncycastle:bcpkix-jdk15on") - testImplementation("org.bouncycastle:bcprov-jdk15on") + testImplementation("org.bouncycastle:bcpkix-jdk18on") + testImplementation("org.bouncycastle:bcprov-jdk18on") testImplementation("org.mockito:mockito-core") testImplementation("org.scala-lang:scala-library") testImplementation("org.scalacheck:scalacheck_2.13") diff --git a/webauthn-server-demo/build.gradle.kts b/webauthn-server-demo/build.gradle.kts index eef456a6a..e3653bf6e 100644 --- a/webauthn-server-demo/build.gradle.kts +++ b/webauthn-server-demo/build.gradle.kts @@ -23,7 +23,7 @@ dependencies { implementation("com.fasterxml.jackson.core:jackson-databind") implementation("com.google.guava:guava") implementation("com.upokecenter:cbor") - implementation("org.bouncycastle:bcprov-jdk15on") + implementation("org.bouncycastle:bcprov-jdk18on") implementation("org.slf4j:slf4j-api") implementation("org.eclipse.jetty:jetty-servlet:9.4.9.v20180320") From fa04cff48844f39950701a798c6bc24ece163cbe Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Wed, 7 Sep 2022 15:29:51 +0200 Subject: [PATCH 104/145] Relax SLF4J version constraint to allow 2.x --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 1060070ae..ef002984a 100644 --- a/build.gradle +++ b/build.gradle @@ -57,7 +57,7 @@ dependencies { api('org.apache.httpcomponents:httpclient:[4.5.2,5)') api('org.bouncycastle:bcpkix-jdk18on:[1.62,2)') api('org.bouncycastle:bcprov-jdk18on:[1.62,2)') - api('org.slf4j:slf4j-api:[1.7.25,2)') + api('org.slf4j:slf4j-api:[1.7.25,3)') } } From a9a2aad0a5c27d2cc5cce199577dbdb0f6d92854 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 13 Sep 2022 19:30:31 +0200 Subject: [PATCH 105/145] Revert "Apply spotless plugin declaratively" This reverts commit ebfeb623b795e6066164d6d22d521df0a21daa68. --- build.gradle | 5 +++-- webauthn-server-attestation/build.gradle.kts | 1 - webauthn-server-core/build.gradle.kts | 1 - webauthn-server-demo/build.gradle.kts | 1 - yubico-util-scala/build.gradle.kts | 1 - yubico-util/build.gradle.kts | 1 - 6 files changed, 3 insertions(+), 7 deletions(-) diff --git a/build.gradle b/build.gradle index eeecba019..c02cc562b 100644 --- a/build.gradle +++ b/build.gradle @@ -94,7 +94,8 @@ subprojects { mavenCentral() } - if (project.plugins.hasPlugin('com.diffplug.spotless')) { + if (project !== project(':test-platform')) { + apply plugin: 'com.diffplug.spotless' spotless { java { googleJavaFormat() @@ -125,7 +126,7 @@ task collectSignatures(type: Sync) { subprojects { project -> - if (project.plugins.hasPlugin('scala') && project.plugins.hasPlugin('com.diffplug.spotless')) { + if (project.plugins.hasPlugin('scala')) { project.scalafix { configFile = rootProject.file('scalafix.conf') diff --git a/webauthn-server-attestation/build.gradle.kts b/webauthn-server-attestation/build.gradle.kts index 6fbfd417d..3225a235d 100644 --- a/webauthn-server-attestation/build.gradle.kts +++ b/webauthn-server-attestation/build.gradle.kts @@ -4,7 +4,6 @@ plugins { scala `maven-publish` signing - id("com.diffplug.spotless") id("info.solidsoft.pitest") id("io.github.cosmicsilence.scalafix") } diff --git a/webauthn-server-core/build.gradle.kts b/webauthn-server-core/build.gradle.kts index 800d3aef5..09ceca25f 100644 --- a/webauthn-server-core/build.gradle.kts +++ b/webauthn-server-core/build.gradle.kts @@ -4,7 +4,6 @@ plugins { scala `maven-publish` signing - id("com.diffplug.spotless") id("info.solidsoft.pitest") id("io.github.cosmicsilence.scalafix") } diff --git a/webauthn-server-demo/build.gradle.kts b/webauthn-server-demo/build.gradle.kts index e3653bf6e..c3c65b514 100644 --- a/webauthn-server-demo/build.gradle.kts +++ b/webauthn-server-demo/build.gradle.kts @@ -3,7 +3,6 @@ plugins { war application scala - id("com.diffplug.spotless") id("io.github.cosmicsilence.scalafix") } diff --git a/yubico-util-scala/build.gradle.kts b/yubico-util-scala/build.gradle.kts index d9baf98c6..7ffbc14c7 100644 --- a/yubico-util-scala/build.gradle.kts +++ b/yubico-util-scala/build.gradle.kts @@ -1,6 +1,5 @@ plugins { scala - id("com.diffplug.spotless") id("io.github.cosmicsilence.scalafix") } diff --git a/yubico-util/build.gradle.kts b/yubico-util/build.gradle.kts index 914805a33..83d58d030 100644 --- a/yubico-util/build.gradle.kts +++ b/yubico-util/build.gradle.kts @@ -3,7 +3,6 @@ plugins { scala `maven-publish` signing - id("com.diffplug.spotless") id("info.solidsoft.pitest") id("io.github.cosmicsilence.scalafix") } From 20194f1da3bcc0fc3053b912ed265110aa05dfad Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 13 Sep 2022 19:36:05 +0200 Subject: [PATCH 106/145] ./gradlew spotlessApply --- .../fido/metadata/FidoMetadataServiceIntegrationTest.scala | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/webauthn-server-attestation/src/integrationTest/scala/com/yubico/fido/metadata/FidoMetadataServiceIntegrationTest.scala b/webauthn-server-attestation/src/integrationTest/scala/com/yubico/fido/metadata/FidoMetadataServiceIntegrationTest.scala index 4c6a5a8fc..39e1f9311 100644 --- a/webauthn-server-attestation/src/integrationTest/scala/com/yubico/fido/metadata/FidoMetadataServiceIntegrationTest.scala +++ b/webauthn-server-attestation/src/integrationTest/scala/com/yubico/fido/metadata/FidoMetadataServiceIntegrationTest.scala @@ -186,7 +186,11 @@ class FidoMetadataServiceIntegrationTest } it("a YubiKey 5Ci.") { - check("YubiKey 5 .*Lightning", RealExamples.YubiKey5Ci, attachmentHintsUsb) + check( + "YubiKey 5 .*Lightning", + RealExamples.YubiKey5Ci, + attachmentHintsUsb, + ) } ignore("a Security Key by Yubico.") { // TODO: Investigate why this fails From c63ab59567403ebae6546965a824a1d3d94ad4a3 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 12 Sep 2022 15:38:07 +0200 Subject: [PATCH 107/145] Delete empty test case container --- .../fido/metadata/FidoMetadataServiceIntegrationTest.scala | 4 ---- 1 file changed, 4 deletions(-) diff --git a/webauthn-server-attestation/src/integrationTest/scala/com/yubico/fido/metadata/FidoMetadataServiceIntegrationTest.scala b/webauthn-server-attestation/src/integrationTest/scala/com/yubico/fido/metadata/FidoMetadataServiceIntegrationTest.scala index 39e1f9311..4a1e4dfbb 100644 --- a/webauthn-server-attestation/src/integrationTest/scala/com/yubico/fido/metadata/FidoMetadataServiceIntegrationTest.scala +++ b/webauthn-server-attestation/src/integrationTest/scala/com/yubico/fido/metadata/FidoMetadataServiceIntegrationTest.scala @@ -60,10 +60,6 @@ class FidoMetadataServiceIntegrationTest val attachmentHintsNfc = attachmentHintsUsb ++ Set(ATTACHMENT_HINT_WIRELESS, ATTACHMENT_HINT_NFC) - describe("by AAGUID") { - describe("correctly identifies") {} - } - describe("correctly identifies") { def check( expectedDescriptionRegex: String, From 43c2db294d8cff311a1f52b3c7f86d26b71d81ea Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 12 Sep 2022 15:45:54 +0200 Subject: [PATCH 108/145] Remove tpm attestation from list of unimplemented features --- webauthn-server-core/README | 1 - 1 file changed, 1 deletion(-) diff --git a/webauthn-server-core/README b/webauthn-server-core/README index a4d096156..4da98cd32 100644 --- a/webauthn-server-core/README +++ b/webauthn-server-core/README @@ -14,7 +14,6 @@ it. == Unimplemented features * Attestation statement formats: - ** https://www.w3.org/TR/webauthn/#sctn-tpm-attestation[`tpm`] ** https://www.w3.org/TR/webauthn/#sctn-android-key-attestation[`android-key`] From 0b2baba5d15dcbf6b94fe3983661695674fef2e8 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 12 Sep 2022 15:47:32 +0200 Subject: [PATCH 109/145] Move Windows Hello example from RegistrationTestData to RealExamples --- .../webauthn/RegistrationTestData.scala | 35 +------- .../webauthn/RelyingPartyCeremoniesSpec.scala | 8 +- .../RelyingPartyRegistrationSpec.scala | 15 ++-- .../yubico/webauthn/data/ExtensionsSpec.scala | 4 +- .../yubico/webauthn/test/RealExamples.scala | 81 ++++++++++++++----- 5 files changed, 75 insertions(+), 68 deletions(-) diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RegistrationTestData.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RegistrationTestData.scala index 0dddc9864..4497fcdfc 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RegistrationTestData.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RegistrationTestData.scala @@ -48,6 +48,7 @@ import com.yubico.webauthn.data.PublicKeyCredentialParameters import com.yubico.webauthn.data.RegistrationExtensionInputs import com.yubico.webauthn.data.RelyingPartyIdentity import com.yubico.webauthn.data.UserIdentity +import com.yubico.webauthn.test.RealExamples import org.bouncycastle.asn1.ASN1ObjectIdentifier import org.bouncycastle.asn1.DERSequence import org.bouncycastle.asn1.DERUTF8String @@ -178,11 +179,11 @@ object RegistrationTestData { Packed.BasicAttestationRsa, Packed.BasicAttestationRsaReal, Packed.SelfAttestation, - Tpm.RealExample, Tpm.ValidEs256, Tpm.ValidEs384, Tpm.ValidEs512, Tpm.ValidRs256, + RealExamples.WindowsHelloTpm.asRegistrationTestData, ) object AndroidKey { @@ -646,38 +647,6 @@ object RegistrationTestData { ), ) - val RealExample: RegistrationTestData = - new RegistrationTestData( - alg = COSEAlgorithmIdentifier.RS256, - // Real attestation object from Windows - attestationObject = - ByteArray.fromBase64Url("o2NmbXRjdHBtZ2F0dFN0bXSmY2FsZzn__mNzaWdZAQBFEaTe-uZvbZBNsIMtJa26eigMUxEM1mBtddR7gdEBH5Hyeo9hFCqiJwYVKUq_iP9hvFaiLzoGbAWDgiG-fa3F-S71c8w83756dyRBMXNHYEvYjfv0TqGyky73V4xyKpf1iHiO_g4t31UjQiyTfypdP_rRcm42KVKgVyRPZzx_AKweN9XKEFfT2Ym3fmqD_scaIeKSyGs9qwH1MbILLUVnRK6fKK6sAA4ZaDVz4gUiSUoK9ZycCC2hfLBq5GjiTLgQF_Q2O3gRTqmU8VfwVsmtN5OMaGOyaFrUk97-RvZVrARXhNzrUAJT7KjTLDZeIA96F3pB_F_q3xd_dgvwVpWHY3ZlcmMyLjBjeDVjglkFuzCCBbcwggOfoAMCAQICEHHcna7VCE3QpRyKgi2uvXYwDQYJKoZIhvcNAQELBQAwQTE_MD0GA1UEAxM2RVVTLU5UQy1LRVlJRC04ODJGMDQ3Qjg3MTIxQ0Y5ODg1RjMxMTYwQkM3QkI1NTg2QUY0NzFCMB4XDTIyMDEyMDE5NTQxNloXDTI3MDYwMzE3NTE0OFowADCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALtT5frDB-WUq6N0VYnlYEalJzSut0JQx3vP29_ub-kZ7csJrm8uGXQGUkPlf4EFehFTnQ1jX_oZ8jNPw1m3rV5ijcuCe3r5GICFD6gpbuErmGS2mDVfe3fl_p0gPvhtulqatb1uYkWfW5SIKix1XWRvm92s3lRQvd-6vX_ExPIP-pEf0tkeINpBNNWgdtx3VdW4KVFTcv-q2FKhqfqXiAdOMHmwmWyXYulppYqW2XC7Pw9QmHZR_C5Urpc5UMmABz4zWSAYOyBMkKsX8koAsk8RgLtus07wW3FhJqi-BYczIe0IxG0q9UL295lkaxreTkfWYZHMMcU4M-Tm1w7QvKsCAwEAAaOCAeowggHmMA4GA1UdDwEB_wQEAwIHgDAMBgNVHRMBAf8EAjAAMG0GA1UdIAEB_wRjMGEwXwYJKwYBBAGCNxUfMFIwUAYIKwYBBQUHAgIwRB5CAFQAQwBQAEEAIAAgAFQAcgB1AHMAdABlAGQAIAAgAFAAbABhAHQAZgBvAHIAbQAgACAASQBkAGUAbgB0AGkAdAB5MBAGA1UdJQQJMAcGBWeBBQgDMFAGA1UdEQEB_wRGMESkQjBAMT4wEAYFZ4EFAgIMB05QQ1Q3NXgwFAYFZ4EFAgEMC2lkOjRFNTQ0MzAwMBQGBWeBBQIDDAtpZDowMDA3MDAwMjAfBgNVHSMEGDAWgBSMmnF_AA0xD8rW7i0pqjSXJYwSHjAdBgNVHQ4EFgQUFmUMIda76eb7Whi8CweaWhe7yNMwgbIGCCsGAQUFBwEBBIGlMIGiMIGfBggrBgEFBQcwAoaBkmh0dHA6Ly9hemNzcHJvZGV1c2Fpa3B1Ymxpc2guYmxvYi5jb3JlLndpbmRvd3MubmV0L2V1cy1udGMta2V5aWQtODgyZjA0N2I4NzEyMWNmOTg4NWYzMTE2MGJjN2JiNTU4NmFmNDcxYi84ODIzMGNhMi0yN2U1LTQxNTEtOWJhMi01OWI1ODJjMzlhYWEuY2VyMA0GCSqGSIb3DQEBCwUAA4ICAQCxsTbR5V8qnw6H6HEWJvrqcRy8fkY_vFUSjUq27hRl0t9D6LuS20l65FFm48yLwCkQbIf-aOBjwWafAbSVnEMig3KP-2Ml8IFtH63Msq9lwDlnXx2PNi7ISOemHNzBNeOG7pd_Zs69XUTq9zCriw9gAILCVCYllBluycdT7wZdjf0Bb5QJtTMuhwNXnOWmjv0VBOfsclWo-SEnnufaIDi0Vcf_TzbgmNn408Ej7R4Njy4qLnhPk64ruuWNJt3xlLMjbJXe_VKdO3lhM7JVFWSNAn8zfvEIwrrgCPhp1k2mFUGxJEvpSTnuZtNF35z4_54K6cEqZiqO-qd4FKt4KYs1GYJDyxttuUySGtnYyZg2aYB6hamg3asRDjBMPqoURsdVJcWQh3dFnD88cbs7Qt4_ytqAY61qfPE7bJ6E33o0X7OtxmECPd3aBJk6nsyXEXNF2vIww1UCrRC0OEr1HsTqA4bQU8KCWV6kduUnvkUWPT8CF0d2ER4wnszb053Tlcf2ebcytTMf_Nd95g520Hhqb2FZALCErijBi04Bu6SNeND1NQ3nxDSKC-CamOYW0ODch05Xzi1V0_sq0zmdKTxMSpg1jOZ1Q9924D4lJkruCB3zcsIBTUxV0EgAM1zGuoqwWjwYXr_8tO4_kEO1Lw8DckZIrk1s3ySsMVC89TRrIVkG7zCCBuswggTToAMCAQICEzMAAAQI5W53M7IUDf4AAAAABAgwDQYJKoZIhvcNAQELBQAwgYwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xNjA0BgNVBAMTLU1pY3Jvc29mdCBUUE0gUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgMjAxNDAeFw0yMTA2MDMxNzUxNDhaFw0yNzA2MDMxNzUxNDhaMEExPzA9BgNVBAMTNkVVUy1OVEMtS0VZSUQtODgyRjA0N0I4NzEyMUNGOTg4NUYzMTE2MEJDN0JCNTU4NkFGNDcxQjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMEye3QdCUOGeseHj_QjLMJVbHBEJKkRbXFsmZi6Mob_3IsVfxO-mQQC-xfY9tDyB1jhxfFAG-rjUfRVBwYYPfaVo2W58Q09Kcpf-43Iw9SRxx2ThP-KPFJPFBofZroloNaTNz3DRaWZ2ha-_PUG2nwTXR7LoIpqMVW1PDzGxb47SNpRmKJxVZhQ2_wRhZZvHRHJpZmrCmHRpTRqWSzQT1jn7Zo9VuMYvp_OFj7-LFpkqi4BYyhi0kTBPDQTpYrBi7RtmF1MhZBmm1HGDhoXHcPSZkN5vq5at4g03R15KWyRDgBcckCAgtewd6Dtd_Zwaejlm57xyGqP6T-AE-N8udh1NPv_PZVlSc4CnCayUTPORuaJ7N-v7Y4wpNSIdipq29hw19WVuO_z7q6GpQbn17arYf6LSoDZfwO8GHXPrtBOYYSZCNKuZ_IK8nomBLJPtN5AzwEZNyLCZIkg0U0sJ-oVr2UEYxlwwZQm5RSDxProaKU-OXq4f_j_0pEu5_DbJx9syR3Nsv6Lt9Zkf3JSJTVtWXoM0-R_82vAJ669PX0LLr603PKWBZbW7zQvtGojT_Pc1FDGfwhcdckxd3MGpEjZwh_1D8elYcxj3Ndw5jClWosZKr33pUcjqeFtSZSur0lbm6vyCfS16XzSMn8IkHmbbXcpgGKHumUCFD8CHJIBAgMBAAGjggGOMIIBijAOBgNVHQ8BAf8EBAMCAoQwGwYDVR0lBBQwEgYJKwYBBAGCNxUkBgVngQUIAzAWBgNVHSAEDzANMAsGCSsGAQQBgjcVHzASBgNVHRMBAf8ECDAGAQH_AgEAMB0GA1UdDgQWBBSMmnF_AA0xD8rW7i0pqjSXJYwSHjAfBgNVHSMEGDAWgBR6jArOL0hiF-KU0a5VwVLscXSkVjBwBgNVHR8EaTBnMGWgY6Bhhl9odHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NybC9NaWNyb3NvZnQlMjBUUE0lMjBSb290JTIwQ2VydGlmaWNhdGUlMjBBdXRob3JpdHklMjAyMDE0LmNybDB9BggrBgEFBQcBAQRxMG8wbQYIKwYBBQUHMAKGYWh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY2VydHMvTWljcm9zb2Z0JTIwVFBNJTIwUm9vdCUyMENlcnRpZmljYXRlJTIwQXV0aG9yaXR5JTIwMjAxNC5jcnQwDQYJKoZIhvcNAQELBQADggIBAHG-1grb-6xpObMtxfFScl8PRLd_GjLFaeAd0kVPls0jzKplG2Am4O87Qg0OUY0VhQ-uD39590gGrWEWnOmdrVJ-R1lJc1yrIZFEASBEedJSvxw9YTNknD59uXtznIP_4Glk-4NpqpcYov2OkBdV59V4dTL5oWFH0vzkZQfvGFxEHwtB9O6Bh0Lk142zXAh5_vf_-hSw3t9adloBAnA0AtPUVkzmgNRGeTpfPm-Iud-MAoUXaccFn2EChjKb9ApbS1ww8ZvX4x2kFU6qctu32g7Vf6CgACc1i-UYDT_E-h6c1O4n7JK2OxVS-DVwybps-cALU0gj-ZMauNMej0_x_NerzvDuQ77eFMTDMY4ZTzYOzlg4Nj0K8y1Bx_KqeTBO0N9CdEG3dBxWCUUFQzAx-i38xJL-dtYTCCFATVhc9FFJ0CQgU07JAGAeuNm_GL8kN46bMXd_ApQYFzDQUWYYXvRIt9mCw0Zd45lpAMuiDKT9TgjUVDNu8LQ8FPK0KeiQVrGHFMhHgg2pbVH9Pvc1jNEeRpCo0BLpZQwuIgEt90mepkt6Va-C9krHsU4y2oalG2LUu-jOC3NWNK8LssYVUCFWtaKh5d-xdTQmjx4uO-1sq9GFntVJ94QnEDhldz0XMopQ8srTGLMqR3MT-GkSNb5X1UFC-X1udXI8YvB3ADr_Z3B1YkFyZWFZATYAAQALAAYEcgAgnf_L82w4OuaZ-5ho3G3LidcVOIS-KAOSLBJBWL-tIq4AEAAQCAAAAAAAAQC6zY02JuN_qf28iVCISa6d6aUM5sS2PsKvZMO0P_-28lLX_9-xbmbEcTPvCj5aIEqVUfCb07U4qCBBUdaWygAbhAckvzYrACm5I8hjMoGkxMkZLw5kX0SjtHx6VfgksElxnu4DcC2g9pqL_McgddZV0zNGrYNj1iamzoxTyOcYyvZjQv_4gU-t9mEc0uqHHv-H6k23p4mensyaGAhkGTV8odyBxsNpdnR8IPnXWPO7tBDTbk4mg3VtclqLhS0-TCh_QnZ6lcEl27wTE8FjwqVdQG9F1Ouyn-eNAAq0EufbzwjSNpuDrlj-Kj5xY_lS5BMEjQUXqP-U5nzW22TehX8VaGNlcnRJbmZvWKH_VENHgBcAIgALt-OVET9fzihC1dzyG5xG70MVdRkPpSEkWQ0nns4zaYkAFGo6XjXu3JY-wup-EHJYWEuLpb45AAAACMgZxo6-OwT1-cIZwQGjXsp0dUws1gAiAAvzCsernlTAewF0QwNOA-iWpyhGxC-k_xBRjTtGNz9m6gAiAAs54tyczU1aCAh_N3Vy3qcAnC9zGeOxtQQKCe8CAanbbGhhdXRoRGF0YVkBZ-MWwK8fdtoeGn4DEn0TAUu4IUP_PMiBiJd4lDRznbBDRQAAAAAImHBYytxLgbbhMN5Q3L6WACBxLUIzn9ngKAM11_UwWG7kCiAvVyO1mYGSsEhfWeyhDaQBAwM5AQAgWQEAus2NNibjf6n9vIlQiEmunemlDObEtj7Cr2TDtD__tvJS1__fsW5mxHEz7wo-WiBKlVHwm9O1OKggQVHWlsoAG4QHJL82KwApuSPIYzKBpMTJGS8OZF9Eo7R8elX4JLBJcZ7uA3AtoPaai_zHIHXWVdMzRq2DY9Ymps6MU8jnGMr2Y0L_-IFPrfZhHNLqhx7_h-pNt6eJnp7MmhgIZBk1fKHcgcbDaXZ0fCD511jzu7QQ025OJoN1bXJai4UtPkwof0J2epXBJdu8ExPBY8KlXUBvRdTrsp_njQAKtBLn288I0jabg65Y_io-cWP5UuQTBI0FF6j_lOZ81ttk3oV_FSFDAQAB"), - clientDataJson = new String( - ByteArray - .fromBase64Url( - "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoia0lnZElRbWFBbXM1NlVOencwREg4dU96M0JERjJVSllhSlA2eklRWDFhOCIsIm9yaWdpbiI6Imh0dHBzOi8vZGV2LmQydXJweXB2cmhiMDV4LmFtcGxpZnlhcHAuY29tIiwiY3Jvc3NPcmlnaW4iOmZhbHNlfQ" - ) - .getBytes, - StandardCharsets.UTF_8, - ), - rpId = RelyingPartyIdentity - .builder() - .id("d2urpypvrhb05x.amplifyapp.com") - .name("") - .build(), - userId = UserIdentity - .builder() - .name("foo") - .displayName("Foo Bar") - .id( - ByteArray.fromBase64Url("AAAA") - ) - .build(), - attestationRootCertificate = Some( - CertificateParser.parsePem("MIIF9TCCA92gAwIBAgIQXbYwTgy/J79JuMhpUB5dyzANBgkqhkiG9w0BAQsFADCBjDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjE2MDQGA1UEAxMtTWljcm9zb2Z0IFRQTSBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAyMDE0MB4XDTE0MTIxMDIxMzExOVoXDTM5MTIxMDIxMzkyOFowgYwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xNjA0BgNVBAMTLU1pY3Jvc29mdCBUUE0gUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgMjAxNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAJ+n+bnKt/JHIRC/oI/xgkgsYdPzP0gpvduDA2GbRtth+L4WUyoZKGBw7uz5bjjP8Aql4YExyjR3EZQ4LqnZChMpoCofbeDR4MjCE1TGwWghGpS0mM3GtWD9XiME4rE2K0VW3pdN0CLzkYbvZbs2wQTFfE62yNQiDjyHFWAZ4BQH4eWa8wrDMUxIAneUCpU6zCwM+l6Qh4ohX063BHzXlTSTc1fDsiPaKuMMjWjK9vp5UHFPa+dMAWr6OljQZPFIg3aZ4cUfzS9y+n77Hs1NXPBn6E4Db679z4DThIXyoKeZTv1aaWOWl/exsDLGt2mTMTyykVV8uD1eRjYriFpmoRDwJKAEMOfaURarzp7hka9TOElGyD2gOV4Fscr2MxAYCywLmOLzA4VDSYLuKAhPSp7yawET30AvY1HRfMwBxetSqWP2+yZRNYJlHpor5QTuRDgzR+Zej+aWx6rWNYx43kLthozeVJ3QCsD5iEI/OZlmWn5WYf7O8LB/1A7scrYv44FD8ck3Z+hxXpkklAsjJMsHZa9mBqh+VR1AicX4uZG8m16x65ZU2uUpBa3rn8CTNmw17ZHOiuSWJtS9+PrZVA8ljgf4QgA1g6NPOEiLG2fn8Gm+r5Ak+9tqv72KDd2FPBJ7Xx4stYj/WjNPtEUhW4rcLK3ktLfcy6ea7Rocw5y5AgMBAAGjUTBPMAsGA1UdDwQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR6jArOL0hiF+KU0a5VwVLscXSkVjAQBgkrBgEEAYI3FQEEAwIBADANBgkqhkiG9w0BAQsFAAOCAgEAW4ioo1+J9VWC0UntSBXcXRm1ePTVamtsxVy/GpP4EmJd3Ub53JzNBfYdgfUL51CppS3ZY6BoagB+DqoA2GbSL+7sFGHBl5ka6FNelrwsH6VVw4xV/8klIjmqOyfatPYsz0sUdZev+reeiGpKVoXrK6BDnUU27/mgPtem5YKWvHB/soofUrLKzZV3WfGdx9zBr8V0xW6vO3CKaqkqU9y6EsQw34n7eJCbEVVQ8VdFd9iV1pmXwaBAfBwkviPTKEP9Cm+zbFIOLr3V3CL9hJj+gkTUuXWlJJ6wVXEG5i4rIbLAV59UrW4LonP+seqvWMJYUFxu/niF0R3fSGM+NU11DtBVkhRZt1u0kFhZqjDz1dWyfT/N7Hke3WsDqUFsBi+8SEw90rWx2aUkLvKo83oU4Mx4na+2I3l9F2a2VNGk4K7l3a00g51miPiq0Da0jqw30PaLluTMTGY5+RnZVh50JD6nk+Ea3wRkU8aiYFnpIxfKBZ72whmYYa/egj9IKeqpR0vuLebbU0fJBf880K1jWD3Z5SFyJXo057Mv0OPw5mttytE585ZIy5JsaRXlsOoWGRXE3kUT/MKR1UoAgR54c8Bsh+9Dq2wqIK9mRn15zvBDeyHG6+czurLopziOUeWokxZN1syrEdKlhFoPYavm6t+PzIcpdxZwHA+V3jLJPfI=") - ), - ) - val ValidEs256: RegistrationTestData = new RegistrationTestData( alg = COSEAlgorithmIdentifier.ES256, attestationObject = diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyCeremoniesSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyCeremoniesSpec.scala index 04794e601..94adc5a50 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyCeremoniesSpec.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyCeremoniesSpec.scala @@ -91,12 +91,12 @@ class RelyingPartyCeremoniesSpec .publicKeyCredentialRequestOptions( PublicKeyCredentialRequestOptions .builder() - .challenge(testData.assertion.challenge) + .challenge(testData.assertion.get.challenge) .allowCredentials( List( PublicKeyCredentialDescriptor .builder() - .id(testData.assertion.id) + .id(testData.assertion.get.id) .build() ).asJava ) @@ -105,12 +105,12 @@ class RelyingPartyCeremoniesSpec .username(testData.user.getName) .build() ) - .response(testData.assertion.credential) + .response(testData.assertion.get.credential) .build() ) assertionResult.isSuccess should be(true) - assertionResult.getCredentialId should equal(testData.assertion.id) + assertionResult.getCredentialId should equal(testData.assertion.get.id) assertionResult.getUserHandle should equal(testData.user.getId) assertionResult.getUsername should equal(testData.user.getName) assertionResult.getSignatureCount should be >= testData.attestation.authenticatorData.getSignatureCounter 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 e0c11aa41..2e9698801 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 @@ -2085,7 +2085,7 @@ class RelyingPartyRegistrationSpec describe("The tpm statement format") { it("is supported.") { - val testData = RegistrationTestData.Tpm.RealExample + val testData = RealExamples.WindowsHelloTpm.asRegistrationTestData val steps = finishRegistration( testData = testData, @@ -3576,7 +3576,7 @@ class RelyingPartyRegistrationSpec } describe("A tpm attestation") { - val testData = RegistrationTestData.Tpm.RealExample + val testData = RealExamples.WindowsHelloTpm.asRegistrationTestData generateTests( testData = testData, clock = Clock.fixed( @@ -3594,7 +3594,8 @@ class RelyingPartyRegistrationSpec def init( policyTreeValidator: Option[Predicate[PolicyNode]] ): FinishRegistrationSteps#Step21 = { - val testData = RegistrationTestData.Tpm.RealExample + val testData = + RealExamples.WindowsHelloTpm.asRegistrationTestData val clock = Clock.fixed( Instant.parse("2022-08-25T16:00:00Z"), ZoneOffset.UTC, @@ -3813,7 +3814,7 @@ class RelyingPartyRegistrationSpec testUntrusted(RegistrationTestData.AndroidSafetynet.BasicAttestation) testUntrusted(RegistrationTestData.FidoU2f.BasicAttestation) testUntrusted(RegistrationTestData.NoneAttestation.Default) - testUntrusted(RegistrationTestData.Tpm.RealExample) + testUntrusted(RealExamples.WindowsHelloTpm.asRegistrationTestData) } } } @@ -3904,7 +3905,7 @@ class RelyingPartyRegistrationSpec } it("accept TPM attestations but report they're untrusted.") { - val testData = RegistrationTestData.Tpm.RealExample + val testData = RealExamples.WindowsHelloTpm.asRegistrationTestData val result = rp.toBuilder .identity(testData.rpId) .origins(Set("https://dev.d2urpypvrhb05x.amplifyapp.com").asJava) @@ -3921,7 +3922,7 @@ class RelyingPartyRegistrationSpec result.isAttestationTrusted should be(false) result.getKeyId.getId should equal( - RegistrationTestData.Tpm.RealExample.response.getId + RealExamples.WindowsHelloTpm.asRegistrationTestData.response.getId ) } @@ -4477,7 +4478,7 @@ class RelyingPartyRegistrationSpec } it("for a tpm attestation.") { - val testData = RegistrationTestData.Tpm.RealExample + val testData = RealExamples.WindowsHelloTpm.asRegistrationTestData val steps = finishRegistration( testData = testData, origins = Some(Set("https://dev.d2urpypvrhb05x.amplifyapp.com")), diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/ExtensionsSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/ExtensionsSpec.scala index 9abddb052..6ded9bce3 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/ExtensionsSpec.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/ExtensionsSpec.scala @@ -321,7 +321,7 @@ class ExtensionsSpec it("can deserialize a real largeBlob write example.") { val testData = RealExamples.LargeBlobWrite val registrationCred = testData.attestation.credential - val assertionCred = testData.assertion.credential + val assertionCred = testData.assertion.get.credential registrationCred.getClientExtensionResults.getExtensionIds.asScala should equal( Set("largeBlob") @@ -341,7 +341,7 @@ class ExtensionsSpec it("can deserialize a real largeBlob read example.") { val testData = RealExamples.LargeBlobRead val registrationCred = testData.attestation.credential - val assertionCred = testData.assertion.credential + val assertionCred = testData.assertion.get.credential registrationCred.getClientExtensionResults.getExtensionIds.asScala should equal( Set("largeBlob") diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/test/RealExamples.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/test/RealExamples.scala index dadc73de0..792b5301e 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/test/RealExamples.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/test/RealExamples.scala @@ -1,5 +1,6 @@ package com.yubico.webauthn.test +import com.yubico.internal.util.CertificateParser import com.yubico.internal.util.JacksonCodecs import com.yubico.webauthn.AssertionRequest import com.yubico.webauthn.AssertionTestData @@ -20,6 +21,7 @@ import com.yubico.webauthn.data.RelyingPartyIdentity import com.yubico.webauthn.data.UserIdentity import java.nio.charset.StandardCharsets +import java.security.cert.X509Certificate sealed trait HasClientData { def clientData: String @@ -43,6 +45,7 @@ object RealExamples { clientData: String, attestationObjectBytes: ByteArray, clientExtensionResultsJson: String = "{}", + attestationRootCertificate: Option[X509Certificate] = None, ) extends HasClientData { def attestationObject: AttestationObject = new AttestationObject(attestationObjectBytes) @@ -93,8 +96,15 @@ object RealExamples { rp: RelyingPartyIdentity, user: UserIdentity, attestation: AttestationExample, - assertion: AssertionExample, + assertion: Option[AssertionExample] = None, ) { + def this( + rp: RelyingPartyIdentity, + user: UserIdentity, + attestation: AttestationExample, + assertion: AssertionExample, + ) = this(rp, user, attestation, Some(assertion)) + def attestationCert: ByteArray = new ByteArray( attestation.attestationObject.getAttestationStatement @@ -115,7 +125,7 @@ object RealExamples { privateKey = None, rpId = rp, userId = user, - assertion = Some( + assertion = assertion.map({ assertion => AssertionTestData( request = AssertionRequest .builder() @@ -129,11 +139,12 @@ object RealExamples { .build(), response = assertion.credential, ) - ), + }), + attestationRootCertificate = attestation.attestationRootCertificate, ) } - val YubiKeyNeo = Example( + val YubiKeyNeo = new Example( RelyingPartyIdentity.builder().id("example.com").name("Example RP").build(), UserIdentity .builder() @@ -157,7 +168,7 @@ object RealExamples { ), ) - val YubiKey4 = Example( + val YubiKey4 = new Example( RelyingPartyIdentity.builder().id("example.com").name("Example RP").build(), UserIdentity .builder() @@ -181,7 +192,7 @@ object RealExamples { ), ) - val YubiKey5 = Example( + val YubiKey5 = new Example( RelyingPartyIdentity.builder().id("example.com").name("Example RP").build(), UserIdentity .builder() @@ -205,7 +216,7 @@ object RealExamples { ), ) - val YubiKey5Nfc = Example( + val YubiKey5Nfc = new Example( RelyingPartyIdentity .builder() .id("demo.yubico.com") @@ -235,7 +246,7 @@ object RealExamples { ), ) - val YubiKey5NfcPost5cNfc = Example( + val YubiKey5NfcPost5cNfc = new Example( RelyingPartyIdentity .builder() .id("demo.yubico.com") @@ -265,7 +276,7 @@ object RealExamples { ), ) - val YubiKey5cNfc = Example( + val YubiKey5cNfc = new Example( RelyingPartyIdentity .builder() .id("demo.yubico.com") @@ -295,7 +306,7 @@ object RealExamples { ), ) - val YubiKey5Nano = Example( + val YubiKey5Nano = new Example( RelyingPartyIdentity.builder().id("example.com").name("Example RP").build(), UserIdentity .builder() @@ -319,7 +330,7 @@ object RealExamples { ), ) - val YubiKey5Ci = Example( + val YubiKey5Ci = new Example( RelyingPartyIdentity.builder().id("example.com").name("Example RP").build(), UserIdentity .builder() @@ -343,7 +354,7 @@ object RealExamples { ), ) - val SecurityKey = Example( + val SecurityKey = new Example( RelyingPartyIdentity.builder().id("example.com").name("Example RP").build(), UserIdentity .builder() @@ -367,7 +378,7 @@ object RealExamples { ), ) - val SecurityKey2 = Example( + val SecurityKey2 = new Example( RelyingPartyIdentity.builder().id("example.com").name("Example RP").build(), UserIdentity .builder() @@ -391,7 +402,7 @@ object RealExamples { ), ) - val SecurityKeyNfc = Example( + val SecurityKeyNfc = new Example( RelyingPartyIdentity.builder().id("example.com").name("Example RP").build(), UserIdentity .builder() @@ -415,7 +426,7 @@ object RealExamples { ), ) - val AppleAttestationIos = Example( + val AppleAttestationIos = new Example( RelyingPartyIdentity .builder() .id("demo.yubico.com") @@ -445,7 +456,7 @@ object RealExamples { ), ) - val AppleAttestationMacos = Example( + val AppleAttestationMacos = new Example( RelyingPartyIdentity .builder() .id("demo.yubico.com") @@ -473,7 +484,7 @@ object RealExamples { ), ) - val YubikeyFips5Nfc = Example( + val YubikeyFips5Nfc = new Example( RelyingPartyIdentity.builder().id("demo.yubico.com").name("").build(), UserIdentity .builder() @@ -498,7 +509,7 @@ object RealExamples { ), ) - val Yubikey5ciFips = Example( + val Yubikey5ciFips = new Example( RelyingPartyIdentity.builder().id("demo.yubico.com").name("").build(), UserIdentity .builder() @@ -522,7 +533,7 @@ object RealExamples { ), ) - val YubikeyBio_5_5_4 = Example( + val YubikeyBio_5_5_4 = new Example( RelyingPartyIdentity .builder() .id("demo.yubico.com") @@ -551,7 +562,7 @@ object RealExamples { ), ) - val YubikeyBio_5_5_5 = Example( + val YubikeyBio_5_5_5 = new Example( RelyingPartyIdentity .builder() .id("demo.yubico.com") @@ -592,7 +603,7 @@ object RealExamples { clientExtensionResultsJson = """{"credProps":{"rk":true}}""", ) - val LargeBlobWrite = Example( + val LargeBlobWrite = new Example( RelyingPartyIdentity.builder().id("localhost").name("").build(), UserIdentity .builder() @@ -623,7 +634,7 @@ object RealExamples { ), ) - val LargeBlobRead = Example( + val LargeBlobRead = new Example( RelyingPartyIdentity.builder().id("localhost").name("").build(), UserIdentity .builder() @@ -654,4 +665,30 @@ object RealExamples { ), ) + val WindowsHelloTpm = + Example( + RelyingPartyIdentity + .builder() + .id("d2urpypvrhb05x.amplifyapp.com") + .name("") + .build(), + UserIdentity + .builder() + .name("foo") + .displayName("Foo Bar") + .id( + ByteArray.fromBase64Url("AAAA") + ) + .build(), + AttestationExample( + base64UrlToString( + "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoia0lnZElRbWFBbXM1NlVOencwREg4dU96M0JERjJVSllhSlA2eklRWDFhOCIsIm9yaWdpbiI6Imh0dHBzOi8vZGV2LmQydXJweXB2cmhiMDV4LmFtcGxpZnlhcHAuY29tIiwiY3Jvc3NPcmlnaW4iOmZhbHNlfQ" + ), + ByteArray.fromBase64Url("o2NmbXRjdHBtZ2F0dFN0bXSmY2FsZzn__mNzaWdZAQBFEaTe-uZvbZBNsIMtJa26eigMUxEM1mBtddR7gdEBH5Hyeo9hFCqiJwYVKUq_iP9hvFaiLzoGbAWDgiG-fa3F-S71c8w83756dyRBMXNHYEvYjfv0TqGyky73V4xyKpf1iHiO_g4t31UjQiyTfypdP_rRcm42KVKgVyRPZzx_AKweN9XKEFfT2Ym3fmqD_scaIeKSyGs9qwH1MbILLUVnRK6fKK6sAA4ZaDVz4gUiSUoK9ZycCC2hfLBq5GjiTLgQF_Q2O3gRTqmU8VfwVsmtN5OMaGOyaFrUk97-RvZVrARXhNzrUAJT7KjTLDZeIA96F3pB_F_q3xd_dgvwVpWHY3ZlcmMyLjBjeDVjglkFuzCCBbcwggOfoAMCAQICEHHcna7VCE3QpRyKgi2uvXYwDQYJKoZIhvcNAQELBQAwQTE_MD0GA1UEAxM2RVVTLU5UQy1LRVlJRC04ODJGMDQ3Qjg3MTIxQ0Y5ODg1RjMxMTYwQkM3QkI1NTg2QUY0NzFCMB4XDTIyMDEyMDE5NTQxNloXDTI3MDYwMzE3NTE0OFowADCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALtT5frDB-WUq6N0VYnlYEalJzSut0JQx3vP29_ub-kZ7csJrm8uGXQGUkPlf4EFehFTnQ1jX_oZ8jNPw1m3rV5ijcuCe3r5GICFD6gpbuErmGS2mDVfe3fl_p0gPvhtulqatb1uYkWfW5SIKix1XWRvm92s3lRQvd-6vX_ExPIP-pEf0tkeINpBNNWgdtx3VdW4KVFTcv-q2FKhqfqXiAdOMHmwmWyXYulppYqW2XC7Pw9QmHZR_C5Urpc5UMmABz4zWSAYOyBMkKsX8koAsk8RgLtus07wW3FhJqi-BYczIe0IxG0q9UL295lkaxreTkfWYZHMMcU4M-Tm1w7QvKsCAwEAAaOCAeowggHmMA4GA1UdDwEB_wQEAwIHgDAMBgNVHRMBAf8EAjAAMG0GA1UdIAEB_wRjMGEwXwYJKwYBBAGCNxUfMFIwUAYIKwYBBQUHAgIwRB5CAFQAQwBQAEEAIAAgAFQAcgB1AHMAdABlAGQAIAAgAFAAbABhAHQAZgBvAHIAbQAgACAASQBkAGUAbgB0AGkAdAB5MBAGA1UdJQQJMAcGBWeBBQgDMFAGA1UdEQEB_wRGMESkQjBAMT4wEAYFZ4EFAgIMB05QQ1Q3NXgwFAYFZ4EFAgEMC2lkOjRFNTQ0MzAwMBQGBWeBBQIDDAtpZDowMDA3MDAwMjAfBgNVHSMEGDAWgBSMmnF_AA0xD8rW7i0pqjSXJYwSHjAdBgNVHQ4EFgQUFmUMIda76eb7Whi8CweaWhe7yNMwgbIGCCsGAQUFBwEBBIGlMIGiMIGfBggrBgEFBQcwAoaBkmh0dHA6Ly9hemNzcHJvZGV1c2Fpa3B1Ymxpc2guYmxvYi5jb3JlLndpbmRvd3MubmV0L2V1cy1udGMta2V5aWQtODgyZjA0N2I4NzEyMWNmOTg4NWYzMTE2MGJjN2JiNTU4NmFmNDcxYi84ODIzMGNhMi0yN2U1LTQxNTEtOWJhMi01OWI1ODJjMzlhYWEuY2VyMA0GCSqGSIb3DQEBCwUAA4ICAQCxsTbR5V8qnw6H6HEWJvrqcRy8fkY_vFUSjUq27hRl0t9D6LuS20l65FFm48yLwCkQbIf-aOBjwWafAbSVnEMig3KP-2Ml8IFtH63Msq9lwDlnXx2PNi7ISOemHNzBNeOG7pd_Zs69XUTq9zCriw9gAILCVCYllBluycdT7wZdjf0Bb5QJtTMuhwNXnOWmjv0VBOfsclWo-SEnnufaIDi0Vcf_TzbgmNn408Ej7R4Njy4qLnhPk64ruuWNJt3xlLMjbJXe_VKdO3lhM7JVFWSNAn8zfvEIwrrgCPhp1k2mFUGxJEvpSTnuZtNF35z4_54K6cEqZiqO-qd4FKt4KYs1GYJDyxttuUySGtnYyZg2aYB6hamg3asRDjBMPqoURsdVJcWQh3dFnD88cbs7Qt4_ytqAY61qfPE7bJ6E33o0X7OtxmECPd3aBJk6nsyXEXNF2vIww1UCrRC0OEr1HsTqA4bQU8KCWV6kduUnvkUWPT8CF0d2ER4wnszb053Tlcf2ebcytTMf_Nd95g520Hhqb2FZALCErijBi04Bu6SNeND1NQ3nxDSKC-CamOYW0ODch05Xzi1V0_sq0zmdKTxMSpg1jOZ1Q9924D4lJkruCB3zcsIBTUxV0EgAM1zGuoqwWjwYXr_8tO4_kEO1Lw8DckZIrk1s3ySsMVC89TRrIVkG7zCCBuswggTToAMCAQICEzMAAAQI5W53M7IUDf4AAAAABAgwDQYJKoZIhvcNAQELBQAwgYwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xNjA0BgNVBAMTLU1pY3Jvc29mdCBUUE0gUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgMjAxNDAeFw0yMTA2MDMxNzUxNDhaFw0yNzA2MDMxNzUxNDhaMEExPzA9BgNVBAMTNkVVUy1OVEMtS0VZSUQtODgyRjA0N0I4NzEyMUNGOTg4NUYzMTE2MEJDN0JCNTU4NkFGNDcxQjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMEye3QdCUOGeseHj_QjLMJVbHBEJKkRbXFsmZi6Mob_3IsVfxO-mQQC-xfY9tDyB1jhxfFAG-rjUfRVBwYYPfaVo2W58Q09Kcpf-43Iw9SRxx2ThP-KPFJPFBofZroloNaTNz3DRaWZ2ha-_PUG2nwTXR7LoIpqMVW1PDzGxb47SNpRmKJxVZhQ2_wRhZZvHRHJpZmrCmHRpTRqWSzQT1jn7Zo9VuMYvp_OFj7-LFpkqi4BYyhi0kTBPDQTpYrBi7RtmF1MhZBmm1HGDhoXHcPSZkN5vq5at4g03R15KWyRDgBcckCAgtewd6Dtd_Zwaejlm57xyGqP6T-AE-N8udh1NPv_PZVlSc4CnCayUTPORuaJ7N-v7Y4wpNSIdipq29hw19WVuO_z7q6GpQbn17arYf6LSoDZfwO8GHXPrtBOYYSZCNKuZ_IK8nomBLJPtN5AzwEZNyLCZIkg0U0sJ-oVr2UEYxlwwZQm5RSDxProaKU-OXq4f_j_0pEu5_DbJx9syR3Nsv6Lt9Zkf3JSJTVtWXoM0-R_82vAJ669PX0LLr603PKWBZbW7zQvtGojT_Pc1FDGfwhcdckxd3MGpEjZwh_1D8elYcxj3Ndw5jClWosZKr33pUcjqeFtSZSur0lbm6vyCfS16XzSMn8IkHmbbXcpgGKHumUCFD8CHJIBAgMBAAGjggGOMIIBijAOBgNVHQ8BAf8EBAMCAoQwGwYDVR0lBBQwEgYJKwYBBAGCNxUkBgVngQUIAzAWBgNVHSAEDzANMAsGCSsGAQQBgjcVHzASBgNVHRMBAf8ECDAGAQH_AgEAMB0GA1UdDgQWBBSMmnF_AA0xD8rW7i0pqjSXJYwSHjAfBgNVHSMEGDAWgBR6jArOL0hiF-KU0a5VwVLscXSkVjBwBgNVHR8EaTBnMGWgY6Bhhl9odHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NybC9NaWNyb3NvZnQlMjBUUE0lMjBSb290JTIwQ2VydGlmaWNhdGUlMjBBdXRob3JpdHklMjAyMDE0LmNybDB9BggrBgEFBQcBAQRxMG8wbQYIKwYBBQUHMAKGYWh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY2VydHMvTWljcm9zb2Z0JTIwVFBNJTIwUm9vdCUyMENlcnRpZmljYXRlJTIwQXV0aG9yaXR5JTIwMjAxNC5jcnQwDQYJKoZIhvcNAQELBQADggIBAHG-1grb-6xpObMtxfFScl8PRLd_GjLFaeAd0kVPls0jzKplG2Am4O87Qg0OUY0VhQ-uD39590gGrWEWnOmdrVJ-R1lJc1yrIZFEASBEedJSvxw9YTNknD59uXtznIP_4Glk-4NpqpcYov2OkBdV59V4dTL5oWFH0vzkZQfvGFxEHwtB9O6Bh0Lk142zXAh5_vf_-hSw3t9adloBAnA0AtPUVkzmgNRGeTpfPm-Iud-MAoUXaccFn2EChjKb9ApbS1ww8ZvX4x2kFU6qctu32g7Vf6CgACc1i-UYDT_E-h6c1O4n7JK2OxVS-DVwybps-cALU0gj-ZMauNMej0_x_NerzvDuQ77eFMTDMY4ZTzYOzlg4Nj0K8y1Bx_KqeTBO0N9CdEG3dBxWCUUFQzAx-i38xJL-dtYTCCFATVhc9FFJ0CQgU07JAGAeuNm_GL8kN46bMXd_ApQYFzDQUWYYXvRIt9mCw0Zd45lpAMuiDKT9TgjUVDNu8LQ8FPK0KeiQVrGHFMhHgg2pbVH9Pvc1jNEeRpCo0BLpZQwuIgEt90mepkt6Va-C9krHsU4y2oalG2LUu-jOC3NWNK8LssYVUCFWtaKh5d-xdTQmjx4uO-1sq9GFntVJ94QnEDhldz0XMopQ8srTGLMqR3MT-GkSNb5X1UFC-X1udXI8YvB3ADr_Z3B1YkFyZWFZATYAAQALAAYEcgAgnf_L82w4OuaZ-5ho3G3LidcVOIS-KAOSLBJBWL-tIq4AEAAQCAAAAAAAAQC6zY02JuN_qf28iVCISa6d6aUM5sS2PsKvZMO0P_-28lLX_9-xbmbEcTPvCj5aIEqVUfCb07U4qCBBUdaWygAbhAckvzYrACm5I8hjMoGkxMkZLw5kX0SjtHx6VfgksElxnu4DcC2g9pqL_McgddZV0zNGrYNj1iamzoxTyOcYyvZjQv_4gU-t9mEc0uqHHv-H6k23p4mensyaGAhkGTV8odyBxsNpdnR8IPnXWPO7tBDTbk4mg3VtclqLhS0-TCh_QnZ6lcEl27wTE8FjwqVdQG9F1Ouyn-eNAAq0EufbzwjSNpuDrlj-Kj5xY_lS5BMEjQUXqP-U5nzW22TehX8VaGNlcnRJbmZvWKH_VENHgBcAIgALt-OVET9fzihC1dzyG5xG70MVdRkPpSEkWQ0nns4zaYkAFGo6XjXu3JY-wup-EHJYWEuLpb45AAAACMgZxo6-OwT1-cIZwQGjXsp0dUws1gAiAAvzCsernlTAewF0QwNOA-iWpyhGxC-k_xBRjTtGNz9m6gAiAAs54tyczU1aCAh_N3Vy3qcAnC9zGeOxtQQKCe8CAanbbGhhdXRoRGF0YVkBZ-MWwK8fdtoeGn4DEn0TAUu4IUP_PMiBiJd4lDRznbBDRQAAAAAImHBYytxLgbbhMN5Q3L6WACBxLUIzn9ngKAM11_UwWG7kCiAvVyO1mYGSsEhfWeyhDaQBAwM5AQAgWQEAus2NNibjf6n9vIlQiEmunemlDObEtj7Cr2TDtD__tvJS1__fsW5mxHEz7wo-WiBKlVHwm9O1OKggQVHWlsoAG4QHJL82KwApuSPIYzKBpMTJGS8OZF9Eo7R8elX4JLBJcZ7uA3AtoPaai_zHIHXWVdMzRq2DY9Ymps6MU8jnGMr2Y0L_-IFPrfZhHNLqhx7_h-pNt6eJnp7MmhgIZBk1fKHcgcbDaXZ0fCD511jzu7QQ025OJoN1bXJai4UtPkwof0J2epXBJdu8ExPBY8KlXUBvRdTrsp_njQAKtBLn288I0jabg65Y_io-cWP5UuQTBI0FF6j_lOZ81ttk3oV_FSFDAQAB"), + attestationRootCertificate = Some( + CertificateParser.parsePem("MIIF9TCCA92gAwIBAgIQXbYwTgy/J79JuMhpUB5dyzANBgkqhkiG9w0BAQsFADCBjDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjE2MDQGA1UEAxMtTWljcm9zb2Z0IFRQTSBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAyMDE0MB4XDTE0MTIxMDIxMzExOVoXDTM5MTIxMDIxMzkyOFowgYwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xNjA0BgNVBAMTLU1pY3Jvc29mdCBUUE0gUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgMjAxNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAJ+n+bnKt/JHIRC/oI/xgkgsYdPzP0gpvduDA2GbRtth+L4WUyoZKGBw7uz5bjjP8Aql4YExyjR3EZQ4LqnZChMpoCofbeDR4MjCE1TGwWghGpS0mM3GtWD9XiME4rE2K0VW3pdN0CLzkYbvZbs2wQTFfE62yNQiDjyHFWAZ4BQH4eWa8wrDMUxIAneUCpU6zCwM+l6Qh4ohX063BHzXlTSTc1fDsiPaKuMMjWjK9vp5UHFPa+dMAWr6OljQZPFIg3aZ4cUfzS9y+n77Hs1NXPBn6E4Db679z4DThIXyoKeZTv1aaWOWl/exsDLGt2mTMTyykVV8uD1eRjYriFpmoRDwJKAEMOfaURarzp7hka9TOElGyD2gOV4Fscr2MxAYCywLmOLzA4VDSYLuKAhPSp7yawET30AvY1HRfMwBxetSqWP2+yZRNYJlHpor5QTuRDgzR+Zej+aWx6rWNYx43kLthozeVJ3QCsD5iEI/OZlmWn5WYf7O8LB/1A7scrYv44FD8ck3Z+hxXpkklAsjJMsHZa9mBqh+VR1AicX4uZG8m16x65ZU2uUpBa3rn8CTNmw17ZHOiuSWJtS9+PrZVA8ljgf4QgA1g6NPOEiLG2fn8Gm+r5Ak+9tqv72KDd2FPBJ7Xx4stYj/WjNPtEUhW4rcLK3ktLfcy6ea7Rocw5y5AgMBAAGjUTBPMAsGA1UdDwQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR6jArOL0hiF+KU0a5VwVLscXSkVjAQBgkrBgEEAYI3FQEEAwIBADANBgkqhkiG9w0BAQsFAAOCAgEAW4ioo1+J9VWC0UntSBXcXRm1ePTVamtsxVy/GpP4EmJd3Ub53JzNBfYdgfUL51CppS3ZY6BoagB+DqoA2GbSL+7sFGHBl5ka6FNelrwsH6VVw4xV/8klIjmqOyfatPYsz0sUdZev+reeiGpKVoXrK6BDnUU27/mgPtem5YKWvHB/soofUrLKzZV3WfGdx9zBr8V0xW6vO3CKaqkqU9y6EsQw34n7eJCbEVVQ8VdFd9iV1pmXwaBAfBwkviPTKEP9Cm+zbFIOLr3V3CL9hJj+gkTUuXWlJJ6wVXEG5i4rIbLAV59UrW4LonP+seqvWMJYUFxu/niF0R3fSGM+NU11DtBVkhRZt1u0kFhZqjDz1dWyfT/N7Hke3WsDqUFsBi+8SEw90rWx2aUkLvKo83oU4Mx4na+2I3l9F2a2VNGk4K7l3a00g51miPiq0Da0jqw30PaLluTMTGY5+RnZVh50JD6nk+Ea3wRkU8aiYFnpIxfKBZ72whmYYa/egj9IKeqpR0vuLebbU0fJBf880K1jWD3Z5SFyJXo057Mv0OPw5mttytE585ZIy5JsaRXlsOoWGRXE3kUT/MKR1UoAgR54c8Bsh+9Dq2wqIK9mRn15zvBDeyHG6+czurLopziOUeWokxZN1syrEdKlhFoPYavm6t+PzIcpdxZwHA+V3jLJPfI=") + ), + ), + ) + } From 5e75688c152ee9c90dc06c39c4719a7c9a2c9ba1 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 13 Sep 2022 19:49:22 +0200 Subject: [PATCH 110/145] Use try-with-resources in FidoMetadataDownloader --- .../yubico/fido/metadata/FidoMetadataDownloader.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/FidoMetadataDownloader.java b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/FidoMetadataDownloader.java index 517936f97..2f12c8d94 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 @@ -822,7 +822,9 @@ private Optional refreshBlobInternal( log.debug("Writing new BLOB to cache..."); if (blobCacheFile != null) { - new FileOutputStream(blobCacheFile).write(downloadedBytes.getBytes()); + try (FileOutputStream f = new FileOutputStream(blobCacheFile)) { + f.write(downloadedBytes.getBytes()); + } } if (blobCacheConsumer != null) { @@ -886,7 +888,9 @@ private X509Certificate retrieveTrustRootCert() cert.checkValidity(Date.from(clock.instant())); if (trustRootCacheFile != null) { - new FileOutputStream(trustRootCacheFile).write(downloaded.getBytes()); + try (FileOutputStream f = new FileOutputStream(trustRootCacheFile)) { + f.write(downloaded.getBytes()); + } } if (trustRootCacheConsumer != null) { @@ -955,8 +959,8 @@ private Optional loadCachedBlobOnly(X509Certificate trustRootCerti private Optional readCacheFile(File cacheFile) throws IOException { if (cacheFile.exists() && cacheFile.canRead() && cacheFile.isFile()) { - try { - return Optional.of(readAll(new FileInputStream(cacheFile))); + try (FileInputStream f = new FileInputStream(cacheFile)) { + return Optional.of(readAll(f)); } catch (FileNotFoundException e) { throw new RuntimeException( "This exception should be impossible, please file a bug report.", e); From 45f4badf7aa4be6c4176cca8b04fa01d56762ece Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 13 Sep 2022 19:49:43 +0200 Subject: [PATCH 111/145] Test that FidoMetadataDownloader does not write cache file if not necessary --- .../yubico/fido/metadata/FidoMetadataDownloaderSpec.scala | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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 531a3c8bc..8d2734604 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 @@ -410,7 +410,7 @@ class FidoMetadataDownloaderSpec } it( - "The trust root is not downloaded if there's a valid one in file cache." + "The trust root is not downloaded and not written to cache if there's a valid one in file cache." ) { val random = new SecureRandom() val trustRootDistinguishedName = @@ -438,6 +438,10 @@ class FidoMetadataDownloaderSpec f.write(trustRootCert.getEncoded) f.close() cacheFile.deleteOnExit() + cacheFile.setLastModified( + cacheFile.lastModified() - 1000 + ) // Set mtime in the past to ensure any write will change it + val initialModTime = cacheFile.lastModified val blob = load( FidoMetadataDownloader @@ -456,6 +460,7 @@ class FidoMetadataDownloaderSpec blob.getHeader.getX5c.get.asScala.last.getIssuerDN.getName should equal( trustRootDistinguishedName ) + cacheFile.lastModified should equal(initialModTime) } it( From bf6858c3890021e05fb1a20eb95b3e8d37715b37 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 13 Sep 2022 19:49:58 +0200 Subject: [PATCH 112/145] Fix argument order in attestation trust failure log message --- .../java/com/yubico/webauthn/FinishRegistrationSteps.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishRegistrationSteps.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishRegistrationSteps.java index fdc305bb4..b4f9fbb16 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishRegistrationSteps.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishRegistrationSteps.java @@ -561,10 +561,10 @@ public boolean attestationTrusted() { } catch (CertPathValidatorException e) { log.info( "Failed to derive trust in attestation statement: {} at cert index {}: {}. Attestation object: {}", - response.getResponse().getAttestationObject(), e.getReason(), e.getIndex(), - e.getMessage()); + e.getMessage(), + response.getResponse().getAttestationObject()); return false; } catch (CertificateException e) { From cde7099fe9d645fed45505b284c4368bce7efee8 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 13 Sep 2022 19:50:30 +0200 Subject: [PATCH 113/145] Move TestWithEachProvider to yubico-util-scala module --- yubico-util-scala/build.gradle.kts | 4 ++-- .../scala/com/yubico/webauthn/TestWithEachProvider.scala | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename {webauthn-server-core/src/test => yubico-util-scala/src/main}/scala/com/yubico/webauthn/TestWithEachProvider.scala (100%) diff --git a/yubico-util-scala/build.gradle.kts b/yubico-util-scala/build.gradle.kts index 7ffbc14c7..8e5ee2718 100644 --- a/yubico-util-scala/build.gradle.kts +++ b/yubico-util-scala/build.gradle.kts @@ -13,8 +13,8 @@ dependencies { implementation(platform(rootProject)) implementation(platform(project(":test-platform"))) + implementation("org.bouncycastle:bcprov-jdk18on") implementation("org.scala-lang:scala-library") implementation("org.scalacheck:scalacheck_2.13") - - testImplementation( "org.scalatest:scalatest_2.13") + implementation("org.scalatest:scalatest_2.13") } diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/TestWithEachProvider.scala b/yubico-util-scala/src/main/scala/com/yubico/webauthn/TestWithEachProvider.scala similarity index 100% rename from webauthn-server-core/src/test/scala/com/yubico/webauthn/TestWithEachProvider.scala rename to yubico-util-scala/src/main/scala/com/yubico/webauthn/TestWithEachProvider.scala From 59fc44f67c1c7cc4cd5c7d0472d902a57e20ca0f Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 13 Sep 2022 19:51:04 +0200 Subject: [PATCH 114/145] Add log hint about policyTreeValidator setting --- .../java/com/yubico/webauthn/FinishRegistrationSteps.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishRegistrationSteps.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishRegistrationSteps.java index b4f9fbb16..3aa0a2906 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishRegistrationSteps.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishRegistrationSteps.java @@ -30,6 +30,7 @@ import COSE.CoseException; import com.upokecenter.cbor.CBORObject; import com.yubico.webauthn.attestation.AttestationTrustSource; +import com.yubico.webauthn.attestation.AttestationTrustSource.TrustRootsResult; import com.yubico.webauthn.data.AttestationObject; import com.yubico.webauthn.data.AttestationType; import com.yubico.webauthn.data.AuthenticatorAttestationResponse; @@ -52,6 +53,7 @@ import java.security.cert.CertificateFactory; import java.security.cert.PKIXCertPathValidatorResult; import java.security.cert.PKIXParameters; +import java.security.cert.PKIXReason; import java.security.cert.TrustAnchor; import java.security.cert.X509Certificate; import java.security.spec.InvalidKeySpecException; @@ -565,6 +567,12 @@ public boolean attestationTrusted() { e.getIndex(), e.getMessage(), response.getResponse().getAttestationObject()); + if (PKIXReason.INVALID_POLICY.equals(e.getReason())) { + log.info( + "You may need to set the policyTreeValidator property on the {} returned by your {}.", + TrustRootsResult.class.getSimpleName(), + AttestationTrustSource.class.getSimpleName()); + } return false; } catch (CertificateException e) { From 823cd019a66c65e40695499d0120b0109d910571 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 13 Sep 2022 19:51:37 +0200 Subject: [PATCH 115/145] Test that RelyingParty trusts MDS results in integration test --- .../FidoMetadataServiceIntegrationTest.scala | 67 ++++++++++++++----- .../webauthn/TestWithEachProvider.scala | 31 +++++---- 2 files changed, 68 insertions(+), 30 deletions(-) diff --git a/webauthn-server-attestation/src/integrationTest/scala/com/yubico/fido/metadata/FidoMetadataServiceIntegrationTest.scala b/webauthn-server-attestation/src/integrationTest/scala/com/yubico/fido/metadata/FidoMetadataServiceIntegrationTest.scala index 4a1e4dfbb..ef1b22bab 100644 --- a/webauthn-server-attestation/src/integrationTest/scala/com/yubico/fido/metadata/FidoMetadataServiceIntegrationTest.scala +++ b/webauthn-server-attestation/src/integrationTest/scala/com/yubico/fido/metadata/FidoMetadataServiceIntegrationTest.scala @@ -6,8 +6,13 @@ import com.yubico.fido.metadata.AttachmentHint.ATTACHMENT_HINT_NFC import com.yubico.fido.metadata.AttachmentHint.ATTACHMENT_HINT_WIRED import com.yubico.fido.metadata.AttachmentHint.ATTACHMENT_HINT_WIRELESS import com.yubico.internal.util.CertificateParser +import com.yubico.webauthn.FinishRegistrationOptions +import com.yubico.webauthn.RelyingParty +import com.yubico.webauthn.TestWithEachProvider import com.yubico.webauthn.data.AttestationObject +import com.yubico.webauthn.test.Helpers import com.yubico.webauthn.test.RealExamples +import org.bouncycastle.jce.provider.BouncyCastleProvider import org.junit.runner.RunWith import org.scalatest.BeforeAndAfter import org.scalatest.funspec.AnyFunSpec @@ -18,11 +23,13 @@ import org.scalatestplus.junit.JUnitRunner import java.io.IOException import java.security.cert.X509Certificate +import java.time.Clock +import java.time.ZoneOffset import java.util import java.util.Optional import scala.jdk.CollectionConverters.IteratorHasAsScala +import scala.jdk.CollectionConverters.SetHasAsJava import scala.jdk.CollectionConverters.SetHasAsScala -import scala.jdk.OptionConverters.RichOption import scala.jdk.OptionConverters.RichOptional import scala.util.Try @@ -32,7 +39,8 @@ import scala.util.Try class FidoMetadataServiceIntegrationTest extends AnyFunSpec with Matchers - with BeforeAndAfter { + with BeforeAndAfter + with TestWithEachProvider { describe("FidoMetadataService") { @@ -60,7 +68,7 @@ class FidoMetadataServiceIntegrationTest val attachmentHintsNfc = attachmentHintsUsb ++ Set(ATTACHMENT_HINT_WIRELESS, ATTACHMENT_HINT_NFC) - describe("correctly identifies") { + describe("correctly identifies and trusts") { def check( expectedDescriptionRegex: String, testData: RealExamples.Example, @@ -101,17 +109,38 @@ class FidoMetadataServiceIntegrationTest def getX5cArray(attestationObject: AttestationObject): JsonNode = attestationObject.getAttestationStatement.get("x5c") - val entries = fidoMds.get - .findEntries( - getAttestationTrustPath( - testData.attestation.attestationObject - ).get, - Some( - new AAGUID( - testData.attestation.attestationObject.getAuthenticatorData.getAttestedCredentialData.get.getAaguid - ) - ).toJava, + val rp = RelyingParty + .builder() + .identity(testData.rp) + .credentialRepository(Helpers.CredentialRepository.empty) + .origins( + Set(testData.attestation.collectedClientData.getOrigin).asJava ) + .allowUntrustedAttestation(false) + .attestationTrustSource(fidoMds.get) + .clock( + Clock.fixed( + CertificateParser + .parseDer(testData.attestationCert.getBytes) + .getNotBefore + .toInstant, + ZoneOffset.UTC, + ) + ) + .build() + + val registrationResult = rp.finishRegistration( + FinishRegistrationOptions + .builder() + .request(testData.asRegistrationTestData.request) + .response(testData.attestation.credential) + .build() + ) + + registrationResult.isAttestationTrusted should be(true) + + val entries = fidoMds.get + .findEntries(registrationResult) .asScala entries should not be empty val metadataStatements = @@ -214,11 +243,13 @@ class FidoMetadataServiceIntegrationTest } it("a YubiKey 5.4 NFC FIPS.") { - check( - "YubiKey 5 FIPS Series with NFC", - RealExamples.YubikeyFips5Nfc, - attachmentHintsNfc, - ) + withProviderContext(List(new BouncyCastleProvider)) { // Needed for JDK<14 because this example uses EdDSA + check( + "YubiKey 5 FIPS Series with NFC", + RealExamples.YubikeyFips5Nfc, + attachmentHintsNfc, + ) + } } it("a YubiKey 5.4 Ci FIPS.") { diff --git a/yubico-util-scala/src/main/scala/com/yubico/webauthn/TestWithEachProvider.scala b/yubico-util-scala/src/main/scala/com/yubico/webauthn/TestWithEachProvider.scala index 908717f0b..7d9d53d0a 100644 --- a/yubico-util-scala/src/main/scala/com/yubico/webauthn/TestWithEachProvider.scala +++ b/yubico-util-scala/src/main/scala/com/yubico/webauthn/TestWithEachProvider.scala @@ -10,6 +10,24 @@ import java.security.Security trait TestWithEachProvider extends Matchers { this: AnyFunSpec => + /** Run the `body` in a context with the given JCA [[Security]] providers, + * then reset the providers to their state before. + */ + def withProviderContext( + providers: List[Provider] + )( + body: => Any + ): Unit = { + val originalProviders = Security.getProviders.toList + Security.getProviders.foreach(prov => Security.removeProvider(prov.getName)) + providers.foreach(Security.addProvider) + + body + + Security.getProviders.foreach(prov => Security.removeProvider(prov.getName)) + originalProviders.foreach(Security.addProvider) + } + def wrapItFunctionWithProviderContext( providerSetName: String, providers: List[Provider], @@ -29,18 +47,7 @@ trait TestWithEachProvider extends Matchers { */ def it(testName: String)(testFun: => Any): Unit = { this.it.apply(testName) { - val originalProviders = Security.getProviders.toList - Security.getProviders.foreach(prov => - Security.removeProvider(prov.getName) - ) - providers.foreach(Security.addProvider) - - testFun - - Security.getProviders.foreach(prov => - Security.removeProvider(prov.getName) - ) - originalProviders.foreach(Security.addProvider) + withProviderContext(providers)(testFun) } } From a61bc6cc7fa783a6977542371efd970cb797d767 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 13 Sep 2022 19:52:13 +0200 Subject: [PATCH 116/145] Accept any policy tree in FidoMetadataService --- .../metadata/FidoMetadataServiceIntegrationTest.scala | 9 +++++++++ .../com/yubico/fido/metadata/FidoMetadataService.java | 1 + 2 files changed, 10 insertions(+) diff --git a/webauthn-server-attestation/src/integrationTest/scala/com/yubico/fido/metadata/FidoMetadataServiceIntegrationTest.scala b/webauthn-server-attestation/src/integrationTest/scala/com/yubico/fido/metadata/FidoMetadataServiceIntegrationTest.scala index ef1b22bab..6a2782ded 100644 --- a/webauthn-server-attestation/src/integrationTest/scala/com/yubico/fido/metadata/FidoMetadataServiceIntegrationTest.scala +++ b/webauthn-server-attestation/src/integrationTest/scala/com/yubico/fido/metadata/FidoMetadataServiceIntegrationTest.scala @@ -2,6 +2,7 @@ package com.yubico.fido.metadata import com.fasterxml.jackson.databind.JsonNode import com.yubico.fido.metadata.AttachmentHint.ATTACHMENT_HINT_EXTERNAL +import com.yubico.fido.metadata.AttachmentHint.ATTACHMENT_HINT_INTERNAL import com.yubico.fido.metadata.AttachmentHint.ATTACHMENT_HINT_NFC import com.yubico.fido.metadata.AttachmentHint.ATTACHMENT_HINT_WIRED import com.yubico.fido.metadata.AttachmentHint.ATTACHMENT_HINT_WIRELESS @@ -267,6 +268,14 @@ class FidoMetadataServiceIntegrationTest attachmentHintsUsb, ) } + + it("a Windows Hello attestation.") { + check( + "Windows Hello.*", + RealExamples.WindowsHelloTpm, + Set(ATTACHMENT_HINT_INTERNAL), + ) + } } } } 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 7f627ac85..1895122ef 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 @@ -616,6 +616,7 @@ public TrustRootsResult findTrustRoots( .collect(Collectors.toSet())) .certStore(certStore) .enableRevocationChecking(false) + .policyTreeValidator(policyNode -> true) .build(); } } From bd522d7289507d002528bb5da35833136a7353ef Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 13 Sep 2022 21:08:16 +0200 Subject: [PATCH 117/145] Explain policyTreeValidator setting better in NEWS and README --- NEWS | 3 ++- README | 15 +++++++++++++++ .../yubico/fido/metadata/FidoMetadataService.java | 8 +++++++- 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/NEWS b/NEWS index f987eeeaa..90b5b6f7c 100644 --- a/NEWS +++ b/NEWS @@ -23,7 +23,8 @@ New features: predicate function will be used to validate the certificate policy tree after successful attestation certificate path validation. This may be required for some JCA providers to accept attestation certificates with critical - certificate policy extensions. + certificate policy extensions. See the JavaDoc for + `TrustRootsResultBuilder.policyTreeValidator(Predicate)` for more information. Fixes: diff --git a/README b/README index 0e2be4146..56d9937ea 100644 --- a/README +++ b/README @@ -624,6 +624,21 @@ The link:webauthn-server-attestation[`webauthn-server-attestation` module] 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.0.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.0.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.0.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.0.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.0.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. + == Building 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 1895122ef..2776e4383 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 @@ -69,7 +69,13 @@ * *

    This class implements {@link AttestationTrustSource}, so it can be configured as the {@link * RelyingPartyBuilder#attestationTrustSource(AttestationTrustSource) attestationTrustSource} - * setting in {@link RelyingParty}. + * setting in {@link RelyingParty}. This implementation always sets {@link + * com.yubico.webauthn.attestation.AttestationTrustSource.TrustRootsResult.TrustRootsResultBuilder#enableRevocationChecking(boolean) + * enableRevocationChecking(false)}, because the FIDO MDS has its own revocation procedures and not + * all attestation certificates provide CRLs; and always sets {@link + * com.yubico.webauthn.attestation.AttestationTrustSource.TrustRootsResult.TrustRootsResultBuilder#policyTreeValidator(Predicate) + * policyTreeValidator} to accept any policy tree, because a Windows Hello attestation certificate + * is known to include a critical certificate policies extension. * *

    The metadata service may be configured with a two stages of filters to select trusted * authenticators. The first stage is the {@link FidoMetadataServiceBuilder#prefilter(Predicate) From 3b38ef6bba55a2fefe89c85fd6cd598b16ec3d8b Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 13 Sep 2022 19:52:34 +0200 Subject: [PATCH 118/145] Don't log about ignoring zero AAGUID when no AAGUID was given --- .../main/java/com/yubico/fido/metadata/FidoMetadataService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 2776e4383..bdf617a48 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 @@ -499,7 +499,7 @@ public Set findEntries( certSubjectKeyIdentifiers, aaguid); - if (!nonzeroAaguid.isPresent()) { + if (aaguid.isPresent() && !nonzeroAaguid.isPresent()) { log.debug("findEntries: ignoring zero AAGUID"); } From 9adcde5ad51cdbad64e628b49844121983bcec44 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 13 Sep 2022 19:52:55 +0200 Subject: [PATCH 119/145] Check hash of cached trust root cert as promised in JavaDoc --- NEWS | 12 ++ .../fido/metadata/FidoMetadataDownloader.java | 17 ++- .../metadata/FidoMetadataDownloaderSpec.scala | 127 +++++++++++++++++- 3 files changed, 147 insertions(+), 9 deletions(-) diff --git a/NEWS b/NEWS index 90b5b6f7c..2fd1e613f 100644 --- a/NEWS +++ b/NEWS @@ -1,5 +1,7 @@ == Version 2.1.0 (unreleased) == +`webauthn-server-core`: + Changes: * Log messages on attestation certificate path validation failure now include @@ -33,6 +35,16 @@ Fixes: `webauthn-server-parent` to unpublished test meta-module. +`webauthn-server-attestation`: + +Fixes: + +* Fixed various typos and mistakes in JavaDocs. +* `FidoMetadataDownloader` now verifies the SHA-256 hash of the cached trust + root certificate, as promised in the JavaDoc of `useTrustRootCacheFile` and + `useTrustRootCache`. + + == Version 2.0.0 == This release removes deprecated APIs and changes some defaults to better align 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 2f12c8d94..cb76f5b7d 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 @@ -867,13 +867,16 @@ private X509Certificate retrieveTrustRootCert() X509Certificate cert = null; if (cachedContents.isPresent()) { - try { - final X509Certificate cachedCert = - CertificateParser.parseDer(cachedContents.get().getBytes()); - cachedCert.checkValidity(Date.from(clock.instant())); - cert = cachedCert; - } catch (CertificateException e) { - // Fall through + final ByteArray verifiedCachedContents = verifyHash(cachedContents.get(), trustRootSha256); + if (verifiedCachedContents != null) { + try { + final X509Certificate cachedCert = + CertificateParser.parseDer(verifiedCachedContents.getBytes()); + cachedCert.checkValidity(Date.from(clock.instant())); + cert = cachedCert; + } catch (CertificateException e) { + // Fall through + } } } 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 8d2734604..0d1683de5 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 @@ -449,7 +449,14 @@ class FidoMetadataDownloaderSpec .expectLegalHeader( "Kom ihåg att du aldrig får snyta dig i mattan!" ) - .useDefaultTrustRoot() + .downloadTrustRoot( + new URL("https://localhost:12345/nonexistent.dev.null"), + Set( + TestAuthenticator.sha256( + new ByteArray(trustRootCert.getEncoded) + ) + ).asJava, + ) .useTrustRootCacheFile(cacheFile) .useBlob(blobJwt) .clock(Clock.fixed(CertValidFrom, ZoneOffset.UTC)) @@ -626,7 +633,14 @@ class FidoMetadataDownloaderSpec .expectLegalHeader( "Kom ihåg att du aldrig får snyta dig i mattan!" ) - .useDefaultTrustRoot() + .downloadTrustRoot( + new URL("https://localhost:12345/nonexistent.dev.null"), + Set( + TestAuthenticator.sha256( + new ByteArray(trustRootCert.getEncoded) + ) + ).asJava, + ) .useTrustRootCache( () => Optional.of(new ByteArray(trustRootCert.getEncoded)), newCache => { @@ -693,6 +707,115 @@ class FidoMetadataDownloaderSpec testWithHashes(Set(goodHash)) should not be null testWithHashes(Set(badHash, goodHash)) should not be null } + + it("The cached trust root cert must match one of the expected SHA256 hashes.") { + val (cachedTrustRootCert, cachedCaKeypair, cachedCaName) = + makeTrustRootCert() + val (cachedRootBlobCert, cachedRootBlobKeypair, _) = + makeCert(cachedCaKeypair, cachedCaName) + val cachedRootBlobJwt = makeBlob( + List(cachedRootBlobCert), + cachedRootBlobKeypair, + LocalDate.now(), + ) + val cachedRootCrls = List[CRL]( + TestAuthenticator.buildCrl( + cachedCaName, + cachedCaKeypair.getPrivate, + "SHA256withECDSA", + CertValidFrom, + CertValidTo, + ) + ) + + val (downloadedTrustRootCert, downloadedCaKeypair, downloadedCaName) = + makeTrustRootCert() + val (downloadedRootBlobCert, downloadedRootBlobKeypair, _) = + makeCert(downloadedCaKeypair, downloadedCaName) + val downloadedRootBlobJwt = makeBlob( + List(downloadedRootBlobCert), + downloadedRootBlobKeypair, + LocalDate.now(), + ) + val downloadedRootCrls = List[CRL]( + TestAuthenticator.buildCrl( + downloadedCaName, + downloadedCaKeypair.getPrivate, + "SHA256withECDSA", + CertValidFrom, + CertValidTo, + ) + ) + + val (server, serverUrl, httpsCert) = + makeHttpServer( + "/trust-root.der", + downloadedTrustRootCert.getEncoded, + ) + startServer(server) + + def testWithHashes( + hashes: Set[ByteArray], + blobJwt: String, + crls: List[CRL], + ): (MetadataBLOB, Option[ByteArray]) = { + var writtenCache: Option[ByteArray] = None + + val blob = load( + FidoMetadataDownloader + .builder() + .expectLegalHeader( + "Kom ihåg att du aldrig får snyta dig i mattan!" + ) + .downloadTrustRoot( + new URL(s"${serverUrl}/trust-root.der"), + hashes.asJava, + ) + .useTrustRootCache( + () => + Optional.of(new ByteArray(cachedTrustRootCert.getEncoded)), + downloaded => { writtenCache = Some(downloaded) }, + ) + .useBlob(blobJwt) + .useCrls(crls.asJava) + .trustHttpsCerts(httpsCert) + .clock(Clock.fixed(CertValidFrom, ZoneOffset.UTC)) + .build() + ) + + (blob, writtenCache) + } + + { + val (blob, writtenCache) = testWithHashes( + Set( + TestAuthenticator.sha256( + new ByteArray(cachedTrustRootCert.getEncoded) + ) + ), + cachedRootBlobJwt, + cachedRootCrls, + ) + blob should not be null + writtenCache should be(None) + } + + { + val (blob, writtenCache) = testWithHashes( + Set( + TestAuthenticator.sha256( + new ByteArray(downloadedTrustRootCert.getEncoded) + ) + ), + downloadedRootBlobJwt, + downloadedRootCrls, + ) + blob should not be null + writtenCache should be( + Some(new ByteArray(downloadedTrustRootCert.getEncoded)) + ) + } + } } describe("2. To validate the digital certificates used in the digital signature, the certificate revocation information MUST be available in the form of CRLs at the respective MDS CRL location e.g. More information can be found at https://fidoalliance.org/metadata/") { From 414baa4f3c1079a3381b056ef3a2c237c1bd6971 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 13 Sep 2022 20:22:37 +0200 Subject: [PATCH 120/145] Add dependency configuration section to webauthn-server-attestation README --- webauthn-server-attestation/README.adoc | 32 +++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/webauthn-server-attestation/README.adoc b/webauthn-server-attestation/README.adoc index e9d662a58..5b4c0a0f4 100644 --- a/webauthn-server-attestation/README.adoc +++ b/webauthn-server-attestation/README.adoc @@ -84,6 +84,38 @@ but we recommend that you do not _require_ a trusted attestation unless you have See link:doc/Migrating_from_v1.adoc[the migration guide]. +== Dependency configuration + +Maven: + +---------- + + com.yubico + webauthn-server-attestation + 2.0.0 + compile + +---------- + +Gradle: + +---------- +compile 'com.yubico:webauthn-server-attestation:2.0.0' +---------- + + +=== Semantic versioning + +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.0.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. +Breaking changes to these will NOT be reflected in version numbers. + + == Getting started Using this module consists of 4 major steps: From f5978a65d472e3ba489d33c87924d43d6e1f4f6b Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 13 Sep 2022 20:16:18 +0200 Subject: [PATCH 121/145] Add "hybrid" transport --- NEWS | 1 + README | 3 +- webauthn-server-attestation/README.adoc | 3 +- .../webauthn/data/AuthenticatorTransport.java | 45 ++++++++++++++++--- .../data/AuthenticatorTransportSpec.scala | 8 +++- 5 files changed, 51 insertions(+), 9 deletions(-) diff --git a/NEWS b/NEWS index 2fd1e613f..fde343213 100644 --- a/NEWS +++ b/NEWS @@ -27,6 +27,7 @@ New features: some JCA providers to accept attestation certificates with critical certificate policy extensions. See the JavaDoc for `TrustRootsResultBuilder.policyTreeValidator(Predicate)` for more information. +* (Experimental) Added constant `AuthenticatorTransport.HYBRID`. Fixes: diff --git a/README b/README index 56d9937ea..a80666515 100644 --- a/README +++ b/README @@ -60,7 +60,8 @@ 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.0.0/com/yubico/webauthn/package-summary.html[Javadoc]. +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.0.0/com/yubico/webauthn/package-summary.html[Javadoc], +*with the exception* of things annotated with `@Deprecated`. 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. diff --git a/webauthn-server-attestation/README.adoc b/webauthn-server-attestation/README.adoc index 5b4c0a0f4..1b477be76 100644 --- a/webauthn-server-attestation/README.adoc +++ b/webauthn-server-attestation/README.adoc @@ -109,7 +109,8 @@ compile 'com.yubico:webauthn-server-attestation:2.0.0' 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.0.0/com/yubico/fido/metadata/package-summary.html[Javadoc]. +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.0.0/com/yubico/fido/metadata/package-summary.html[Javadoc], +*with the exception* of things annotated with `@Deprecated`. 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. diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorTransport.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorTransport.java index c8f47b154..884e606d8 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorTransport.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorTransport.java @@ -58,23 +58,54 @@ public class AuthenticatorTransport implements Comparable5.8.4. + * Authenticator Transport Enumeration (enum AuthenticatorTransport) + */ public static final AuthenticatorTransport USB = new AuthenticatorTransport("usb"); /** * Indicates the respective authenticator can be contacted over Near Field Communication (NFC). + * + * @see 5.8.4. + * Authenticator Transport Enumeration (enum AuthenticatorTransport) */ public static final AuthenticatorTransport NFC = new AuthenticatorTransport("nfc"); /** * Indicates the respective authenticator can be contacted over Bluetooth Smart (Bluetooth Low * Energy / BLE). + * + * @see 5.8.4. + * Authenticator Transport Enumeration (enum AuthenticatorTransport) */ public static final AuthenticatorTransport BLE = new AuthenticatorTransport("ble"); + /** + * Indicates the respective authenticator can be contacted using a combination of (often separate) + * data-transport and proximity mechanisms. This supports, for example, authentication on a + * desktop computer using a smartphone. + * + * @deprecated EXPERIMENTAL: This feature is from a not yet mature standard; it could change as + * the standard matures. + * @see 5.8.4. + * Authenticator Transport Enumeration (enum AuthenticatorTransport) + */ + @Deprecated + public static final AuthenticatorTransport HYBRID = new AuthenticatorTransport("hybrid"); + /** * Indicates the respective authenticator is contacted using a client device-specific transport. * These authenticators are not removable from the client device. + * + * @see 5.8.4. + * Authenticator Transport Enumeration (enum AuthenticatorTransport) */ public static final AuthenticatorTransport INTERNAL = new AuthenticatorTransport("internal"); @@ -83,13 +114,13 @@ public class AuthenticatorTransport implements Comparableid is the same as that of any of {@link #USB}, {@link #NFC}, {@link - * #BLE} or {@link #INTERNAL}, returns that constant instance. Otherwise returns a new - * instance containing id. + * #BLE}, {@link #HYBRID} or {@link #INTERNAL}, returns that constant instance. Otherwise + * returns a new instance containing id. * @see #valueOf(String) */ @JsonCreator @@ -101,8 +132,8 @@ public static AuthenticatorTransport of(@NonNull String id) { } /** - * @return If name equals "USB", "NFC", "BLE" - * or "INTERNAL", returns the constant by that name. + * @return If name equals "USB", "NFC", "BLE", + * "HYBRID" or "INTERNAL", returns the constant by that name. * @throws IllegalArgumentException if name is anything else. * @see #of(String) */ @@ -114,6 +145,8 @@ public static AuthenticatorTransport valueOf(String name) { return NFC; case "BLE": return BLE; + case "HYBRID": + return HYBRID; case "INTERNAL": return INTERNAL; default: diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/AuthenticatorTransportSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/AuthenticatorTransportSpec.scala index a1fbca6a7..3f0dd0696 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/AuthenticatorTransportSpec.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/AuthenticatorTransportSpec.scala @@ -48,13 +48,16 @@ class AuthenticatorTransportSpec it("BLE.") { AuthenticatorTransport.BLE.getId should equal("ble") } + it("HYBRID.") { + AuthenticatorTransport.HYBRID.getId should equal("hybrid") + } it("INTERNAL.") { AuthenticatorTransport.INTERNAL.getId should equal("internal") } } it("has a values() function.") { - AuthenticatorTransport.values().length should equal(4) + AuthenticatorTransport.values().length should equal(5) AuthenticatorTransport.values() should not be theSameInstanceAs( AuthenticatorTransport.values() ) @@ -70,6 +73,9 @@ class AuthenticatorTransportSpec AuthenticatorTransport.valueOf( "BLE" ) should be theSameInstanceAs AuthenticatorTransport.BLE + AuthenticatorTransport.valueOf( + "HYBRID" + ) should be theSameInstanceAs AuthenticatorTransport.HYBRID AuthenticatorTransport.valueOf( "INTERNAL" ) should be theSameInstanceAs AuthenticatorTransport.INTERNAL From 15ff965bce83475d9ce7c525849117102ed4019b Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 13 Sep 2022 20:44:00 +0200 Subject: [PATCH 122/145] Fix typo --- .../main/java/com/yubico/fido/metadata/FidoMetadataService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 bdf617a48..ffbacac8e 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 @@ -77,7 +77,7 @@ * policyTreeValidator} to accept any policy tree, because a Windows Hello attestation certificate * is known to include a critical certificate policies extension. * - *

    The metadata service may be configured with a two stages of filters to select trusted + *

    The metadata service may be configured with two stages of filters to select trusted * authenticators. The first stage is the {@link FidoMetadataServiceBuilder#prefilter(Predicate) * prefilter} setting, which is executed once when the {@link FidoMetadataService} instance is * constructed. The second stage is the {@link FidoMetadataServiceBuilder#filter(Predicate) filter} From 2f00c8e95d3c51bec5c35e58ad7c62565240aee5 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Wed, 14 Sep 2022 01:00:57 +0200 Subject: [PATCH 123/145] Copy JavaDoc to predefined methods in TrustRootsResult --- .../attestation/AttestationTrustSource.java | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/attestation/AttestationTrustSource.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/attestation/AttestationTrustSource.java index a11d57dcf..b18264602 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/attestation/AttestationTrustSource.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/attestation/AttestationTrustSource.java @@ -143,10 +143,42 @@ private TrustRootsResult( this.policyTreeValidator = policyTreeValidator; } + /** + * A {@link CertStore} of additional CRLs and/or intermediate certificates to use during + * certificate path validation, if any. This will not be used if {@link + * TrustRootsResultBuilder#trustRoots(Set) trustRoots} is empty. + * + *

    Any certificates included in this {@link CertStore} are NOT considered trusted; they will + * be trusted only if they chain to any of the {@link TrustRootsResultBuilder#trustRoots(Set) + * trustRoots}. + * + *

    The default is null. + */ public Optional getCertStore() { return Optional.ofNullable(certStore); } + /** + * If non-null, the PolicyQualifiersRejected flag will be set to false during certificate path + * validation. See {@link + * java.security.cert.PKIXParameters#setPolicyQualifiersRejected(boolean)}. + * + *

    The given {@link Predicate} will be used to validate the policy tree. The {@link + * Predicate} should return true if the policy tree is acceptable, and false + * otherwise. + * + *

    Depending on your "PKIX" JCA provider configuration, this may be required if + * any certificate in the certificate path contains a certificate policies extension marked + * critical. If this is not set, then such a certificate will be rejected by the certificate + * path validator from the default provider. + * + *

    Consult the Java + * PKI Programmer's Guide for how to use the {@link PolicyNode} argument of the {@link + * Predicate}. + * + *

    The default is null. + */ public Optional> getPolicyTreeValidator() { return Optional.ofNullable(policyTreeValidator); } @@ -157,6 +189,11 @@ public static TrustRootsResultBuilder.Step1 builder() { public static class TrustRootsResultBuilder { public static class Step1 { + /** + * A set of attestation root certificates trusted to certify the relevant attestation + * statement. If the attestation statement is not trusted, or if no trust roots were found, + * this should be an empty set. + */ public TrustRootsResultBuilder trustRoots(@NonNull Set trustRoots) { return new TrustRootsResultBuilder().trustRoots(trustRoots); } From f1d7dd2cbe53be85c4fbdcda4c22b142e3d9c50d Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Wed, 14 Sep 2022 16:49:30 +0200 Subject: [PATCH 124/145] Add AttestationConveyancePreference.ENTERPRISE --- NEWS | 1 + .../data/AttestationConveyancePreference.java | 15 +++++++++- .../com/yubico/webauthn/data/EnumsSpec.scala | 28 +++++++++++++++++++ 3 files changed, 43 insertions(+), 1 deletion(-) diff --git a/NEWS b/NEWS index fde343213..249fc3ed7 100644 --- a/NEWS +++ b/NEWS @@ -27,6 +27,7 @@ New features: some JCA providers to accept attestation certificates with critical certificate policy extensions. See the JavaDoc for `TrustRootsResultBuilder.policyTreeValidator(Predicate)` for more information. +* Added enum value `AttestationConveyancePreference.ENTERPRISE`. * (Experimental) Added constant `AuthenticatorTransport.HYBRID`. Fixes: diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AttestationConveyancePreference.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AttestationConveyancePreference.java index 442849945..8442b736c 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AttestationConveyancePreference.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AttestationConveyancePreference.java @@ -73,7 +73,20 @@ public enum AttestationConveyancePreference { * Indicates that the Relying Party wants to receive the attestation statement as generated by the * authenticator. */ - DIRECT("direct"); + DIRECT("direct"), + + /** + * This value indicates that the Relying Party wants to receive an attestation statement that may + * include uniquely identifying information. This is intended for controlled deployments within an + * enterprise where the organization wishes to tie registrations to specific authenticators. User + * agents MUST NOT provide such an attestation unless the user agent or authenticator + * configuration permits it for the requested RP ID. + * + *

    If permitted, the user agent SHOULD signal to the authenticator (at invocation time) that + * enterprise attestation is requested, and convey the resulting AAGUID and attestation statement, + * unaltered, to the Relying Party. + */ + ENTERPRISE("enterprise"); @JsonValue @Getter @NonNull private final String value; diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/EnumsSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/EnumsSpec.scala index f5f20dd16..8e35e8558 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/EnumsSpec.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/EnumsSpec.scala @@ -22,6 +22,34 @@ class EnumsSpec describe("AttestationConveyancePreference") { describe("can be parsed from JSON") { + it("""value: "none"""") { + json.readValue( + "\"none\"", + classOf[AttestationConveyancePreference], + ) should be theSameInstanceAs AttestationConveyancePreference.NONE + } + + it("""value: "indirect"""") { + json.readValue( + "\"indirect\"", + classOf[AttestationConveyancePreference], + ) should be theSameInstanceAs AttestationConveyancePreference.INDIRECT + } + + it("""value: "direct"""") { + json.readValue( + "\"direct\"", + classOf[AttestationConveyancePreference], + ) should be theSameInstanceAs AttestationConveyancePreference.DIRECT + } + + it("""value: "enterprise"""") { + json.readValue( + "\"enterprise\"", + classOf[AttestationConveyancePreference], + ) should be theSameInstanceAs AttestationConveyancePreference.ENTERPRISE + } + it("but throws IllegalArgumentException for unknown values.") { val result = Try( json.readValue("\"foo\"", classOf[AttestationConveyancePreference]) From 027a3597fe744ad3a1e29a0dc4e0c299c3cf611e Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Wed, 14 Sep 2022 18:00:42 +0200 Subject: [PATCH 125/145] Rename filter parameter to prefilter --- .../com/yubico/fido/metadata/FidoMds3Spec.scala | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/webauthn-server-attestation/src/test/scala/com/yubico/fido/metadata/FidoMds3Spec.scala b/webauthn-server-attestation/src/test/scala/com/yubico/fido/metadata/FidoMds3Spec.scala index 55f0958d3..c323076e7 100644 --- a/webauthn-server-attestation/src/test/scala/com/yubico/fido/metadata/FidoMds3Spec.scala +++ b/webauthn-server-attestation/src/test/scala/com/yubico/fido/metadata/FidoMds3Spec.scala @@ -204,11 +204,11 @@ class FidoMds3Spec extends AnyFunSpec with Matchers { def makeMds( blobTuple: (String, X509Certificate, java.util.Set[CRL]), attestationCrls: Set[CRL] = Set.empty, - )(filter: MetadataBLOBPayloadEntry => Boolean): FidoMetadataService = + )(prefilter: MetadataBLOBPayloadEntry => Boolean): FidoMetadataService = FidoMetadataService .builder() .useBlob(makeDownloader(blobTuple).loadCachedBlob()) - .prefilter(filter.asJava) + .prefilter(prefilter.asJava) .certStore( CertStore.getInstance( "Collection", @@ -239,8 +239,8 @@ class FidoMds3Spec extends AnyFunSpec with Matchers { }""") it("Filtering in getFilteredEntries works as expected.") { - def count(filter: MetadataBLOBPayloadEntry => Boolean): Long = - makeMds(blobTuple)(filter).findEntries(_ => true).size + def count(prefilter: MetadataBLOBPayloadEntry => Boolean): Long = + makeMds(blobTuple)(prefilter).findEntries(_ => true).size implicit class MetadataBLOBPayloadEntryWithAbbreviatedAttestationCertificateKeyIdentifiers( entry: MetadataBLOBPayloadEntry @@ -384,10 +384,10 @@ class FidoMds3Spec extends AnyFunSpec with Matchers { .build() def finishRegistration( - filter: MetadataBLOBPayloadEntry => Boolean + prefilter: MetadataBLOBPayloadEntry => Boolean ): RegistrationResult = { val mds = - makeMds(blobTuple, attestationCrls = attestationCrls)(filter) + makeMds(blobTuple, attestationCrls = attestationCrls)(prefilter) RelyingParty .builder() .identity(rpIdentity) From 23846c1e671e9f89f5ae7dd43166cec53063fa68 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Wed, 14 Sep 2022 18:42:08 +0200 Subject: [PATCH 126/145] Omit zero AAGUIDs from runtime filter argument --- NEWS | 5 ++ .../fido/metadata/FidoMetadataService.java | 5 +- .../yubico/fido/metadata/FidoMds3Spec.scala | 72 ++++++++++++++++++- 3 files changed, 78 insertions(+), 4 deletions(-) diff --git a/NEWS b/NEWS index 249fc3ed7..24a7da9ad 100644 --- a/NEWS +++ b/NEWS @@ -39,6 +39,11 @@ Fixes: `webauthn-server-attestation`: +Changes: + +* The `AuthenticatorToBeFiltered` argument of the `FidoMetadataService` runtime + filter now omits zero AAGUIDs. + Fixes: * Fixed various typos and mistakes in JavaDocs. 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 ffbacac8e..9176bf504 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 @@ -432,6 +432,9 @@ public static class AuthenticatorToBeFiltered { * The AAGUID from the attested * credential data of a credential about ot be registered. + * + *

    This will not be present if the attested credential data contained an AAGUID of all + * zeroes. */ public Optional getAaguid() { return Optional.ofNullable(aaguid); @@ -522,7 +525,7 @@ public Set findEntries( new AuthenticatorToBeFiltered( attestationCertificateChain, metadataBLOBPayloadEntry, - aaguid.orElse(null)))) + nonzeroAaguid.orElse(null)))) .collect(Collectors.toSet()); log.debug( diff --git a/webauthn-server-attestation/src/test/scala/com/yubico/fido/metadata/FidoMds3Spec.scala b/webauthn-server-attestation/src/test/scala/com/yubico/fido/metadata/FidoMds3Spec.scala index c323076e7..ffa8ee50b 100644 --- a/webauthn-server-attestation/src/test/scala/com/yubico/fido/metadata/FidoMds3Spec.scala +++ b/webauthn-server-attestation/src/test/scala/com/yubico/fido/metadata/FidoMds3Spec.scala @@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.node.ArrayNode import com.fasterxml.jackson.databind.node.JsonNodeFactory import com.fasterxml.jackson.databind.node.ObjectNode import com.fasterxml.jackson.databind.node.TextNode +import com.yubico.fido.metadata.FidoMetadataService.Filters.AuthenticatorToBeFiltered import com.yubico.internal.util.CertificateParser import com.yubico.webauthn.FinishRegistrationOptions import com.yubico.webauthn.RegistrationResult @@ -204,8 +205,11 @@ class FidoMds3Spec extends AnyFunSpec with Matchers { def makeMds( blobTuple: (String, X509Certificate, java.util.Set[CRL]), attestationCrls: Set[CRL] = Set.empty, - )(prefilter: MetadataBLOBPayloadEntry => Boolean): FidoMetadataService = - FidoMetadataService + )( + prefilter: MetadataBLOBPayloadEntry => Boolean, + filter: Option[AuthenticatorToBeFiltered => Boolean] = None, + ): FidoMetadataService = { + val builder = FidoMetadataService .builder() .useBlob(makeDownloader(blobTuple).loadCachedBlob()) .prefilter(prefilter.asJava) @@ -215,7 +219,9 @@ class FidoMds3Spec extends AnyFunSpec with Matchers { new CollectionCertStoreParameters(attestationCrls.asJava), ) ) - .build() + filter.foreach(f => builder.filter(f.asJava)) + builder.build() + } val blobTuple = makeBlob(s"""{ "legalHeader" : "Kom ihåg att du aldrig får snyta dig i mattan!", @@ -405,6 +411,66 @@ class FidoMds3Spec extends AnyFunSpec with Matchers { _.getAaguid.toScala.contains(aaguidB) ).isAttestationTrusted should be(false) } + + describe("Zero AAGUIDs") { + val zeroAaguid = + new AAGUID(ByteArray.fromHex("00000000000000000000000000000000")) + + it("are not used to find metadata entries.") { + aaguidA should not equal zeroAaguid + + val blobTuple = makeBlob(s"""{ + "legalHeader" : "Kom ihåg att du aldrig får snyta dig i mattan!", + "nextUpdate" : "2022-12-01", + "no" : 0, + "entries": [ + ${makeEntry(aaguid = Some(aaguidA))}, + ${makeEntry(aaguid = Some(zeroAaguid))} + ] + }""") + var filterRan = false + val mds = makeMds(blobTuple)( + _ => true, + filter = Some({ _ => + filterRan = true + true + }), + ) + + mds.findEntries(zeroAaguid) shouldBe empty + filterRan should be(false) + } + + it("are omitted in the argument to the runtime filter.") { + aaguidA should not equal zeroAaguid + + val (cert, _) = TestAuthenticator.generateAttestationCertificate() + val acki: String = new ByteArray( + CertificateParser.computeSubjectKeyIdentifier(cert) + ).getHex + val blobTuple = makeBlob(s"""{ + "legalHeader" : "Kom ihåg att du aldrig får snyta dig i mattan!", + "nextUpdate" : "2022-12-01", + "no" : 0, + "entries": [ + ${makeEntry(acki = Some(Set(acki)), aaguid = Some(aaguidA))} + ] + }""") + var filterRan = false + val mds = makeMds(blobTuple)( + _ => true, + filter = Some({ authenticatorToBeFiltered => + filterRan = true + authenticatorToBeFiltered.getAaguid.toScala should be(None) + true + }), + ) + + mds.findEntries(List(cert).asJava, zeroAaguid).size should be(1) + filterRan should be(true) + } + } + } describe("2.1. Check whether the status report of the authenticator model has changed compared to the cached entry by looking at the fields timeOfLastStatusChange and statusReport.") { From ec4d8d9b7ab4eb767cc9ba564e8d2cdcc7ee7111 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Wed, 14 Sep 2022 19:09:15 +0200 Subject: [PATCH 127/145] Reduce visibility of internals in TpmAttestationStatementVerifier --- .../TpmAttestationStatementVerifier.java | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/TpmAttestationStatementVerifier.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/TpmAttestationStatementVerifier.java index 47d2e3895..e367ba94f 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/TpmAttestationStatementVerifier.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/TpmAttestationStatementVerifier.java @@ -75,7 +75,7 @@ final class TpmAttestationStatementVerifier * https://www.trustedcomputinggroup.org/wp-content/uploads/TPM-Rev-2.0-Part-2-Structures-01.38.pdf */ static final class Attributes { - public static final int SIGN_ENCRYPT = 1 << 18; + static final int SIGN_ENCRYPT = 1 << 18; private static final int SHALL_BE_ZERO = (1 << 0) // 0 Reserved @@ -360,8 +360,8 @@ private void verifyPublicKeysMatch(AttestationObject attestationObject, TpmtPubl } static final class TpmAlgAsym { - public static final int RSA = 0x0001; - public static final int ECC = 0x0023; + static final int RSA = 0x0001; + static final int ECC = 0x0023; } private interface Parameters {} @@ -376,7 +376,7 @@ private static class TpmtPublic { Unique unique; ByteArray rawBytes; - public static TpmtPublic parse(byte[] pubArea) throws IOException { + private static TpmtPublic parse(byte[] pubArea) throws IOException { try (ByteInputStream reader = new ByteInputStream(pubArea)) { final int signAlg = reader.readUnsignedShort(); final int nameAlg = reader.readUnsignedShort(); @@ -433,7 +433,7 @@ public static TpmtPublic parse(byte[] pubArea) throws IOException { * nvPublicArea contents of the TPMS_NV_PUBLIC associated with handle * */ - public ByteArray name() { + private ByteArray name() { final ByteArray hash; switch (this.nameAlg) { case TpmAlgHash.SHA1: @@ -464,13 +464,13 @@ public ByteArray name() { } static class TpmAlgHash { - public static final int SHA1 = 0x0004; - public static final int SHA256 = 0x000B; - public static final int SHA384 = 0x000C; - public static final int SHA512 = 0x000D; + static final int SHA1 = 0x0004; + static final int SHA256 = 0x000B; + static final int SHA384 = 0x000C; + static final int SHA512 = 0x000D; } - public void verifyX5cRequirements(X509Certificate cert, ByteArray aaguid) + private void verifyX5cRequirements(X509Certificate cert, ByteArray aaguid) throws CertificateParsingException { ExceptionUtil.assure( cert.getVersion() == 3, @@ -529,7 +529,7 @@ public void verifyX5cRequirements(X509Certificate cert, ByteArray aaguid) } static final class TpmRsaScheme { - public static final int RSASSA = 0x0014; + static final int RSASSA = 0x0014; } /** @@ -542,7 +542,7 @@ private static class TpmsRsaParms implements Parameters { long exponent; - public static TpmsRsaParms parse(ByteInputStream reader) throws IOException { + private static TpmsRsaParms parse(ByteInputStream reader) throws IOException { final int symmetric = reader.readUnsignedShort(); ExceptionUtil.assure( symmetric == TPM_ALG_NULL, @@ -573,7 +573,7 @@ public static TpmsRsaParms parse(ByteInputStream reader) throws IOException { private static class Tpm2bPublicKeyRsa implements Unique { ByteArray bytes; - public static Tpm2bPublicKeyRsa parse(ByteInputStream reader) throws IOException { + private static Tpm2bPublicKeyRsa parse(ByteInputStream reader) throws IOException { return new Tpm2bPublicKeyRsa(new ByteArray(reader.read(reader.readUnsignedShort()))); } } @@ -582,7 +582,7 @@ public static Tpm2bPublicKeyRsa parse(ByteInputStream reader) throws IOException private static class TpmsEccParms implements Parameters { int curve_id; - public static TpmsEccParms parse(ByteInputStream reader) throws IOException { + private static TpmsEccParms parse(ByteInputStream reader) throws IOException { final int symmetric = reader.readUnsignedShort(); final int scheme = reader.readUnsignedShort(); ExceptionUtil.assure( @@ -614,7 +614,7 @@ private static class TpmsEccPoint implements Unique { ByteArray x; ByteArray y; - public static TpmsEccPoint parse(ByteInputStream reader) throws IOException { + private static TpmsEccPoint parse(ByteInputStream reader) throws IOException { final ByteArray x = new ByteArray(reader.read(reader.readUnsignedShort())); final ByteArray y = new ByteArray(reader.read(reader.readUnsignedShort())); @@ -630,10 +630,10 @@ public static TpmsEccPoint parse(ByteInputStream reader) throws IOException { */ private static class TpmEccCurve { - public static final int NONE = 0x0000; - public static final int NIST_P256 = 0x0003; - public static final int NIST_P384 = 0x0004; - public static final int NIST_P521 = 0x0005; + private static final int NONE = 0x0000; + private static final int NIST_P256 = 0x0003; + private static final int NIST_P384 = 0x0004; + private static final int NIST_P521 = 0x0005; } /** From 3840cf43e8f04c8fa68842a6c1bd875f6e5a8128 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Wed, 14 Sep 2022 19:17:25 +0200 Subject: [PATCH 128/145] Drop unneeded dependencies --- NEWS | 3 +++ webauthn-server-attestation/build.gradle.kts | 2 -- webauthn-server-core/build.gradle.kts | 3 +-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/NEWS b/NEWS index 24a7da9ad..62d155e89 100644 --- a/NEWS +++ b/NEWS @@ -35,6 +35,7 @@ Fixes: * Fixed various typos and mistakes in JavaDocs. * Moved version constraints for test dependencies from meta-module `webauthn-server-parent` to unpublished test meta-module. +* `yubico-util` dependency removed from downstream compile scope. `webauthn-server-attestation`: @@ -50,6 +51,8 @@ Fixes: * `FidoMetadataDownloader` now verifies the SHA-256 hash of the cached trust root certificate, as promised in the JavaDoc of `useTrustRootCacheFile` and `useTrustRootCache`. +* BouncyCastle dependency dropped. +* Guava dependency dropped (but still remains in core module). == Version 2.0.0 == diff --git a/webauthn-server-attestation/build.gradle.kts b/webauthn-server-attestation/build.gradle.kts index 3225a235d..e1d848308 100644 --- a/webauthn-server-attestation/build.gradle.kts +++ b/webauthn-server-attestation/build.gradle.kts @@ -37,9 +37,7 @@ dependencies { api(project(":webauthn-server-core")) implementation(project(":yubico-util")) - implementation("com.google.guava:guava") implementation("com.fasterxml.jackson.core:jackson-databind") - implementation("org.bouncycastle:bcprov-jdk18on") implementation("org.slf4j:slf4j-api") testImplementation(platform(project(":test-platform"))) diff --git a/webauthn-server-core/build.gradle.kts b/webauthn-server-core/build.gradle.kts index 09ceca25f..9c32eea8c 100644 --- a/webauthn-server-core/build.gradle.kts +++ b/webauthn-server-core/build.gradle.kts @@ -20,8 +20,7 @@ java { dependencies { api(platform(rootProject)) - api(project(":yubico-util")) - + implementation(project(":yubico-util")) implementation("com.augustcellars.cose:cose-java") implementation("com.fasterxml.jackson.core:jackson-databind") implementation("com.google.guava:guava") From b5bcf1122b9e2f36280c81ec6bf6addbaeca1939 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Wed, 14 Sep 2022 19:51:08 +0200 Subject: [PATCH 129/145] Move BouncyCastle version constraints to test-platform module --- build.gradle | 2 -- test-platform/build.gradle.kts | 2 ++ webauthn-server-demo/build.gradle.kts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index c02cc562b..71cb17678 100644 --- a/build.gradle +++ b/build.gradle @@ -55,8 +55,6 @@ dependencies { api('com.google.guava:guava:[24.1.1,32)') api('com.upokecenter:cbor:[4.5.1,5)') api('org.apache.httpcomponents:httpclient:[4.5.2,5)') - api('org.bouncycastle:bcpkix-jdk18on:[1.62,2)') - api('org.bouncycastle:bcprov-jdk18on:[1.62,2)') api('org.slf4j:slf4j-api:[1.7.25,3)') } } diff --git a/test-platform/build.gradle.kts b/test-platform/build.gradle.kts index 4be89d628..d440d8d92 100644 --- a/test-platform/build.gradle.kts +++ b/test-platform/build.gradle.kts @@ -13,5 +13,7 @@ dependencies { api("org.scalatestplus:junit-4-13_2.13:3.2.13.0") api("org.scalatestplus:scalacheck-1-16_2.13:3.2.13.0") api("uk.org.lidalia:slf4j-test:1.2.0") + api("org.bouncycastle:bcpkix-jdk18on:[1.62,2)") + api("org.bouncycastle:bcprov-jdk18on:[1.62,2)") } } diff --git a/webauthn-server-demo/build.gradle.kts b/webauthn-server-demo/build.gradle.kts index c3c65b514..99f5b84b6 100644 --- a/webauthn-server-demo/build.gradle.kts +++ b/webauthn-server-demo/build.gradle.kts @@ -14,6 +14,7 @@ val coreTestsOutput = project(":webauthn-server-core").extensions.getByType(Sour dependencies { implementation(platform(rootProject)) + implementation(platform(project(":test-platform"))) implementation(project(":webauthn-server-attestation")) implementation(project(":webauthn-server-core")) @@ -33,7 +34,6 @@ dependencies { runtimeOnly("org.glassfish.jersey.containers:jersey-container-servlet:2.36") runtimeOnly("org.glassfish.jersey.inject:jersey-hk2:2.36") - testImplementation(platform(project(":test-platform"))) testImplementation(coreTestsOutput) testImplementation(project(":yubico-util-scala")) From 17cf9917eaef6ee753164b989dd63e0b5c0e7f68 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Wed, 14 Sep 2022 20:01:57 +0200 Subject: [PATCH 130/145] Remove unused import --- .../com/yubico/webauthn/TpmAttestationStatementVerifier.java | 1 - 1 file changed, 1 deletion(-) diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/TpmAttestationStatementVerifier.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/TpmAttestationStatementVerifier.java index e367ba94f..dc215eee4 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/TpmAttestationStatementVerifier.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/TpmAttestationStatementVerifier.java @@ -49,7 +49,6 @@ import java.util.Arrays; import java.util.List; import javax.naming.InvalidNameException; -import javax.naming.directory.Attributes; import javax.naming.ldap.LdapName; import lombok.Value; import lombok.extern.slf4j.Slf4j; From 531360bab86b01606e588fd0654472ee86e56214 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Wed, 14 Sep 2022 20:08:45 +0200 Subject: [PATCH 131/145] Add changes suggested by IntelliJ code cleanup --- build.gradle | 4 ++-- buildSrc/src/main/groovy/com/yubico/gradle/GitUtils.groovy | 2 +- .../com/yubico/gradle/pitest/tasks/PitestMergeTask.groovy | 4 ++-- webauthn-server-attestation/build.gradle.kts | 3 ++- .../java/com/yubico/fido/metadata/AuthenticatorGetInfo.java | 3 +-- .../java/com/yubico/fido/metadata/CtapCertificationId.java | 2 +- .../yubico/fido/metadata/CtapPinUvAuthProtocolVersion.java | 2 +- webauthn-server-core/build.gradle.kts | 5 +++-- .../main/java/com/yubico/webauthn/RegisteredCredential.java | 2 +- .../main/java/com/yubico/webauthn/RegistrationResult.java | 2 +- .../webauthn/data/PublicKeyCredentialCreationOptions.java | 2 +- .../yubico/webauthn/data/PublicKeyCredentialDescriptor.java | 2 +- .../yubico/webauthn/data/PublicKeyCredentialParameters.java | 2 +- .../webauthn/data/PublicKeyCredentialRequestOptions.java | 2 +- .../java/com/yubico/webauthn/data/RelyingPartyIdentity.java | 2 +- .../src/main/java/com/yubico/webauthn/data/UserIdentity.java | 2 +- .../webauthn/attestation/matcher/ExtensionMatcher.java | 3 ++- .../src/main/java/demo/webauthn/WebAuthnRestResource.java | 2 +- 18 files changed, 24 insertions(+), 22 deletions(-) diff --git a/build.gradle b/build.gradle index 71cb17678..4abe16238 100644 --- a/build.gradle +++ b/build.gradle @@ -18,7 +18,7 @@ plugins { import io.franzbecker.gradle.lombok.LombokPlugin import io.franzbecker.gradle.lombok.task.DelombokTask -import com.yubico.gradle.GitUtils; +import com.yubico.gradle.GitUtils rootProject.description = "Metadata root for the com.yubico:webauthn-server-* module family" @@ -201,7 +201,7 @@ subprojects { project -> if (project.hasProperty('publishMe') && project.publishMe) { if (GitUtils.getGitCommit(projectDir) == null) { - throw new RuntimeException("Failed to get git commit ID"); + throw new RuntimeException("Failed to get git commit ID") } publishing { diff --git a/buildSrc/src/main/groovy/com/yubico/gradle/GitUtils.groovy b/buildSrc/src/main/groovy/com/yubico/gradle/GitUtils.groovy index 65e63d910..c3c143e31 100644 --- a/buildSrc/src/main/groovy/com/yubico/gradle/GitUtils.groovy +++ b/buildSrc/src/main/groovy/com/yubico/gradle/GitUtils.groovy @@ -1,4 +1,4 @@ -package com.yubico.gradle; +package com.yubico.gradle public class GitUtils { diff --git a/buildSrc/src/main/groovy/com/yubico/gradle/pitest/tasks/PitestMergeTask.groovy b/buildSrc/src/main/groovy/com/yubico/gradle/pitest/tasks/PitestMergeTask.groovy index 268a79cc1..d1ecd235c 100644 --- a/buildSrc/src/main/groovy/com/yubico/gradle/pitest/tasks/PitestMergeTask.groovy +++ b/buildSrc/src/main/groovy/com/yubico/gradle/pitest/tasks/PitestMergeTask.groovy @@ -14,7 +14,7 @@ import org.gradle.api.tasks.TaskAction class PitestMergeTask extends DefaultTask { @OutputFile - def File destinationFile = project.file("${project.buildDir}/reports/pitest/mutations.xml") + File destinationFile = project.file("${project.buildDir}/reports/pitest/mutations.xml") PitestMergeTask() { project.subprojects.each { subproject -> @@ -24,7 +24,7 @@ class PitestMergeTask extends DefaultTask { } } - def Set findMutationsXmlFiles(File f, Set found) { + Set findMutationsXmlFiles(File f, Set found) { if (f.isDirectory()) { Set result = found for (File child : f.listFiles()) { diff --git a/webauthn-server-attestation/build.gradle.kts b/webauthn-server-attestation/build.gradle.kts index e1d848308..fd4af93dc 100644 --- a/webauthn-server-attestation/build.gradle.kts +++ b/webauthn-server-attestation/build.gradle.kts @@ -1,4 +1,5 @@ -import com.yubico.gradle.GitUtils; +import com.yubico.gradle.GitUtils + plugins { `java-library` scala diff --git a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/AuthenticatorGetInfo.java b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/AuthenticatorGetInfo.java index 78cefbd64..1d878b93a 100644 --- a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/AuthenticatorGetInfo.java +++ b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/AuthenticatorGetInfo.java @@ -1,7 +1,6 @@ package com.yubico.fido.metadata; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.core.JacksonException; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.DeserializationContext; @@ -360,7 +359,7 @@ private static class SetFromIntJsonDeserializer extends JsonDeserializer> { @Override public Set deserialize(JsonParser p, DeserializationContext ctxt) - throws IOException, JacksonException { + throws IOException { final int bitset = p.getNumberValue().intValue(); return Arrays.stream(UserVerificationMethod.values()) .filter(uvm -> (uvm.getValue() & bitset) != 0) diff --git a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/CtapCertificationId.java b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/CtapCertificationId.java index 0357fcb81..a43815bc6 100644 --- a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/CtapCertificationId.java +++ b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/CtapCertificationId.java @@ -57,7 +57,7 @@ public enum CtapCertificationId { */ FIDO("FIDO"); - @JsonValue private String id; + @JsonValue private final String id; CtapCertificationId(String id) { this.id = id; diff --git a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/CtapPinUvAuthProtocolVersion.java b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/CtapPinUvAuthProtocolVersion.java index 254c2a823..99410961e 100644 --- a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/CtapPinUvAuthProtocolVersion.java +++ b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/CtapPinUvAuthProtocolVersion.java @@ -29,7 +29,7 @@ public enum CtapPinUvAuthProtocolVersion { */ TWO(2); - @JsonValue private int value; + @JsonValue private final int value; CtapPinUvAuthProtocolVersion(int value) { this.value = value; diff --git a/webauthn-server-core/build.gradle.kts b/webauthn-server-core/build.gradle.kts index 9c32eea8c..0872c64c2 100644 --- a/webauthn-server-core/build.gradle.kts +++ b/webauthn-server-core/build.gradle.kts @@ -1,4 +1,5 @@ -import com.yubico.gradle.GitUtils; +import com.yubico.gradle.GitUtils + plugins { `java-library` scala @@ -67,7 +68,7 @@ tasks.jar { "Implementation-Version" to project.version, "Implementation-Vendor" to "Yubico", "Implementation-Source-Url" to "https://github.com/Yubico/java-webauthn-server", - "Git-Commit" to com.yubico.gradle.GitUtils.getGitCommitOrUnknown(projectDir), + "Git-Commit" to GitUtils.getGitCommitOrUnknown(projectDir), )) } } 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 0ba783bf0..41012537a 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 @@ -113,7 +113,7 @@ public static RegisteredCredentialBuilder.MandatoryStages builder() { public static class RegisteredCredentialBuilder { public static class MandatoryStages { - private RegisteredCredentialBuilder builder = new RegisteredCredentialBuilder(); + private final RegisteredCredentialBuilder builder = new RegisteredCredentialBuilder(); /** * {@link RegisteredCredentialBuilder#credentialId(ByteArray) credentialId} is a required 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 afa059e9f..2f8af9741 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 @@ -285,7 +285,7 @@ static RegistrationResultBuilder.MandatoryStages builder() { static class RegistrationResultBuilder { static class MandatoryStages { - private RegistrationResultBuilder builder = new RegistrationResultBuilder(); + private final RegistrationResultBuilder builder = new RegistrationResultBuilder(); Step2 keyId(PublicKeyCredentialDescriptor keyId) { builder.keyId(keyId); diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialCreationOptions.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialCreationOptions.java index 40baa5af2..c1ac9517a 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialCreationOptions.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialCreationOptions.java @@ -238,7 +238,7 @@ public static class PublicKeyCredentialCreationOptionsBuilder { private AuthenticatorSelectionCriteria authenticatorSelection = null; public static class MandatoryStages { - private PublicKeyCredentialCreationOptionsBuilder builder = + private final PublicKeyCredentialCreationOptionsBuilder builder = new PublicKeyCredentialCreationOptionsBuilder(); /** diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialDescriptor.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialDescriptor.java index 87548b099..b2487b5c1 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialDescriptor.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialDescriptor.java @@ -112,7 +112,7 @@ public static class PublicKeyCredentialDescriptorBuilder { private Set transports = null; public static class MandatoryStages { - private PublicKeyCredentialDescriptorBuilder builder = + private final PublicKeyCredentialDescriptorBuilder builder = new PublicKeyCredentialDescriptorBuilder(); /** diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialParameters.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialParameters.java index 5e58aa3ed..e2e59d7b4 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialParameters.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialParameters.java @@ -106,7 +106,7 @@ public static PublicKeyCredentialParametersBuilder.MandatoryStages builder() { public static class PublicKeyCredentialParametersBuilder { public static class MandatoryStages { - private PublicKeyCredentialParametersBuilder builder = + private final PublicKeyCredentialParametersBuilder builder = new PublicKeyCredentialParametersBuilder(); /** diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialRequestOptions.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialRequestOptions.java index d5a8baec9..4834d81a4 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialRequestOptions.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialRequestOptions.java @@ -170,7 +170,7 @@ public static class PublicKeyCredentialRequestOptionsBuilder { private List allowCredentials = null; public static class MandatoryStages { - private PublicKeyCredentialRequestOptionsBuilder builder = + private final PublicKeyCredentialRequestOptionsBuilder builder = new PublicKeyCredentialRequestOptionsBuilder(); /** diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/RelyingPartyIdentity.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/RelyingPartyIdentity.java index d299c3804..7067c135d 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/RelyingPartyIdentity.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/RelyingPartyIdentity.java @@ -79,7 +79,7 @@ public static RelyingPartyIdentityBuilder.MandatoryStages builder() { public static class RelyingPartyIdentityBuilder { public static class MandatoryStages { - private RelyingPartyIdentityBuilder builder = new RelyingPartyIdentityBuilder(); + private final RelyingPartyIdentityBuilder builder = new RelyingPartyIdentityBuilder(); /** * A unique identifier for the Relying Party, which sets the messages) { } private String writeJson(Object o) throws JsonProcessingException { - if (uriInfo.getQueryParameters().keySet().contains("pretty")) { + if (uriInfo.getQueryParameters().containsKey("pretty")) { return jsonMapper.writerWithDefaultPrettyPrinter().writeValueAsString(o); } else { return jsonMapper.writeValueAsString(o); From e3f8fe1913462afac66c5d1b94f73b05c4c943d4 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Wed, 14 Sep 2022 21:14:12 +0200 Subject: [PATCH 132/145] Fix release-verify-signatures workflow --- .github/workflows/release-verify-signatures.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/release-verify-signatures.yml b/.github/workflows/release-verify-signatures.yml index 5b2058bc5..fa1f87eb7 100644 --- a/.github/workflows/release-verify-signatures.yml +++ b/.github/workflows/release-verify-signatures.yml @@ -17,6 +17,8 @@ jobs: steps: - name: check out code uses: actions/checkout@v3 + with: + ref: ${{ github.ref }} - name: Set up JDK uses: actions/setup-java@v3 From b36bdd05b2295fe52527fd1fbb22a8cb0b7969f1 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Wed, 14 Sep 2022 20:49:36 +0200 Subject: [PATCH 133/145] Show distribution name in release-verify-signatures job title --- .github/workflows/release-verify-signatures.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-verify-signatures.yml b/.github/workflows/release-verify-signatures.yml index fa1f87eb7..410f6b84b 100644 --- a/.github/workflows/release-verify-signatures.yml +++ b/.github/workflows/release-verify-signatures.yml @@ -6,7 +6,7 @@ on: jobs: verify: - name: Verify signatures (JDK ${{matrix.java}}) + name: Verify signatures (JDK ${{ matrix.java }} ${{ matrix.distribution }}) runs-on: ubuntu-latest strategy: From b6a8ee4694952652415d91517bd8645c63d764e1 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Wed, 14 Sep 2022 20:49:55 +0200 Subject: [PATCH 134/145] Upgrade Apache httpclient dependency to version 5 --- build.gradle | 2 +- webauthn-server-core/build.gradle.kts | 2 +- .../webauthn/AndroidSafetynetAttestationStatementVerifier.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 4abe16238..9bd30f144 100644 --- a/build.gradle +++ b/build.gradle @@ -54,7 +54,7 @@ dependencies { api('com.fasterxml.jackson.datatype:jackson-datatype-jsr310:[2.13.2,3)') api('com.google.guava:guava:[24.1.1,32)') api('com.upokecenter:cbor:[4.5.1,5)') - api('org.apache.httpcomponents:httpclient:[4.5.2,5)') + api('org.apache.httpcomponents.client5:httpclient5:[5.0.0,6)') api('org.slf4j:slf4j-api:[1.7.25,3)') } } diff --git a/webauthn-server-core/build.gradle.kts b/webauthn-server-core/build.gradle.kts index 0872c64c2..8ddc51f50 100644 --- a/webauthn-server-core/build.gradle.kts +++ b/webauthn-server-core/build.gradle.kts @@ -26,7 +26,7 @@ dependencies { implementation("com.fasterxml.jackson.core:jackson-databind") implementation("com.google.guava:guava") implementation("com.upokecenter:cbor") - implementation("org.apache.httpcomponents:httpclient") + implementation("org.apache.httpcomponents.client5:httpclient5") implementation("org.slf4j:slf4j-api") testImplementation(platform(project(":test-platform"))) diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/AndroidSafetynetAttestationStatementVerifier.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/AndroidSafetynetAttestationStatementVerifier.java index c135374a0..542921101 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/AndroidSafetynetAttestationStatementVerifier.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/AndroidSafetynetAttestationStatementVerifier.java @@ -24,7 +24,7 @@ import javax.net.ssl.SSLException; import lombok.Value; import lombok.extern.slf4j.Slf4j; -import org.apache.http.conn.ssl.DefaultHostnameVerifier; +import org.apache.hc.client5.http.ssl.DefaultHostnameVerifier; @Slf4j class AndroidSafetynetAttestationStatementVerifier From 91a1ffcb69104869d26e6e585029afb88c295b5c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 15 Sep 2022 13:01:55 +0000 Subject: [PATCH 135/145] Bump spotless-plugin-gradle from 6.10.0 to 6.11.0 Bumps [spotless-plugin-gradle](https://github.com/diffplug/spotless) from 6.10.0 to 6.11.0. - [Release notes](https://github.com/diffplug/spotless/releases) - [Changelog](https://github.com/diffplug/spotless/blob/main/CHANGES.md) - [Commits](https://github.com/diffplug/spotless/compare/gradle/6.10.0...gradle/6.11.0) --- updated-dependencies: - dependency-name: com.diffplug.spotless:spotless-plugin-gradle dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index b071b8243..d10aff5a4 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ buildscript { } dependencies { classpath 'com.cinnober.gradle:semver-git:2.5.0' - classpath 'com.diffplug.spotless:spotless-plugin-gradle:6.10.0' + classpath 'com.diffplug.spotless:spotless-plugin-gradle:6.11.0' classpath 'io.github.cosmicsilence:gradle-scalafix:0.1.13' } } From 8cf2b92353a450771515bf64354d00c126e80b6f Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Thu, 15 Sep 2022 16:57:19 +0200 Subject: [PATCH 136/145] Promote signature failure and cache corruption logs from DEBUG to WARN --- NEWS | 2 ++ .../java/com/yubico/fido/metadata/FidoMetadataDownloader.java | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/NEWS b/NEWS index 62d155e89..99e707796 100644 --- a/NEWS +++ b/NEWS @@ -44,6 +44,8 @@ Changes: * The `AuthenticatorToBeFiltered` argument of the `FidoMetadataService` runtime filter now omits zero AAGUIDs. +* Promoted log messages in `FidoMetadataDownloader` about BLOB signature failure + and cache corruption from DEBUG level to WARN level. Fixes: 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 cb76f5b7d..adfc5337e 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 @@ -834,7 +834,7 @@ private Optional refreshBlobInternal( return Optional.of(downloadedBlob); } catch (FidoMetadataDownloaderException e) { if (e.getReason() == Reason.BAD_SIGNATURE && cached.isPresent()) { - log.debug("New BLOB has bad signature - falling back to cached BLOB."); + log.warn("New BLOB has bad signature - falling back to cached BLOB."); return cached; } else { throw e; @@ -954,7 +954,7 @@ private Optional loadCachedBlobOnly(X509Certificate trustRootCerti try { return parseAndVerifyBlob(cached, trustRootCertificate); } catch (Exception e) { - log.debug("Failed to read or parse cached BLOB.", e); + log.warn("Failed to read or parse cached BLOB.", e); return null; } }); From 6d3b5b300b0a9a2714bb19dab871ecb1bcce8806 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Thu, 15 Sep 2022 16:58:00 +0200 Subject: [PATCH 137/145] Fall back to cached BLOB if MDS BLOB download fails --- NEWS | 2 ++ .../fido/metadata/FidoMetadataDownloader.java | 7 +++++ .../metadata/FidoMetadataDownloaderSpec.scala | 30 ++++++++++++------- 3 files changed, 29 insertions(+), 10 deletions(-) diff --git a/NEWS b/NEWS index 99e707796..35b02d215 100644 --- a/NEWS +++ b/NEWS @@ -55,6 +55,8 @@ Fixes: `useTrustRootCache`. * BouncyCastle dependency dropped. * Guava dependency dropped (but still remains in core module). +* If BLOB download fails, `FidoMetadataDownloader` now correctly falls back to + cache if available. == Version 2.0.0 == 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 adfc5337e..fd79fb6ec 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 @@ -839,6 +839,13 @@ private Optional refreshBlobInternal( } else { throw e; } + } catch (Exception e) { + if (cached.isPresent()) { + log.warn("Failed to download new BLOB - falling back to cached BLOB.", e); + return cached; + } else { + throw e; + } } } 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 0d1683de5..eb60e5b52 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 @@ -9,6 +9,7 @@ import com.yubico.webauthn.TestAuthenticator import com.yubico.webauthn.data.ByteArray import com.yubico.webauthn.data.COSEAlgorithmIdentifier import org.bouncycastle.asn1.x500.X500Name +import org.eclipse.jetty.http.HttpStatus import org.eclipse.jetty.server.HttpConfiguration import org.eclipse.jetty.server.HttpConnectionFactory import org.eclipse.jetty.server.Request @@ -198,14 +199,16 @@ class FidoMetadataDownloaderSpec path: String, response: String, ): (Server, String, X509Certificate) = - makeHttpServer(Map(path -> response.getBytes(StandardCharsets.UTF_8))) + makeHttpServer( + Map(path -> (200, response.getBytes(StandardCharsets.UTF_8))) + ) private def makeHttpServer( path: String, response: Array[Byte], ): (Server, String, X509Certificate) = - makeHttpServer(Map(path -> response)) + makeHttpServer(Map(path -> (200, response))) private def makeHttpServer( - responses: Map[String, Array[Byte]] + responses: Map[String, (Int, Array[Byte])] ): (Server, String, X509Certificate) = { val tlsKey = TestAuthenticator.generateEcKeypair() val tlsCert = TestAuthenticator.buildCertificate( @@ -248,9 +251,9 @@ class FidoMetadataDownloaderSpec response: HttpServletResponse, ): Unit = { responses.get(target) match { - case Some(responseBody) => { + case Some((status, responseBody)) => { response.getOutputStream.write(responseBody) - response.setStatus(200) + response.setStatus(status) } case None => response.setStatus(404) } @@ -1062,7 +1065,7 @@ class FidoMetadataDownloaderSpec blob.getNo should equal(blobNo) } - it("The BLOB is downloaded if the cached one is out of date.") { + it("The cache is used if the BLOB download fails.") { val oldBlobNo = 1 val newBlobNo = 2 @@ -1093,7 +1096,12 @@ class FidoMetadataDownloaderSpec ) val (server, serverUrl, httpsCert) = - makeHttpServer("/blob.jwt", newBlobJwt) + makeHttpServer( + Map( + "/blob.jwt" -> (HttpStatus.TOO_MANY_REQUESTS_429, newBlobJwt + .getBytes(StandardCharsets.UTF_8)) + ) + ) startServer(server) val blob = load( @@ -1117,7 +1125,7 @@ class FidoMetadataDownloaderSpec .build() ).getPayload blob should not be null - blob.getNo should equal(newBlobNo) + blob.getNo should equal(oldBlobNo) } } @@ -1152,8 +1160,10 @@ class FidoMetadataDownloaderSpec val (server, _, httpsCert) = makeHttpServer( Map( - "/chain.pem" -> certChainPem.getBytes(StandardCharsets.UTF_8), - "/blob.jwt" -> blobJwt.getBytes(StandardCharsets.UTF_8), + "/chain.pem" -> (200, certChainPem.getBytes( + StandardCharsets.UTF_8 + )), + "/blob.jwt" -> (200, blobJwt.getBytes(StandardCharsets.UTF_8)), ) ) startServer(server) From e330336a65e24959b57c729080ebbc8caee08255 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Fri, 16 Sep 2022 18:21:10 +0200 Subject: [PATCH 138/145] Use github.ref_name instead of GITHUB_REF --- .../workflows/release-verify-signatures.yml | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/.github/workflows/release-verify-signatures.yml b/.github/workflows/release-verify-signatures.yml index 410f6b84b..df03e964c 100644 --- a/.github/workflows/release-verify-signatures.yml +++ b/.github/workflows/release-verify-signatures.yml @@ -18,7 +18,7 @@ jobs: - name: check out code uses: actions/checkout@v3 with: - ref: ${{ github.ref }} + ref: ${{ github.ref_name }} - name: Set up JDK uses: actions/setup-java@v3 @@ -36,20 +36,16 @@ jobs: - name: Verify signatures from GitHub release run: | - export TAGNAME=${GITHUB_REF#refs/tags/} + wget https://github.com/${GITHUB_REPOSITORY}/releases/download/${{ github.ref_name }}/webauthn-server-attestation-${{ github.ref_name }}.jar.asc + wget https://github.com/${GITHUB_REPOSITORY}/releases/download/${{ github.ref_name }}/webauthn-server-core-${{ github.ref_name }}.jar.asc - wget https://github.com/${GITHUB_REPOSITORY}/releases/download/${TAGNAME}/webauthn-server-attestation-${TAGNAME}.jar.asc - wget https://github.com/${GITHUB_REPOSITORY}/releases/download/${TAGNAME}/webauthn-server-core-${TAGNAME}.jar.asc - - gpg --no-default-keyring --keyring yubico --verify webauthn-server-attestation-${TAGNAME}.jar.asc webauthn-server-attestation/build/libs/webauthn-server-attestation-${TAGNAME}.jar - gpg --no-default-keyring --keyring yubico --verify webauthn-server-core-${TAGNAME}.jar.asc webauthn-server-core/build/libs/webauthn-server-core-${TAGNAME}.jar + gpg --no-default-keyring --keyring yubico --verify webauthn-server-attestation-${{ github.ref_name }}.jar.asc webauthn-server-attestation/build/libs/webauthn-server-attestation-${{ github.ref_name }}.jar + gpg --no-default-keyring --keyring yubico --verify webauthn-server-core-${{ github.ref_name }}.jar.asc webauthn-server-core/build/libs/webauthn-server-core-${{ github.ref_name }}.jar - name: Verify signatures from Maven Central run: | - export TAGNAME=${GITHUB_REF#refs/tags/} - - wget -O webauthn-server-core-${TAGNAME}.jar.mavencentral.asc https://repo1.maven.org/maven2/com/yubico/webauthn-server-core/${TAGNAME}/webauthn-server-core-${TAGNAME}.jar.asc - wget -O webauthn-server-attestation-${TAGNAME}.jar.mavencentral.asc https://repo1.maven.org/maven2/com/yubico/webauthn-server-attestation/${TAGNAME}/webauthn-server-attestation-${TAGNAME}.jar.asc + wget -O webauthn-server-core-${{ github.ref_name }}.jar.mavencentral.asc https://repo1.maven.org/maven2/com/yubico/webauthn-server-core/${{ github.ref_name }}/webauthn-server-core-${{ github.ref_name }}.jar.asc + wget -O webauthn-server-attestation-${{ github.ref_name }}.jar.mavencentral.asc https://repo1.maven.org/maven2/com/yubico/webauthn-server-attestation/${{ github.ref_name }}/webauthn-server-attestation-${{ github.ref_name }}.jar.asc - gpg --no-default-keyring --keyring yubico --verify webauthn-server-attestation-${TAGNAME}.jar.mavencentral.asc webauthn-server-attestation/build/libs/webauthn-server-attestation-${TAGNAME}.jar - gpg --no-default-keyring --keyring yubico --verify webauthn-server-core-${TAGNAME}.jar.mavencentral.asc webauthn-server-core/build/libs/webauthn-server-core-${TAGNAME}.jar + gpg --no-default-keyring --keyring yubico --verify webauthn-server-attestation-${{ github.ref_name }}.jar.mavencentral.asc webauthn-server-attestation/build/libs/webauthn-server-attestation-${{ github.ref_name }}.jar + gpg --no-default-keyring --keyring yubico --verify webauthn-server-core-${{ github.ref_name }}.jar.mavencentral.asc webauthn-server-core/build/libs/webauthn-server-core-${{ github.ref_name }}.jar From f57defe4a4fc71d958a310b6bb3d49b33f90160a Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Fri, 16 Sep 2022 18:45:10 +0200 Subject: [PATCH 139/145] Store keyring in current working dir --- .github/workflows/release-verify-signatures.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release-verify-signatures.yml b/.github/workflows/release-verify-signatures.yml index df03e964c..c69c737dd 100644 --- a/.github/workflows/release-verify-signatures.yml +++ b/.github/workflows/release-verify-signatures.yml @@ -32,20 +32,20 @@ jobs: ./gradlew jar - name: Fetch keys - run: gpg --no-default-keyring --keyring yubico --keyserver hkps://keys.openpgp.org --recv-keys 57A9DEED4C6D962A923BB691816F3ED99921835E + run: gpg --no-default-keyring --keyring ./yubico.keyring --keyserver hkps://keys.openpgp.org --recv-keys 57A9DEED4C6D962A923BB691816F3ED99921835E - name: Verify signatures from GitHub release run: | wget https://github.com/${GITHUB_REPOSITORY}/releases/download/${{ github.ref_name }}/webauthn-server-attestation-${{ github.ref_name }}.jar.asc wget https://github.com/${GITHUB_REPOSITORY}/releases/download/${{ github.ref_name }}/webauthn-server-core-${{ github.ref_name }}.jar.asc - gpg --no-default-keyring --keyring yubico --verify webauthn-server-attestation-${{ github.ref_name }}.jar.asc webauthn-server-attestation/build/libs/webauthn-server-attestation-${{ github.ref_name }}.jar - gpg --no-default-keyring --keyring yubico --verify webauthn-server-core-${{ github.ref_name }}.jar.asc webauthn-server-core/build/libs/webauthn-server-core-${{ github.ref_name }}.jar + gpg --no-default-keyring --keyring ./yubico.keyring --verify webauthn-server-attestation-${{ github.ref_name }}.jar.asc webauthn-server-attestation/build/libs/webauthn-server-attestation-${{ github.ref_name }}.jar + gpg --no-default-keyring --keyring ./yubico.keyring --verify webauthn-server-core-${{ github.ref_name }}.jar.asc webauthn-server-core/build/libs/webauthn-server-core-${{ github.ref_name }}.jar - name: Verify signatures from Maven Central run: | wget -O webauthn-server-core-${{ github.ref_name }}.jar.mavencentral.asc https://repo1.maven.org/maven2/com/yubico/webauthn-server-core/${{ github.ref_name }}/webauthn-server-core-${{ github.ref_name }}.jar.asc wget -O webauthn-server-attestation-${{ github.ref_name }}.jar.mavencentral.asc https://repo1.maven.org/maven2/com/yubico/webauthn-server-attestation/${{ github.ref_name }}/webauthn-server-attestation-${{ github.ref_name }}.jar.asc - gpg --no-default-keyring --keyring yubico --verify webauthn-server-attestation-${{ github.ref_name }}.jar.mavencentral.asc webauthn-server-attestation/build/libs/webauthn-server-attestation-${{ github.ref_name }}.jar - gpg --no-default-keyring --keyring yubico --verify webauthn-server-core-${{ github.ref_name }}.jar.mavencentral.asc webauthn-server-core/build/libs/webauthn-server-core-${{ github.ref_name }}.jar + gpg --no-default-keyring --keyring ./yubico.keyring --verify webauthn-server-attestation-${{ github.ref_name }}.jar.mavencentral.asc webauthn-server-attestation/build/libs/webauthn-server-attestation-${{ github.ref_name }}.jar + gpg --no-default-keyring --keyring ./yubico.keyring --verify webauthn-server-core-${{ github.ref_name }}.jar.mavencentral.asc webauthn-server-core/build/libs/webauthn-server-core-${{ github.ref_name }}.jar From b1a8f60c718a651478a4a350a7ff8475f104b48b Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Fri, 16 Sep 2022 18:45:50 +0200 Subject: [PATCH 140/145] Trigger signature verification workflow on fewer events --- .github/workflows/release-verify-signatures.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-verify-signatures.yml b/.github/workflows/release-verify-signatures.yml index c69c737dd..9a2378272 100644 --- a/.github/workflows/release-verify-signatures.yml +++ b/.github/workflows/release-verify-signatures.yml @@ -2,7 +2,7 @@ name: Reproducible binary on: release: - types: [published, created, edited, prereleased] + types: [published, edited] jobs: verify: From 499e1857bd7fefc9e50f53637fa81aa4c08e5f23 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Fri, 16 Sep 2022 18:56:14 +0200 Subject: [PATCH 141/145] Upload release signatures to GitHub automatically --- .../workflows/release-verify-signatures.yml | 65 +++++++++++++++---- build.gradle | 14 ---- doc/releasing.md | 37 ++++------- 3 files changed, 65 insertions(+), 51 deletions(-) diff --git a/.github/workflows/release-verify-signatures.yml b/.github/workflows/release-verify-signatures.yml index 9a2378272..23b7dac62 100644 --- a/.github/workflows/release-verify-signatures.yml +++ b/.github/workflows/release-verify-signatures.yml @@ -1,14 +1,42 @@ name: Reproducible binary +# This workflow waits for release signatures to appear on Maven Central, +# then rebuilds the artifacts and verifies them against those signatures, +# and finally uploads the signatures to the GitHub release. + on: release: types: [published, edited] jobs: + download: + name: Download keys and signatures + runs-on: ubuntu-latest + + steps: + - name: Fetch keys + run: gpg --no-default-keyring --keyring ./yubico.keyring --keyserver hkps://keys.openpgp.org --recv-keys 57A9DEED4C6D962A923BB691816F3ED99921835E + + - name: Download signatures from Maven Central + timeout-minutes: 60 + run: | + until wget https://repo1.maven.org/maven2/com/yubico/webauthn-server-attestation/${{ github.ref_name }}/webauthn-server-attestation-${{ github.ref_name }}.jar.asc; do sleep 180; done + until wget https://repo1.maven.org/maven2/com/yubico/webauthn-server-core/${{ github.ref_name }}/webauthn-server-core-${{ github.ref_name }}.jar.asc; do sleep 180; done + + - name: Store keyring and signatures as artifact + uses: actions/upload-artifact@v3 + with: + name: keyring-and-signatures + retention-days: 1 + path: | + yubico.keyring + *.jar.asc + verify: name: Verify signatures (JDK ${{ matrix.java }} ${{ matrix.distribution }}) - + needs: download runs-on: ubuntu-latest + strategy: matrix: java: [17] @@ -31,21 +59,34 @@ jobs: java --version ./gradlew jar - - name: Fetch keys - run: gpg --no-default-keyring --keyring ./yubico.keyring --keyserver hkps://keys.openpgp.org --recv-keys 57A9DEED4C6D962A923BB691816F3ED99921835E + - name: Retrieve keyring and signatures + uses: actions/download-artifact@v3 + with: + name: keyring-and-signatures - - name: Verify signatures from GitHub release + - name: Verify signatures from Maven Central run: | - wget https://github.com/${GITHUB_REPOSITORY}/releases/download/${{ github.ref_name }}/webauthn-server-attestation-${{ github.ref_name }}.jar.asc - wget https://github.com/${GITHUB_REPOSITORY}/releases/download/${{ github.ref_name }}/webauthn-server-core-${{ github.ref_name }}.jar.asc - gpg --no-default-keyring --keyring ./yubico.keyring --verify webauthn-server-attestation-${{ github.ref_name }}.jar.asc webauthn-server-attestation/build/libs/webauthn-server-attestation-${{ github.ref_name }}.jar gpg --no-default-keyring --keyring ./yubico.keyring --verify webauthn-server-core-${{ github.ref_name }}.jar.asc webauthn-server-core/build/libs/webauthn-server-core-${{ github.ref_name }}.jar - - name: Verify signatures from Maven Central + upload: + name: Upload signatures to GitHub + needs: verify + runs-on: ubuntu-latest + + permissions: + contents: write # Allow uploading release artifacts + + steps: + - name: Retrieve signatures + uses: actions/download-artifact@v3 + with: + name: keyring-and-signatures + + - name: Upload signatures to GitHub run: | - wget -O webauthn-server-core-${{ github.ref_name }}.jar.mavencentral.asc https://repo1.maven.org/maven2/com/yubico/webauthn-server-core/${{ github.ref_name }}/webauthn-server-core-${{ github.ref_name }}.jar.asc - wget -O webauthn-server-attestation-${{ github.ref_name }}.jar.mavencentral.asc https://repo1.maven.org/maven2/com/yubico/webauthn-server-attestation/${{ github.ref_name }}/webauthn-server-attestation-${{ github.ref_name }}.jar.asc + RELEASE_DATA=$(curl -H "Authorization: Bearer ${{ github.token }}" ${{ github.api_url }}/repos/${{ github.repository }}/releases/tags/${{ github.ref_name }}) + UPLOAD_URL=$(jq -r .upload_url <<<"${RELEASE_DATA}" | sed 's/{?name,label}//') - gpg --no-default-keyring --keyring ./yubico.keyring --verify webauthn-server-attestation-${{ github.ref_name }}.jar.mavencentral.asc webauthn-server-attestation/build/libs/webauthn-server-attestation-${{ github.ref_name }}.jar - gpg --no-default-keyring --keyring ./yubico.keyring --verify webauthn-server-core-${{ github.ref_name }}.jar.mavencentral.asc webauthn-server-core/build/libs/webauthn-server-core-${{ github.ref_name }}.jar + curl -X POST -H "Authorization: Bearer ${{ github.token }}" -H 'Content-Type: text/plain' --data-binary @webauthn-server-attestation-${{ github.ref_name }}.jar.asc "${UPLOAD_URL}?name=webauthn-server-attestation-${{ github.ref_name }}.jar.asc" + curl -X POST -H "Authorization: Bearer ${{ github.token }}" -H 'Content-Type: text/plain' --data-binary @webauthn-server-core-${{ github.ref_name }}.jar.asc "${UPLOAD_URL}?name=webauthn-server-core-${{ github.ref_name }}.jar.asc" diff --git a/build.gradle b/build.gradle index 9bd30f144..adb65c4d6 100644 --- a/build.gradle +++ b/build.gradle @@ -116,12 +116,6 @@ task assembleJavadoc(type: Sync) { destinationDir = file("${rootProject.buildDir}/javadoc") } -task collectSignatures(type: Sync) { - destinationDir = file("${rootProject.buildDir}/dist") - duplicatesStrategy DuplicatesStrategy.FAIL - include '*.jar', '*.jar.asc' -} - subprojects { project -> if (project.plugins.hasPlugin('scala')) { @@ -247,14 +241,6 @@ subprojects { project -> useGpgCmd() sign publishing.publications.jars } - - tasks.withType(Sign) { Sign signTask -> - rootProject.tasks.collectSignatures { - from signTask.inputs.files - from signTask.outputs.files - } - signTask.finalizedBy rootProject.tasks.collectSignatures - } } } diff --git a/doc/releasing.md b/doc/releasing.md index fbcb7a912..29d7e0ce1 100644 --- a/doc/releasing.md +++ b/doc/releasing.md @@ -28,13 +28,7 @@ Release candidate versions $ ./gradlew publishToSonatype closeAndReleaseSonatypeStagingRepository ``` - 6. Wait for the artifacts to become downloadable at - https://repo1.maven.org/maven2/com/yubico/webauthn-server-core/ . This is - needed for one of the GitHub Actions release workflows and usually takes - less than 30 minutes (long before the artifacts become searchable on the - main Maven Central website). - - 7. 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 @@ -52,19 +46,19 @@ Release candidate versions $ git push origin main 1.4.0-RC1 ``` - 8. Make GitHub release. + 7. Make GitHub release. - Use the new tag as the release tag - Check the pre-release checkbox - Copy the release notes from `NEWS` into the GitHub release notes; reformat from ASCIIdoc to Markdown and remove line wraps. Include only changes/additions since the previous release or pre-release. - - Attach the signature files from - `build/dist/webauthn-server-attestation-X.Y.Z-RCN.jar.asc` - and - `build/dist/webauthn-server-core-X.Y.Z-RCN.jar.asc`. - Note which JDK version was used to build the artifacts. + 8. Check that the ["Reproducible binary" + workflow](/Yubico/java-webauthn-server/actions/workflows/release-verify-signatures.yml) + runs and succeeds. + Release versions --- @@ -128,27 +122,20 @@ Release versions $ ./gradlew publishToSonatype closeAndReleaseSonatypeStagingRepository ``` -11. Wait for the artifacts to become downloadable at - https://repo1.maven.org/maven2/com/yubico/webauthn-server-core/ . This is - needed for one of the GitHub Actions release workflows and usually takes - less than 30 minutes (long before the artifacts become searchable on the - main Maven Central website). - -12. Push to GitHub: +11. Push to GitHub: ``` $ git push origin main 1.4.0 ``` -13. Make GitHub release. +12. Make GitHub release. - Use the new tag as the release tag - Copy the release notes from `NEWS` into the GitHub release notes; reformat from ASCIIdoc to Markdown and remove line wraps. Include all changes since the previous release (not just changes since the previous pre-release). - - Attach the signature files from - `build/dist/webauthn-server-attestation-X.Y.Z.jar.asc` - and - `build/dist/webauthn-server-core-X.Y.Z.jar.asc`. - - Note which JDK version was used to build the artifacts. + +13. Check that the ["Reproducible binary" + workflow](/Yubico/java-webauthn-server/actions/workflows/release-verify-signatures.yml) + runs and succeeds. From 16d7066c1b1b73e0dae2081a3ed295b06ba2529b Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Fri, 16 Sep 2022 19:28:57 +0200 Subject: [PATCH 142/145] Check JDK version before building release --- build.gradle | 19 +++++++++++++++---- doc/releasing.md | 38 +++++++++++++++++--------------------- 2 files changed, 32 insertions(+), 25 deletions(-) diff --git a/build.gradle b/build.gradle index adb65c4d6..d9f83cfb8 100644 --- a/build.gradle +++ b/build.gradle @@ -116,6 +116,14 @@ task assembleJavadoc(type: Sync) { destinationDir = file("${rootProject.buildDir}/javadoc") } +task checkJavaVersionBeforeRelease { + doFirst { + if (JavaVersion.current() != JavaVersion.VERSION_17) { + throw new RuntimeException('Release must be built using JDK 17. Current JDK version: ' + JavaVersion.current()) + } + } +} + subprojects { project -> if (project.plugins.hasPlugin('scala')) { @@ -148,16 +156,19 @@ subprojects { project -> reproducibleFileOrder = true } - tasks.withType(Sign) { - it.dependsOn check - } - tasks.withType(AbstractTestTask) { testLogging { showStandardStreams = isCiBuild } } + tasks.withType(AbstractCompile) { shouldRunAfter checkJavaVersionBeforeRelease } + tasks.withType(AbstractTestTask) { shouldRunAfter checkJavaVersionBeforeRelease } + tasks.withType(Sign) { + it.dependsOn check + dependsOn checkJavaVersionBeforeRelease + } + if (project.hasProperty('publishMe') && project.publishMe) { task sourcesJar(type: Jar) { archiveClassifier = 'sources' diff --git a/doc/releasing.md b/doc/releasing.md index 29d7e0ce1..4d44edf84 100644 --- a/doc/releasing.md +++ b/doc/releasing.md @@ -6,15 +6,13 @@ Release candidate versions 1. Make sure release notes in `NEWS` are up to date. - 2. Make sure you're running Gradle in JDK 17. - - 3. Run the tests one more time: + 2. Run the tests one more time: ``` $ ./gradlew clean check ``` - 4. Tag the head commit with an `X.Y.Z-RCN` tag: + 3. 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" @@ -22,13 +20,13 @@ Release candidate versions No tag body needed. - 5. Publish to Sonatype Nexus: + 4. Publish to Sonatype Nexus: ``` $ ./gradlew publishToSonatype closeAndReleaseSonatypeStagingRepository ``` - 6. Push to GitHub. + 5. 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 @@ -46,7 +44,7 @@ Release candidate versions $ git push origin main 1.4.0-RC1 ``` - 7. Make GitHub release. + 6. Make GitHub release. - Use the new tag as the release tag - Check the pre-release checkbox @@ -55,7 +53,7 @@ Release candidate versions changes/additions since the previous release or pre-release. - Note which JDK version was used to build the artifacts. - 8. Check that the ["Reproducible binary" + 7. Check that the ["Reproducible binary" workflow](/Yubico/java-webauthn-server/actions/workflows/release-verify-signatures.yml) runs and succeeds. @@ -65,9 +63,7 @@ Release versions 1. Make sure release notes in `NEWS` are up to date. - 2. Make sure you're running Gradle in JDK 17. - - 3. Make a no-fast-forward merge from the last (non release candidate) release + 2. Make a no-fast-forward merge from the last (non release candidate) release to the commit to be released: ``` @@ -89,26 +85,26 @@ Release versions $ git branch -d release-1.4.0 ``` - 4. Remove the "(unreleased)" tag from `NEWS`. + 3. Remove the "(unreleased)" tag from `NEWS`. - 5. Update the version in the dependency snippets in the README. + 4. Update the version in the dependency snippets in the README. - 6. Update the version in JavaDoc links in the READMEs. + 5. Update the version in JavaDoc links in the READMEs. - 7. Amend these changes into the merge commit: + 6. Amend these changes into the merge commit: ``` $ git add NEWS $ git commit --amend --reset-author ``` - 8. Run the tests one more time: + 7. Run the tests one more time: ``` $ ./gradlew clean check ``` - 9. Tag the merge commit with an `X.Y.Z` tag: + 8. Tag the merge commit with an `X.Y.Z` tag: ``` $ git tag -a -s 1.4.0 -m "Release 1.4.0" @@ -116,19 +112,19 @@ Release versions No tag body needed since that's included in the commit. -10. Publish to Sonatype Nexus: + 9. Publish to Sonatype Nexus: ``` $ ./gradlew publishToSonatype closeAndReleaseSonatypeStagingRepository ``` -11. Push to GitHub: +10. Push to GitHub: ``` $ git push origin main 1.4.0 ``` -12. Make GitHub release. +11. Make GitHub release. - Use the new tag as the release tag - Copy the release notes from `NEWS` into the GitHub release notes; reformat @@ -136,6 +132,6 @@ Release versions the previous release (not just changes since the previous pre-release). - Note which JDK version was used to build the artifacts. -13. Check that the ["Reproducible binary" +12. Check that the ["Reproducible binary" workflow](/Yubico/java-webauthn-server/actions/workflows/release-verify-signatures.yml) runs and succeeds. From 9f3a2631759d236191041fe6765578eda6e55bcb Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Fri, 16 Sep 2022 22:06:30 +0200 Subject: [PATCH 143/145] Fix leading zeroes when negating Y coordinate of EC key --- .../webauthn/RelyingPartyRegistrationSpec.scala | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) 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 2e9698801..337e4aaeb 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 @@ -2343,13 +2343,16 @@ class RelyingPartyRegistrationSpec ) .getBytes ) + val yneg = TestAuthenticator.Es256PrimeModulus + .subtract( + new BigInteger(1, cose.get(-3).GetByteString()) + ) + val ynegBytes = yneg.toByteArray.dropWhile(_ == 0) cose.Set( -3, - TestAuthenticator.Es256PrimeModulus - .subtract( - new BigInteger(1, cose.get(-3).GetByteString()) - ), // Setting to BigInteger seems to work, but Array[Byte] does not + Array.fill[Byte](32 - ynegBytes.length)(0) ++ ynegBytes, ) + val testData = (RegistrationTestData.from _).tupled( makeCred( authDataAndKeypair = Some((authData, keypair)), From 9ed156e82a7caed02277e9ff42bb1ffb76bbe9cc Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Fri, 16 Sep 2022 22:06:49 +0200 Subject: [PATCH 144/145] Remove unnecessary Bytes.concat calls --- .../main/java/com/yubico/webauthn/WebAuthnCodecs.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/WebAuthnCodecs.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/WebAuthnCodecs.java index 5779a8451..b5f8e079d 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 @@ -64,11 +64,10 @@ static ByteArray ecPublicKeyToRaw(ECPublicKey key) { return new ByteArray( Bytes.concat( new byte[] {0x04}, - Bytes.concat( - xPadding, Arrays.copyOfRange(x, Math.max(0, x.length - fieldSizeBytes), x.length)), - Bytes.concat( - yPadding, - Arrays.copyOfRange(y, Math.max(0, y.length - fieldSizeBytes), y.length)))); + xPadding, + Arrays.copyOfRange(x, Math.max(0, x.length - fieldSizeBytes), x.length), + yPadding, + Arrays.copyOfRange(y, Math.max(0, y.length - fieldSizeBytes), y.length))); } static ByteArray rawEcKeyToCose(ByteArray key) { From 85a4148e0e5231428611a9a5c6192fcc65c7e1d8 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 11 Oct 2022 20:21:37 +0200 Subject: [PATCH 145/145] Delombok TrustRootsResult builder JavaDoc As of version 1.18.24, Lombok fails to copy JavaDoc from the source definition to generated methods in a static inner class. --- NEWS | 1 + .../attestation/AttestationTrustSource.java | 94 +++++++++++++++++++ 2 files changed, 95 insertions(+) diff --git a/NEWS b/NEWS index 35b02d215..6f2d22502 100644 --- a/NEWS +++ b/NEWS @@ -36,6 +36,7 @@ Fixes: * Moved version constraints for test dependencies from meta-module `webauthn-server-parent` to unpublished test meta-module. * `yubico-util` dependency removed from downstream compile scope. +* Fixed missing JavaDoc on `TrustRootsResult` getters and builder setters. `webauthn-server-attestation`: diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/attestation/AttestationTrustSource.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/attestation/AttestationTrustSource.java index b18264602..290437efc 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/attestation/AttestationTrustSource.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/attestation/AttestationTrustSource.java @@ -198,6 +198,100 @@ public TrustRootsResultBuilder trustRoots(@NonNull Set trustRoo return new TrustRootsResultBuilder().trustRoots(trustRoots); } } + + /** + * A set of attestation root certificates trusted to certify the relevant attestation + * statement. If the attestation statement is not trusted, or if no trust roots were found, + * this should be an empty set. + */ + // TODO: Let this auto-generate (investigate why Lombok fails to copy javadoc) + public AttestationTrustSource.TrustRootsResult.TrustRootsResultBuilder trustRoots( + @NonNull final Set trustRoots) { + if (trustRoots == null) { + throw new java.lang.NullPointerException("trustRoots is marked non-null but is null"); + } + this.trustRoots = trustRoots; + return this; + } + + /** + * A {@link CertStore} of additional CRLs and/or intermediate certificates to use during + * certificate path validation, if any. This will not be used if {@link + * TrustRootsResultBuilder#trustRoots(Set) trustRoots} is empty. + * + *

    Any certificates included in this {@link CertStore} are NOT considered trusted; they + * will be trusted only if they chain to any of the {@link + * TrustRootsResultBuilder#trustRoots(Set) trustRoots}. + * + *

    The default is null. + */ + // TODO: Let this auto-generate (investigate why Lombok fails to copy javadoc) + public AttestationTrustSource.TrustRootsResult.TrustRootsResultBuilder certStore( + final CertStore certStore) { + this.certStore$value = certStore; + certStore$set = true; + return this; + } + + /** + * Whether certificate revocation should be checked during certificate path validation. + * + *

    The default is true. + */ + // TODO: Let this auto-generate (investigate why Lombok fails to copy javadoc) + public AttestationTrustSource.TrustRootsResult.TrustRootsResultBuilder + enableRevocationChecking(final boolean enableRevocationChecking) { + this.enableRevocationChecking$value = enableRevocationChecking; + enableRevocationChecking$set = true; + return this; + } + + /** + * If non-null, the PolicyQualifiersRejected flag will be set to false during certificate path + * validation. See {@link + * java.security.cert.PKIXParameters#setPolicyQualifiersRejected(boolean)}. + * + *

    The given {@link Predicate} will be used to validate the policy tree. The {@link + * Predicate} should return true if the policy tree is acceptable, and + * false + * otherwise. + * + *

    Depending on your "PKIX" JCA provider configuration, this may be required + * if any certificate in the certificate path contains a certificate policies extension marked + * critical. If this is not set, then such a certificate will be rejected by the certificate + * path validator from the default provider. + * + *

    Consult the Java + * PKI Programmer's Guide for how to use the {@link PolicyNode} argument of the {@link + * Predicate}. + * + *

    The default is null. + */ + // TODO: Let this auto-generate (investigate why Lombok fails to copy javadoc) + public AttestationTrustSource.TrustRootsResult.TrustRootsResultBuilder policyTreeValidator( + final Predicate policyTreeValidator) { + this.policyTreeValidator$value = policyTreeValidator; + policyTreeValidator$set = true; + return this; + } + } + + /** + * A set of attestation root certificates trusted to certify the relevant attestation statement. + * If the attestation statement is not trusted, or if no trust roots were found, this should be + * an empty set. + */ + // TODO: Let this auto-generate (investigate why Lombok fails to copy javadoc) + @NonNull + public Set getTrustRoots() { + return this.trustRoots; + } + + /** Whether certificate revocation should be checked during certificate path validation. */ + // TODO: Let this auto-generate (investigate why Lombok fails to copy javadoc) + public boolean isEnableRevocationChecking() { + return this.enableRevocationChecking; } } }