diff --git a/.github/actions/pit-results-comment/action.yml b/.github/actions/pit-results-comment/action.yml index 4f7b25364..2f2ca22b4 100644 --- a/.github/actions/pit-results-comment/action.yml +++ b/.github/actions/pit-results-comment/action.yml @@ -49,7 +49,7 @@ runs: 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}" + ./.github/actions/pit-results-comment/stats-to-comment.sh "${PREV_STATS_FILE}" "${NEW_STATS_FILE}" "${{ inputs.prev-commit }}" "${{ github.sha }}" > "${RESULTS_COMMENT_FILE}" curl -X POST \ -H "Authorization: Bearer ${{ inputs.token }}" \ diff --git a/.github/actions/pit-results-comment/stats-to-comment.sh b/.github/actions/pit-results-comment/stats-to-comment.sh index d96ab0a33..143c43f55 100755 --- a/.github/actions/pit-results-comment/stats-to-comment.sh +++ b/.github/actions/pit-results-comment/stats-to-comment.sh @@ -63,10 +63,17 @@ EOF "${1}" "${2}" --raw-output if [[ -n "${3}" ]]; then - cat << EOF + if [[ -n "${4}" ]]; then + cat << EOF + +Previous run: ${3} - [Diff](/${GITHUB_REPOSITORY}/compare/${3}...${4}) +EOF + else + cat << EOF Previous run: ${3} EOF + fi cat << EOF diff --git a/NEWS b/NEWS index 9c3271281..874fefb0f 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,46 @@ +== Version 2.4.0 == + +`webauthn-server-core`: + +New features: + +* Added support for RS384 and RS512 signature algorithms. + ** Thanks to GitHub user JohnnyJayJay for the contribution, see + https://github.com/Yubico/java-webauthn-server/pull/235 +* Added `userHandle` field to `AssertionRequest` as part of the second bug fix + below. `userHandle` is mutually exclusive with `username`. This was originally + released in pre-release `1.12.3-RC3`, but was accidentally left out of the + `1.12.3` release. + +Fixes: + +* During `RelyingParty.finishRegistration()` if an `attestationTrustSource` is + configured, if the `aaguid` in the authenticator data is zero, the call to + `AttestationTrustSource.findTrustRoots` will fall back to reading the AAGUID + from the attestation certificate if possible. +* Fixed bug in `RelyingParty.finishAssertion` where if + `StartAssertionOptions.userHandle` was set, it did not propagate to + `RelyingParty.finishAssertion` and caused an error saying username and user + handle are both absent unless a user handle was returned by the authenticator. + This was originally released in pre-release `1.12.3-RC3`, but was accidentally + left out of the `1.12.3` release. +* Fixed regression in + `PublicKeyCredentialCreationOptions.toCredentialsCreateJson()`, which has not + been emitting a `requireResidentKey` member since version `2.0.0`. This meant + the JSON output was not backwards compatible with browsers that only support + the Level 1 version of the WebAuthn spec. + + +`webauthn-server-attestation`: + +Fixes: + +* `findEntries` and `findTrustRoots` methods in `FidoMetadataService` now + attempt to read AAGUID from the attestation certificate if the `aaguid` + argument is absent or zero. +* Method `FidoMetadataService.Filters.allOf` now has `@SafeVarargs` annotation. + + == Version 2.3.0 == New features: diff --git a/README b/README index 940d4fc69..7f3c4d4d0 100644 --- a/README +++ b/README @@ -64,7 +64,7 @@ Maven: com.yubico webauthn-server-core - 2.3.0 + 2.4.0 compile ---------- @@ -72,7 +72,7 @@ Maven: Gradle: ---------- -compile 'com.yubico:webauthn-server-core:2.3.0' +compile 'com.yubico:webauthn-server-core:2.4.0' ---------- NOTE: You may need additional dependencies with JCA providers to support some signature algorithms. @@ -85,7 +85,7 @@ The library will log warnings if you try to configure it for algorithms with no This library uses link:https://semver.org/[semantic versioning]. The public API consists of all public classes, methods and fields in the `com.yubico.webauthn` package and its subpackages, i.e., everything covered by the -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.3.0/com/yubico/webauthn/package-summary.html[Javadoc], +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.0/com/yubico/webauthn/package-summary.html[Javadoc], *with the exception* of features annotated with a `@Deprecated` annotation and a `@deprecated EXPERIMENTAL:` tag in JavaDoc. Such features are considered unstable and may receive breaking changes without a @@ -108,7 +108,7 @@ In addition to the main `webauthn-server-core` module, there is also: == Documentation See the -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.3.0/com/yubico/webauthn/package-summary.html[Javadoc] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.0/com/yubico/webauthn/package-summary.html[Javadoc] for in-depth API documentation. @@ -118,20 +118,20 @@ Using this library comes in two parts: the server side and the client side. The server side involves: 1. Implement the - link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.3.0/com/yubico/webauthn/CredentialRepository.html[`CredentialRepository`] + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.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.3.0/com/yubico/webauthn/RelyingParty.html[`RelyingParty`] + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.0/com/yubico/webauthn/RelyingParty.html[`RelyingParty`] class. 3. Use the - link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.3.0/com/yubico/webauthn/RelyingParty.html#startRegistration(com.yubico.webauthn.StartRegistrationOptions)[`RelyingParty.startRegistration(...)`] + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.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.3.0/com/yubico/webauthn/RelyingParty.html#finishRegistration(com.yubico.webauthn.FinishRegistrationOptions)[`RelyingParty.finishRegistration(...)`] + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.0/com/yubico/webauthn/RelyingParty.html#finishRegistration(com.yubico.webauthn.FinishRegistrationOptions)[`RelyingParty.finishRegistration(...)`] methods to perform registration ceremonies. 4. Use the - link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.3.0/com/yubico/webauthn/RelyingParty.html#startAssertion(com.yubico.webauthn.StartAssertionOptions)[`RelyingParty.startAssertion(...)`] + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.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.3.0/com/yubico/webauthn/RelyingParty.html#finishAssertion(com.yubico.webauthn.FinishAssertionOptions)[`RelyingParty.finishAssertion(...)`] + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.0/com/yubico/webauthn/RelyingParty.html#finishAssertion(com.yubico.webauthn.FinishAssertionOptions)[`RelyingParty.finishAssertion(...)`] methods to perform authentication ceremonies. 5. Use the outputs of `finishRegistration` and `finishAssertion` to update your database, initiate sessions, etc. @@ -151,7 +151,7 @@ link:webauthn-server-demo[`webauthn-server-demo`] for a complete demo server. === 1. Implement a `CredentialRepository` The -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.3.0/com/yubico/webauthn/CredentialRepository.html[`CredentialRepository`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.0/com/yubico/webauthn/CredentialRepository.html[`CredentialRepository`] interface abstracts your database in a database-agnostic way. The concrete implementation will be different for every project, but you can use link:https://github.com/Yubico/java-webauthn-server/blob/main/webauthn-server-demo/src/main/java/demo/webauthn/InMemoryRegistrationStorage.java[`InMemoryRegistrationStorage`] @@ -160,11 +160,11 @@ as a simple example. === 2. Instantiate a `RelyingParty` The -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.3.0/com/yubico/webauthn/RelyingParty.html[`RelyingParty`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.0/com/yubico/webauthn/RelyingParty.html[`RelyingParty`] class is the main entry point to the library. You can instantiate it using its builder methods, passing in your -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.3.0/com/yubico/webauthn/CredentialRepository.html[`CredentialRepository`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.0/com/yubico/webauthn/CredentialRepository.html[`CredentialRepository`] implementation (called `MyCredentialRepository` here) as an argument: [source,java] @@ -186,7 +186,7 @@ RelyingParty rp = RelyingParty.builder() A registration ceremony consists of 5 main steps: 1. Generate registration parameters using - link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.3.0/com/yubico/webauthn/RelyingParty.html#startRegistration(com.yubico.webauthn.StartRegistrationOptions)[`RelyingParty.startRegistration(...)`]. + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.0/com/yubico/webauthn/RelyingParty.html#startRegistration(com.yubico.webauthn.StartRegistrationOptions)[`RelyingParty.startRegistration(...)`]. 2. Send registration parameters to the client and call https://developer.mozilla.org/en-US/docs/Web/API/CredentialsContainer/create[`navigator.credentials.create()`]. 3. With `cred` as the result of the successfully resolved promise, @@ -194,7 +194,7 @@ A registration ceremony consists of 5 main steps: and https://www.w3.org/TR/webauthn-2/#ref-for-dom-authenticatorattestationresponse-gettransports[`cred.response.getTransports()`] and return their results along with `cred` to the server. 4. Validate the response using - link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.3.0/com/yubico/webauthn/RelyingParty.html#finishRegistration(com.yubico.webauthn.FinishRegistrationOptions)[`RelyingParty.finishRegistration(...)`]. + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.0/com/yubico/webauthn/RelyingParty.html#finishRegistration(com.yubico.webauthn.FinishRegistrationOptions)[`RelyingParty.finishRegistration(...)`]. 5. Update your database using the `finishRegistration` output. This example uses GitHub's link:https://github.com/github/webauthn-json[webauthn-json] library to do both (2) and (3) in one function call. @@ -226,15 +226,15 @@ return credentialCreateJson; // Send to client ---------- You will need to keep this -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.3.0/com/yubico/webauthn/data/PublicKeyCredentialCreationOptions.html[`PublicKeyCredentialCreationOptions`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.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.3.0/com/yubico/webauthn/RelyingParty.html#finishRegistration(com.yubico.webauthn.FinishRegistrationOptions)[`RelyingParty.finishRegistration(...)`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.0/com/yubico/webauthn/RelyingParty.html#finishRegistration(com.yubico.webauthn.FinishRegistrationOptions)[`RelyingParty.finishRegistration(...)`] later. If needed, you can use the -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.3.0/com/yubico/webauthn/data/PublicKeyCredentialCreationOptions.html#toJson()[`toJson()`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.0/com/yubico/webauthn/data/PublicKeyCredentialCreationOptions.html#toJson()[`toJson()`] and -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.3.0/com/yubico/webauthn/data/PublicKeyCredentialCreationOptions.html#fromJson(java.lang.String)[`fromJson(String)`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.0/com/yubico/webauthn/data/PublicKeyCredentialCreationOptions.html#fromJson(java.lang.String)[`fromJson(String)`] methods to serialize and deserialize the value for storage. Now call the WebAuthn API on the client side: @@ -292,7 +292,7 @@ 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 - link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.3.0/com/yubico/webauthn/RelyingParty.html#startAssertion(com.yubico.webauthn.StartAssertionOptions)[`RelyingParty.startAssertion(...)`]. + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.0/com/yubico/webauthn/RelyingParty.html#startAssertion(com.yubico.webauthn.StartAssertionOptions)[`RelyingParty.startAssertion(...)`]. 2. Send authentication parameters to the client, call https://developer.mozilla.org/en-US/docs/Web/API/CredentialsContainer/get[`navigator.credentials.get()`] and return the response. @@ -300,7 +300,7 @@ Like registration ceremonies, an authentication ceremony consists of 5 main step https://www.w3.org/TR/webauthn-2/#ref-for-dom-publickeycredential-getclientextensionresults[`cred.getClientExtensionResults()`] and return the result along with `cred` to the server. 4. Validate the response using - link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.3.0/com/yubico/webauthn/RelyingParty.html#finishAssertion(com.yubico.webauthn.FinishAssertionOptions)[`RelyingParty.finishAssertion(...)`]. + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.0/com/yubico/webauthn/RelyingParty.html#finishAssertion(com.yubico.webauthn.FinishAssertionOptions)[`RelyingParty.finishAssertion(...)`]. 5. Update your database using the `finishAssertion` output, and act upon the result (for example, grant login access). This example uses GitHub's link:https://github.com/github/webauthn-json[webauthn-json] library to do both (2) and (3) in one function call. @@ -317,15 +317,15 @@ return credentialGetJson; // Send to client ---------- Again, you will need to keep this -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.3.0/com/yubico/webauthn/AssertionRequest.html[`AssertionRequest`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.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.3.0/com/yubico/webauthn/RelyingParty.html#finishAssertion(com.yubico.webauthn.FinishAssertionOptions)[`RelyingParty.finishAssertion(...)`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.0/com/yubico/webauthn/RelyingParty.html#finishAssertion(com.yubico.webauthn.FinishAssertionOptions)[`RelyingParty.finishAssertion(...)`] later. If needed, you can use the -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.3.0/com/yubico/webauthn/AssertionRequest.html#toJson()[`toJson()`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.0/com/yubico/webauthn/AssertionRequest.html#toJson()[`toJson()`] and -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.3.0/com/yubico/webauthn/AssertionRequest.html#fromJson(java.lang.String)[`fromJson(String)`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.0/com/yubico/webauthn/AssertionRequest.html#fromJson(java.lang.String)[`fromJson(String)`] methods to serialize and deserialize the value for storage. Now call the WebAuthn API on the client side: @@ -366,7 +366,7 @@ throw new RuntimeException("Authentication failed"); ---------- Finally, if the previous step was successful, update your database using the -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.3.0/com/yubico/webauthn/AssertionResult.html[`AssertionResult`]. +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.0/com/yubico/webauthn/AssertionResult.html[`AssertionResult`]. Most importantly, you should update the signature counter. That might look something like this: [source,java] @@ -430,7 +430,7 @@ AssertionRequest request = rp.startAssertion(StartAssertionOptions.builder() ---------- Then -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.3.0/com/yubico/webauthn/RelyingParty.html#finishAssertion(com.yubico.webauthn.FinishAssertionOptions)[`RelyingParty.finishAssertion(...)`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.0/com/yubico/webauthn/RelyingParty.html#finishAssertion(com.yubico.webauthn.FinishAssertionOptions)[`RelyingParty.finishAssertion(...)`] will enforce that user verification was performed. However, there is no guarantee that the user's authenticator will support this unless the user has some credential created with the @@ -467,14 +467,14 @@ To migrate to using the WebAuthn API, you need to do the following: 1. Follow the link:#getting-started[Getting started] guide above to set up WebAuthn support in general. + -Note that unlike a U2F AppID, the WebAuthn link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.3.0/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.4.0/com/yubico/webauthn/data/RelyingPartyIdentity.RelyingPartyIdentityBuilder.html#id(java.lang.String)[RP ID] consists of only the domain name of the AppID. WebAuthn does not support link:https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-appid-and-facets-v1.2-ps-20170411.html[U2F Trusted Facet Lists]. 2. Set the - link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.3.0/com/yubico/webauthn/RelyingParty.RelyingPartyBuilder.html#appId(com.yubico.webauthn.extension.appid.AppId)[`appId()`] + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.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.3.0/com/yubico/webauthn/RelyingParty.html[`RelyingParty`] + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.0/com/yubico/webauthn/RelyingParty.html[`RelyingParty`] instance. The argument to the `appid()` setting should be the same as you used for the `appId` argument to the link:https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-javascript-api-v1.2-ps-20170411.html#high-level-javascript-api[U2F `register` and `sign` functions]. @@ -492,22 +492,22 @@ extensions and configure the `RelyingParty` to accept the given AppId when verif privacy consideration. 4. When your - link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.3.0/com/yubico/webauthn/CredentialRepository.html[`CredentialRepository`] + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.0/com/yubico/webauthn/CredentialRepository.html[`CredentialRepository`] creates a - link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.3.0/com/yubico/webauthn/RegisteredCredential.html[`RegisteredCredential`] + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.0/com/yubico/webauthn/RegisteredCredential.html[`RegisteredCredential`] for a U2F credential, use the U2F key handle as the - link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.3.0/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.4.0/com/yubico/webauthn/RegisteredCredential.RegisteredCredentialBuilder.html#credentialId(com.yubico.webauthn.data.ByteArray)[credential ID]. If you store key handles base64 encoded, you should decode them using - link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.3.0/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.4.0/com/yubico/webauthn/data/ByteArray.html#fromBase64(java.lang.String)[`ByteArray.fromBase64`] or - link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.3.0/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.4.0/com/yubico/webauthn/data/ByteArray.html#fromBase64Url(java.lang.String)[`ByteArray.fromBase64Url`] as appropriate before passing them to the `RegisteredCredential`. 5. When your `CredentialRepository` creates a `RegisteredCredential` for a U2F credential, use the - link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.3.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.3.0/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.4.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.4.0/com/yubico/webauthn/RegisteredCredential.RegisteredCredentialBuilder.html#publicKeyCose(com.yubico.webauthn.data.ByteArray)[`publicKeyCose()`] to set the credential public key. 6. Replace calls to the U2F @@ -641,17 +641,17 @@ provides optional additional features for working with attestation. See the module documentation for more details. Alternatively, you can use the -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.3.0/com/yubico/webauthn/attestation/AttestationTrustSource.html[`AttestationTrustSource`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.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.3.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.4.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.3.0/com/yubico/webauthn/RelyingParty.html[`RelyingParty`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.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.3.0/com/yubico/webauthn/attestation/AttestationTrustSource.TrustRootsResult.TrustRootsResultBuilder.html#enableRevocationChecking(boolean)[`enableRevocationChecking`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.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.3.0/com/yubico/webauthn/attestation/AttestationTrustSource.TrustRootsResult.TrustRootsResultBuilder.html#policyTreeValidator(java.util.function.Predicate)[`policyTreeValidator`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.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. diff --git a/build.gradle b/build.gradle index 3824c5cfe..5de185d18 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.12.1' + classpath 'com.diffplug.spotless:spotless-plugin-gradle:6.13.0' classpath 'io.github.cosmicsilence:gradle-scalafix:0.1.13' } } @@ -144,6 +144,8 @@ subprojects { project -> } tasks.withType(JavaCompile) { + options.compilerArgs += '-Xlint:unchecked' + options.deprecation = true options.encoding = 'UTF-8' } tasks.withType(ScalaCompile) { diff --git a/doc/Migrating_from_v1.adoc b/doc/Migrating_from_v1.adoc index eb19d0848..b8f04803b 100644 --- a/doc/Migrating_from_v1.adoc +++ b/doc/Migrating_from_v1.adoc @@ -26,10 +26,12 @@ Here is a high-level outline of what needs to be updated: - Update `getUserVerification()` and `getResidentKey()` calls to expect `Optional` values. -This migration guide is written for version `2.0.0` of the +Although the next section references version `2.4.0-RC2` for reasons detailed there, +this migration guide is written for version `2.0.0` of the `webauthn-server-core` module. Later `2.x` versions may introduce new features -but should remain compatible without further changes; consult the release notes -for a full list of new features. +but should remain compatible without further changes; please consult the +link:https://developers.yubico.com/java-webauthn-server/Release_Notes.html[release notes] +for an up to date list of new features. == Replace dependency on `webauthn-server-core-minimal` @@ -46,7 +48,7 @@ Maven example: - webauthn-server-core-minimal - 1.12.2 + webauthn-server-core -+ 2.0.0 ++ 2.4.0-RC2 compile ---------- @@ -56,10 +58,30 @@ Gradle: [source,diff] ---------- -compile 'com.yubico:webauthn-server-core-minimal:1.12.2' -+compile 'com.yubico:webauthn-server-core:2.0.0' ++compile 'com.yubico:webauthn-server-core:2.4.0-RC2' ---------- +[WARNING] +.*Backwards-incompatible regression in versions 2.0.0 to 2.4.0-RC1* +========== +Versions in the inclusive range `2.0.0` to `2.4.0-RC1` have +a backwards-incompatible regression in +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/latest/com/yubico/webauthn/data/PublicKeyCredentialCreationOptions.html#toCredentialsCreateJson()[`PublicKeyCredentialCreationOptions.toCredentialsCreateJson()`]: +When the +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.3.0/com/yubico/webauthn/StartRegistrationOptions.StartRegistrationOptionsBuilder.html#authenticatorSelection(com.yubico.webauthn.data.AuthenticatorSelectionCriteria)[`authenticatorSelection`].link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.3.0/com/yubico/webauthn/data/AuthenticatorSelectionCriteria.AuthenticatorSelectionCriteriaBuilder.html#residentKey(com.yubico.webauthn.data.ResidentKeyRequirement)[`residentKey`] +parameter is set, a corresponding +link:https://www.w3.org/TR/webauthn-2/#dom-authenticatorselectioncriteria-requireresidentkey[`requireResidentKey`] +member is not emitted in the JSON output. +This is not backwards compatible with browsers that only support the +link:https://www.w3.org/TR/2019/REC-webauthn-1-20190304/#authenticatorSelection[Level 1 version of the WebAuthn spec]. +The regression is fixed in version `2.4.0-RC2` and greater. +We therefore urge users to upgrade from versions `1.x` directly to `2.4.0-RC2` or greater to maintain backwards compatibility. +Please consult the link:https://developers.yubico.com/java-webauthn-server/Release_Notes.html[release notes] +for an up to date list of additional changes and new features added since version `2.0.0`. +========== + + == Add JCA provider for EdDSA The library no longer depends explicitly on BouncyCastle for cryptography back-ends. diff --git a/doc/releasing.md b/doc/releasing.md index 4d44edf84..ce198f8b4 100644 --- a/doc/releasing.md +++ b/doc/releasing.md @@ -54,7 +54,7 @@ Release candidate versions - Note which JDK version was used to build the artifacts. 7. Check that the ["Reproducible binary" - workflow](/Yubico/java-webauthn-server/actions/workflows/release-verify-signatures.yml) + workflow](https://github.com/Yubico/java-webauthn-server/actions/workflows/release-verify-signatures.yml) runs and succeeds. @@ -133,5 +133,5 @@ Release versions - Note which JDK version was used to build the artifacts. 12. Check that the ["Reproducible binary" - workflow](/Yubico/java-webauthn-server/actions/workflows/release-verify-signatures.yml) + workflow](https://github.com/Yubico/java-webauthn-server/actions/workflows/release-verify-signatures.yml) runs and succeeds. diff --git a/webauthn-server-attestation/README.adoc b/webauthn-server-attestation/README.adoc index 344fb70a3..131ff7f3e 100644 --- a/webauthn-server-attestation/README.adoc +++ b/webauthn-server-attestation/README.adoc @@ -21,7 +21,7 @@ This module does four things: - Re-download the metadata BLOB when out of date or invalid. - Provide utilities for selecting trusted metadata entries and authenticators. - Integrate with the - link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.3.0/com/yubico/webauthn/RelyingParty.html[`RelyingParty`] + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.0/com/yubico/webauthn/RelyingParty.html[`RelyingParty`] class in the base library, to provide trust root certificates for verifying attestation statements during credential registrations. @@ -30,18 +30,18 @@ Notable *non-features* include: - *Scheduled BLOB downloads.* + The -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.3.0/com/yubico/fido/metadata/FidoMetadataDownloader.html[`FidoMetadataDownloader`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.4.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.3.0/com/yubico/fido/metadata/FidoMetadataDownloader.html#loadCachedBlob()[`loadCachedBlob()`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.4.0/com/yubico/fido/metadata/FidoMetadataDownloader.html#loadCachedBlob()[`loadCachedBlob()`] or -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.3.0/com/yubico/fido/metadata/FidoMetadataDownloader.html#refreshBlob()[`refreshBlob()`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.4.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.3.0/com/yubico/fido/metadata/FidoMetadataService.html[`FidoMetadataService`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.4.0/com/yubico/fido/metadata/FidoMetadataService.html[`FidoMetadataService`] will never re-download a new BLOB once instantiated. + You should use some external scheduling mechanism to re-run `loadCachedBlob()` @@ -54,12 +54,12 @@ classes keep no internal mutable state. + The FIDO Metadata Service may from time to time report security issues with particular authenticator models. The -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.3.0/com/yubico/fido/metadata/FidoMetadataService.html[`FidoMetadataService`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.4.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.3.0/com/yubico/webauthn/RelyingParty.RelyingPartyBuilder.html#allowUntrustedAttestation(boolean)[`.allowUntrustedAttestation(false)`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.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.3.0/com/yubico/webauthn/RelyingParty.html[`RelyingParty`], +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.0/com/yubico/webauthn/RelyingParty.html[`RelyingParty`], but this will not affect any credentials already registered. @@ -94,7 +94,7 @@ Maven: com.yubico webauthn-server-attestation - 2.3.0 + 2.4.0 compile ---------- @@ -102,7 +102,7 @@ Maven: Gradle: ---------- -compile 'com.yubico:webauthn-server-attestation:2.3.0' +compile 'com.yubico:webauthn-server-attestation:2.4.0' ---------- @@ -111,7 +111,7 @@ compile 'com.yubico:webauthn-server-attestation:2.3.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.3.0/com/yubico/fido/metadata/package-summary.html[Javadoc]. +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.4.0/com/yubico/fido/metadata/package-summary.html[Javadoc]. Package-private classes and methods are NOT part of the public API. The `com.yubico:yubico-util` module is NOT part of the public API. @@ -123,23 +123,23 @@ Breaking changes to these will NOT be reflected in version numbers. Using this module consists of 4 major steps: 1. Create a - link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.3.0/com/yubico/fido/metadata/FidoMetadataDownloader.html[`FidoMetadataDownloader`] + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.4.0/com/yubico/fido/metadata/FidoMetadataDownloader.html[`FidoMetadataDownloader`] instance to download and cache metadata BLOBs, and a - link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.3.0/com/yubico/fido/metadata/FidoMetadataService.html[`FidoMetadataService`] + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.4.0/com/yubico/fido/metadata/FidoMetadataService.html[`FidoMetadataService`] instance to make use of the downloaded BLOB. See the JavaDoc for these classes for details on how to construct them. + [WARNING] ===== Unlike other classes in this module and the core library, -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.3.0/com/yubico/fido/metadata/FidoMetadataDownloader.html[`FidoMetadataDownloader`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.4.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.3.0/com/yubico/fido/metadata/FidoMetadataDownloader.html#loadCachedBlob()[`loadCachedBlob()`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.4.0/com/yubico/fido/metadata/FidoMetadataDownloader.html#loadCachedBlob()[`loadCachedBlob()`] and -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.3.0/com/yubico/fido/metadata/FidoMetadataDownloader.html#refreshBlob()[`refreshBlob()`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.4.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.3.0/com/yubico/fido/metadata/FidoMetadataService.html[`FidoMetadataService`], +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.4.0/com/yubico/fido/metadata/FidoMetadataService.html[`FidoMetadataService`], on the other hand, is thread safe, and `FidoMetadataDownloader` instances can be reused for subsequent `loadCachedBlob()` and `refreshBlob()` calls @@ -162,18 +162,18 @@ FidoMetadataService mds = FidoMetadataService.builder() ---------- 2. Set the - link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.3.0/com/yubico/fido/metadata/FidoMetadataService.html[`FidoMetadataService`] + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.4.0/com/yubico/fido/metadata/FidoMetadataService.html[`FidoMetadataService`] as the - link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.3.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.4.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.3.0/com/yubico/webauthn/RelyingParty.html[`RelyingParty`] + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.0/com/yubico/webauthn/RelyingParty.html[`RelyingParty`] instance, and set - link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.3.0/com/yubico/webauthn/RelyingParty.RelyingPartyBuilder.html#attestationConveyancePreference(com.yubico.webauthn.data.AttestationConveyancePreference)[`attestationConveyancePreference(AttestationConveyancePreference.DIRECT)`] + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.0/com/yubico/webauthn/RelyingParty.RelyingPartyBuilder.html#attestationConveyancePreference(com.yubico.webauthn.data.AttestationConveyancePreference)[`attestationConveyancePreference(AttestationConveyancePreference.DIRECT)`] on `RelyingParty` to request an attestation statement for new registrations. Optionally also set - link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.3.0/com/yubico/webauthn/RelyingParty.RelyingPartyBuilder.html#allowUntrustedAttestation(boolean)[`.allowUntrustedAttestation(false)`] + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.0/com/yubico/webauthn/RelyingParty.RelyingPartyBuilder.html#allowUntrustedAttestation(boolean)[`.allowUntrustedAttestation(false)`] on `RelyingParty` to require trusted attestation for new registrations. + [source,java] @@ -188,9 +188,9 @@ RelyingParty rp = RelyingParty.builder() ---------- 3. After performing registrations, inspect the - link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.3.0/com/yubico/webauthn/RegistrationResult.html#isAttestationTrusted()[`isAttestationTrusted()`] + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.0/com/yubico/webauthn/RegistrationResult.html#isAttestationTrusted()[`isAttestationTrusted()`] result in - link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.3.0/com/yubico/webauthn/RegistrationResult.html[`RegistrationResult`] + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.0/com/yubico/webauthn/RegistrationResult.html[`RegistrationResult`] to determine whether the authenticator presented an attestation statement that could be verified by any of the trusted attestation certificates in the FIDO Metadata Service. + @@ -207,7 +207,7 @@ if (result.isAttestationTrusted()) { ---------- 4. If needed, use the `findEntries` methods of - link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.3.0/com/yubico/fido/metadata/FidoMetadataService.html[`FidoMetadataService`] + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.4.0/com/yubico/fido/metadata/FidoMetadataService.html[`FidoMetadataService`] to retrieve additional authenticator metadata for new registrations. + [source,java] @@ -219,7 +219,7 @@ Set metadata = mds.findEntries(result); ---------- By default, -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.3.0/com/yubico/fido/metadata/FidoMetadataDownloader.html[`FidoMetadataDownloader`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.4.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. @@ -230,19 +230,19 @@ for details. == Selecting trusted authenticators The -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.3.0/com/yubico/fido/metadata/FidoMetadataService.html[`FidoMetadataService`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.4.0/com/yubico/fido/metadata/FidoMetadataService.html[`FidoMetadataService`] class can be configured with filters for which authenticators to trust. When the `FidoMetadataService` is used as the -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.3.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.4.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.3.0/com/yubico/webauthn/RelyingParty.html[`RelyingParty`], +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.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.3.0/com/yubico/webauthn/RegistrationResult.html#isAttestationTrusted()[`.isAttestationTrusted()`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.0/com/yubico/webauthn/RegistrationResult.html#isAttestationTrusted()[`.isAttestationTrusted()`] result in -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.3.0/com/yubico/webauthn/RegistrationResult.html[`RegistrationResult`]. +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.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.3.0/com/yubico/webauthn/RelyingParty.RelyingPartyBuilder.html#allowUntrustedAttestation(boolean)[`.allowUntrustedAttestation(false)`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.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, @@ -305,17 +305,17 @@ entry, and the default registration-time filter excludes any authenticator with a matching `ATTESTATION_KEY_COMPROMISE` status report entry. To customize the filters, configure the -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.3.0/com/yubico/fido/metadata/FidoMetadataService.FidoMetadataServiceBuilder.html#prefilter(java.util.function.Predicate)[`.prefilter(Predicate)`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.4.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.3.0/com/yubico/fido/metadata/FidoMetadataService.FidoMetadataServiceBuilder.html#filter(java.util.function.Predicate)[`.filter(Predicate)`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.4.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.3.0/com/yubico/fido/metadata/FidoMetadataService.html[`FidoMetadataService`]. +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.4.0/com/yubico/fido/metadata/FidoMetadataService.html[`FidoMetadataService`]. The filters are predicate functions; each metadata entry will be included in the data source if and only if the prefilter predicate returns `true` for that entry. Similarly during registration or metadata lookup, the authenticator will be matched with each metadata entry only if the registration-time filter returns `true` for that pair of authenticator and metadata entry. You can also use the -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.3.0/com/yubico/fido/metadata/FidoMetadataService.Filters.html#allOf(java.util.function.Predicate\...)[`FidoMetadataService.Filters.allOf()`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.4.0/com/yubico/fido/metadata/FidoMetadataService.Filters.html#allOf(java.util.function.Predicate\...)[`FidoMetadataService.Filters.allOf()`] combinator to merge several predicates into one. [NOTE] @@ -325,10 +325,10 @@ This is true for both the prefilter and the registration-time filter. If you want to maintain the default filter in addition to the new behaviour, you must include the default condition in the new filter. For example, you can use -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.3.0/com/yubico/fido/metadata/FidoMetadataService.Filters.html#allOf(java.util.function.Predicate\...)[`FidoMetadataService.Filters.allOf()`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.4.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.3.0/com/yubico/fido/metadata/FidoMetadataService.Filters.html[`FidoMetadataService.Filters`]. +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.4.0/com/yubico/fido/metadata/FidoMetadataService.Filters.html[`FidoMetadataService.Filters`]. ===== @@ -349,9 +349,9 @@ This is why any enforceable attestation policy must disallow unknown trust roots Note that unknown and untrusted attestation is allowed by default, but can be disallowed by explicitly configuring -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.3.0/com/yubico/webauthn/RelyingParty.html[`RelyingParty`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.0/com/yubico/webauthn/RelyingParty.html[`RelyingParty`] with -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.3.0/com/yubico/webauthn/RelyingParty.RelyingPartyBuilder.html#allowUntrustedAttestation(boolean)[`.allowUntrustedAttestation(false)`]. +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.0/com/yubico/webauthn/RelyingParty.RelyingPartyBuilder.html#allowUntrustedAttestation(boolean)[`.allowUntrustedAttestation(false)`]. == Alignment with FIDO MDS spec @@ -361,17 +361,17 @@ link:https://fidoalliance.org/specs/mds/fido-metadata-service-v3.0-ps-20210518.h The library implements these as closely as possible, but with some slight departures from the spec: * Processing rules steps 1-7 are implemented as specified, by the - link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.3.0/com/yubico/fido/metadata/FidoMetadataDownloader.html[`FidoMetadataDownloader`] + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.4.0/com/yubico/fido/metadata/FidoMetadataDownloader.html[`FidoMetadataDownloader`] class. All "SHOULD" clauses are also respected, with some caveats: ** Step 3 states "The `nextUpdate` field of the Metadata BLOB specifies a date when the download SHOULD occur at latest". `FidoMetadataDownloader` does not automatically re-download the BLOB. Instead, each time the - link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.3.0/com/yubico/fido/metadata/FidoMetadataDownloader.html#loadCachedBlob()[`loadCachedBlob()`] + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.4.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.3.0/com/yubico/fido/metadata/FidoMetadataDownloader.html#refreshBlob()[`refreshBlob()`] + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.4.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. + @@ -383,7 +383,7 @@ until the next execution of `.loadCachedBlob()` or `.refreshBlob()`. * Metadata entries are not stored or cached individually, instead the BLOB is cached as a whole. In processing rules step 8, neither `FidoMetadataDownloader` nor - link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.3.0/com/yubico/fido/metadata/FidoMetadataService.html[`FidoMetadataService`] + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.4.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. @@ -395,7 +395,7 @@ There are also some other requirements throughout the spec, which may not be obv states that "The Relying party MUST reject the Metadata Statement if the `authenticatorVersion` has not increased" in an `UPDATE_AVAILABLE` status report. Thus, - link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.3.0/com/yubico/fido/metadata/FidoMetadataService.html[`FidoMetadataService`] + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.4.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. @@ -405,16 +405,16 @@ There are also some other requirements throughout the spec, which may not be obv link:https://fidoalliance.org/specs/mds/fido-metadata-service-v3.0-ps-20210518.html#info-statuses[AuthenticatorStatus section] states that "FIDO Servers MUST silently ignore all unknown AuthenticatorStatus values". Thus any unknown status values will be parsed as - link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.3.0/com/yubico/fido/metadata/AuthenticatorStatus.html#UNKNOWN[`AuthenticatorStatus.UNKNOWN`], + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.4.0/com/yubico/fido/metadata/AuthenticatorStatus.html#UNKNOWN[`AuthenticatorStatus.UNKNOWN`], and - link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.3.0/com/yubico/fido/metadata/MetadataBLOBPayloadEntry.html[`MetadataBLOBPayloadEntry`] + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.4.0/com/yubico/fido/metadata/MetadataBLOBPayloadEntry.html[`MetadataBLOBPayloadEntry`] will silently ignore any status report with that status. == Overriding certificate path validation The -link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.3.0/com/yubico/fido/metadata/FidoMetadataDownloader.html[`FidoMetadataDownloader`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.4.0/com/yubico/fido/metadata/FidoMetadataDownloader.html[`FidoMetadataDownloader`] class uses `CertPathValidator.getInstance("PKIX")` to retrieve a `CertPathValidator` instance. If you need to override any aspect of certificate path validation, such as CRL retrieval or OCSP, you may provide a custom `CertPathValidator` provider for the `"PKIX"` algorithm. diff --git a/webauthn-server-attestation/src/integrationTest/scala/com/yubico/fido/metadata/FidoMetadataServiceIntegrationTest.scala b/webauthn-server-attestation/src/integrationTest/scala/com/yubico/fido/metadata/FidoMetadataServiceIntegrationTest.scala index 6a2782ded..7f8ba27f6 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 @@ -1,6 +1,5 @@ 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 @@ -10,7 +9,6 @@ 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 @@ -22,13 +20,9 @@ import org.scalatest.tags.Network import org.scalatest.tags.Slow 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.RichOptional @@ -76,40 +70,6 @@ class FidoMetadataServiceIntegrationTest attachmentHints: Set[AttachmentHint], ): Unit = { - def getAttestationTrustPath( - attestationObject: AttestationObject - ): Option[util.List[X509Certificate]] = { - val x5cNode: JsonNode = getX5cArray(attestationObject) - if (x5cNode != null && x5cNode.isArray) { - val certs: util.List[X509Certificate] = - new util.ArrayList[X509Certificate](x5cNode.size) - for (binary <- x5cNode.elements().asScala) { - if (binary.isBinary) - try certs.add( - CertificateParser.parseDer(binary.binaryValue) - ) - catch { - case e: IOException => - throw new RuntimeException( - "binary.isBinary() was true but binary.binaryValue() failed", - e, - ) - } - else - throw new IllegalArgumentException( - String.format( - "Each element of \"x5c\" property of attestation statement must be a binary value, was: %s", - binary.getNodeType, - ) - ) - } - Some(certs) - } else None - } - - def getX5cArray(attestationObject: AttestationObject): JsonNode = - attestationObject.getAttestationStatement.get("x5c") - val rp = RelyingParty .builder() .identity(testData.rp) @@ -159,7 +119,7 @@ class FidoMetadataServiceIntegrationTest .toSet should equal(attachmentHints) } - ignore("a YubiKey NEO.") { // TODO: Investigate why this fails + it("a YubiKey NEO.") { check("YubiKey NEO", RealExamples.YubiKeyNeo, attachmentHintsNfc) } @@ -205,7 +165,7 @@ class FidoMetadataServiceIntegrationTest it("a YubiKey 5 Nano.") { check( - "YubiKey ?5 Series", + "YubiKey 5 Series", RealExamples.YubiKey5Nano, attachmentHintsUsb, ) @@ -219,7 +179,7 @@ class FidoMetadataServiceIntegrationTest ) } - ignore("a Security Key by Yubico.") { // TODO: Investigate why this fails + it("a Security Key by Yubico.") { check( "Security Key by Yubico", RealExamples.SecurityKey, @@ -235,9 +195,9 @@ class FidoMetadataServiceIntegrationTest ) } - ignore("a Security Key NFC by Yubico.") { // TODO: Investigate why this fails + it("a Security Key NFC by Yubico.") { check( - "Security Key NFC by Yubico", + "Security Key by Yubico with NFC", RealExamples.SecurityKeyNfc, attachmentHintsNfc, ) @@ -255,18 +215,30 @@ class FidoMetadataServiceIntegrationTest it("a YubiKey 5.4 Ci FIPS.") { check( - "YubiKey 5 .*FIPS .*Lightning", + "YubiKey 5 FIPS .*Lightning", RealExamples.Yubikey5ciFips, attachmentHintsUsb, ) } it("a YubiKey Bio.") { + check( + "YubiKey Bio Series", + RealExamples.YubikeyBio_5_5_4, + attachmentHintsUsb, + ) check( "YubiKey Bio Series", RealExamples.YubikeyBio_5_5_5, attachmentHintsUsb, ) + withProviderContext(List(new BouncyCastleProvider)) { // Needed for JDK<14 because this example uses EdDSA + check( + "YubiKey Bio Series", + RealExamples.YubikeyBio_5_5_6, + attachmentHintsUsb, + ) + } } it("a Windows Hello attestation.") { diff --git a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/AAGUID.java b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/AAGUID.java index 3ef4524cf..e5adeff30 100644 --- a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/AAGUID.java +++ b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/AAGUID.java @@ -50,7 +50,7 @@ public class AAGUID { * @param value a {@link ByteArray} of length exactly 16. */ public AAGUID(ByteArray value) { - ExceptionUtil.assure( + ExceptionUtil.assertTrue( value.size() == 16, "AAGUID as bytes must be exactly 16 bytes long, was %d: %s", value.size(), diff --git a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/FidoMetadataDownloaderException.java b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/FidoMetadataDownloaderException.java index 59f7d3711..acb059060 100644 --- a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/FidoMetadataDownloaderException.java +++ b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/FidoMetadataDownloaderException.java @@ -1,9 +1,8 @@ package com.yubico.fido.metadata; +import lombok.Getter; import lombok.NonNull; -import lombok.Value; -@Value public class FidoMetadataDownloaderException extends Exception { public enum Reason { @@ -16,19 +15,20 @@ public enum Reason { } } - @NonNull + @NonNull @Getter /** The reason why this exception was thrown. */ private final Reason reason; /** A {@link Throwable} that caused this exception. May be null. */ - private final Throwable cause; + @Getter private final Throwable cause; - FidoMetadataDownloaderException(Reason reason, Throwable cause) { + FidoMetadataDownloaderException(@NonNull Reason reason, Throwable cause) { + super(cause); this.reason = reason; this.cause = cause; } - FidoMetadataDownloaderException(Reason reason) { + FidoMetadataDownloaderException(@NonNull Reason reason) { this(reason, null); } 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 9176bf504..ae404db3c 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 @@ -26,6 +26,7 @@ import com.yubico.fido.metadata.FidoMetadataService.Filters.AuthenticatorToBeFiltered; import com.yubico.internal.util.CertificateParser; +import com.yubico.internal.util.OptionalUtil; import com.yubico.webauthn.RegistrationResult; import com.yubico.webauthn.RelyingParty; import com.yubico.webauthn.RelyingParty.RelyingPartyBuilder; @@ -351,6 +352,7 @@ public static class Filters { * @return A filter which only accepts inputs that satisfy ALL of the given * filters. */ + @SafeVarargs public static Predicate allOf(Predicate... filters) { return (entry) -> Stream.of(filters).allMatch(filter -> filter.test(entry)); } @@ -453,10 +455,10 @@ public Optional getAaguid() { *
  • It satisfies the {@link FidoMetadataServiceBuilder#prefilter(Predicate) prefilter}. *
  • It satisfies AT LEAST ONE of the following: *
      - *
    • aaguid is present and equals the {@link + *
    • _aaguid is present and equals the {@link * MetadataBLOBPayloadEntry#getAaguid() AAGUID} of the metadata entry. - *
    • aaguid is present and equals the {@link - * MetadataBLOBPayloadEntry#getAaguid() AAGUID} of the {@link + *
    • _aaguid is present and equals the {@link + * MetadataStatement#getAaguid() AAGUID} of the {@link * MetadataBLOBPayloadEntry#getMetadataStatement() metadata statement}, if any, in * the metadata entry. *
    • The certificate subject key identifier of any certificate in @@ -471,15 +473,23 @@ public Optional getAaguid() { * the metadata entry. *
    *
  • It satisfies the {@link FidoMetadataServiceBuilder#filter(Predicate) filter} together - * with attestationCertificateChain and aaguid. + * with attestationCertificateChain and _aaguid. + * + * In the above, _aaguid is the first of the following that is {@link + * Optional#isPresent() present} and not {@link AAGUID#isZero() zero}, or empty otherwise: + *
      + *
    • The aaguid argument. + *
    • The value of the X.509 extension with OID 1.3.6.1.4.1.45724.1.1.4 + * (id-fido-gen-ce-aaguid), if any, in the first certificate in + * attestationCertificateChain, if any. *
    * * @see #findEntries(List) * @see #findEntries(List, AAGUID) */ public Set findEntries( - @NonNull List attestationCertificateChain, - @NonNull Optional aaguid) { + @NonNull final List attestationCertificateChain, + @NonNull final Optional aaguid) { final Set certSubjectKeyIdentifiers = attestationCertificateChain.stream() @@ -495,16 +505,26 @@ public Set findEntries( }) .collect(Collectors.toSet()); - final Optional nonzeroAaguid = aaguid.filter(a -> !a.isZero()); + final Optional nonzeroAaguid = + OptionalUtil.orElseOptional( + aaguid.filter(a -> !a.isZero()), + () -> { + log.debug("findEntries: attempting to look up AAGUID from certificate"); + if (attestationCertificateChain.isEmpty()) { + return Optional.empty(); + } else { + return CertificateParser.parseFidoAaguidExtension( + attestationCertificateChain.get(0)) + .map(ByteArray::new) + .map(AAGUID::new); + } + }); log.debug( - "findEntries(certSubjectKeyIdentifiers = {}, aaguid = {})", + "findEntries(certSubjectKeyIdentifiers = {}, aaguid = {}, nonzeroAaguid= {})", certSubjectKeyIdentifiers, - aaguid); - - if (aaguid.isPresent() && !nonzeroAaguid.isPresent()) { - log.debug("findEntries: ignoring zero AAGUID"); - } + aaguid, + nonzeroAaguid); final Set result = Stream.concat( @@ -593,7 +613,7 @@ public Set findEntries(@NonNull AAGUID aaguid) { * * @param filter a {@link Predicate} which returns true for metadata entries to * include in the result. - * @return All metadata entries which which satisfy the {@link + * @return All metadata entries which satisfy the {@link * FidoMetadataServiceBuilder#prefilter(Predicate) prefilter} AND for which the filter * returns true. * @see #findEntries(List, Optional) @@ -617,8 +637,7 @@ public TrustRootsResult findTrustRoots( .trustRoots( findEntries(attestationCertificateChain, aaguid.map(AAGUID::new)).stream() .map(MetadataBLOBPayloadEntry::getMetadataStatement) - .filter(Optional::isPresent) - .map(Optional::get) + .flatMap(OptionalUtil::stream) .flatMap( metadataStatement -> metadataStatement.getAttestationRootCertificates().stream()) 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 ffa8ee50b..6a0feb143 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 @@ -444,7 +444,8 @@ class FidoMds3Spec extends AnyFunSpec with Matchers { it("are omitted in the argument to the runtime filter.") { aaguidA should not equal zeroAaguid - val (cert, _) = TestAuthenticator.generateAttestationCertificate() + val (cert, _) = + TestAuthenticator.generateAttestationCertificate(extensions = Nil) val acki: String = new ByteArray( CertificateParser.computeSubjectKeyIdentifier(cert) ).getHex @@ -621,7 +622,7 @@ class FidoMds3Spec extends AnyFunSpec with Matchers { def makeStatusReportsBlob( statusReports: String, timeOfLastStatusChange: String, - authenticatorVersion: Int = 1, + authenticatorVersion: Int, ): (String, X509Certificate, java.util.Set[CRL]) = makeBlob(s"""{ "legalHeader" : "Kom ihåg att du aldrig får snyta dig i mattan!", 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 eb60e5b52..4cc0f14cb 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 @@ -334,7 +334,7 @@ class FidoMetadataDownloaderSpec ) blob should not be null - blob.getHeader.getX5c.get.asScala.last.getIssuerDN.getName should equal( + blob.getHeader.getX5c.get.asScala.last.getIssuerX500Principal.getName should equal( trustRootDistinguishedName ) writtenCache should equal( @@ -404,7 +404,7 @@ class FidoMetadataDownloaderSpec .build() ) blob should not be null - blob.getHeader.getX5c.get.asScala.last.getIssuerDN.getName should equal( + blob.getHeader.getX5c.get.asScala.last.getIssuerX500Principal.getName should equal( newTrustRootDistinguishedName ) writtenCache should equal( @@ -467,7 +467,7 @@ class FidoMetadataDownloaderSpec .build() ) blob should not be null - blob.getHeader.getX5c.get.asScala.last.getIssuerDN.getName should equal( + blob.getHeader.getX5c.get.asScala.last.getIssuerX500Principal.getName should equal( trustRootDistinguishedName ) cacheFile.lastModified should equal(initialModTime) @@ -527,7 +527,7 @@ class FidoMetadataDownloaderSpec .build() ) blob should not be null - blob.getHeader.getX5c.get.asScala.last.getIssuerDN.getName should equal( + blob.getHeader.getX5c.get.asScala.last.getIssuerX500Principal.getName should equal( trustRootDistinguishedName ) cacheFile.exists() should be(true) @@ -600,7 +600,7 @@ class FidoMetadataDownloaderSpec .build() ) blob should not be null - blob.getHeader.getX5c.get.asScala.last.getIssuerDN.getName should equal( + blob.getHeader.getX5c.get.asScala.last.getIssuerX500Principal.getName should equal( newTrustRootDistinguishedName ) cacheFile.exists() should be(true) @@ -656,7 +656,7 @@ class FidoMetadataDownloaderSpec .build() ) blob should not be null - blob.getHeader.getX5c.get.asScala.last.getIssuerDN.getName should equal( + blob.getHeader.getX5c.get.asScala.last.getIssuerX500Principal.getName should equal( trustRootDistinguishedName ) writtenCache should equal(None) diff --git a/webauthn-server-attestation/src/test/scala/com/yubico/fido/metadata/Generators.scala b/webauthn-server-attestation/src/test/scala/com/yubico/fido/metadata/Generators.scala index 2329a8c35..96fd904ab 100644 --- a/webauthn-server-attestation/src/test/scala/com/yubico/fido/metadata/Generators.scala +++ b/webauthn-server-attestation/src/test/scala/com/yubico/fido/metadata/Generators.scala @@ -1,9 +1,13 @@ package com.yubico.fido.metadata +import com.yubico.scalacheck.gen.GenUtil.halfsized +import com.yubico.scalacheck.gen.GenUtil.maxSized import com.yubico.scalacheck.gen.JavaGenerators.arbitraryUrl import com.yubico.webauthn.TestAuthenticator import com.yubico.webauthn.data.AuthenticatorTransport +import com.yubico.webauthn.data.ByteArray import com.yubico.webauthn.data.Generators.arbitraryAuthenticatorTransport +import com.yubico.webauthn.data.Generators.arbitraryByteArray import com.yubico.webauthn.data.Generators.arbitraryPublicKeyCredentialParameters import com.yubico.webauthn.data.Generators.byteArray import com.yubico.webauthn.data.PublicKeyCredentialParameters @@ -25,80 +29,74 @@ object Generators { implicit val arbitraryMetadataBLOBHeader: Arbitrary[MetadataBLOBHeader] = Arbitrary( - for { - alg <- arbitrary[String] - typ <- Gen.option(Gen.const("JWT")) - x5u <- arbitrary[Option[URL]] - x5c <- Gen.option( - Gen - .chooseNum(0, 4) - .flatMap(n => - Gen.listOfN( - n, - TestAuthenticator.generateAttestationCertificate()._1, - ) - ) - ) - } yield MetadataBLOBHeader - .builder() - .alg(alg) - .typ(typ.orNull) - .x5u(x5u.orNull) - .x5c(x5c.map(_.asJava).orNull) - .build() + halfsized( + for { + alg <- arbitrary[String] + typ <- Gen.option(Gen.const("JWT")) + x5u <- arbitrary[Option[URL]] + x5c <- maxSized( + 2, + Gen.option( + Gen.listOf(TestAuthenticator.generateAttestationCertificate()._1) + ), + ) + } yield MetadataBLOBHeader + .builder() + .alg(alg) + .typ(typ.orNull) + .x5u(x5u.orNull) + .x5c(x5c.map(_.asJava).orNull) + .build() + ) ) implicit val arbitraryMetadataBLOBPayload: Arbitrary[MetadataBLOBPayload] = Arbitrary( - for { - legalHeader <- arbitrary[Option[String]] - no <- arbitrary[Int] - nextUpdate <- arbitrary[LocalDate] - entries <- - Gen - .chooseNum(0, 4) - .flatMap(n => - Gen.containerOfN[Set, MetadataBLOBPayloadEntry]( - n, - arbitrary[MetadataBLOBPayloadEntry], - ) - ) - } yield new MetadataBLOBPayload( - legalHeader.orNull, - no, - nextUpdate, - entries.asJava, + halfsized( + for { + legalHeader <- arbitrary[Option[String]] + no <- arbitrary[Int] + nextUpdate <- arbitrary[LocalDate] + entries <- arbitrary[Set[MetadataBLOBPayloadEntry]] + } yield new MetadataBLOBPayload( + legalHeader.orNull, + no, + nextUpdate, + entries.asJava, + ) ) ) implicit val arbitraryMetadataBLOBPayloadEntry : Arbitrary[MetadataBLOBPayloadEntry] = Arbitrary( - for { - aaid <- arbitrary[Option[AAID]] - aaguid <- arbitrary[Option[AAGUID]] - attestationCertificateKeyIdentifiers <- Gen.option( - Gen.containerOf[Set, String](byteArray(32, 32).map(_.getHex)) - ) - metadataStatement <- arbitrary[Option[MetadataStatement]] - biometricStatusReports <- arbitrary[Option[List[BiometricStatusReport]]] - statusReports <- arbitrary[List[StatusReport]] - timeOfLastStatusChange <- arbitrary[LocalDate] - rogueListURL <- arbitrary[Option[URL]] - rogueListHash <- Gen.option(byteArray(1, 512)) - } yield MetadataBLOBPayloadEntry - .builder() - .aaid(aaid.orNull) - .aaguid(aaguid.orNull) - .attestationCertificateKeyIdentifiers( - attestationCertificateKeyIdentifiers.map(_.asJava).orNull - ) - .metadataStatement(metadataStatement.orNull) - .biometricStatusReports(biometricStatusReports.map(_.asJava).orNull) - .statusReports(statusReports.asJava) - .timeOfLastStatusChange(timeOfLastStatusChange) - .rogueListURL(rogueListURL.orNull) - .rogueListHash(rogueListHash.orNull) - .build() + halfsized( + for { + aaid <- arbitrary[Option[AAID]] + aaguid <- arbitrary[Option[AAGUID]] + attestationCertificateKeyIdentifiers <- Gen.option( + Gen.containerOf[Set, String](byteArray(32, 32).map(_.getHex)) + ) + metadataStatement <- arbitrary[Option[MetadataStatement]] + biometricStatusReports <- arbitrary[Option[List[BiometricStatusReport]]] + statusReports <- arbitrary[List[StatusReport]] + timeOfLastStatusChange <- arbitrary[LocalDate] + rogueListURL <- arbitrary[Option[URL]] + rogueListHash <- arbitrary[Option[ByteArray]] + } yield MetadataBLOBPayloadEntry + .builder() + .aaid(aaid.orNull) + .aaguid(aaguid.orNull) + .attestationCertificateKeyIdentifiers( + attestationCertificateKeyIdentifiers.map(_.asJava).orNull + ) + .metadataStatement(metadataStatement.orNull) + .biometricStatusReports(biometricStatusReports.map(_.asJava).orNull) + .statusReports(statusReports.asJava) + .timeOfLastStatusChange(timeOfLastStatusChange) + .rogueListURL(rogueListURL.orNull) + .rogueListHash(rogueListHash.orNull) + .build() + ) ) implicit val arbitraryAaid: Arbitrary[AAID] = Arbitrary(for { @@ -112,114 +110,119 @@ object Generators { implicit val arbitraryBiometricStatusReport : Arbitrary[BiometricStatusReport] = Arbitrary( - for { - certLevel <- arbitrary[Int] - modality <- arbitrary[UserVerificationMethod] - effectiveDate <- arbitrary[Option[LocalDate]] - certificationDescriptor <- arbitrary[Option[String]] - certificateNumber <- arbitrary[Option[String]] - certificationPolicyVersion <- arbitrary[Option[String]] - certificationRequirementsVersion <- arbitrary[Option[String]] - } yield BiometricStatusReport - .builder() - .certLevel(certLevel) - .modality(modality) - .effectiveDate(effectiveDate.orNull) - .certificationDescriptor(certificationDescriptor.orNull) - .certificateNumber(certificateNumber.orNull) - .certificationPolicyVersion(certificationPolicyVersion.orNull) - .certificationRequirementsVersion(certificationRequirementsVersion.orNull) - .build() - ) - - implicit val arbitraryMetadataStatement: Arbitrary[MetadataStatement] = - Arbitrary( + halfsized( for { - legalHeader <- arbitrary[Option[String]] - aaid <- arbitrary[Option[AAID]] - aaguid <- arbitrary[Option[AAGUID]] - attestationCertificateKeyIdentifiers <- arbitrary[Option[Set[String]]] - description <- arbitrary[Option[String]] - alternativeDescriptions <- arbitrary[Option[AlternativeDescriptions]] - authenticatorVersion <- arbitrary[Long] - protocolFamily <- arbitrary[ProtocolFamily] - schema <- arbitrary[Int] - upv <- arbitrary[Set[Version]] - authenticationAlgorithms <- arbitrary[Set[AuthenticationAlgorithm]] - publicKeyAlgAndEncodings <- - arbitrary[Set[PublicKeyRepresentationFormat]] - attestationTypes <- arbitrary[Set[AuthenticatorAttestationType]] - userVerificationDetails <- - arbitrary[Set[Set[VerificationMethodDescriptor]]] - keyProtection <- arbitrary[Set[KeyProtectionType]] - isKeyRestricted <- arbitrary[Option[Boolean]] - isFreshUserVerificationRequired <- arbitrary[Option[Boolean]] - matcherProtection <- arbitrary[Set[MatcherProtectionType]] - cryptoStrength <- arbitrary[Option[Int]] - attachmentHint <- arbitrary[Option[Set[AttachmentHint]]] - tcDisplay <- arbitrary[Set[TransactionConfirmationDisplayType]] - tcDisplayContentType <- arbitrary[Option[String]] - tcDisplayPNGCharacteristics <- - arbitrary[Option[List[DisplayPNGCharacteristicsDescriptor]]] - attestationRootCertificates <- - Gen - .chooseNum(0, 4) - .flatMap(n => - Gen.containerOfN[Set, X509Certificate]( - n, - TestAuthenticator.generateAttestationCaCertificate()._1, - ) - ) - icon <- arbitrary[Option[String]] - supportedExtensions <- arbitrary[Option[Set[ExtensionDescriptor]]] - authenticatorGetInfo <- arbitrary[Option[AuthenticatorGetInfo]] - } yield MetadataStatement + certLevel <- arbitrary[Int] + modality <- arbitrary[UserVerificationMethod] + effectiveDate <- arbitrary[Option[LocalDate]] + certificationDescriptor <- arbitrary[Option[String]] + certificateNumber <- arbitrary[Option[String]] + certificationPolicyVersion <- arbitrary[Option[String]] + certificationRequirementsVersion <- arbitrary[Option[String]] + } yield BiometricStatusReport .builder() - .legalHeader(legalHeader.orNull) - .aaid(aaid.orNull) - .aaguid(aaguid.orNull) - .attestationCertificateKeyIdentifiers( - attestationCertificateKeyIdentifiers.map(_.asJava).orNull + .certLevel(certLevel) + .modality(modality) + .effectiveDate(effectiveDate.orNull) + .certificationDescriptor(certificationDescriptor.orNull) + .certificateNumber(certificateNumber.orNull) + .certificationPolicyVersion(certificationPolicyVersion.orNull) + .certificationRequirementsVersion( + certificationRequirementsVersion.orNull ) - .description(description.orNull) - .alternativeDescriptions(alternativeDescriptions.orNull) - .authenticatorVersion(authenticatorVersion) - .protocolFamily(protocolFamily) - .schema(schema) - .upv(upv.asJava) - .authenticationAlgorithms(authenticationAlgorithms.asJava) - .publicKeyAlgAndEncodings(publicKeyAlgAndEncodings.asJava) - .attestationTypes(attestationTypes.asJava) - .userVerificationDetails(userVerificationDetails.map(_.asJava).asJava) - .keyProtection(keyProtection.asJava) - .isKeyRestricted(isKeyRestricted.map(java.lang.Boolean.valueOf).orNull) - .isFreshUserVerificationRequired( - isFreshUserVerificationRequired.map(java.lang.Boolean.valueOf).orNull - ) - .matcherProtection(matcherProtection.asJava) - .cryptoStrength(cryptoStrength.map(Integer.valueOf).orNull) - .attachmentHint(attachmentHint.map(_.asJava).orNull) - .tcDisplay(tcDisplay.asJava) - .tcDisplayContentType(tcDisplayContentType.orNull) - .tcDisplayPNGCharacteristics( - tcDisplayPNGCharacteristics.map(_.asJava).orNull - ) - .attestationRootCertificates(attestationRootCertificates.asJava) - .icon(icon.orNull) - .supportedExtensions(supportedExtensions.map(_.asJava).orNull) - .authenticatorGetInfo(authenticatorGetInfo.orNull) .build() ) + ) + + implicit val arbitraryMetadataStatement: Arbitrary[MetadataStatement] = + Arbitrary( + halfsized( + for { + legalHeader <- arbitrary[Option[String]] + aaid <- arbitrary[Option[AAID]] + aaguid <- arbitrary[Option[AAGUID]] + attestationCertificateKeyIdentifiers <- arbitrary[Option[Set[String]]] + description <- arbitrary[Option[String]] + alternativeDescriptions <- arbitrary[Option[AlternativeDescriptions]] + authenticatorVersion <- arbitrary[Long] + protocolFamily <- arbitrary[ProtocolFamily] + schema <- arbitrary[Int] + upv <- arbitrary[Set[Version]] + authenticationAlgorithms <- arbitrary[Set[AuthenticationAlgorithm]] + publicKeyAlgAndEncodings <- + arbitrary[Set[PublicKeyRepresentationFormat]] + attestationTypes <- arbitrary[Set[AuthenticatorAttestationType]] + userVerificationDetails <- + arbitrary[Set[Set[VerificationMethodDescriptor]]] + keyProtection <- arbitrary[Set[KeyProtectionType]] + isKeyRestricted <- arbitrary[Option[Boolean]] + isFreshUserVerificationRequired <- arbitrary[Option[Boolean]] + matcherProtection <- arbitrary[Set[MatcherProtectionType]] + cryptoStrength <- arbitrary[Option[Int]] + attachmentHint <- arbitrary[Option[Set[AttachmentHint]]] + tcDisplay <- arbitrary[Set[TransactionConfirmationDisplayType]] + tcDisplayContentType <- arbitrary[Option[String]] + tcDisplayPNGCharacteristics <- + arbitrary[Option[List[DisplayPNGCharacteristicsDescriptor]]] + attestationRootCertificates <- maxSized( + 2, + Gen.containerOf[Set, X509Certificate]( + TestAuthenticator.generateAttestationCaCertificate()._1 + ), + ) + icon <- arbitrary[Option[String]] + supportedExtensions <- arbitrary[Option[Set[ExtensionDescriptor]]] + authenticatorGetInfo <- arbitrary[Option[AuthenticatorGetInfo]] + } yield MetadataStatement + .builder() + .legalHeader(legalHeader.orNull) + .aaid(aaid.orNull) + .aaguid(aaguid.orNull) + .attestationCertificateKeyIdentifiers( + attestationCertificateKeyIdentifiers.map(_.asJava).orNull + ) + .description(description.orNull) + .alternativeDescriptions(alternativeDescriptions.orNull) + .authenticatorVersion(authenticatorVersion) + .protocolFamily(protocolFamily) + .schema(schema) + .upv(upv.asJava) + .authenticationAlgorithms(authenticationAlgorithms.asJava) + .publicKeyAlgAndEncodings(publicKeyAlgAndEncodings.asJava) + .attestationTypes(attestationTypes.asJava) + .userVerificationDetails(userVerificationDetails.map(_.asJava).asJava) + .keyProtection(keyProtection.asJava) + .isKeyRestricted( + isKeyRestricted.map(java.lang.Boolean.valueOf).orNull + ) + .isFreshUserVerificationRequired( + isFreshUserVerificationRequired.map(java.lang.Boolean.valueOf).orNull + ) + .matcherProtection(matcherProtection.asJava) + .cryptoStrength(cryptoStrength.map(Integer.valueOf).orNull) + .attachmentHint(attachmentHint.map(_.asJava).orNull) + .tcDisplay(tcDisplay.asJava) + .tcDisplayContentType(tcDisplayContentType.orNull) + .tcDisplayPNGCharacteristics( + tcDisplayPNGCharacteristics.map(_.asJava).orNull + ) + .attestationRootCertificates(attestationRootCertificates.asJava) + .icon(icon.orNull) + .supportedExtensions(supportedExtensions.map(_.asJava).orNull) + .authenticatorGetInfo(authenticatorGetInfo.orNull) + .build() + ) + ) implicit val arbitraryAlternativeDescriptions - : Arbitrary[AlternativeDescriptions] = Arbitrary(for { + : Arbitrary[AlternativeDescriptions] = Arbitrary(halfsized(for { entries: Map[String, String] <- Gen.mapOf(for { prefix <- Gen.alphaLowerStr.suchThat(_.length >= 2).map(_.take(2)) suffix <- Gen.option(Gen.alphaUpperStr.suchThat(_.length >= 2).map(_.take(2))) text <- arbitrary[String] } yield (s"${prefix}${suffix.map(s => s"_${s}").getOrElse("")}", text)) - } yield new AlternativeDescriptions(entries.asJava)) + } yield new AlternativeDescriptions(entries.asJava))) implicit val arbitraryVersion: Arbitrary[Version] = Arbitrary(for { major <- arbitrary[Int] @@ -298,7 +301,7 @@ object Generators { compression <- arbitrary[Short] filter <- arbitrary[Short] interlace <- arbitrary[Short] - plte <- arbitrary[Option[List[RgbPaletteEntry]]] + plte <- halfsized(arbitrary[Option[List[RgbPaletteEntry]]]) } yield DisplayPNGCharacteristicsDescriptor .builder() .width(width) @@ -322,89 +325,93 @@ object Generators { implicit val arbitraryExtensionDescriptor: Arbitrary[ExtensionDescriptor] = Arbitrary( - for { - id <- arbitrary[String] - tag <- arbitrary[Option[Int]] - data <- arbitrary[Option[String]] - failIfUnknown <- arbitrary[Boolean] - } yield ExtensionDescriptor - .builder() - .id(id) - .tag(tag.map(Integer.valueOf).orNull) - .data(data.orNull) - .failIfUnknown(failIfUnknown) - .build() + halfsized( + for { + id <- arbitrary[String] + tag <- arbitrary[Option[Int]] + data <- arbitrary[Option[String]] + failIfUnknown <- arbitrary[Boolean] + } yield ExtensionDescriptor + .builder() + .id(id) + .tag(tag.map(Integer.valueOf).orNull) + .data(data.orNull) + .failIfUnknown(failIfUnknown) + .build() + ) ) implicit val arbitraryAuthenticatorGetInfo: Arbitrary[AuthenticatorGetInfo] = Arbitrary( - for { - versions <- arbitrary[Set[CtapVersion]] - extensions <- arbitrary[Option[Set[String]]] - aaguid <- arbitrary[Option[AAGUID]] - options <- arbitrary[Option[SupportedCtapOptions]] - maxMsgSize <- arbitrary[Option[Int]] - pinUvAuthProtocols <- - arbitrary[Option[Set[CtapPinUvAuthProtocolVersion]]] - maxCredentialCountInList <- arbitrary[Option[Int]] - maxCredentialIdLength <- arbitrary[Option[Int]] - transports <- arbitrary[Option[Set[AuthenticatorTransport]]] - algorithms <- arbitrary[Option[List[PublicKeyCredentialParameters]]] - maxSerializedLargeBlobArray <- arbitrary[Option[Int]] - forcePINChange <- arbitrary[Option[Boolean]] - minPINLength <- arbitrary[Option[Int]] - firmwareVersion <- arbitrary[Option[Int]] - maxCredBlobLength <- arbitrary[Option[Int]] - maxRPIDsForSetMinPINLength <- arbitrary[Option[Int]] - preferredPlatformUvAttempts <- arbitrary[Option[Int]] - uvModality <- arbitrary[Option[Set[UserVerificationMethod]]] - certifications <- arbitrary[Option[Map[CtapCertificationId, Int]]] - remainingDiscoverableCredentials <- arbitrary[Option[Int]] - vendorPrototypeConfigCommands <- arbitrary[Option[Set[Int]]] - } yield AuthenticatorGetInfo - .builder() - .versions(versions.asJava) - .extensions(extensions.map(_.asJava).orNull) - .aaguid(aaguid.orNull) - .options(options.orNull) - .maxMsgSize(maxMsgSize.map(Integer.valueOf).orNull) - .pinUvAuthProtocols(pinUvAuthProtocols.map(_.asJava).orNull) - .maxCredentialCountInList( - maxCredentialCountInList.map(Integer.valueOf).orNull - ) - .maxCredentialIdLength( - maxCredentialIdLength.map(Integer.valueOf).orNull - ) - .transports(transports.map(_.asJava).orNull) - .algorithms(algorithms.map(_.asJava).orNull) - .maxSerializedLargeBlobArray( - maxSerializedLargeBlobArray.map(Integer.valueOf).orNull - ) - .forcePINChange(forcePINChange.map(java.lang.Boolean.valueOf).orNull) - .minPINLength(minPINLength.map(Integer.valueOf).orNull) - .firmwareVersion(firmwareVersion.map(Integer.valueOf).orNull) - .maxCredBlobLength(maxCredBlobLength.map(Integer.valueOf).orNull) - .maxRPIDsForSetMinPINLength( - maxRPIDsForSetMinPINLength.map(Integer.valueOf).orNull - ) - .preferredPlatformUvAttempts( - preferredPlatformUvAttempts.map(Integer.valueOf).orNull - ) - .uvModality(uvModality.map(_.asJava).orNull) - .certifications( - certifications - .map(_.map({ case (k, v) => (k, Integer.valueOf(v)) }).asJava) - .orNull - ) - .remainingDiscoverableCredentials( - remainingDiscoverableCredentials.map(Integer.valueOf).orNull - ) - .vendorPrototypeConfigCommands( - vendorPrototypeConfigCommands - .map(_.map(Integer.valueOf).asJava) - .orNull - ) - .build() + halfsized( + for { + versions <- arbitrary[Set[CtapVersion]] + extensions <- arbitrary[Option[Set[String]]] + aaguid <- arbitrary[Option[AAGUID]] + options <- arbitrary[Option[SupportedCtapOptions]] + maxMsgSize <- arbitrary[Option[Int]] + pinUvAuthProtocols <- + arbitrary[Option[Set[CtapPinUvAuthProtocolVersion]]] + maxCredentialCountInList <- arbitrary[Option[Int]] + maxCredentialIdLength <- arbitrary[Option[Int]] + transports <- arbitrary[Option[Set[AuthenticatorTransport]]] + algorithms <- arbitrary[Option[List[PublicKeyCredentialParameters]]] + maxSerializedLargeBlobArray <- arbitrary[Option[Int]] + forcePINChange <- arbitrary[Option[Boolean]] + minPINLength <- arbitrary[Option[Int]] + firmwareVersion <- arbitrary[Option[Int]] + maxCredBlobLength <- arbitrary[Option[Int]] + maxRPIDsForSetMinPINLength <- arbitrary[Option[Int]] + preferredPlatformUvAttempts <- arbitrary[Option[Int]] + uvModality <- arbitrary[Option[Set[UserVerificationMethod]]] + certifications <- arbitrary[Option[Map[CtapCertificationId, Int]]] + remainingDiscoverableCredentials <- arbitrary[Option[Int]] + vendorPrototypeConfigCommands <- arbitrary[Option[Set[Int]]] + } yield AuthenticatorGetInfo + .builder() + .versions(versions.asJava) + .extensions(extensions.map(_.asJava).orNull) + .aaguid(aaguid.orNull) + .options(options.orNull) + .maxMsgSize(maxMsgSize.map(Integer.valueOf).orNull) + .pinUvAuthProtocols(pinUvAuthProtocols.map(_.asJava).orNull) + .maxCredentialCountInList( + maxCredentialCountInList.map(Integer.valueOf).orNull + ) + .maxCredentialIdLength( + maxCredentialIdLength.map(Integer.valueOf).orNull + ) + .transports(transports.map(_.asJava).orNull) + .algorithms(algorithms.map(_.asJava).orNull) + .maxSerializedLargeBlobArray( + maxSerializedLargeBlobArray.map(Integer.valueOf).orNull + ) + .forcePINChange(forcePINChange.map(java.lang.Boolean.valueOf).orNull) + .minPINLength(minPINLength.map(Integer.valueOf).orNull) + .firmwareVersion(firmwareVersion.map(Integer.valueOf).orNull) + .maxCredBlobLength(maxCredBlobLength.map(Integer.valueOf).orNull) + .maxRPIDsForSetMinPINLength( + maxRPIDsForSetMinPINLength.map(Integer.valueOf).orNull + ) + .preferredPlatformUvAttempts( + preferredPlatformUvAttempts.map(Integer.valueOf).orNull + ) + .uvModality(uvModality.map(_.asJava).orNull) + .certifications( + certifications + .map(_.map({ case (k, v) => (k, Integer.valueOf(v)) }).asJava) + .orNull + ) + .remainingDiscoverableCredentials( + remainingDiscoverableCredentials.map(Integer.valueOf).orNull + ) + .vendorPrototypeConfigCommands( + vendorPrototypeConfigCommands + .map(_.map(Integer.valueOf).asJava) + .orNull + ) + .build() + ) ) implicit val arbitrarySupportedCtapOptions: Arbitrary[SupportedCtapOptions] = @@ -454,36 +461,40 @@ object Generators { ) implicit val arbitraryStatusReport: Arbitrary[StatusReport] = Arbitrary( - for { - status <- arbitrary[AuthenticatorStatus] - effectiveDate <- arbitrary[Option[LocalDate]] - authenticatorVersion <- arbitrary[Option[Long]] - certificate <- Gen.option( - Gen.delay( - Gen - .const(TestAuthenticator.generateAttestationCertificate()) - .map(_._1) + halfsized( + for { + status <- arbitrary[AuthenticatorStatus] + effectiveDate <- arbitrary[Option[LocalDate]] + authenticatorVersion <- arbitrary[Option[Long]] + certificate <- Gen.option( + Gen.delay( + Gen + .const(TestAuthenticator.generateAttestationCertificate()) + .map(_._1) + ) ) - ) - url <- arbitrary[Option[String]] - certificationDescriptor <- arbitrary[Option[String]] - certificateNumber <- arbitrary[Option[String]] - certificationPolicyVersion <- arbitrary[Option[String]] - certificationRequirementsVersion <- arbitrary[Option[String]] - } yield StatusReport - .builder() - .status(status) - .effectiveDate(effectiveDate.orNull) - .authenticatorVersion( - authenticatorVersion.map(java.lang.Long.valueOf).orNull - ) - .certificate(certificate.orNull) - .url(url.orNull) - .certificationDescriptor(certificationDescriptor.orNull) - .certificateNumber(certificateNumber.orNull) - .certificationPolicyVersion(certificationPolicyVersion.orNull) - .certificationRequirementsVersion(certificationRequirementsVersion.orNull) - .build() + url <- arbitrary[Option[String]] + certificationDescriptor <- arbitrary[Option[String]] + certificateNumber <- arbitrary[Option[String]] + certificationPolicyVersion <- arbitrary[Option[String]] + certificationRequirementsVersion <- arbitrary[Option[String]] + } yield StatusReport + .builder() + .status(status) + .effectiveDate(effectiveDate.orNull) + .authenticatorVersion( + authenticatorVersion.map(java.lang.Long.valueOf).orNull + ) + .certificate(certificate.orNull) + .url(url.orNull) + .certificationDescriptor(certificationDescriptor.orNull) + .certificateNumber(certificateNumber.orNull) + .certificationPolicyVersion(certificationPolicyVersion.orNull) + .certificationRequirementsVersion( + certificationRequirementsVersion.orNull + ) + .build() + ) ) } 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 94458a644..3c6d10390 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 @@ -47,25 +47,8 @@ class JsonIoSpec def test[A](tpe: TypeReference[A])(implicit a: Arbitrary[A]): Unit = { val cn = tpe.getType.getTypeName describe(s"${cn}") { - it("can be serialized to JSON.") { - forAll { value: A => - val encoded: String = json.writeValueAsString(value) - - encoded should not be empty - } - } - - it("can be deserialized from JSON.") { - forAll { value: A => - val encoded: String = json.writeValueAsString(value) - val decoded: A = json.readValue(encoded, tpe) - - decoded should equal(value) - } - } - it("is identical after multiple serialization round-trips.") { - forAll { value: A => + forAll(minSuccessful(10)) { value: A => val encoded: String = json.writeValueAsString(value) val decoded: A = json.readValue(encoded, tpe) decoded should equal(value) 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 542921101..bdbf6b3b4 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 @@ -70,13 +70,13 @@ public boolean verifyAttestationSignature( attestationObject.getAuthenticatorData().getBytes().concat(clientDataJsonHash); ByteArray hashSignedData = Crypto.sha256(signedData); ByteArray nonceByteArray = ByteArray.fromBase64(payload.get("nonce").textValue()); - ExceptionUtil.assure( + ExceptionUtil.assertTrue( hashSignedData.equals(nonceByteArray), "Nonce does not equal authenticator data + client data. Expected nonce: %s, was nonce: %s", hashSignedData.getBase64Url(), nonceByteArray.getBase64Url()); - ExceptionUtil.assure( + ExceptionUtil.assertTrue( payload.get("ctsProfileMatch").booleanValue(), "Expected ctsProfileMatch to be true, was: %s", payload.get("ctsProfileMatch")); @@ -133,7 +133,7 @@ private boolean verifySignature(JsonWebSignatureCustom jws) { } // Verify the hostname of the certificate. - ExceptionUtil.assure( + ExceptionUtil.assertTrue( verifyHostname(attestationCertificate), "Certificate isn't issued for the hostname attest.android.com: %s", attestationCertificate); diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/AssertionRequest.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/AssertionRequest.java index 13f29b89b..333e4ae76 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/AssertionRequest.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/AssertionRequest.java @@ -37,7 +37,7 @@ /** * A combination of a {@link PublicKeyCredentialRequestOptions} and, optionally, a {@link - * #getUsername() username}. + * #getUsername() username} or {@link #getUserHandle() user handle}. */ @Value @Builder(toBuilder = true) @@ -52,27 +52,59 @@ public class AssertionRequest { /** * The username of the user to authenticate, if the user has already been identified. * - *

    If this is absent, this indicates that this is a request for an assertion by a client-side-resident + *

    This is mutually exclusive with {@link #getUserHandle() userHandle}; setting this will unset + * {@link #getUserHandle() userHandle}. When parsing from JSON, {@link #getUserHandle() + * userHandle} takes precedence over this. + * + *

    If both this and {@link #getUserHandle() userHandle} are empty, this indicates that this is + * a request for an assertion by a client-side-discoverable * credential, and identification of the user has been deferred until the response is * received. */ private final String username; + /** + * The user handle of the user to authenticate, if the user has already been identified. + * + *

    This is mutually exclusive with {@link #getUsername() username}; setting this will unset + * {@link #getUsername() username}. When parsing from JSON, this takes precedence over {@link + * #getUsername() username}. + * + *

    If both this and {@link #getUsername() username} are empty, this indicates that this is a + * request for an assertion by a client-side-discoverable + * credential, and identification of the user has been deferred until the response is + * received. + */ + private final ByteArray userHandle; + @JsonCreator private AssertionRequest( @NonNull @JsonProperty("publicKeyCredentialRequestOptions") PublicKeyCredentialRequestOptions publicKeyCredentialRequestOptions, - @JsonProperty("username") String username) { + @JsonProperty("username") String username, + @JsonProperty("userHandle") ByteArray userHandle) { this.publicKeyCredentialRequestOptions = publicKeyCredentialRequestOptions; - this.username = username; + + if (userHandle != null) { + this.username = null; + this.userHandle = userHandle; + } else { + this.username = username; + this.userHandle = null; + } } /** * The username of the user to authenticate, if the user has already been identified. * - *

    If this is absent, this indicates that this is a request for an assertion by a client-side-resident + *

    This is mutually exclusive with {@link #getUserHandle()}; if this is present, then {@link + * #getUserHandle()} will be empty. + * + *

    If both this and {@link #getUserHandle()} are empty, this indicates that this is a request + * for an assertion by a client-side-discoverable * credential, and identification of the user has been deferred until the response is * received. */ @@ -80,6 +112,22 @@ public Optional getUsername() { return Optional.ofNullable(username); } + /** + * The user handle of the user to authenticate, if the user has already been identified. + * + *

    This is mutually exclusive with {@link #getUsername()}; if this is present, then {@link + * #getUsername()} will be empty. + * + *

    If both this and {@link #getUsername()} are empty, this indicates that this is a request for + * an assertion by a client-side-discoverable + * credential, and identification of the user has been deferred until the response is + * received. + */ + public Optional getUserHandle() { + return Optional.ofNullable(userHandle); + } + /** * Serialize this {@link AssertionRequest} value to JSON suitable for sending to the client. * @@ -140,6 +188,7 @@ public static AssertionRequestBuilder.MandatoryStages builder() { public static class AssertionRequestBuilder { private String username = null; + private ByteArray userHandle = null; public static class MandatoryStages { private final AssertionRequestBuilder builder = new AssertionRequestBuilder(); @@ -161,8 +210,11 @@ public AssertionRequestBuilder publicKeyCredentialRequestOptions( /** * The username of the user to authenticate, if the user has already been identified. * - *

    If this is absent, this indicates that this is a request for an assertion by a client-side-resident + *

    This is mutually exclusive with {@link #userHandle(ByteArray)}; setting this to non-empty + * will unset {@link #userHandle(ByteArray)}. + * + *

    If this is empty, this indicates that this is a request for an assertion by a client-side-discoverable * credential, and identification of the user has been deferred until the response is * received. */ @@ -173,13 +225,55 @@ public AssertionRequestBuilder username(@NonNull Optional username) { /** * The username of the user to authenticate, if the user has already been identified. * - *

    If this is absent, this indicates that this is a request for an assertion by a client-side-resident + *

    This is mutually exclusive with {@link #userHandle(ByteArray)}; setting this to non- + * null will unset {@link #userHandle(ByteArray)}. + * + *

    If this is empty, this indicates that this is a request for an assertion by a client-side-discoverable * credential, and identification of the user has been deferred until the response is * received. */ public AssertionRequestBuilder username(String username) { this.username = username; + if (username != null) { + this.userHandle = null; + } + return this; + } + + /** + * The user handle of the user to authenticate, if the user has already been identified. + * + *

    This is mutually exclusive with {@link #username(String)}; setting this to non-empty will + * unset {@link #username(String)}. + * + *

    If both this and {@link #username(String)} are empty, this indicates that this is a + * request for an assertion by a client-side-discoverable + * credential, and identification of the user has been deferred until the response is + * received. + */ + public AssertionRequestBuilder userHandle(@NonNull Optional userHandle) { + return this.userHandle(userHandle.orElse(null)); + } + + /** + * The user handle of the user to authenticate, if the user has already been identified. + * + *

    This is mutually exclusive with {@link #username(String)}; setting this to non-null + * will unset {@link #username(String)}. + * + *

    If both this and {@link #username(String)} are empty, this indicates that this is a + * request for an assertion by a client-side-discoverable + * credential, and identification of the user has been deferred until the response is + * received. + */ + public AssertionRequestBuilder userHandle(ByteArray userHandle) { + if (userHandle != null) { + this.username = null; + } + this.userHandle = userHandle; return this; } } 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 60ddff1bc..4a66b9c22 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 @@ -95,8 +95,6 @@ public static boolean verifySignature( } public static ByteArray sha256(ByteArray bytes) { - //noinspection UnstableApiUsage - // TODO remove noinspection return new ByteArray(Hashing.sha256().hashBytes(bytes.getBytes()).asBytes()); } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/FidoU2fAttestationStatementVerifier.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/FidoU2fAttestationStatementVerifier.java index 616153e14..ac3f1f2f1 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/FidoU2fAttestationStatementVerifier.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/FidoU2fAttestationStatementVerifier.java @@ -87,7 +87,7 @@ private static ByteArray getRawUserPublicKey(AttestationObject attestationObject try { pubkey = WebAuthnCodecs.importCosePublicKey(pubkeyCose); } catch (InvalidKeySpecException | NoSuchAlgorithmException e) { - throw ExceptionUtil.wrapAndLog(log, "Failed to decode public key: " + pubkeyCose.getHex(), e); + throw ExceptionUtil.wrapAndLog(log, "Failed to decode public key: " + pubkeyCose, e); } final ECPublicKey ecPubkey; 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 c0dc4a499..033b7a363 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishAssertionSteps.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishAssertionSteps.java @@ -24,13 +24,10 @@ package com.yubico.webauthn; -import static com.yubico.internal.util.ExceptionUtil.assure; +import static com.yubico.internal.util.ExceptionUtil.assertTrue; import COSE.CoseException; -import com.yubico.webauthn.FinishRegistrationSteps.Step18; -import com.yubico.webauthn.FinishRegistrationSteps.Step19; -import com.yubico.webauthn.FinishRegistrationSteps.Step20; -import com.yubico.webauthn.FinishRegistrationSteps.Step21; +import com.yubico.internal.util.OptionalUtil; import com.yubico.webauthn.data.AuthenticatorAssertionResponse; import com.yubico.webauthn.data.ByteArray; import com.yubico.webauthn.data.COSEAlgorithmIdentifier; @@ -116,7 +113,7 @@ public void validate() { .getAllowCredentials() .ifPresent( allowed -> { - assure( + assertTrue( allowed.stream().anyMatch(allow -> allow.getId().equals(response.getId())), "Unrequested credential ID: %s", response.getId()); @@ -128,24 +125,20 @@ public void validate() { class Step6 implements Step { private final Optional userHandle = - response - .getResponse() - .getUserHandle() - .map(Optional::of) - .orElseGet( - () -> - request.getUsername().flatMap(credentialRepository::getUserHandleForUsername)); + OptionalUtil.orElseOptional( + request.getUserHandle(), + () -> + OptionalUtil.orElseOptional( + response.getResponse().getUserHandle(), + () -> + request + .getUsername() + .flatMap(credentialRepository::getUserHandleForUsername))); private final Optional username = - request - .getUsername() - .map(Optional::of) - .orElseGet( - () -> - response - .getResponse() - .getUserHandle() - .flatMap(credentialRepository::getUsernameForUserHandle)); + OptionalUtil.orElseOptional( + request.getUsername(), + () -> userHandle.flatMap(credentialRepository::getUsernameForUserHandle)); private final Optional registration = userHandle.flatMap(uh -> credentialRepository.lookup(response.getId(), uh)); @@ -157,25 +150,35 @@ public Step7 nextStep() { @Override public void validate() { - assure( - request.getUsername().isPresent() || response.getResponse().getUserHandle().isPresent(), + assertTrue( + request.getUsername().isPresent() + || request.getUserHandle().isPresent() + || response.getResponse().getUserHandle().isPresent(), "At least one of username and user handle must be given; none was."); + if (request.getUserHandle().isPresent() + && response.getResponse().getUserHandle().isPresent()) { + assertTrue( + request.getUserHandle().get().equals(response.getResponse().getUserHandle().get()), + "User handle set in request (%s) does not match user handle in response (%s).", + request.getUserHandle().get(), + response.getResponse().getUserHandle().get()); + } - assure( + assertTrue( userHandle.isPresent(), "User handle not found for username: %s", request.getUsername(), response.getResponse().getUserHandle()); - assure( + assertTrue( username.isPresent(), "Username not found for userHandle: %s", request.getUsername(), response.getResponse().getUserHandle()); - assure(registration.isPresent(), "Unknown credential: %s", response.getId()); + assertTrue(registration.isPresent(), "Unknown credential: %s", response.getId()); - assure( + assertTrue( userHandle.get().equals(registration.get().getUserHandle()), "User handle %s does not own credential %s", userHandle.get(), @@ -184,7 +187,7 @@ public void validate() { final Optional usernameFromRequest = request.getUsername(); final Optional userHandleFromResponse = response.getResponse().getUserHandle(); if (usernameFromRequest.isPresent() && userHandleFromResponse.isPresent()) { - assure( + assertTrue( userHandleFromResponse.equals( credentialRepository.getUserHandleForUsername(usernameFromRequest.get())), "User handle %s in response does not match username %s in request", @@ -207,7 +210,7 @@ public Step8 nextStep() { @Override public void validate() { - assure( + assertTrue( credential.isPresent(), "Unknown credential. Credential ID: %s, user handle: %s", response.getId(), @@ -223,9 +226,9 @@ class Step8 implements Step { @Override public void validate() { - assure(clientData() != null, "Missing client data."); - assure(authenticatorData() != null, "Missing authenticator data."); - assure(signature() != null, "Missing signature."); + assertTrue(clientData() != null, "Missing client data."); + assertTrue(authenticatorData() != null, "Missing authenticator data."); + assertTrue(signature() != null, "Missing signature."); } @Override @@ -255,7 +258,7 @@ class Step10 implements Step { @Override public void validate() { - assure(clientData() != null, "Missing client data."); + assertTrue(clientData() != null, "Missing client data."); } @Override @@ -276,7 +279,7 @@ class Step11 implements Step { @Override public void validate() { - assure( + assertTrue( CLIENT_DATA_TYPE.equals(clientData.getType()), "The \"type\" in the client data must be exactly \"%s\", was: %s", CLIENT_DATA_TYPE, @@ -296,7 +299,7 @@ class Step12 implements Step { @Override public void validate() { - assure( + assertTrue( request .getPublicKeyCredentialRequestOptions() .getChallenge() @@ -318,7 +321,7 @@ class Step13 implements Step { @Override public void validate() { final String responseOrigin = response.getResponse().getClientData().getOrigin(); - assure( + assertTrue( OriginMatcher.isAllowed(responseOrigin, origins, allowOriginPort, allowOriginSubdomain), "Incorrect origin: " + responseOrigin); } @@ -354,7 +357,7 @@ class Step15 implements Step { @Override public void validate() { try { - assure( + assertTrue( Crypto.sha256(rpId) .equals(response.getResponse().getParsedAuthenticatorData().getRpIdHash()), "Wrong RP ID hash."); @@ -362,7 +365,7 @@ public void validate() { Optional appid = request.getPublicKeyCredentialRequestOptions().getExtensions().getAppid(); if (appid.isPresent()) { - assure( + assertTrue( Crypto.sha256(appid.get().getId()) .equals(response.getResponse().getParsedAuthenticatorData().getRpIdHash()), "Wrong RP ID hash."); @@ -385,7 +388,7 @@ class Step16 implements Step { @Override public void validate() { - assure( + assertTrue( response.getResponse().getParsedAuthenticatorData().getFlags().UP, "User Presence is required."); } @@ -407,7 +410,7 @@ public void validate() { .getPublicKeyCredentialRequestOptions() .getUserVerification() .equals(Optional.of(UserVerificationRequirement.REQUIRED))) { - assure( + assertTrue( response.getResponse().getParsedAuthenticatorData().getFlags().UV, "User Verification is required."); } @@ -428,7 +431,7 @@ class PendingStep16 implements Step { @Override public void validate() { - assure( + assertTrue( !credential.isBackupEligible().isPresent() || response.getResponse().getParsedAuthenticatorData().getFlags().BE == credential.isBackupEligible().get(), @@ -465,7 +468,7 @@ class Step19 implements Step { @Override public void validate() { - assure(clientDataJsonHash().size() == 32, "Failed to compute hash of client data"); + assertTrue(clientDataJsonHash().size() == 32, "Failed to compute hash of client data"); } @Override 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 fe127aa06..172da13dd 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishRegistrationSteps.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishRegistrationSteps.java @@ -24,11 +24,13 @@ package com.yubico.webauthn; -import static com.yubico.internal.util.ExceptionUtil.assure; +import static com.yubico.internal.util.ExceptionUtil.assertTrue; import static com.yubico.internal.util.ExceptionUtil.wrapAndLog; import COSE.CoseException; import com.upokecenter.cbor.CBORObject; +import com.yubico.internal.util.CertificateParser; +import com.yubico.internal.util.OptionalUtil; import com.yubico.webauthn.attestation.AttestationTrustSource; import com.yubico.webauthn.attestation.AttestationTrustSource.TrustRootsResult; import com.yubico.webauthn.data.AttestationObject; @@ -40,6 +42,7 @@ import com.yubico.webauthn.data.CollectedClientData; import com.yubico.webauthn.data.PublicKeyCredential; import com.yubico.webauthn.data.PublicKeyCredentialCreationOptions; +import com.yubico.webauthn.data.PublicKeyCredentialParameters; import com.yubico.webauthn.data.UserVerificationRequirement; import java.io.IOException; import java.security.InvalidAlgorithmParameterException; @@ -128,7 +131,7 @@ default RegistrationResult run() { class Step6 implements Step { @Override public void validate() { - assure(clientData() != null, "Client data must not be null."); + assertTrue(clientData() != null, "Client data must not be null."); } @Override @@ -147,7 +150,7 @@ class Step7 implements Step { @Override public void validate() { - assure( + assertTrue( CLIENT_DATA_TYPE.equals(clientData.getType()), "The \"type\" in the client data must be exactly \"%s\", was: %s", CLIENT_DATA_TYPE, @@ -166,7 +169,7 @@ class Step8 implements Step { @Override public void validate() { - assure(request.getChallenge().equals(clientData.getChallenge()), "Incorrect challenge."); + assertTrue(request.getChallenge().equals(clientData.getChallenge()), "Incorrect challenge."); } @Override @@ -182,7 +185,7 @@ class Step9 implements Step { @Override public void validate() { final String responseOrigin = clientData.getOrigin(); - assure( + assertTrue( OriginMatcher.isAllowed(responseOrigin, origins, allowOriginPort, allowOriginSubdomain), "Incorrect origin: " + responseOrigin); } @@ -212,7 +215,7 @@ public Step11 nextStep() { class Step11 implements Step { @Override public void validate() { - assure(clientDataJsonHash().size() == 32, "Failed to compute hash of client data"); + assertTrue(clientDataJsonHash().size() == 32, "Failed to compute hash of client data"); } @Override @@ -231,7 +234,7 @@ class Step12 implements Step { @Override public void validate() { - assure(attestation() != null, "Malformed attestation object."); + assertTrue(attestation() != null, "Malformed attestation object."); } @Override @@ -251,7 +254,7 @@ class Step13 implements Step { @Override public void validate() { - assure( + assertTrue( Crypto.sha256(rpId) .equals(response.getResponse().getAttestation().getAuthenticatorData().getRpIdHash()), "Wrong RP ID hash."); @@ -270,7 +273,7 @@ class Step14 implements Step { @Override public void validate() { - assure( + assertTrue( response.getResponse().getParsedAuthenticatorData().getFlags().UP, "User Presence is required."); } @@ -293,7 +296,7 @@ public void validate() { .flatMap(AuthenticatorSelectionCriteria::getUserVerification) .orElse(UserVerificationRequirement.PREFERRED) == UserVerificationRequirement.REQUIRED) { - assure( + assertTrue( response.getResponse().getParsedAuthenticatorData().getFlags().UV, "User Verification is required."); } @@ -322,13 +325,13 @@ public void validate() { .getCredentialPublicKey(); CBORObject publicKeyCbor = CBORObject.DecodeFromBytes(publicKeyCose.getBytes()); final int alg = publicKeyCbor.get(CBORObject.FromObject(3)).AsInt32(); - assure( + assertTrue( request.getPubKeyCredParams().stream() .anyMatch(pkcparam -> pkcparam.getAlg().getId() == alg), "Unrequested credential key algorithm: got %d, expected one of: %s", alg, request.getPubKeyCredParams().stream() - .map(pkcparam -> pkcparam.getAlg()) + .map(PublicKeyCredentialParameters::getAlg) .collect(Collectors.toList())); try { WebAuthnCodecs.importCosePublicKey(publicKeyCose); @@ -392,12 +395,12 @@ class Step19 implements Step { public void validate() { attestationStatementVerifier.ifPresent( verifier -> { - assure( + assertTrue( verifier.verifyAttestationSignature(attestation, clientDataJsonHash), "Invalid attestation signature."); }); - assure(attestationType() != null, "Failed to determine attestation type"); + assertTrue(attestationType() != null, "Failed to determine attestation type"); } @Override @@ -475,13 +478,22 @@ private Optional findTrustRoots() { atp -> attestationTrustSource.findTrustRoots( atp, - Optional.of( - attestation - .getAuthenticatorData() - .getAttestedCredentialData() - .get() - .getAaguid()) - .filter(aaguid -> !aaguid.equals(ZERO_AAGUID))))); + OptionalUtil.orElseOptional( + Optional.of( + attestation + .getAuthenticatorData() + .getAttestedCredentialData() + .get() + .getAaguid()) + .filter(aaguid -> !aaguid.equals(ZERO_AAGUID)), + () -> { + if (!atp.isEmpty()) { + return CertificateParser.parseFidoAaguidExtension(atp.get(0)) + .map(ByteArray::new); + } else { + return Optional.empty(); + } + })))); } } @@ -509,7 +521,7 @@ public Step21( @Override public void validate() { - assure( + assertTrue( allowUntrustedAttestation || attestationTrusted, "Failed to derive trust for attestation key."); } @@ -604,7 +616,7 @@ class Step22 implements Step { @Override public void validate() { - assure( + assertTrue( credentialRepository.lookupAll(response.getId()).isEmpty(), "Credential ID is already registered: %s", response.getId()); diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/PackedAttestationStatementVerifier.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/PackedAttestationStatementVerifier.java index 11055b963..ed513dbb4 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/PackedAttestationStatementVerifier.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/PackedAttestationStatementVerifier.java @@ -106,7 +106,7 @@ private boolean verifySelfAttestationSignature( throw new RuntimeException(e); } - final Long keyAlgId = + final long keyAlgId = CBORObject.DecodeFromBytes( attestationObject .getAuthenticatorData() @@ -115,7 +115,8 @@ private boolean verifySelfAttestationSignature( .getCredentialPublicKey() .getBytes()) .get(CBORObject.FromObject(3)) - .AsInt64(); + .AsNumber() + .ToInt64IfExact(); final COSEAlgorithmIdentifier keyAlg = COSEAlgorithmIdentifier.fromId(keyAlgId) .orElseThrow( @@ -123,7 +124,7 @@ private boolean verifySelfAttestationSignature( new IllegalArgumentException( "Unsupported COSE algorithm identifier: " + keyAlgId)); - final Long sigAlgId = attestationObject.getAttestationStatement().get("alg").asLong(); + final long sigAlgId = attestationObject.getAttestationStatement().get("alg").asLong(); final COSEAlgorithmIdentifier sigAlg = COSEAlgorithmIdentifier.fromId(sigAlgId) .orElseThrow( @@ -188,7 +189,7 @@ private boolean verifyX5cSignature( throw new IllegalArgumentException( "Packed attestation statement must have field \"alg\"."); } - ExceptionUtil.assure( + ExceptionUtil.assertTrue( algNode.isIntegralNumber(), "Field \"alg\" in packed attestation statement must be a COSEAlgorithmIdentifier."); final Long sigAlgId = algNode.asLong(); @@ -275,16 +276,16 @@ public boolean verifyX5cRequirements(X509Certificate cert, ByteArray aaguid) { final Set countries = CollectionUtil.immutableSet(new HashSet<>(Arrays.asList(Locale.getISOCountries()))); - ExceptionUtil.assure( + ExceptionUtil.assertTrue( getDnField("C", cert).filter(countries::contains).isPresent(), "Invalid attestation certificate country code: %s", getDnField("C", cert)); - ExceptionUtil.assure( + ExceptionUtil.assertTrue( getDnField("O", cert).filter(o -> !((String) o).isEmpty()).isPresent(), "Organization (O) field of attestation certificate DN must be present."); - ExceptionUtil.assure( + ExceptionUtil.assertTrue( getDnField("OU", cert).filter(ouValue::equals).isPresent(), "Organization Unit (OU) field of attestation certificate DN must be exactly \"%s\", was: %s", ouValue, @@ -293,12 +294,12 @@ public boolean verifyX5cRequirements(X509Certificate cert, ByteArray aaguid) { CertificateParser.parseFidoAaguidExtension(cert) .ifPresent( extensionAaguid -> { - ExceptionUtil.assure( + ExceptionUtil.assertTrue( Arrays.equals(aaguid.getBytes(), extensionAaguid), "X.509 extension \"id-fido-gen-ce-aaguid\" is present but does not match the authenticator AAGUID."); }); - ExceptionUtil.assure( + ExceptionUtil.assertTrue( cert.getBasicConstraints() == -1, "Attestation certificate must not be a CA certificate."); return true; 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 b2378f523..7c0daf12d 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 @@ -218,6 +218,8 @@ public class RelyingParty { *

  • {@link com.yubico.webauthn.data.PublicKeyCredentialParameters#ES256 ES384} *
  • {@link com.yubico.webauthn.data.PublicKeyCredentialParameters#ES256 ES512} *
  • {@link com.yubico.webauthn.data.PublicKeyCredentialParameters#RS256 RS256} + *
  • {@link com.yubico.webauthn.data.PublicKeyCredentialParameters#RS384 RS384} + *
  • {@link com.yubico.webauthn.data.PublicKeyCredentialParameters#RS512 RS512} * * * @see PublicKeyCredentialCreationOptions#getAttestation() @@ -232,7 +234,9 @@ public class RelyingParty { PublicKeyCredentialParameters.EdDSA, PublicKeyCredentialParameters.ES384, PublicKeyCredentialParameters.ES512, - PublicKeyCredentialParameters.RS256)); + PublicKeyCredentialParameters.RS256, + PublicKeyCredentialParameters.RS384, + PublicKeyCredentialParameters.RS512)); /** * If true, the origin matching rule is relaxed to allow any port number. @@ -427,6 +431,8 @@ private static List filterAvailableAlgorithms( break; case RS256: + case RS384: + case RS512: case RS1: KeyFactory.getInstance("RSA"); break; @@ -555,6 +561,7 @@ public AssertionRequest startAssertion(StartAssertionOptions startAssertionOptio return AssertionRequest.builder() .publicKeyCredentialRequestOptions(pkcro.build()) .username(startAssertionOptions.getUsername()) + .userHandle(startAssertionOptions.getUserHandle()) .build(); } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/StartAssertionOptions.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/StartAssertionOptions.java index 4c95d9f0a..465d1a0d8 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/StartAssertionOptions.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/StartAssertionOptions.java @@ -96,7 +96,7 @@ public class StartAssertionOptions { *

    The default is empty (absent). * * @see Client-side-resident + * href="https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#client-side-discoverable-public-key-credential-source">Client-side-discoverable * credential */ public Optional getUsername() { @@ -122,7 +122,7 @@ public Optional getUsername() { * @see #getUsername() * @see User Handle * @see Client-side-resident + * href="https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#client-side-discoverable-public-key-credential-source">Client-side-discoverable * credential */ public Optional getUserHandle() { @@ -185,7 +185,7 @@ public static class StartAssertionOptionsBuilder { * @see #userHandle(Optional) * @see #userHandle(ByteArray) * @see Client-side-resident + * href="https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#client-side-discoverable-public-key-credential-source">Client-side-discoverable * credential */ public StartAssertionOptionsBuilder username(@NonNull Optional username) { @@ -217,7 +217,7 @@ public StartAssertionOptionsBuilder username(@NonNull Optional username) * @see #userHandle(Optional) * @see #userHandle(ByteArray) * @see Client-side-resident + * href="https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#client-side-discoverable-public-key-credential-source">Client-side-discoverable * credential */ public StartAssertionOptionsBuilder username(String username) { @@ -247,7 +247,7 @@ public StartAssertionOptionsBuilder username(String username) { * @see User * Handle * @see Client-side-resident + * href="https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#client-side-discoverable-public-key-credential-source">Client-side-discoverable * credential */ public StartAssertionOptionsBuilder userHandle(@NonNull Optional userHandle) { @@ -279,7 +279,7 @@ public StartAssertionOptionsBuilder userHandle(@NonNull Optional user * @see #username(Optional) * @see #userHandle(Optional) * @see Client-side-resident + * href="https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#client-side-discoverable-public-key-credential-source">Client-side-discoverable * credential */ public StartAssertionOptionsBuilder userHandle(ByteArray userHandle) { 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 2b22fe7d7..ee051e770 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 @@ -82,7 +82,7 @@ static final class Attributes { | (1 << 3) // 3 Reserved | (0x3 << 8) // 9:8 Reserved | (0xF << 12) // 15:12 Reserved - | ((0xFFFFFFFF << 19) & ((1 << 32) - 1)) // 31:19 Reserved + | ((0xFFFFFFFF << 19) & ((1 << 31) | ((1 << 31) - 1))) // 31:19 Reserved ; } @@ -101,14 +101,14 @@ public boolean verifyAttestationSignature( ObjectNode attStmt = attestationObject.getAttestationStatement(); JsonNode verNode = attStmt.get("ver"); - ExceptionUtil.assure( + ExceptionUtil.assertTrue( 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( + ExceptionUtil.assertTrue( algNode != null && algNode.canConvertToLong(), "attStmt.alg must be set to an integer value, was: %s", algNode); @@ -119,7 +119,7 @@ public boolean verifyAttestationSignature( new IllegalArgumentException("Unknown COSE algorithm identifier: " + algNode)); JsonNode x5cNode = attStmt.get("x5c"); - ExceptionUtil.assure( + ExceptionUtil.assertTrue( x5cNode != null && x5cNode.isArray(), "attStmt.x5c must be set to an array value, was: %s", x5cNode); @@ -137,7 +137,7 @@ public boolean verifyAttestationSignature( final X509Certificate aikCert = x5c.get(0); JsonNode sigNode = attStmt.get("sig"); - ExceptionUtil.assure( + ExceptionUtil.assertTrue( sigNode != null && sigNode.isBinary(), "attStmt.sig must be set to a binary value, was: %s", sigNode); @@ -149,13 +149,13 @@ public boolean verifyAttestationSignature( } JsonNode certInfoNode = attStmt.get("certInfo"); - ExceptionUtil.assure( + ExceptionUtil.assertTrue( certInfoNode != null && certInfoNode.isBinary(), "attStmt.certInfo must be set to a binary value, was: %s", certInfoNode); JsonNode pubAreaNode = attStmt.get("pubArea"); - ExceptionUtil.assure( + ExceptionUtil.assertTrue( pubAreaNode != null && pubAreaNode.isBinary(), "attStmt.pubArea must be set to a binary value, was: %s", pubAreaNode); @@ -217,10 +217,12 @@ private void validateCertInfo( break; case ES384: + case RS384: expectedExtraData = Crypto.sha384(attToBeSigned); break; case ES512: + case RS512: expectedExtraData = Crypto.sha512(attToBeSigned); break; @@ -235,14 +237,14 @@ private void validateCertInfo( default: throw new UnsupportedOperationException("Signing algorithm not implemented: " + alg); } - ExceptionUtil.assure( + ExceptionUtil.assertTrue( 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( + ExceptionUtil.assertTrue( certInfo.attestedName.equals(pubArea.name()), "Incorrect certInfo.attestedName."); // Sub-step 5 handled by parsing above @@ -250,7 +252,7 @@ private void validateCertInfo( // 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( + ExceptionUtil.assertTrue( Crypto.verifySignature(aikCert, certInfo.getRawBytes(), sig, alg), "Incorrect TPM attestation signature."); @@ -287,7 +289,7 @@ private void verifyPublicKeysMatch(AttestationObject attestationObject, TpmtPubl signedCredentialPublicKey = kf.generatePublic(spec); } - ExceptionUtil.assure( + ExceptionUtil.assertTrue( Arrays.equals(credentialPubKey.getEncoded(), signedCredentialPublicKey.getEncoded()), "Signed public key in TPM attestation is not identical to credential public key in authData."); break; @@ -333,19 +335,19 @@ private void verifyPublicKeysMatch(AttestationObject attestationObject, TpmtPubl "Unsupported elliptic curve: " + params.curve_id); } - ExceptionUtil.assure( + ExceptionUtil.assertTrue( 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( + ExceptionUtil.assertTrue( 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( + ExceptionUtil.assertTrue( 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, @@ -382,7 +384,7 @@ private static TpmtPublic parse(byte[] pubArea) throws IOException { final int nameAlg = reader.readUnsignedShort(); final int attributes = reader.readInt(); - ExceptionUtil.assure( + ExceptionUtil.assertTrue( (attributes & Attributes.SHALL_BE_ZERO) == 0, "Attributes contains 1 bits in reserved position(s): 0x%08x", attributes); @@ -393,7 +395,7 @@ private static TpmtPublic parse(byte[] pubArea) throws IOException { final Parameters parameters; final Unique unique; - ExceptionUtil.assure( + ExceptionUtil.assertTrue( (attributes & Attributes.SIGN_ENCRYPT) == Attributes.SIGN_ENCRYPT, "Public key is expected to have the SIGN_ENCRYPT attribute set, attributes were: 0x%08x", attributes); @@ -408,7 +410,7 @@ private static TpmtPublic parse(byte[] pubArea) throws IOException { throw new UnsupportedOperationException("Signing algorithm not implemented: " + signAlg); } - ExceptionUtil.assure( + ExceptionUtil.assertTrue( reader.available() == 0, "%d remaining bytes in TPMT_PUBLIC buffer", reader.available()); @@ -472,12 +474,12 @@ static class TpmAlgHash { private void verifyX5cRequirements(X509Certificate cert, ByteArray aaguid) throws CertificateParsingException { - ExceptionUtil.assure( + ExceptionUtil.assertTrue( cert.getVersion() == 3, "Invalid TPM attestation certificate: Version MUST be 3, but was: %s", cert.getVersion()); - ExceptionUtil.assure( + ExceptionUtil.assertTrue( cert.getSubjectX500Principal().getName().isEmpty(), "Invalid TPM attestation certificate: subject MUST be empty, but was: %s", cert.getSubjectX500Principal()); @@ -505,26 +507,26 @@ private void verifyX5cRequirements(X509Certificate cert, ByteArray aaguid) } } } - ExceptionUtil.assure( + ExceptionUtil.assertTrue( 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( + ExceptionUtil.assertTrue( 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( + ExceptionUtil.assertTrue( cert.getBasicConstraints() == -1, "Invalid TPM attestation certificate: MUST NOT be a CA certificate, but was."); CertificateParser.parseFidoAaguidExtension(cert) .ifPresent( extensionAaguid -> { - ExceptionUtil.assure( + ExceptionUtil.assertTrue( 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."); }); @@ -546,13 +548,13 @@ private static class TpmsRsaParms implements Parameters { private static TpmsRsaParms parse(ByteInputStream reader) throws IOException { final int symmetric = reader.readUnsignedShort(); - ExceptionUtil.assure( + ExceptionUtil.assertTrue( 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( + ExceptionUtil.assertTrue( 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); @@ -560,7 +562,7 @@ private static TpmsRsaParms parse(ByteInputStream reader) throws IOException { reader.skipBytes(2); // key_bits is not used by this implementation int exponent = reader.readInt(); - ExceptionUtil.assure( + ExceptionUtil.assertTrue( 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 @@ -587,11 +589,11 @@ private static class TpmsEccParms implements Parameters { private static TpmsEccParms parse(ByteInputStream reader) throws IOException { final int symmetric = reader.readUnsignedShort(); final int scheme = reader.readUnsignedShort(); - ExceptionUtil.assure( + ExceptionUtil.assertTrue( symmetric == TPM_ALG_NULL, "ECC key is expected to have \"symmetric\" set to TPM_ALG_NULL, was: 0x%04x", symmetric); - ExceptionUtil.assure( + ExceptionUtil.assertTrue( scheme == TPM_ALG_NULL, "ECC key is expected to have \"scheme\" set to TPM_ALG_NULL, was: 0x%04x", scheme); @@ -663,14 +665,15 @@ private static TpmsAttest parse(byte[] certInfo) throws IOException { // Verify that magic is set to TPM_GENERATED_VALUE. // see https://w3c.github.io/webauthn/#sctn-tpm-attestation // verification procedure - ExceptionUtil.assure( + ExceptionUtil.assertTrue( 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); + ExceptionUtil.assertTrue( + type.equals(TPM_ST_ATTEST_CERTIFY), "type field is invalid: %s", type); // qualifiedSigner is not used by this implementation reader.skipBytes(reader.readUnsignedShort()); 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 b5f8e079d..60aacab0d 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/WebAuthnCodecs.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/WebAuthnCodecs.java @@ -184,6 +184,10 @@ static String getJavaAlgorithmName(COSEAlgorithmIdentifier alg) { return "SHA512withECDSA"; case RS256: return "SHA256withRSA"; + case RS384: + return "SHA384withRSA"; + case RS512: + return "SHA512withRSA"; case RS1: return "SHA1withRSA"; default: diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AttestedCredentialData.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AttestedCredentialData.java index 1d110d9d1..48d7873a8 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AttestedCredentialData.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AttestedCredentialData.java @@ -53,10 +53,7 @@ public class AttestedCredentialData { * The credential public key encoded in COSE_Key format, as defined in Section 7 of RFC 8152. */ - @NonNull - // TODO: verify requirements - // https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#sctn-attested-credential-data - private final ByteArray credentialPublicKey; + @NonNull private final ByteArray credentialPublicKey; @JsonCreator private AttestedCredentialData( diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorData.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorData.java index f15c7fbdd..169e4c5d7 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorData.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorData.java @@ -100,7 +100,7 @@ public class AuthenticatorData { /** Decode an {@link AuthenticatorData} object from a raw authenticator data byte array. */ @JsonCreator public AuthenticatorData(@NonNull ByteArray bytes) { - ExceptionUtil.assure( + ExceptionUtil.assertTrue( bytes.size() >= FIXED_LENGTH_PART_END_INDEX, "%s byte array must be at least %d bytes, was %d: %s", AuthenticatorData.class.getSimpleName(), @@ -150,12 +150,12 @@ private static VariableLengthParseResult parseAttestedCredentialData( final int CREDENTIAL_ID_LENGTH_INDEX = AAGUID_END; final int CREDENTIAL_ID_LENGTH_END = CREDENTIAL_ID_LENGTH_INDEX + 2; - ExceptionUtil.assure( + ExceptionUtil.assertTrue( bytes.length >= CREDENTIAL_ID_LENGTH_END, "Attested credential data must contain at least %d bytes, was %d: %s", CREDENTIAL_ID_LENGTH_END, bytes.length, - new ByteArray(bytes).getHex()); + new ByteArray(bytes)); byte[] credentialIdLengthBytes = Arrays.copyOfRange(bytes, CREDENTIAL_ID_LENGTH_INDEX, CREDENTIAL_ID_LENGTH_END); @@ -174,12 +174,12 @@ private static VariableLengthParseResult parseAttestedCredentialData( final int CREDENTIAL_PUBLIC_KEY_INDEX = CREDENTIAL_ID_END; final int CREDENTIAL_PUBLIC_KEY_AND_EXTENSION_DATA_END = bytes.length; - ExceptionUtil.assure( + ExceptionUtil.assertTrue( bytes.length >= CREDENTIAL_ID_END, "Expected credential ID of length %d, but attested credential data and extension data is only %d bytes: %s", CREDENTIAL_ID_END, bytes.length, - new ByteArray(bytes).getHex()); + new ByteArray(bytes)); ByteArrayInputStream indefiniteLengthBytes = new ByteArrayInputStream( diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorSelectionCriteria.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorSelectionCriteria.java index ec3566bd0..1a19d4dda 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorSelectionCriteria.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorSelectionCriteria.java @@ -100,6 +100,14 @@ public Optional getAuthenticatorAttachment() { *

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

    When this is set, {@link PublicKeyCredentialCreationOptions#toCredentialsCreateJson()} will + * also emit a + * requireResidentKey member for backwards compatibility with WebAuthn Level 1. + * It will be set to true if this is set to {@link ResidentKeyRequirement#REQUIRED + * REQUIRED} and false if this is set to anything else. When this is not set, a + * requireResidentKey member will not be emitted. + * * @see ResidentKeyRequirement * @see §5.4.6. @@ -112,6 +120,19 @@ public Optional getResidentKey() { return Optional.ofNullable(residentKey); } + /** + * For backwards compatibility with requireResidentKey. + * + * @see 5.4.4. + * Authenticator Selection Criteria (dictionary AuthenticatorSelectionCriteria) member + * requireResidentKey + */ + @JsonProperty + private Boolean isRequireResidentKey() { + return getResidentKey().map(rk -> rk == ResidentKeyRequirement.REQUIRED).orElse(null); + } + /** * Describes the Relying Party's requirements regarding user 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 f003121c5..4b9ca5803 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 @@ -48,6 +48,8 @@ public enum COSEAlgorithmIdentifier { ES384(-35), ES512(-36), RS256(-257), + RS384(-258), + RS512(-259), RS1(-65535); @JsonValue @Getter private final long id; diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/CollectedClientData.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/CollectedClientData.java index f1e7a392f..7a9f89279 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/CollectedClientData.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/CollectedClientData.java @@ -81,7 +81,7 @@ public CollectedClientData(@NonNull ByteArray clientDataJSON) throws IOException, Base64UrlException { JsonNode clientData = JacksonCodecs.json().readTree(clientDataJSON.getBytes()); - ExceptionUtil.assure( + ExceptionUtil.assertTrue( clientData != null && clientData.isObject(), "Collected client data must be JSON object."); this.clientDataJson = clientDataJSON; diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/Extensions.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/Extensions.java index 4fb9b37c6..db831a05c 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/Extensions.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/Extensions.java @@ -428,8 +428,10 @@ static Optional> parseAuthenticatorExtensionOutput(CBORObject cbo uvmEntry -> new UvmEntry( UserVerificationMethod.fromValue(uvmEntry.get(0).AsInt32Value()), - KeyProtectionType.fromValue(uvmEntry.get(1).AsInt16()), - MatcherProtectionType.fromValue(uvmEntry.get(2).AsInt16()))) + KeyProtectionType.fromValue( + uvmEntry.get(1).AsNumber().ToInt16IfExact()), + MatcherProtectionType.fromValue( + uvmEntry.get(2).AsNumber().ToInt16IfExact()))) .collect(Collectors.toList())); } else { return Optional.empty(); @@ -470,7 +472,7 @@ private static boolean validateAuthenticatorExtensionOutput(CBORObject extension } for (CBORObject i : entry.getValues()) { - if (!i.isIntegral()) { + if (!(i.isNumber() && i.AsNumber().IsInteger())) { log.debug("Invalid type for uvmEntry element: expected integer, was: {}", i.getType()); return false; } 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 c1ac9517a..a5f252c31 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 @@ -382,6 +382,8 @@ private static List filterAvailableAlgorithms( break; case RS256: + case RS384: + case RS512: case RS1: KeyFactory.getInstance("RSA"); break; @@ -419,6 +421,14 @@ private static List filterAvailableAlgorithms( Signature.getInstance("SHA256withRSA"); break; + case RS384: + Signature.getInstance("SHA384withRSA"); + break; + + case RS512: + Signature.getInstance("SHA512withRSA"); + break; + case RS1: Signature.getInstance("SHA1withRSA"); 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 e2e59d7b4..4848bcd45 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 @@ -100,6 +100,20 @@ private PublicKeyCredentialParameters( public static final PublicKeyCredentialParameters RS256 = builder().alg(COSEAlgorithmIdentifier.RS256).build(); + /** + * Algorithm {@link COSEAlgorithmIdentifier#RS384} and type {@link + * PublicKeyCredentialType#PUBLIC_KEY}. + */ + public static final PublicKeyCredentialParameters RS384 = + builder().alg(COSEAlgorithmIdentifier.RS384).build(); + + /** + * Algorithm {@link COSEAlgorithmIdentifier#RS512} and type {@link + * PublicKeyCredentialType#PUBLIC_KEY}. + */ + public static final PublicKeyCredentialParameters RS512 = + builder().alg(COSEAlgorithmIdentifier.RS512).build(); + public static PublicKeyCredentialParametersBuilder.MandatoryStages builder() { return new PublicKeyCredentialParametersBuilder.MandatoryStages(); } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/TokenBindingInfo.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/TokenBindingInfo.java index d267b0c42..13e400db2 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/TokenBindingInfo.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/TokenBindingInfo.java @@ -24,7 +24,7 @@ package com.yubico.webauthn.data; -import static com.yubico.internal.util.ExceptionUtil.assure; +import static com.yubico.internal.util.ExceptionUtil.assertTrue; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; @@ -56,12 +56,12 @@ public class TokenBindingInfo { @NonNull @JsonProperty("status") TokenBindingStatus status, @NonNull @JsonProperty("id") Optional id) { if (status == TokenBindingStatus.PRESENT) { - assure( + assertTrue( id.isPresent(), "Token binding ID must be present if status is \"%s\".", TokenBindingStatus.PRESENT); } else { - assure( + assertTrue( !id.isPresent(), "Token binding ID must not be present if status is not \"%s\".", TokenBindingStatus.PRESENT); 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 6053501c3..99eab5f0d 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/Generators.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/Generators.scala @@ -1,5 +1,6 @@ package com.yubico.webauthn +import com.yubico.scalacheck.gen.GenUtil.halfsized import com.yubico.webauthn.data.AssertionExtensionInputs import com.yubico.webauthn.data.AttestationType import com.yubico.webauthn.data.AuthenticatorAssertionResponse @@ -22,78 +23,86 @@ import scala.jdk.OptionConverters.RichOption object Generators { implicit val arbitraryAssertionResult: Arbitrary[AssertionResult] = Arbitrary( - for { - credentialResponse <- - arbitrary[PublicKeyCredential[ - AuthenticatorAssertionResponse, - ClientAssertionExtensionOutputs, - ]] - credential <- arbitrary[RegisteredCredential] - signatureCounterValid <- arbitrary[Boolean] - success <- arbitrary[Boolean] - username <- arbitrary[String] - } yield new AssertionResult( - success, - credentialResponse, - credential, - username, - signatureCounterValid, + halfsized( + for { + credentialResponse <- + arbitrary[PublicKeyCredential[ + AuthenticatorAssertionResponse, + ClientAssertionExtensionOutputs, + ]] + credential <- arbitrary[RegisteredCredential] + signatureCounterValid <- arbitrary[Boolean] + success <- arbitrary[Boolean] + username <- arbitrary[String] + } yield new AssertionResult( + success, + credentialResponse, + credential, + username, + signatureCounterValid, + ) ) ) implicit val arbitraryRegistrationResult: Arbitrary[RegistrationResult] = Arbitrary( - for { - credential <- - arbitrary[PublicKeyCredential[ - AuthenticatorAttestationResponse, - ClientRegistrationExtensionOutputs, - ]] - attestationTrusted <- arbitrary[Boolean] - attestationTrustPath <- generateAttestationCertificateChain - attestationType <- arbitrary[AttestationType] - } yield new RegistrationResult( - credential, - attestationTrusted, - attestationType, - Some(attestationTrustPath.asJava).toJava, + halfsized( + for { + credential <- + arbitrary[PublicKeyCredential[ + AuthenticatorAttestationResponse, + ClientRegistrationExtensionOutputs, + ]] + attestationTrusted <- arbitrary[Boolean] + attestationTrustPath <- generateAttestationCertificateChain + attestationType <- arbitrary[AttestationType] + } yield new RegistrationResult( + credential, + attestationTrusted, + attestationType, + Some(attestationTrustPath.asJava).toJava, + ) ) ) implicit val arbitraryRegisteredCredential: Arbitrary[RegisteredCredential] = Arbitrary( - for { - credentialId <- arbitrary[ByteArray] - userHandle <- arbitrary[ByteArray] - publicKeyCose <- arbitrary[ByteArray] - signatureCount <- arbitrary[Int] - } yield RegisteredCredential - .builder() - .credentialId(credentialId) - .userHandle(userHandle) - .publicKeyCose(publicKeyCose) - .signatureCount(signatureCount) - .build() + halfsized( + for { + credentialId <- arbitrary[ByteArray] + userHandle <- arbitrary[ByteArray] + publicKeyCose <- arbitrary[ByteArray] + signatureCount <- arbitrary[Int] + } yield RegisteredCredential + .builder() + .credentialId(credentialId) + .userHandle(userHandle) + .publicKeyCose(publicKeyCose) + .signatureCount(signatureCount) + .build() + ) ) implicit val arbitraryStartAssertionOptions : Arbitrary[StartAssertionOptions] = Arbitrary( - for { - extensions <- arbitrary[Option[AssertionExtensionInputs]] - timeout <- Gen.option(Gen.posNum[Long]) - usernameOrUserHandle <- arbitrary[Option[Either[String, ByteArray]]] - userVerification <- arbitrary[Option[UserVerificationRequirement]] - } yield { - val b = StartAssertionOptions.builder() - extensions.foreach(b.extensions) - timeout.foreach(b.timeout) - usernameOrUserHandle.foreach { - case Left(username) => b.username(username) - case Right(userHandle) => b.userHandle(userHandle) + halfsized( + for { + extensions <- arbitrary[Option[AssertionExtensionInputs]] + timeout <- Gen.option(Gen.posNum[Long]) + usernameOrUserHandle <- arbitrary[Option[Either[String, ByteArray]]] + userVerification <- arbitrary[Option[UserVerificationRequirement]] + } yield { + val b = StartAssertionOptions.builder() + extensions.foreach(b.extensions) + timeout.foreach(b.timeout) + usernameOrUserHandle.foreach { + case Left(username) => b.username(username) + case Right(userHandle) => b.userHandle(userHandle) + } + userVerification.foreach(b.userVerification) + b.build() } - userVerification.foreach(b.userVerification) - b.build() - } + ) ) def generateAttestationCertificateChain: Gen[List[X509Certificate]] = 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 ef93443fb..484399d24 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 @@ -140,6 +140,8 @@ object RegistrationTestDataGenerator extends App { td.Packed.BasicAttestation, td.Packed.BasicAttestationEdDsa, td.Packed.BasicAttestationRsa, + td.Packed.BasicAttestationRs384, + td.Packed.BasicAttestationRs512, td.Packed.BasicAttestationRs1, td.Packed.BasicAttestationWithoutAaguidExtension, td.Packed.BasicAttestationWithWrongAaguidExtension, @@ -150,6 +152,8 @@ object RegistrationTestDataGenerator extends App { td.Tpm.ValidEs384, td.Tpm.ValidEs512, td.Tpm.ValidRs256, + td.Tpm.ValidRs384, + td.Tpm.ValidRs512, td.Tpm.ValidRs1, ).zipWithIndex } { @@ -178,6 +182,8 @@ object RegistrationTestData { Packed.BasicAttestation, Packed.BasicAttestationEdDsa, Packed.BasicAttestationRsa, + Packed.BasicAttestationRs384, + Packed.BasicAttestationRs512, Packed.BasicAttestationRsaReal, Packed.SelfAttestation, Tpm.ValidEs256, @@ -185,6 +191,8 @@ object RegistrationTestData { Tpm.ValidEs384, Tpm.ValidEs512, Tpm.ValidRs256, + Tpm.ValidRs384, + Tpm.ValidRs512, RealExamples.WindowsHelloTpm.asRegistrationTestData, RealExamples.ThinkpadTpm.asRegistrationTestData, ) @@ -449,6 +457,56 @@ object RegistrationTestData { ) } + val BasicAttestationRs384: RegistrationTestData = new RegistrationTestData( + alg = COSEAlgorithmIdentifier.RS384, + attestationObject = + ByteArray.fromHex("bf68617574684461746159016849960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97634100000539000102030405060708090a0b0c0d0e0f0020c088fcbe4ba7f49b972e6cb5fc5819ede68bb6e6267b4a1832af45facff40517a40103033901012059010100c5e44335f0b4b5450eeec98576cd39d7b0b87ff76d330f9c0892fa9a1f7d378bdb2d8fb488c6dab3cbeba47a32ef696156fdc5d07f594ce8cd4d15f8053988b453468440fc3419531ff3605a025855497f36bba5d776660bea8d4a6f9c654b78f362be5bd1c669c2b49fd15fa2d751fb95a80138c75a1b3df18fe5ca6ad3185e24e7da6e0405f78a4353c33c9ebd48021dd2e6c2590311a59f8c3b62945aec3a570687e16a83ffed75a8fd2a12126461d70906185396457008cfe46514495875507d5c4f1aa8237b337cb5319ac5bd4808389990cc74281daf9237b49bcfc8bab24fa4953d6dad3347a5c623d9edd105af3e5d6767fde27c38ff06df6b4aee7d214301000163666d74667061636b65646761747453746d74bf63616c67390101637369675901009b18b1991728261445bede30204b7a86f92cfa5584b6c8895f00d92a6157b5b9da4336ff2c72a2a7e26f2ae6cc9f2e32bf4daa4599b116bff0bc8af9e2cd57f3329e1614eaece02d852bf660d3c419d42d7429225f2d6b3e9a4b68376070c17ffcd85cf1430641731bbe49204088779d8e586ebbd1f64a3edad071ade30ae8ecfb90c8b3fb5ff471d9fe71cbc1ea488c1a263ec9d438c505c91cb4a17d8a1ee5ae47c88876cbf20c74e55196b99bec606ade7ba82b64793eaa91f1207d70c80eba19589ec0e423c886cc616457420dc1b3c04f2b5791772a3c023a246c4d3efcca0169933ee6130cc4af37d5456372e7f6553cc07221aa741566af78744f4ab0637835638159034c3082034830820230a003020102020209b0300d06092a864886f70d01010c050030673123302106035504030c1a59756269636f20576562417574686e20756e6974207465737473310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b3009060355040613025345301e170d3138303930363137343230305a170d3138303931333137343230305a30673123302106035504030c1a59756269636f20576562417574686e20756e6974207465737473310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b300906035504061302534530820122300d06092a864886f70d01010105000382010f003082010a0282010100bbf836d1dd6e2627f2a80a457f49adae909846863ef3eba0cc1a605813b93ef2b683d896a1dcaf74a7db3463c9f0d5dbec2ed30f5edb857e0dbd34576caa1716c43910f4325484a1a6dcf3bf63f2343ce46b88f00210e0cb0b2fbc3065ec8ad302807ce431f4edb9b9b07bdee10fe1fbcbf41451e6f6b04ea996215c2667b80a04fa376c5d17e953b70af71fe8be7dd7f4a845bdf4b2fa654531debefa99d66fe0c1a04c924bcc3be146da68c2de3a97dded885277a84f450e6669a95f1b901057f1ac6d91fa701b0a8170fa415fa28aa4fa242fe4eaea8fc8c44c8258301a25cda21bb9ddd37addd7e2154f43811036045e5b856453e85078401f0659593d950203010001300d06092a864886f70d01010c050003820101000514466869a9656754f0f81121dda9efa3723882dc1600d4b2c6516022c4873be034c8ffd939e191da50b4c7d7198e37c7596fa879343cb8406e0c92b8978078e02b0ce3c7e6e4421b665f6b63b0e566dc758e17f5b9cc6c42d58845740857cb5f488f6f6f81bcd71d9f46346f17cf61021275e577c114588a1fbc8de5f5ebfe97c2267f99c437323891efce43e989e0189dfac9dd331103adce542906794cc59e361526d8ca7016652be3843c270673019ecc296072bb763d8d3cd815a5d1185471e363cdd37edbb812219acab23e90cc57002465e8fe4434a33b10b4e116634c94d16455ef5d887cdcfa424f47ff59bc2aef7d0588a3af60c90c823f61d7a6ffff"), + clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","type":"webauthn.create","tokenBinding":{"status":"supported"}}""", + privateKey = Some( + ByteArray.fromHex("308204be020100300d06092a864886f70d0101010500048204a8308204a40201000282010100c5e44335f0b4b5450eeec98576cd39d7b0b87ff76d330f9c0892fa9a1f7d378bdb2d8fb488c6dab3cbeba47a32ef696156fdc5d07f594ce8cd4d15f8053988b453468440fc3419531ff3605a025855497f36bba5d776660bea8d4a6f9c654b78f362be5bd1c669c2b49fd15fa2d751fb95a80138c75a1b3df18fe5ca6ad3185e24e7da6e0405f78a4353c33c9ebd48021dd2e6c2590311a59f8c3b62945aec3a570687e16a83ffed75a8fd2a12126461d70906185396457008cfe46514495875507d5c4f1aa8237b337cb5319ac5bd4808389990cc74281daf9237b49bcfc8bab24fa4953d6dad3347a5c623d9edd105af3e5d6767fde27c38ff06df6b4aee7d0203010001028201001742701febede18c7f67d3a9eb3fcdf7ab1ed473a99321d78e2e706423255d9d03a3044c0cf38a8b2d81c1f057024ad99516f8e43bc3ac4584b3f5cc1419221747de76f7086dbb3848fe1b2a193276bbcfc708214304f89397fb096fbaeb3106c35cacd13003e93468748c70783c64b7746cadd015a662a3523c3e9f1f1536bccd0dc7139c5c813443f654305924d3296b6fcf01e1539217a1ce0c634164c42d40617048cbb81a401d0a457f1230957f40c3f16f58414d4de96921aa353dfb294b94b65cafbf05bdb46895a423121a7918dbde57a3e1b30c4a40bff4086597b7558f1fd535e5c4cc8e0a46a80700e43d4afada920266d2bf0eb456a4a1fe250502818100ede3f68c1142913405c4a7bdd5550f123c75d29405e342e61d22e40a9c547799f8729f18b3d1066af094178ce4fb2cfd885239487ed96ce94d770018754dc963ef8bb13bbec9b33905f1d030a2b2b7778c326454b3cc7e2c54e46042e50ed7da12da02505ec817dd5889ab3355a6bd406d4d1c0705ffd6ec4c04d88b683887a702818100d4f4cc350d2e2942c28a7b9e3f83a9d2faf22f24d78bc53407d29614365f013470d55b995afa29978c14c786a76f8fee9e2b692ad5e11e57ea349914758025f9316b5f5290e2f12e771cd2917aacd80b76a43a5e7c60d84de67e0b6556da75f2b163f943952262e8c42d659b157a94e0b3905c4ce8a0c943c5a6057c1aa45d3b02818100ac013a510861d34f84242f0d096519229c68acbbae8e25def08e3bc8984452be176ff92d09474796a720ccee68da5c2b6d17d6a75e60a3690543d7e3d75d4912632fe41448dcda238ef2cb0f7f742d47d92cce72981671dc67fd40c4dd8e1ff063d511fb3eacfae46692142167fac9b7fdcfd54616c667862f690991b2e7bcdd02818100b4ff93890bb8ce4cf5b86a35285aa9beae97a546350591091615008611685247d61721918867d36e011bb0325ca14fbe4a252f6fbef565aae75ee935206158e52201d6b5007c42ed7143c81cea1d7a4ad3fde5b6651493043301b281e17e307da4140aca4c393bc406e966d09742e6c2cd1bc7b77e891a4745f843f52557c9fd0281800ff1fabad5587a4089746854833fb3ef15296c15c13959b6bf0d4f01777525f8c5fb87d9c3cd7b0c63fd182930e9ee9eaad35b5c361f3c2553629dc358b1b591651bfc2104f068fa6175044c40b910aebf7ce6f58ce9b24224182059e2838e20e213523d1fe05e2c49f999d355da66107aa398b844c8993c8e1b7f7ae9cc8026") + ), + attestationCertChain = List( + RegistrationTestDataGenerator.importAttestationCa( + "MIIDSDCCAjCgAwIBAgICCbAwDQYJKoZIhvcNAQEMBQAwZzEjMCEGA1UEAwwaWXViaWNvIFdlYkF1dGhuIHVuaXQgdGVzdHMxDzANBgNVBAoMBll1YmljbzEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjELMAkGA1UEBhMCU0UwHhcNMTgwOTA2MTc0MjAwWhcNMTgwOTEzMTc0MjAwWjBnMSMwIQYDVQQDDBpZdWJpY28gV2ViQXV0aG4gdW5pdCB0ZXN0czEPMA0GA1UECgwGWXViaWNvMSIwIAYDVQQLDBlBdXRoZW50aWNhdG9yIEF0dGVzdGF0aW9uMQswCQYDVQQGEwJTRTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALv4NtHdbiYn8qgKRX9Jra6QmEaGPvProMwaYFgTuT7ytoPYlqHcr3Sn2zRjyfDV2+wu0w9e24V+Db00V2yqFxbEORD0MlSEoabc879j8jQ85GuI8AIQ4MsLL7wwZeyK0wKAfOQx9O25ubB73uEP4fvL9BRR5vawTqmWIVwmZ7gKBPo3bF0X6VO3Cvcf6L591/SoRb30svplRTHevvqZ1m/gwaBMkkvMO+FG2mjC3jqX3e2IUneoT0UOZmmpXxuQEFfxrG2R+nAbCoFw+kFfooqk+iQv5Orqj8jETIJYMBolzaIbud3Tet3X4hVPQ4EQNgReW4VkU+hQeEAfBllZPZUCAwEAATANBgkqhkiG9w0BAQwFAAOCAQEABRRGaGmpZWdU8PgRId2p76NyOILcFgDUssZRYCLEhzvgNMj/2TnhkdpQtMfXGY43x1lvqHk0PLhAbgySuJeAeOArDOPH5uRCG2Zfa2Ow5WbcdY4X9bnMbELViEV0CFfLX0iPb2+BvNcdn0Y0bxfPYQISdeV3wRRYih+8jeX16/6XwiZ/mcQ3MjiR785D6YngGJ36yd0zEQOtzlQpBnlMxZ42FSbYynAWZSvjhDwnBnMBnswpYHK7dj2NPNgVpdEYVHHjY83Tftu4EiGayrI+kMxXACRl6P5ENKM7ELThFmNMlNFkVe9diHzc+kJPR/9ZvCrvfQWIo69gyQyCP2HXpg==", + "RSA", + "MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC7+DbR3W4mJ/KoCkV/Sa2ukJhGhj7z66DMGmBYE7k+8raD2Jah3K90p9s0Y8nw1dvsLtMPXtuFfg29NFdsqhcWxDkQ9DJUhKGm3PO/Y/I0PORriPACEODLCy+8MGXsitMCgHzkMfTtubmwe97hD+H7y/QUUeb2sE6pliFcJme4CgT6N2xdF+lTtwr3H+i+fdf0qEW99LL6ZUUx3r76mdZv4MGgTJJLzDvhRtpowt46l93tiFJ3qE9FDmZpqV8bkBBX8axtkfpwGwqBcPpBX6KKpPokL+Tq6o/IxEyCWDAaJc2iG7nd03rd1+IVT0OBEDYEXluFZFPoUHhAHwZZWT2VAgMBAAECggEBAJkG6EpOhOAXYZugURfheb5GBVJU9GdMCupfBtRtqkAvBJut9mPr8AN+rByoqLyivpo3PKikxv6Ussa4F/xlNMraEMNWqqrYF2prMx07VvFkKWnKX+qupvNmNgR1OmUqV8MPq51zdj0bGKsvDTIY5hdB4YGxc+CdhEzX5mzI72OJjAQHbw4mOUMjepPPU1sdp/DFtwgv5UN+AhFc5fbIkiQv/AROiKXTegi03xuUGzdoarnSw6t6qdiD/JCqj4vW9bQFm4daCqMbC910ngXxpo4HdD4qbGf8U7gUjHIVwXtc7c7UNDe9QL7sKMdbmtqDxj/Yl13AopLXZ1IC5ZUYbsECgYEA/B7m43Fc4aBknSStNZcdOvYV89bxpGZ3gedSsESp1OoSCsP3IyrzmADK3Q88nFa2AyJKQY58vX6h8VRbR6DzN2TpR5Ui7qB2RLRuK9xcIcILxLtAg4KUJovu0VJLNjDlCqrrMzYKWT71I8rgf9/M6rwDYopYGyk7ohoju8EjUY0CgYEAvtyfUKtfLWNcuieAFzPZA0KLLmWI0ng8xWo9LeP6WKc9hYZZMPY8+E3ZS5EcINfIeMuoU/9MMMBDfsgsoOLdQBNsbX8m66h2qt8KQWTLZptBiSOoBRH3rZIf1xuBC5nwD5Loig2CQDjrvv5ccaEO7t9NA4Z7UPXY9HdAYIC/ZikCgYBkVGfNWu97Wjiv0EidauVW8VcLEh5XLe+g4k0lmC19bSiA4DsY457MfoQ8NDQKgvcriBnEvM8nGZ2YS9mHR6WCBcZPlimwjGqELMkq6yY+yNmmEF4791q9fDItWnJTvmFnPV0bpAW6PjOPasysFoOVZfxy2lr1dBMnDv/pV5KWgQKBgQCT1rjg952Fvs14tFgXoOWcDNNaYPOWc+Q+1ogFH+4u9XxGDUbREisv+r0yN3ieSAbU6ou8ZKhTqtmdPtiy1oeitmjqd+9h4t/og1OiS7zyAZjF7YScSMqc++8F5BwVLGwy5AyTwtr9fBm/m69npOW4SeeRr32dvJEM68JF/fRD4QKBgQDzljYrmgfa3yzST7IVoaF5ZmmuoIBKQhsh+tglpYdRBzT0jr3vk0AWdm2DUEPvnSoroqBMA5CYMV248XT0jvyZRFY87HOnJkFdagf5i5bGMbMIEqpuwxU9zMXUM+dnz8+vjmWwHnM/eQy4PWV/FoXG/Ll40bRX6admbJix0Oe/og==", + ) + ), + ) { + override def regenerate() = + TestAuthenticator.createBasicAttestedCredential( + keyAlgorithm = COSEAlgorithmIdentifier.RS384, + attestationMaker = AttestationMaker.packed( + AttestationSigner.selfsigned(COSEAlgorithmIdentifier.RS384) + ), + ) + } + + val BasicAttestationRs512: RegistrationTestData = new RegistrationTestData( + alg = COSEAlgorithmIdentifier.RS512, + attestationObject = + ByteArray.fromHex("bf68617574684461746159016849960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97634100000539000102030405060708090a0b0c0d0e0f0020144ff435d36e8b657e2143922addec5614af740a0f7acc1aaa9f46a85dc73df2a40103033901022059010100b4380cf9bb40185d3f5b141c2a8daec8076e91a9c281f3aa7a981f08375f7528bb9d0fd3348b2f00999d515a24ad345491836f2822535e21391feb4a0a948b277a04b8a6af8e89ad437481aa53a376b97e5b11896f75ceaaa032eb985147f2e4bd4a729e9b64d1b863fa852193012184488ae3634322b59fb63ffac42da9786060e6bb6e6878209377e22a9903104743f7ceb35aaaaf6868072368b5cf65d4f78b805f849e249c9c7d1f7751663caa58bbe978c677358a3c1cec5abb21e44b8950416f77ee0afae12968252d49c2a9d16a9844cb4bb111492140517d0aa9214fa85e3012423dd46f8f4d8e649020bbafe8f1e73762ce1cb2e733c51b180cb537214301000163666d74667061636b65646761747453746d74bf63616c673901026373696759010062313cd97a9d81f54fb88b423218f3632897fb833800339cbdb2f153d87033f7ba81d32467f3784af15e5f12633e2fefcded70c3f560922aca9cf9e2978e7b5a13498b4758feb33d621e5fde54f74fa1f26874fdc4e492e12f0ca6f012cce9916023635292ea902d5dbf12cecf29165200e35a2739afd312263a93fa4e0c29454fee6e5c2ed84c9e9cc1c8220d47274648477ea68642d9a0cb385fa132b95dae73ff900f0fc5e08cd584e01855fc36a6fbfc4facfe3fd4afd1418771ac831c204b33c73b1543c1f309597f97c0af9f710ff2ae40c5cf4144b2c5f2e62b59ed3ce04a6a714300ce9ed8343f8f760f3ab415f1e6666bbce50c8e2fdf2b2c9ab6eb637835638159034c3082034830820230a00302010202020cca300d06092a864886f70d01010d050030673123302106035504030c1a59756269636f20576562417574686e20756e6974207465737473310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b3009060355040613025345301e170d3138303930363137343230305a170d3138303931333137343230305a30673123302106035504030c1a59756269636f20576562417574686e20756e6974207465737473310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b300906035504061302534530820122300d06092a864886f70d01010105000382010f003082010a0282010100d19dec1547c6169901207d39de3147951fe95aa932790fd06e87731f0c74c7a30f2ca34b5996f48222eb4813a6b7c06f3920fdec4b27737356160c138091b971ffd11d55f4411f65c5017de8e1ff0792ef326ae9e561ee2bb628aefba26be8af16219b61c9f9c6bf54c6979c8eea63cc41530a2c02065a890f667a685a456f9c1d6a81e64cf42aba1f82503063004cc7d461c205a8307a4dc59c4d3a499081031e4a3d189ec29ae437b68c62db8efdd9f6fd04da29c184acbce1bf093a29111e5eca22175df4535952584b3e909157e7692c76cd106cf9125d001a519285354798ad4e7efa5e45b1f34e5fb869ada9243874cc759fa7a9d584c2025c14c8cb710203010001300d06092a864886f70d01010d050003820101008b7d7fce57afb896444250e8d099f96c84e3363627c938fc15b6d2a1a5514874d39ced9cb1d86be91b53ef139df2953604293aa30a25a387de12efb5d4b877bf7bd1a1e334a848dcceebe679ce05440e6063069b42d8666298389bdc20dd0c5b3afe353723212404eebe2e4b95a9a75dafa9e9dabba5f7fc19d7f2941988a8ac70e1b812f826528dfe7c002275b5765448d2246779d6cf201bb2976df1dd2c1461f268706fe9a5ad33fd65d58c6107b0fbc7d3da2226ee9fb6304ab381f9342223dfed894999313f1c013fdd432a2ade171cb47562d9d4a8a2a6d33eebbab70934e249dce03e0b87fdbf33f1a183fc697c9c843cc949350a547213a87d75d100ffff"), + clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","type":"webauthn.create","tokenBinding":{"status":"supported"}}""", + privateKey = Some( + ByteArray.fromHex("308204bd020100300d06092a864886f70d0101010500048204a7308204a30201000282010100b4380cf9bb40185d3f5b141c2a8daec8076e91a9c281f3aa7a981f08375f7528bb9d0fd3348b2f00999d515a24ad345491836f2822535e21391feb4a0a948b277a04b8a6af8e89ad437481aa53a376b97e5b11896f75ceaaa032eb985147f2e4bd4a729e9b64d1b863fa852193012184488ae3634322b59fb63ffac42da9786060e6bb6e6878209377e22a9903104743f7ceb35aaaaf6868072368b5cf65d4f78b805f849e249c9c7d1f7751663caa58bbe978c677358a3c1cec5abb21e44b8950416f77ee0afae12968252d49c2a9d16a9844cb4bb111492140517d0aa9214fa85e3012423dd46f8f4d8e649020bbafe8f1e73762ce1cb2e733c51b180cb5370203010001028201006b07e6dce2127ce5d45cb922c93b0014982558a923759e4b1f27fd3619fcbd4e05ae8fd975993bbef57c72f6405605803c337ceeaf0428213f15efdd374f651d7ae016f217cd6582db4ef43b3e1514cbb179507ef90d54420d86705933dcb12a9c28fccda9a844cd67c33f11e386866b53d1f89dd91f62128a609103b5c2b2543939ba84a7bd88f876433754c24b72f3c50cdae49e43283b486515a5ca287ab18b4f68c0e5a0fc5b08ffa619cdb2e4bec2e4aa0884dc27f3e5c2e3d22122e68e0ec5214cf3b5f1ffe4915c692b9ad7e225ce111ed91807b8584d3ff84c66a55471fddde411ddb7cf131e529beee2b38590805db780a599c317e5e4b17d56dd9102818100f7d0541ba1e580a1281f294153095013cad88f9d4d41586ad48849a641349ddd797b6a73c6c690c29570a745ca10c8babdb714671cce3c0b5c1c2e531adc6ee1750c4610b2271547fa87956ae994c53a239ce0fbf62caf9c25003cd7ac9b39bebc93223e0218d532877a56da897beae8542045cc1be8bdbaff5e49bfb44cdf0502818100ba2c18daa8fcabfba1484a19ae000304ec6f3a2f4d4058e6d156cbe7c3a9e0416dbd514027d236c2d613df1fc03c8b9006762f31b37b4167661eeb4c473e88bc9afac1565cfb27ecabcc204c2eaa63ffc0a22edb61d64d710da14dcaca2026d1a8ad42836aeca52d68f9a142a3c533040a10c5518eb65e00e7d073f47eaca00b0281801fcfd98c368b3ca8f37a94943331a5daf4963a516a2272543c7646661646c7e12f801d5941722a112097f69129f05fa441486851184c8d3eb413560b0b0eb319342a6030327e7be7e28c572d03513ac44ce00dadaa9b6febae804a4f317437c47976b5d599f550210d6d320b19cd1389c18ae70adda651fcd85d65403bc8067502818010407336fb537b4bef0b59749e6cdfd69931287a229b40677dd4bede0f858fcf065e656e5d4b8b7e3ca3e571671da1ed43b323718a4273362c82fc755f2ec54ef99474362ecdb9f17e19c6a3ffdaddf9e07e07eb1cc25166521347b0312ed754ac0ddbe58efaf37c6052925237ebaa056b3f858a161433668ed5f29960497f7b02818100a1e9ff8b24499fe1db468cc0377b30bf35788214639eb1ab54801b77d5e9b27ec196cb3cbcaebf9ab57282cdbb811bab96773b40bc5856c3ed6bcd5743cde5fa897e5733939cddc445dc9590a753be7df06dfeaaebc7365758696d1cd2a546a5e828aeda960c812cb41178caa4e0fcf9f295e595b74d3dca4ba742ae6593d666") + ), + attestationCertChain = List( + RegistrationTestDataGenerator.importAttestationCa( + "MIIDSDCCAjCgAwIBAgICDMowDQYJKoZIhvcNAQENBQAwZzEjMCEGA1UEAwwaWXViaWNvIFdlYkF1dGhuIHVuaXQgdGVzdHMxDzANBgNVBAoMBll1YmljbzEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjELMAkGA1UEBhMCU0UwHhcNMTgwOTA2MTc0MjAwWhcNMTgwOTEzMTc0MjAwWjBnMSMwIQYDVQQDDBpZdWJpY28gV2ViQXV0aG4gdW5pdCB0ZXN0czEPMA0GA1UECgwGWXViaWNvMSIwIAYDVQQLDBlBdXRoZW50aWNhdG9yIEF0dGVzdGF0aW9uMQswCQYDVQQGEwJTRTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANGd7BVHxhaZASB9Od4xR5Uf6VqpMnkP0G6Hcx8MdMejDyyjS1mW9IIi60gTprfAbzkg/exLJ3NzVhYME4CRuXH/0R1V9EEfZcUBfejh/weS7zJq6eVh7iu2KK77omvorxYhm2HJ+ca/VMaXnI7qY8xBUwosAgZaiQ9memhaRW+cHWqB5kz0KrofglAwYwBMx9RhwgWoMHpNxZxNOkmQgQMeSj0YnsKa5De2jGLbjv3Z9v0E2inBhKy84b8JOikRHl7KIhdd9FNZUlhLPpCRV+dpLHbNEGz5El0AGlGShTVHmK1OfvpeRbHzTl+4aa2pJDh0zHWfp6nVhMICXBTIy3ECAwEAATANBgkqhkiG9w0BAQ0FAAOCAQEAi31/zlevuJZEQlDo0Jn5bITjNjYnyTj8FbbSoaVRSHTTnO2csdhr6RtT7xOd8pU2BCk6owolo4feEu+11Lh3v3vRoeM0qEjczuvmec4FRA5gYwabQthmYpg4m9wg3QxbOv41NyMhJATuvi5LlamnXa+p6dq7pff8GdfylBmIqKxw4bgS+CZSjf58ACJ1tXZUSNIkZ3nWzyAbspdt8d0sFGHyaHBv6aWtM/1l1YxhB7D7x9PaIibun7YwSrOB+TQiI9/tiUmZMT8cAT/dQyoq3hcctHVi2dSooqbTPuu6twk04knc4D4Lh/2/M/Ghg/xpfJyEPMlJNQpUchOofXXRAA==", + "RSA", + "MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDRnewVR8YWmQEgfTneMUeVH+laqTJ5D9Buh3MfDHTHow8so0tZlvSCIutIE6a3wG85IP3sSydzc1YWDBOAkblx/9EdVfRBH2XFAX3o4f8Hku8yaunlYe4rtiiu+6Jr6K8WIZthyfnGv1TGl5yO6mPMQVMKLAIGWokPZnpoWkVvnB1qgeZM9Cq6H4JQMGMATMfUYcIFqDB6TcWcTTpJkIEDHko9GJ7CmuQ3toxi24792fb9BNopwYSsvOG/CTopER5eyiIXXfRTWVJYSz6QkVfnaSx2zRBs+RJdABpRkoU1R5itTn76XkWx805fuGmtqSQ4dMx1n6ep1YTCAlwUyMtxAgMBAAECggEBANBdxSHaOOSZr28WTAG8xsVL9XEzo4KH388fQaZpgWQ5iIn8wJgL4H3ELFF3h1A9L9KAIylSA6NV0QsVcgVp1gemHb6lhKl/hnBw7TIkBJkIzFE3yc1ErbYx2vsmE+xkXjcHrSdl2K5h3umSKARApneRr/P6jwC12my+l4tHwKIRpMEO9Bxj5bTLp81uGTOT68B3nOiB9UJQ3Anb5gDTWalSnRZIZy8oS4cpfFAGrN7NE9Amm4JaK6FRzTAD/Z2nuCmOZBAzlx8qQJhr9bJa3DANwhDJux2lPpqldukpOd2N5eHXXNMQ85qlcber74o2TOqtRXP5fER07GAN99oaImECgYEA/4ca3YzJChGdVCTHTCllzHqpyLAT8+nv4hVjQp+H/RQc6ZkzDDRweMCQlfnvtDywT9H6kRHsZM1Px+GYomEeHaHEMU4xZAaKFuny7ohjx8nxHFdSYCNfnmq9WFowAxN61O2a8o7zXv+A7OpRzrKP+g+hDnrikASyqat211VMQN0CgYEA0gEYkAjr9FJFcQoyly6/sCjIDnWkNyPUa7+4ko1FWB2UDa6eaq/eSMrefg4OcIviije1KcEhXAeU1+ciLrI2jdSxcnIcRikbd8+oAVHzZPTOKy4nP0QKoBulvTN/8/Zu715qF9lmrZfa0jQLYs1dp16ZeRSHcaxYpjUBlLO4oaUCgYBld5jLcSRDw0reJtyc+bNaxzq0XncN3E9NT4Di68ZsUJhKinMi3Y/r40uGwoDU6WR5zb/Z62wbewu7K3IYyMfUrG/jxFEIjzA2eR/maHJ221HLF0G2u1U06t3VP7rg/dNAyjlFKE6r4nmnmkRx96YEfkBOJ63f0n2/sj62s0BcYQKBgQC3zV3CMwzRenBsz5AX4kLD2+29OhnQaPuyksro+dyHktvSXdMpbWQQMf8qNQNOXiCY+MkHEpIwCjKsBRBV7oTw/geRM26rua7g3k8dWKy+38TS5kJTjSn/mDMntbt3u2i8+NXCqfTEWvSaphKRF02w/4sz/lPNmhq83gfULriaQQKBgQCu/gBexP0ImPy7O9MsOE9Zyi4FpOFB4X3KbAUjaev6grxrKo/cxc3M/1GsZ4DNQq4PrmA0l6yEp0FJD70v2OLsxtxNkHTKiMWb2cvNr1IwmYRUW3YGW6N+VCXpXrri5NSNMTeuMvfj2j60Hu64rUJn/MoAo1UGJpJ6FT9vCjvHsQ==", + ) + ), + ) { + override def regenerate() = + TestAuthenticator.createBasicAttestedCredential( + keyAlgorithm = COSEAlgorithmIdentifier.RS512, + attestationMaker = AttestationMaker.packed( + AttestationSigner.selfsigned(COSEAlgorithmIdentifier.RS512) + ), + ) + } + val BasicAttestationRs1: RegistrationTestData = new RegistrationTestData( alg = COSEAlgorithmIdentifier.RS1, attestationObject = @@ -867,6 +925,74 @@ object RegistrationTestData { ) } + val ValidRs384: RegistrationTestData = new RegistrationTestData( + alg = COSEAlgorithmIdentifier.RS384, + attestationObject = + ByteArray.fromHex("bf68617574684461746159016849960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97634100000539000102030405060708090a0b0c0d0e0f0020c28bda3318d7721b2d62f1ac5bc3ba6cfa634dce5e1613c42c54e9c2920533dda40103033901012059010100a086d9533f5cd8550dd2a5a1207b3c18845cc20dcaeb40c5a148c2a1ab3b1d34c73f7b2069b39de1e22032eb56077bb09f2025d93d6dbdf66cf200227268caf0de22f6d4b68c36aef28df970bba05bfc9c43073dc780d61ce76e55d02810ef9f835988a3e86cce1b9af830497f47176f9fbe1ed924c0ccccbdb582850c4d39a2bbefa9ee59296e247c684a49f1cbc0df144b3cd7355f732b0da221b397438584fd860a8f243595290a2a26ce7244c9d1a52f669a14ed92ac253b68fb37d489c3d0b467e9755a08409c1000afc5b46d71999c9453f3c0a168e28e950f871e67ad977a99fc5c12a6d732d3d646fae44cdb12ba8bec46347a993d9059ebf9ddfdab214301000163666d746374706d6761747453746d74bf63616c6739010163736967590100559308d2dd2e8fa7bb4e0c11f904e3dbe6f5f695dd59a47ede3fc3f4f8c532aa19db498215ea90fa6aa1c383620da9ec321918aacd61df83d1f6602e4369392df2d2763e0895d62fcd62c8c7769cbc3e77c04f46e195e01df35beaa4e760e6806b471d231cde499959c5323fa0fb9c2292fecb9fbce73c2647d8092250f2a03b9c67d5d5bef65240f08b80e0d278d98a6fe1e4738e4e46526aaf48aeac11c91673a37be6f912eae880815ca63f968991692c43f0c668e53608ac1086a8c892915f4541a22e84b9abafedc2e70e9443bdd3375204192d8ab01cfba2652e270672e61ad3a8c274ee9e296c29d514b3f5ff52b23367f52a1429e89aacf6bc0e810963783563825903913082038d30820275a003020102020211f0300d06092a864886f70d01010c0500306a3126302406035504030c1d59756269636f20576562417574686e20756e6974207465737473204341310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b3009060355040613025345301e170d3138303930363137343230305a170d3138303931333137343230305a300030820122300d06092a864886f70d01010105000382010f003082010a028201010080d4fcffce24bcde9b675454a95f42d9de18a98680c8919c388b58510e9412adc58c3576f4604a3cf3a4335bb318e6b2bc7e8f4d314b269948bf2fb76ce698d0b117a059281d8aff63860372111cd27b1b917f6719a5db9d24654fcef58ba10617c47381bc18729a90db38f9722eb326ef59ee9a59ca388409b1413ddb51afe5949ef5b588d7926c40f27fed80e8912b461dcce555726400dd75620da2822975f7e9920e0e711872c333af43429dadde27a3114a6b4d37927846df77cf2fe35dc94851fb753ca3371faab3a33cf6ee573922223863ddc1d5dd89bec4c66ee3cd54837905972632bdad6492262eb815c7579a6f677f878f311aa51278734f1aa70203010001a381a63081a33021060b2b0601040182e51c01010404120410000102030405060708090a0b0c0d0e0f30690603551d110101ff045f305da45b305931573014060567810502010c0b69643a30303030303030303014060567810502030c0b69643a30303030303030303029060567810502020c20544553545f59756269636f5f6a6176612d776562617574686e2d73657276657230130603551d250101ff0409300706056781050803300d06092a864886f70d01010c050003820101008dcb1be0feeafd6d2b06959868df474fee3b78087919163eaf6bfce38143653a4bbbe5ef1634149786d1765a0fd4ca5348fd84198025fcf6442a18e97f9e0b6d669b7f49ba4ade87dc6e2584da9ac1b5072b7f29a211178f5717730d7306a9bdc8dbf0a4a9fbc94b15d39e6b12661ebfac3c29bbe5e0d154fc833f15e41717e49133656fd130bd63bee22c4e7e54474442eed20158d8a98e861cf3e55742696e5d4e899ef7a63b06c75ef1460caac67d2883ff55d1fbf791db1eae3ae2c685c3207dba8fce74a50419c874821d94245bed7c22d84097407f3f74327c155a7fd8caaa33fbceb95a4907315e3bd09599ee682b425130dba69824d5755273a0a069590367308203633082024ba003020102020210cc300d06092a864886f70d01010c0500306a3126302406035504030c1d59756269636f20576562417574686e20756e6974207465737473204341310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b3009060355040613025345301e170d3138303930363137343230305a170d3138303931333137343230305a306a3126302406035504030c1d59756269636f20576562417574686e20756e6974207465737473204341310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b300906035504061302534530820122300d06092a864886f70d01010105000382010f003082010a028201010090bd7c9cfcccffa596fd45a4586d14906a4ff94826756b0c1856ea3f1ae151ae95156c3584c3cc08bdbd866a8291735b27fefec11848766cf964645efc47452ea59a13b6ae2c77a6a2b37e0c74ff9af5226d3a12848e7be84d809cf81cf1b46e0af5d7b1ce8745dc8572c60f79a331405dc98530d62c8be77e14a9da8e440a4a14192b2eae1d4bdf714728c11b9d3eb82b8d6371ab0a0a295d7d691f9432f4ab859133d1cf312949fdcbe2138d2b7e4399b477243eaf84063d16afecaddbaed16d915a8740eace0a88c0faab3db51433187c5fffe3077249dbd8021c0f3e8950cec31d46c7d6c4aa200c00ac70d7ba583ee0dfa9877374c9981fa5d1c3d7c90b0203010001a3133011300f0603551d130101ff040530030101ff300d06092a864886f70d01010c0500038201010007f0ebe81b798b51e85a6e4eae0b725c46fcd7b65bf30322980e39bd0595b84e381413ae51bc9a7072c8944012edaa41a5f40a28e06916bb8cf8946962f240352ed3c57fc03eef87f0d832196677b46b1ee5f40358a9a24637a637078338b333a75f084f8912fd3e4d1a3469934b8510b9e8ed0b5a762e82c1b8d7306208b8a43b8aa4adc15104da5ca45c4de158094d588a600c48642ef6a008aeb1f8c4f36d1067e75fd06c1b568c1d2ab299f0a8cee0092dce2cd8f2bbf1bd06e9fcc429da23bafcbae8e2946ef390532ba1112b46e7d64e2a30706655d815b62da3f3e94bb386ef3d6e57a9df3f154deb87ca588b394cdc6ecf1bb0d5ced92f8382693d736863657274496e666f5889ff5443478017000000304474eddbe2aaad0976688da49bb2427d2d18fd545d7abf10c699ce19c8760a5a3a4e734eb62428defd21b88daca43f8b000000000000000011111111222222223300000000000000000032000c9f655881ca92c820f85e04e5403a5e4e8b72cc3ef68f5d0f35fb5dd47a073160671851f641932b9aa95bc96024156dae00006376657263322e3067707562417265615901170001000c00040000000000100014080000010001010100a086d9533f5cd8550dd2a5a1207b3c18845cc20dcaeb40c5a148c2a1ab3b1d34c73f7b2069b39de1e22032eb56077bb09f2025d93d6dbdf66cf200227268caf0de22f6d4b68c36aef28df970bba05bfc9c43073dc780d61ce76e55d02810ef9f835988a3e86cce1b9af830497f47176f9fbe1ed924c0ccccbdb582850c4d39a2bbefa9ee59296e247c684a49f1cbc0df144b3cd7355f732b0da221b397438584fd860a8f243595290a2a26ce7244c9d1a52f669a14ed92ac253b68fb37d489c3d0b467e9755a08409c1000afc5b46d71999c9453f3c0a168e28e950f871e67ad977a99fc5c12a6d732d3d646fae44cdb12ba8bec46347a993d9059ebf9ddfdabffff"), + clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","type":"webauthn.create","tokenBinding":{"status":"supported"}}""", + privateKey = Some( + ByteArray.fromHex("308204be020100300d06092a864886f70d0101010500048204a8308204a40201000282010100a086d9533f5cd8550dd2a5a1207b3c18845cc20dcaeb40c5a148c2a1ab3b1d34c73f7b2069b39de1e22032eb56077bb09f2025d93d6dbdf66cf200227268caf0de22f6d4b68c36aef28df970bba05bfc9c43073dc780d61ce76e55d02810ef9f835988a3e86cce1b9af830497f47176f9fbe1ed924c0ccccbdb582850c4d39a2bbefa9ee59296e247c684a49f1cbc0df144b3cd7355f732b0da221b397438584fd860a8f243595290a2a26ce7244c9d1a52f669a14ed92ac253b68fb37d489c3d0b467e9755a08409c1000afc5b46d71999c9453f3c0a168e28e950f871e67ad977a99fc5c12a6d732d3d646fae44cdb12ba8bec46347a993d9059ebf9ddfdab0203010001028201010097b579d47c3091df28362904733f245783586aa9405a3f17c7ca8ceedf75f9af349321194bec4dccf9b936864502c379f3991d4c070b1d19b472ad7fe0a27b1152ceb679e79ff1da3b2fc44b2f776917fed23618c3e055fa711a4c8d72203766886b6880879bb4da500639146cee520ed368899cec682de55d711a4e0587426cb1c20c0d9c019de56317604002bfd501f96218dac96fba922c418fd812c3a3103931ac04db859d1a18125cfa448dc02c83c365ea991ddce45bb1175aa4f348c4a75d24f084f39929a40c91bb2c782681e48679cf0c1e635c588e8139658d74c83a2e5c2a530b7216de6dc0b2398354d3a6547ca7d4ce353d90ecd5fbeb124aa902818100df274427e772a8ecb7c76557575cc744d213d161e4ea396488ae30adfc35eda523f5cc11a9afe05cd6c918820f7ce287d5fea801d70c5d9f1dbb78ef8b495706af247f6ac0a0ce4f2cc189168cf853a41e027e43ceebf4e4c470b66a21b80d5556981163855cba88a6f632e622bcb9043a44521874361673c97a7c4e949cad1d02818100b827b91b4fc55a52fd566106944471ef25369ac26b24e1379d12476cab605db75e2fd9154f86c62ff7cae534039e04b740e232a8f5e3a98eba4b12cd94461121b9a4206f7ce320ea575ebea879ff463073f323b13a8746f2778e6a15d00abd0a13e13d0af80247a445861bb47de95461bdff04f758ff500e3b766d377b7103670281804690b028be33afdf4b2e2e89b4028eb0e08d8bc49d12c41b5a6d5acf69d5d3d448cecd3d389f791f627c2cd7d3f5f5dc667b24bd903744d3b01f3c5ae37cc99c3f7e171cb6d522e83e8ae4c2d0c92609dbc386120338f233f53a7f34887d1f1a414bcd13df7437384733cb5ca2d772da3762ab63383c725522fd2c99dcbcbeb102818069e3155179edbd40e8c0292bf246e4c8203aa483d3bdb1ee1b57ae4ff2be87446f58cdd6ae128d94794365c521ab5384d73ef8e823f292c529a30f1dbbfb09d0bd807cd1fe1a4f0bcfceff8bba122916a52511c9cf20878fd564c2e4e5e9b6c6bba59046e551d245c76014401501fbedf3a45603af5da677788360cb3d243f5302818100d8e0387a2fd83c82190026e8795b6f8834853126bdee577aa896873ec5c4712cf871a9444d3bdd752481fb710b18129b6a2adf30f51c8d2337f58d974cd1a43720dae6b35401a87bc7803d2cf1774981ffdef01419dc1e7eeb65b24e27cd61ff73a27023cba8cec21a3269f327d8de8faf9cf9b4c74e5fe63329e65e602e5eb7") + ), + attestationCertChain = List( + RegistrationTestDataGenerator.importAttestationCa( + "MIIDjTCCAnWgAwIBAgICEfAwDQYJKoZIhvcNAQEMBQAwajEmMCQGA1UEAwwdWXViaWNvIFdlYkF1dGhuIHVuaXQgdGVzdHMgQ0ExDzANBgNVBAoMBll1YmljbzEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjELMAkGA1UEBhMCU0UwHhcNMTgwOTA2MTc0MjAwWhcNMTgwOTEzMTc0MjAwWjAAMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAgNT8/84kvN6bZ1RUqV9C2d4YqYaAyJGcOItYUQ6UEq3FjDV29GBKPPOkM1uzGOayvH6PTTFLJplIvy+3bOaY0LEXoFkoHYr/Y4YDchEc0nsbkX9nGaXbnSRlT871i6EGF8RzgbwYcpqQ2zj5ci6zJu9Z7ppZyjiECbFBPdtRr+WUnvW1iNeSbEDyf+2A6JErRh3M5VVyZADddWINooIpdffpkg4OcRhywzOvQ0Kdrd4noxFKa003knhG33fPL+NdyUhR+3U8ozcfqrOjPPbuVzkiIjhj3cHV3Ym+xMZu481Ug3kFlyYyva1kkiYuuBXHV5pvZ3+HjzEapRJ4c08apwIDAQABo4GmMIGjMCEGCysGAQQBguUcAQEEBBIEEAABAgMEBQYHCAkKCwwNDg8waQYDVR0RAQH/BF8wXaRbMFkxVzAUBgVngQUCAQwLaWQ6MDAwMDAwMDAwFAYFZ4EFAgMMC2lkOjAwMDAwMDAwMCkGBWeBBQICDCBURVNUX1l1Ymljb19qYXZhLXdlYmF1dGhuLXNlcnZlcjATBgNVHSUBAf8ECTAHBgVngQUIAzANBgkqhkiG9w0BAQwFAAOCAQEAjcsb4P7q/W0rBpWYaN9HT+47eAh5GRY+r2v844FDZTpLu+XvFjQUl4bRdloP1MpTSP2EGYAl/PZEKhjpf54LbWabf0m6St6H3G4lhNqawbUHK38pohEXj1cXcw1zBqm9yNvwpKn7yUsV055rEmYev6w8Kbvl4NFU/IM/FeQXF+SRM2Vv0TC9Y77iLE5+VEdEQu7SAVjYqY6GHPPlV0Jpbl1OiZ73pjsGx17xRgyqxn0og/9V0fv3kdserjrixoXDIH26j850pQQZyHSCHZQkW+18IthAl0B/P3QyfBVaf9jKqjP7zrlaSQcxXjvQlZnuaCtCUTDbppgk1XVSc6CgaQ==", + "RSA", + "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCA1Pz/ziS83ptnVFSpX0LZ3hiphoDIkZw4i1hRDpQSrcWMNXb0YEo886QzW7MY5rK8fo9NMUsmmUi/L7ds5pjQsRegWSgdiv9jhgNyERzSexuRf2cZpdudJGVPzvWLoQYXxHOBvBhympDbOPlyLrMm71numlnKOIQJsUE921Gv5ZSe9bWI15JsQPJ/7YDokStGHczlVXJkAN11Yg2igil19+mSDg5xGHLDM69DQp2t3iejEUprTTeSeEbfd88v413JSFH7dTyjNx+qs6M89u5XOSIiOGPdwdXdib7Exm7jzVSDeQWXJjK9rWSSJi64FcdXmm9nf4ePMRqlEnhzTxqnAgMBAAECggEAMZtRi+JBjSQeLKxxKQKQSDnXvzcWUaSXxcIKELQPWh3lSjawBispisy59jih/r2eJyyIW03WxRcSxuNFur4UK491LH4ID1AdRKIuo3ZpZbaXh+/JsDuIE7sW86MaM1iecvpnC5Z0x3QywObwTgIjY6OYOmLenhoi5WSGXZ4clyDAjq6lz/f6njX4nLN9biNH0nPwgxapbFaQ2p6Qw49WFztkDXgw4YQA9CALYFcOL/fUauOIaWKAoFSXRwfiLQQUwGaPlGVOGGJnvQDrTSFnUqqNXqH9t/2zqO9xomjCgXRU0b/zba3zsYR0J0hYUh0YoHO/Hk1AFs2VrF/FqKEIsQKBgQC/FCI1hkxvm7C2Rt/ByDVEdSC98Hmoau0N9z6q1qor/EZVeKTKgYVBczbu0QCWfL2WLJ93qGuENZDUOBuh5ueUO/tMXmqfaperUdWWk7L6ElA/inxiNVKq1v6KHFOQqBNVJDNAmkcHZ6gWnFVaHjXmxBEvBKSQGwAWFmJJuhqfOQKBgQCsmrC4HRqybjHsHSzZRd3dcgbmKiLtmdbw032wiyPiv6Vut7nZ0NPmiPWqaxyZYtX16vI4kEVZ8IluasjnsaIPcZKIgAI4NDmkQZGQ4DOgeUcRTpFN2sHufGxmmf+tM7v2fBC7Q0gnAnfM31Hsz8n/dg/qc05jbDiZ5aS/yamo3wKBgQCC8phTGBtv7UGYWU/k7IDczmxG3vNw4P5eMM/Iol5y0GufDZPZmBOre/rshV0ixI/kx+XtSgWM0GzVkzIUrTqNUuHwP1BQuesBJI78p3HjgQNv2EdPwn1JyRcdrTXzj8vX8HwTTOdagsYl4LN5k/Salkm0cDka7PYNLP/kyN6PuQKBgFKZDCxvMRFmDlnRdF7dQljwckC+tUxCrEs+yg0r6JZf48jh/vwvJNhTfkx5SYxVcdJnBlbvI2Dw7LN8Qnwt00HUtazApU9EHrlt7z0HLW2D2/B6SqqZHukDfdRzqZi3AyHnKRKUFfklAzN1Qv0ySpYHZ4Jof4Cwjz2GWZq15Iy9AoGAX9mbT8QvC8/vLnxmvgz98tsDVarT6Tx7wcEMgyNC/2CUaPUf8P8rkP/Zfym/nTC8Ew/c5N5nLSivU/sPeLjpSQ/cFGZXXysTsVARSeQAvm4UvQy9+b6YoKWl/IOFWISp7TvKjpH3mPiJ4iX37xI7JeGcpUhFbj/q89NNTQMuVJo=", + ), + RegistrationTestDataGenerator.importAttestationCa( + "MIIDYzCCAkugAwIBAgICEMwwDQYJKoZIhvcNAQEMBQAwajEmMCQGA1UEAwwdWXViaWNvIFdlYkF1dGhuIHVuaXQgdGVzdHMgQ0ExDzANBgNVBAoMBll1YmljbzEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjELMAkGA1UEBhMCU0UwHhcNMTgwOTA2MTc0MjAwWhcNMTgwOTEzMTc0MjAwWjBqMSYwJAYDVQQDDB1ZdWJpY28gV2ViQXV0aG4gdW5pdCB0ZXN0cyBDQTEPMA0GA1UECgwGWXViaWNvMSIwIAYDVQQLDBlBdXRoZW50aWNhdG9yIEF0dGVzdGF0aW9uMQswCQYDVQQGEwJTRTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJC9fJz8zP+llv1FpFhtFJBqT/lIJnVrDBhW6j8a4VGulRVsNYTDzAi9vYZqgpFzWyf+/sEYSHZs+WRkXvxHRS6lmhO2rix3pqKzfgx0/5r1Im06EoSOe+hNgJz4HPG0bgr117HOh0XchXLGD3mjMUBdyYUw1iyL534UqdqORApKFBkrLq4dS99xRyjBG50+uCuNY3GrCgopXX1pH5Qy9KuFkTPRzzEpSf3L4hONK35DmbR3JD6vhAY9Fq/srduu0W2RWodA6s4KiMD6qz21FDMYfF//4wdySdvYAhwPPolQzsMdRsfWxKogDACscNe6WD7g36mHc3TJmB+l0cPXyQsCAwEAAaMTMBEwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQwFAAOCAQEAB/Dr6Bt5i1HoWm5OrgtyXEb817Zb8wMimA45vQWVuE44FBOuUbyacHLIlEAS7apBpfQKKOBpFruM+JRpYvJANS7TxX/APu+H8NgyGWZ3tGse5fQDWKmiRjemNweDOLMzp18IT4kS/T5NGjRpk0uFELno7Qtadi6CwbjXMGIIuKQ7iqStwVEE2lykXE3hWAlNWIpgDEhkLvagCK6x+MTzbRBn51/QbBtWjB0qspnwqM7gCS3OLNjyu/G9Bun8xCnaI7r8uujilG7zkFMroRErRufWTiowcGZV2BW2LaPz6Uuzhu89blep3z8VTeuHyliLOUzcbs8bsNXO2S+Dgmk9cw==", + "RSA", + "MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCQvXyc/Mz/pZb9RaRYbRSQak/5SCZ1awwYVuo/GuFRrpUVbDWEw8wIvb2GaoKRc1sn/v7BGEh2bPlkZF78R0UupZoTtq4sd6ais34MdP+a9SJtOhKEjnvoTYCc+BzxtG4K9dexzodF3IVyxg95ozFAXcmFMNYsi+d+FKnajkQKShQZKy6uHUvfcUcowRudPrgrjWNxqwoKKV19aR+UMvSrhZEz0c8xKUn9y+ITjSt+Q5m0dyQ+r4QGPRav7K3brtFtkVqHQOrOCojA+qs9tRQzGHxf/+MHcknb2AIcDz6JUM7DHUbH1sSqIAwArHDXulg+4N+ph3N0yZgfpdHD18kLAgMBAAECggEARzbD+iWz41Cg4oqCWvOIe6mjIZ6rNXhu4gbZiCT8mYjRV1H/XwxK9j4M4vbCuTfNsPcYdv8wn/vsFMYBqhSS6GmYTnzCY6SXO1Qe/5gNLzvPLXi2JbxJILoJPrIg45eH0SK2doiMLAZdLmRettVhZS7/+OVXa2GGi5U1IRCAT6L3LoIxq1YycV4sFIl51B5TNIluqm8VHejlQ2mlc0TthpLLhvwWFQvLp3+qt5Il67MXTexEFTUMjCECCidqR5JM67rt5lzJKwMEi1GqtLd23A1p6raEyVXfS8wQZL4SRzu1JklE7nUM4nKukEFUNWpUUp/SYXqycjSnpWDYii+QQQKBgQDOiz4JcjGvDd0c0QdiAeobhgW5BcC5i85WKupTuQGPwLjpZuQJKnasMkXpf0sq6Gg3O8W89psj9qzNPnfRaY67li7Ymo34WLVGg7+UdDPl/7JN8SR6jPlTdH96iem2hLGnJQ8uLi5Ay3Fpj9/GVQY7xiYKWyTWRUGq+qbv/Y+FIQKBgQCzZcpm6z483F8IWWmBGUDRHjNx5Th2tIA9A0E0eNCsIMJ+EuBZCRqxpg487QsLCuOLz5NCGTSWKkZQ3XU9HynzKaDZY3zwuqw0YfConpmIjYnwmfaq2/4bKC22PTeVvhb+ghkG05U6YWmolgkGM9zlpMyb7VXrd5QbNSPdCuJcqwKBgGVu2nuXGjFHFLTHLuIB4K+9pOfVnG1C7IVCtCuDqvGnCuiNACZENV2hntXqDsc2tZ+Seiyvy0bhKMHvELbGKTOUsNLtLBWvsu67WwWT/7zPUwiWCX0p6HQEvWo3epCJIBneyFK8mTh28O6PmqzzKomGaT4ivrd/8Zz/VaZ8ltQhAoGATXmlWqM3grMtO37ZyI4uZuFjGEoFA4baZv8T1uRiQiP3utjOGMWMyLHNLlS00zUFpiikEQSvqDZjnaK2cgoWZNRSie+kUpZbrlepxjiQV9/Ada8YTxuo9vN4Il73tWydo5Zt1nvj042kQtFg3lPhjy+HycNKuuEujj152olLzvcCgYBNKJCD1w40qWy51usqrFgYLA0X8dBGc4yv1AEJVknGl/bfhlwovc82PgzEGLq9fXjKHQEeQtY+K56erFpCrLBU7JPVtfck9LX+HGN4D6LD7ycTIQVtA3AwW/b58OQqIqULO4++Vc3BvI/qoX5oBnnpXn7XnZUbtUSJF/4GxGJ6Vw==", + ), + ), + ) { + override def regenerate() = + TestAuthenticator.createBasicAttestedCredential( + keyAlgorithm = COSEAlgorithmIdentifier.RS384, + attestationMaker = AttestationMaker.tpm( + AttestationSigner.ca( + alg = COSEAlgorithmIdentifier.RS384, + certSubject = new X500Name(Array.empty[RDN]), + certExtensions = tpmCertExtensions, + ) + ), + ) + } + + val ValidRs512: RegistrationTestData = new RegistrationTestData( + alg = COSEAlgorithmIdentifier.RS512, + attestationObject = + ByteArray.fromHex("bf68617574684461746159016849960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97634100000539000102030405060708090a0b0c0d0e0f0020720f0707397ade996822ef5474c9a796b1a89113fc7d79521e7008f414e6948ea4010303390102205901010099116dd8d7264698788f7916ceab8950ddef91a6627aef32f6fb682cb9d581ba4d979c22ee99b456e518399af900dc41b712086a85fa1f57b14e5a7e19c94c283024125f43ee03e62efd10470172cb890c3fddecef2f32937b74172175b66351691d49d441ebb6305d02fa844a2e0566c9e96aac07d28cf91a4d4c975c03f630a606a43537cf48e661aa16d8a035e302f12ca4d529ed8ac82c57b811dfa7fef0a84d1e8d3b27d3cd0cc140f45c82a737239f470bd737b0c52202173b37941d106ee456f6853a4adb12c5b42006f055808a256b96aaeb7a3c44ad2c03f11484735e603da10c1e7d1bbc29648c30ea38c4bbd4a9cb78b939b5a58b2d8ff8a8ba27214301000163666d746374706d6761747453746d74bf63616c67390102637369675901004a0200672a612871d63d6c5ac4483d044d8af21ea2e18864a1e49fddaeeddc505b74e332a76119e5eaecdeb1bf8fc9504d3f0e2c09d3c83a9357315d201b67332562030aa6a8a3ffc6573f05a12b595b69834f6412a4e1ccd8209d8742703e2e0637ad2ed0eaf8030bf29cb1b1344a5e2e9b316c84930bd65bae7ef8fea18913ee300db3e99965532f7f062024fc4424dee6e583b635b67d130e12947fba7dbcad8213d1c9a37e7abf1ee145ac84e54f02208cf5411fed5e8119f3302b8f4f9ef010c5c037f72704fe4653d2c7431df0e94f34854c5d8337e7f267b1a3e7ecce40b683fb1b2a859a92396cfc673e2bd1b72dc2fbb2f91ed5542e838e31a8801463783563825903913082038d30820275a00302010202021ba4300d06092a864886f70d01010d0500306a3126302406035504030c1d59756269636f20576562417574686e20756e6974207465737473204341310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b3009060355040613025345301e170d3138303930363137343230305a170d3138303931333137343230305a300030820122300d06092a864886f70d01010105000382010f003082010a0282010100935b5d593d9f8f6eec753e61735c7228915d78e5e4ce715437cedbd6c7a684acfbc01807e6f3762453ba48a29613a766db7aeac1682d2e23def0a9d7e2366caeee90f447188b55036a956bea104e9a4d7075da6312853f4ec3a57a04ddf31aaf505ec93249ff3324bc93053a88322c6a51901439c25c6b615e451567dee94d937afd2dbfd6406875eb174ee0e00d9716de98ee232c2d5fa6024391dd7e5eca424afb8daae8c26d65523296a891e0aaa7df792b9de7dc94d250ddc85a8c562bd331cd7616e07b208e281722110526e2eeb28a830b6ca051599368ee5ebd1306a4398f1f8f214c410b7e24bff8ca7193774454747e3cd6b4d0ba7c361981662b210203010001a381a63081a33021060b2b0601040182e51c01010404120410000102030405060708090a0b0c0d0e0f30690603551d110101ff045f305da45b305931573014060567810502010c0b69643a30303030303030303014060567810502030c0b69643a30303030303030303029060567810502020c20544553545f59756269636f5f6a6176612d776562617574686e2d73657276657230130603551d250101ff0409300706056781050803300d06092a864886f70d01010d0500038201010000a2dc0579f375ba042eeaf34837b8aefd0e43e228263f93da51ab3e991faefa282290b496004aa60b6454bfc1ffcfd004e84be4a71a5069b99d6eefad41629a7b8ba5b576ad954bd50a232b4f69f7d7bf3ad1882d6f2602e3508df6a670df11c9afe773493a94b004795e622f7448c56e4e45c11aa1a20a5244d76559c08d0c53e9886d470724cabd9a4f4d7a21fa37059d28a9e1604998312b5820888167406265ca6b6e39087216f9601d1eb3071947319e87451e9af1077dbf5e91a23623091051371c942698f2ad831c437f3ab9fcf1e94c1728fa201d86d353adfea9aec127255e0ca9b95e3d676d4feb6f6d327ee8bfa88d5f5df42aca2b23644ab010590367308203633082024ba00302010202021ed2300d06092a864886f70d01010d0500306a3126302406035504030c1d59756269636f20576562417574686e20756e6974207465737473204341310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b3009060355040613025345301e170d3138303930363137343230305a170d3138303931333137343230305a306a3126302406035504030c1d59756269636f20576562417574686e20756e6974207465737473204341310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b300906035504061302534530820122300d06092a864886f70d01010105000382010f003082010a028201010081f2dad381970f5bd571081f92db0a78a660f45e27b0bd11d321e906f1c03a17b72660eff210aa908e82edbb3e42a6c781e37fe1c42025b899d12683ce2512583018ebd54b129559465dbd3fd231d95271e23a60403a0104785d0e17b25256f4dfa4b1ab4b7d60e8985664642059335eca925fd985d1165c460d7ab4dc33b1e8883d9bfa45893df6d3b67cabe64122e935d6b6b084b5eae512b18f726c4db58295cb198b6e320197a5e3421af3705299bb3ae31c82c2016c123c0afec05e980b0df8840c225ce318373eac9cfbd1ac9b53d9ae67a0e59bd0f620e5af738fba98f3645df4f152e42ccce1b0f4d2b5295e136865f8a5f8ffc061666dfecac0ce390203010001a3133011300f0603551d130101ff040530030101ff300d06092a864886f70d01010d050003820101005824dab316433c723cd4ced60e46e3e630fca9048ba190c6d5ecb293c0a6f8e6a5186f5edb87350ba4126c374ccd03828292258e4770976b151e190b462198198f5048d9511973c3b1daedd5e0fa38c0bfe397cdf806681116a3f059b43ab008f35eeea352e62cfda1f76c8a37493ec598474783a2fd0bad36af0cef4a2ad838978295e02b4d6defd60c9c5f931b7e0e9e9b6513a4873f9ce15306c5c8944be2d01178ea62db5ae6c3c538c9c2792943e9209e622417d20aea8c876521ec25367e442823d1a8b545ef3251fbc1d53a21eb2b994231e70e1bddf290a356c30e7e049d9f7ee00503ec36a17ba6da0b01da6f8b545880551aa9da44f484ce38d4036863657274496e666f58a9ff5443478017000000409b34053f3da1fca291f1c83578b9fe3e950f89bfe9a6ac802dfc3170d9436d95a1c1229e0534b0ed34fda227f58c970bfabfd804ef554187baf643d44b150135000000000000000011111111222222223300000000000000000042000dfeb6906ef87911b6e6ebd3126d8f2d2c6bb2d72a96aa1a6db27e0547264077fffed8664eb459611c9f8094935453401316f519df3e52ecc8032a218029d33c6700006376657263322e3067707562417265615901170001000d0004000000000010001408000001000101010099116dd8d7264698788f7916ceab8950ddef91a6627aef32f6fb682cb9d581ba4d979c22ee99b456e518399af900dc41b712086a85fa1f57b14e5a7e19c94c283024125f43ee03e62efd10470172cb890c3fddecef2f32937b74172175b66351691d49d441ebb6305d02fa844a2e0566c9e96aac07d28cf91a4d4c975c03f630a606a43537cf48e661aa16d8a035e302f12ca4d529ed8ac82c57b811dfa7fef0a84d1e8d3b27d3cd0cc140f45c82a737239f470bd737b0c52202173b37941d106ee456f6853a4adb12c5b42006f055808a256b96aaeb7a3c44ad2c03f11484735e603da10c1e7d1bbc29648c30ea38c4bbd4a9cb78b939b5a58b2d8ff8a8ba27ffff"), + clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","type":"webauthn.create","tokenBinding":{"status":"supported"}}""", + privateKey = Some( + ByteArray.fromHex("308204bd020100300d06092a864886f70d0101010500048204a7308204a3020100028201010099116dd8d7264698788f7916ceab8950ddef91a6627aef32f6fb682cb9d581ba4d979c22ee99b456e518399af900dc41b712086a85fa1f57b14e5a7e19c94c283024125f43ee03e62efd10470172cb890c3fddecef2f32937b74172175b66351691d49d441ebb6305d02fa844a2e0566c9e96aac07d28cf91a4d4c975c03f630a606a43537cf48e661aa16d8a035e302f12ca4d529ed8ac82c57b811dfa7fef0a84d1e8d3b27d3cd0cc140f45c82a737239f470bd737b0c52202173b37941d106ee456f6853a4adb12c5b42006f055808a256b96aaeb7a3c44ad2c03f11484735e603da10c1e7d1bbc29648c30ea38c4bbd4a9cb78b939b5a58b2d8ff8a8ba270203010001028201000d41281cedcc7fb276461e3b2e5c4640bd67205aa30e782616a3008b56f0391293e37bfebe608af0375858aca5c140516473e84ca91b5699765e0d91fbd3a587995b9647af8f2dc141f261f57417a7ae4f643c6866f1d454570d5f6f634d0ede9ed68d6d16e43d5b84c25c45165353de69bf8fa023f14489d1903e00a1542a7e3aae6929ba4ec754ec4c01864fcd1e774a3b7f090078ad8fe618e8b79c07e644ae34bdd0534b6cdee6c08018548984f66641fb026afdd85d3a44bc715b2aa2e8583af5a99ccc81c7d55317425501e5b5aa82bd0958a3b0c0b068349fdea13ea6f043300d78c316d5f358297f1a023469b839c5c52a0f459a76524a0a8856c2b102818100fb570a0770ee0eabb7f51fed98bbdb14146a28518128af749f854fabb680ff6a597a3f17ba968ec7b8c4e4acf964044828741375b2cf513bef4f52d1dd39924d64b9037d109c85cdf0ffe1f7d44e0f8802fc48979a80587a84a8200162bf7d197c237ff6b91aac742e0e48d3022639f50c49405d6d69b8d7bc23f2a125421579028181009be7f3abcb18e8dd2a96f96e0f37589ddbafcc78dae698fd0b51c317971b9decddf06b4c219f3380db4c4d71fcdc9e27cf59847f002c2296bf6600b8391b9c2cdca3af4af8a5f421845780152637df0c9790021b73f9926cc160b5935918d6cde92db47a6cf918d434bcd5d5fc3a7b0c48f122a7a851ba9eabb42bf6e223849f02818100a652e5ff209b58b808273d76b4d0f3dc28da4b4e0c63c9202b044441c4a73edeb8d1adf8dcf00f1259d269e591afbf29a52393511b0018a8c9e7bb4dc7d0f66122db5054adee76995ef76628e3a4b8a07021554485e8932498aecd673d5aacc575a1e46777fd0fcc5e41f3ad3749e6a6a3f7c19151fb5967e24803a2e20e0639028180062b223ffcd42a7a7db1e5828e459153059b2a0aea164f9d4b725bb6b63ad87fc3b43c7a91a5fbe2b04a8f91e000569d9a9d9f196b4753c30525a307a6f2c9b618b0bd41c91ebfcf07ae7299e39e384c063f236634ab7e38a15a133516445e535d537a9d916c35a847c1e4f0077fc4d892963fd9c4561f7d21ac0a454563445f0281803aa51050b258667e701df840ce5975f5e6706c5e58b5a5b6c9100de05a7c3d420f41bfdac8c0c9fd98036f0d12ed2c4faf5fc07215a0a7e4a4517009117b1a2c24126dee8cab6d6d56c6fd3301983fdf2c77ffe58f83e0175e6793c38fea968bce6b640f67389769c4fbd4664c9130d2eef6c819f621f199027f4e824027c303") + ), + attestationCertChain = List( + RegistrationTestDataGenerator.importAttestationCa( + "MIIDjTCCAnWgAwIBAgICG6QwDQYJKoZIhvcNAQENBQAwajEmMCQGA1UEAwwdWXViaWNvIFdlYkF1dGhuIHVuaXQgdGVzdHMgQ0ExDzANBgNVBAoMBll1YmljbzEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjELMAkGA1UEBhMCU0UwHhcNMTgwOTA2MTc0MjAwWhcNMTgwOTEzMTc0MjAwWjAAMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAk1tdWT2fj27sdT5hc1xyKJFdeOXkznFUN87b1semhKz7wBgH5vN2JFO6SKKWE6dm23rqwWgtLiPe8KnX4jZsru6Q9EcYi1UDapVr6hBOmk1wddpjEoU/TsOlegTd8xqvUF7JMkn/MyS8kwU6iDIsalGQFDnCXGthXkUVZ97pTZN6/S2/1kBodesXTuDgDZcW3pjuIywtX6YCQ5Hdfl7KQkr7jarowm1lUjKWqJHgqqffeSud59yU0lDdyFqMVivTMc12FuB7II4oFyIRBSbi7rKKgwtsoFFZk2juXr0TBqQ5jx+PIUxBC34kv/jKcZN3RFR0fjzWtNC6fDYZgWYrIQIDAQABo4GmMIGjMCEGCysGAQQBguUcAQEEBBIEEAABAgMEBQYHCAkKCwwNDg8waQYDVR0RAQH/BF8wXaRbMFkxVzAUBgVngQUCAQwLaWQ6MDAwMDAwMDAwFAYFZ4EFAgMMC2lkOjAwMDAwMDAwMCkGBWeBBQICDCBURVNUX1l1Ymljb19qYXZhLXdlYmF1dGhuLXNlcnZlcjATBgNVHSUBAf8ECTAHBgVngQUIAzANBgkqhkiG9w0BAQ0FAAOCAQEAAKLcBXnzdboELurzSDe4rv0OQ+IoJj+T2lGrPpkfrvooIpC0lgBKpgtkVL/B/8/QBOhL5KcaUGm5nW7vrUFimnuLpbV2rZVL1QojK09p99e/OtGILW8mAuNQjfamcN8Rya/nc0k6lLAEeV5iL3RIxW5ORcEaoaIKUkTXZVnAjQxT6YhtRwckyr2aT016Ifo3BZ0oqeFgSZgxK1ggiIFnQGJlymtuOQhyFvlgHR6zBxlHMZ6HRR6a8Qd9v16RojYjCRBRNxyUJpjyrYMcQ386ufzx6UwXKPogHYbTU63+qa7BJyVeDKm5Xj1nbU/rb20yfui/qI1fXfQqyisjZEqwEA==", + "RSA", + "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCTW11ZPZ+Pbux1PmFzXHIokV145eTOcVQ3ztvWx6aErPvAGAfm83YkU7pIopYTp2bbeurBaC0uI97wqdfiNmyu7pD0RxiLVQNqlWvqEE6aTXB12mMShT9Ow6V6BN3zGq9QXskySf8zJLyTBTqIMixqUZAUOcJca2FeRRVn3ulNk3r9Lb/WQGh16xdO4OANlxbemO4jLC1fpgJDkd1+XspCSvuNqujCbWVSMpaokeCqp995K53n3JTSUN3IWoxWK9MxzXYW4HsgjigXIhEFJuLusoqDC2ygUVmTaO5evRMGpDmPH48hTEELfiS/+Mpxk3dEVHR+PNa00Lp8NhmBZishAgMBAAECggEAUsSP009cr0kDwfsO66gyavzzfrPKZ/aZ8lrbenFb48vx//y/e4amhlMNID1KhLGTgZYyA/6K2g7F63HK08H0G/HeM4c3jxNqPtS875THQb5be6b13PJBE/GqobXYIPONI1yKMBgGIujwjrfyH4vnDLTRc7rZo+WgpD2zf0tiyfJBWPwpwhVbqMp2OIFKg2iLMdoDOfEEFswEQySxjzi+/gMR/kPb2U1JKFM5Lg77UKNb+t9YvZ1Qd5BL7pAwbFMjhpLz3F2V569+wzdwWj50WVpaVlBZwIGxn7pV/Tw26jl2DoV6Hc+16CpsqUd3baOuaWIv/sJZNSgYqM2ctYIzrQKBgQDJ7zBf4Eg3lSAJ2Ds3Gg0/jxDnwQaNQ9GHvYTXOFAXrkpntvwQxgntR4+X5RGiQDgCvE1Vvdi5ZR4erSFQ/dCaMSAarjnCm6LAuYYtIpycH6vOwaAjOm5p2hCvXykw/27cOsTHeSsJtxUd6n9pcnfajL/ZSFovgPhydEctQJbU1wKBgQC6z2AWhvKth0usyHSjUgAP+QRCMSFgRKpuf5AMXADmAgcvJlxuyPBLDlOtHdaJ9SN9wJuFnSU+rmQM0PaS1d3Aa+sWsJIs67lrSHO+w6nsEYbZrs3hH8MALqcMgU+25MY+mRMftHfXtsWHaOEu1Vh9sykGfz7SErnGBLRc5oAIxwKBgQChb0gEDgCN9vkDBcvpNDmNK2m/bRA41RPoabmOeWWGWP8AxUfkfP4opIIGU8nyJVbh0PoeZsShCla2/X/aCN/AtS9ORSTGELhfTLIY2UfMhIFMrHzCTQ9CLmQSX4hFtJ9DDvSL57Fhde062mJ7wVhR7x3crjvzKC73CUBxy+YJRwKBgQCnOAojIBkLDBjJSYZey4ASzCzrs17U9aI51yXyakjDmv0jT4td/7BY/zIXvKXWSADFCCwupkQ4n5Ifhs2xEo+1NuTxIo02eKs5RVmWYT8xeV7kbH0OD4hWGWye3QGmDZMHZa6gqsK77XdThqZLbd4QZtdKYYyyLuDsSDnLDul88QKBgG8kw4SrT5SjlpkzT2XZJg/ehPI//wnuL2jm1kqDr1Y+MZ78YOrTTGwRKFK1eBAuNh7mAhZ4LN4kYiwLQ5ucJ/ipqXknp00Hvkc88lW3DX7pBf4ZoXgooz8mctCvXaZgDLfR861rMJXYQw+XQjbyslbVShQLbdWTDbwOiqUZcDpN", + ), + RegistrationTestDataGenerator.importAttestationCa( + "MIIDYzCCAkugAwIBAgICHtIwDQYJKoZIhvcNAQENBQAwajEmMCQGA1UEAwwdWXViaWNvIFdlYkF1dGhuIHVuaXQgdGVzdHMgQ0ExDzANBgNVBAoMBll1YmljbzEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjELMAkGA1UEBhMCU0UwHhcNMTgwOTA2MTc0MjAwWhcNMTgwOTEzMTc0MjAwWjBqMSYwJAYDVQQDDB1ZdWJpY28gV2ViQXV0aG4gdW5pdCB0ZXN0cyBDQTEPMA0GA1UECgwGWXViaWNvMSIwIAYDVQQLDBlBdXRoZW50aWNhdG9yIEF0dGVzdGF0aW9uMQswCQYDVQQGEwJTRTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAIHy2tOBlw9b1XEIH5LbCnimYPReJ7C9EdMh6QbxwDoXtyZg7/IQqpCOgu27PkKmx4Hjf+HEICW4mdEmg84lElgwGOvVSxKVWUZdvT/SMdlSceI6YEA6AQR4XQ4XslJW9N+ksatLfWDomFZkZCBZM17Kkl/ZhdEWXEYNerTcM7HoiD2b+kWJPfbTtnyr5kEi6TXWtrCEterlErGPcmxNtYKVyxmLbjIBl6XjQhrzcFKZuzrjHILCAWwSPAr+wF6YCw34hAwiXOMYNz6snPvRrJtT2a5noOWb0PYg5a9zj7qY82Rd9PFS5CzM4bD00rUpXhNoZfil+P/AYWZt/srAzjkCAwEAAaMTMBEwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQ0FAAOCAQEAWCTasxZDPHI81M7WDkbj5jD8qQSLoZDG1eyyk8Cm+OalGG9e24c1C6QSbDdMzQOCgpIljkdwl2sVHhkLRiGYGY9QSNlRGXPDsdrt1eD6OMC/45fN+AZoERaj8Fm0OrAI817uo1LmLP2h92yKN0k+xZhHR4Oi/QutNq8M70oq2DiXgpXgK01t79YMnF+TG34OnptlE6SHP5zhUwbFyJRL4tAReOpi21rmw8U4ycJ5KUPpIJ5iJBfSCuqMh2Uh7CU2fkQoI9GotUXvMlH7wdU6IesrmUIx5w4b3fKQo1bDDn4EnZ9+4AUD7Dahe6baCwHab4tUWIBVGqnaRPSEzjjUAw==", + "RSA", + "MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCB8trTgZcPW9VxCB+S2wp4pmD0XiewvRHTIekG8cA6F7cmYO/yEKqQjoLtuz5CpseB43/hxCAluJnRJoPOJRJYMBjr1UsSlVlGXb0/0jHZUnHiOmBAOgEEeF0OF7JSVvTfpLGrS31g6JhWZGQgWTNeypJf2YXRFlxGDXq03DOx6Ig9m/pFiT3207Z8q+ZBIuk11rawhLXq5RKxj3JsTbWClcsZi24yAZel40Ia83BSmbs64xyCwgFsEjwK/sBemAsN+IQMIlzjGDc+rJz70aybU9muZ6Dlm9D2IOWvc4+6mPNkXfTxUuQszOGw9NK1KV4TaGX4pfj/wGFmbf7KwM45AgMBAAECggEAS2SiLwpFoUSPjle/Mc3hwmQNZlnmPzVCzTMkZsIF2+58dUjSjae7vcjhD5qOIc9vet2KCWtnl1sF6wGkgQqjHQUywEjsmGiL9jZWoVuLTmH17uIdi8XbZ0OKAa4f6IPI6KQ97HsM0BfCooT2TopSMpHm4LNsXwXRHVeetKX5XCMHcUBAQYtd2DhHST3BCGeWWvxy+dbaX0TzscUfb7OkVoXYaCb+88c2GL74cNjz24D8MzEXUd2HBP2J7nBLHlUGWI1LIkbYLuNZ5oPSJOxZ3gZtD/xPzRvLY7aORKplu4AYfwgW24hHf0b3IQfDwVV+uhwDgJV7De0rh96kbIId8QKBgQD0jZ5ZQXxYK8GZGsyy48LZ07k/3/sMo483RY3y0We0JxKwBCkqZPfBjG5c2dSJuJsOkTKJfuhI7kdkp/+FJjLNpCqmcUbVYs4OSWBK2aoFltOJ210XFY09lzTLScfOcXf11jSiiFQ0HvxrOPpIWIoMn0K/VKppHnEoF2zlFRyepQKBgQCIB/oCkzLClWqghsDyz1Yr2HVQQsYUZWpKkzSLfw7VIPnppRQd7j5KysN6p9wuWX0+6SvJhhjM/Tn1n6vSco8Sn2f5XDq/ayblKrfAS/LV0/Z1M7G6U4Kcjr7Yd+mIPxCFqgES/XHZ+10Ikdp5R7Vr46L/Wq0pUS+0Z007imnRBQKBgHLAgS0grVgyMAXHrYXDmgrcbnCqiQLFPM6StKjb2e2O6BXv3eEmv5ryalbnX/O/zAJp32zlP9n49UcmDaBM7EnSXrD7NmGqm0XY6HY27LDytRBa/rN2SXA9I2jAliEo3UFd4hTiI6DRaWBmvAp2gVCq6ocdE1mAD1jgpRhZb7SBAoGAR4MR/sqNc9gC7xMIWl1/aptnyOLhqRVLlJrgk7ke/hJQ73B2K+n0W3NO4qteSAuJmUoRV+ckIJe7IZJoTMEmz953VZMT20+kafNUGEaVCa5dsW2UsGR4lH9CeyBG5/ZnZC1kVSxh7vuDBB9RIFL/YBGSvfVYdREWKBvqcTOpv1UCgYBjQNVSvXNOHb/0ChcFyAIZtN+p/7ykLyjVMbPK0TkWWGfVNSUXvz2/otUEiGQjnIMgQ+PHNOOLdmCwiSjgx1s1rbl6/xAFWS4/GlugPAug0u859sIdTkMjDmotAsBSATHHG4S4TYG7ArCBsxhVzkUdK4pH7vldSjSyOf4YPypyeQ==", + ), + ), + ) { + override def regenerate() = + TestAuthenticator.createBasicAttestedCredential( + keyAlgorithm = COSEAlgorithmIdentifier.RS512, + attestationMaker = AttestationMaker.tpm( + AttestationSigner.ca( + alg = COSEAlgorithmIdentifier.RS512, + certSubject = new X500Name(Array.empty[RDN]), + certExtensions = tpmCertExtensions, + ) + ), + ) + } + val ValidRs1: RegistrationTestData = new RegistrationTestData( alg = COSEAlgorithmIdentifier.RS1, attestationObject = @@ -1048,6 +1174,13 @@ case class RegistrationTestData( ) } + def attestationStatementFormat: String = + JacksonCodecs.cbor + .readTree(attestationObject.getBytes) + .asInstanceOf[ObjectNode] + .get("fmt") + .textValue + def setAttestationStatementFormat(value: String): RegistrationTestData = editAttestationObject( "fmt", @@ -1080,6 +1213,8 @@ case class RegistrationTestData( PublicKeyCredentialParameters.ES384, PublicKeyCredentialParameters.ES512, PublicKeyCredentialParameters.RS256, + PublicKeyCredentialParameters.RS384, + PublicKeyCredentialParameters.RS512, ).asJava ) .extensions(requestedExtensions) 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 f817ad746..78daeb962 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 @@ -185,6 +185,7 @@ class RelyingPartyAssertionSpec rpId: RelyingPartyIdentity = Defaults.rpId, signature: ByteArray = Defaults.signature, userHandleForResponse: Option[ByteArray] = Some(Defaults.userHandle), + userHandleForRequest: Option[ByteArray] = None, userHandleForUser: ByteArray = Defaults.userHandle, usernameForRequest: Option[String] = Some(Defaults.username), usernameForUser: String = Defaults.username, @@ -210,6 +211,7 @@ class RelyingPartyAssertionSpec .build() ) .username(usernameForRequest.toJava) + .userHandle(userHandleForRequest.toJava) .build() val response = PublicKeyCredential @@ -418,9 +420,15 @@ class RelyingPartyAssertionSpec ) result.isSuccess should be(true) - result.getUserHandle should equal(registrationTestData.userId.getId) - result.getCredentialId should equal(registrationTestData.response.getId) - result.getCredentialId should equal(testData.response.getId) + result.getCredential.getUserHandle should equal( + registrationTestData.userId.getId + ) + result.getCredential.getCredentialId should equal( + registrationTestData.response.getId + ) + result.getCredential.getCredentialId should equal( + testData.response.getId + ) credRepo.lookupCount should equal(1) } @@ -642,9 +650,9 @@ class RelyingPartyAssertionSpec ) { val steps = finishAssertion( credentialRepository = credentialRepository, - usernameForRequest = Some(owner.username), - userHandleForUser = owner.userHandle, userHandleForResponse = Some(nonOwner.userHandle), + userHandleForUser = owner.userHandle, + usernameForRequest = Some(owner.username), ) val step: FinishAssertionSteps#Step6 = steps.begin.next @@ -672,9 +680,9 @@ class RelyingPartyAssertionSpec it("Succeeds if credential ID is owned by the given user handle.") { val steps = finishAssertion( credentialRepository = credentialRepository, - usernameForRequest = Some(owner.username), - userHandleForUser = owner.userHandle, userHandleForResponse = Some(owner.userHandle), + userHandleForUser = owner.userHandle, + usernameForRequest = Some(owner.username), ) val step: FinishAssertionSteps#Step6 = steps.begin.next @@ -701,13 +709,30 @@ class RelyingPartyAssertionSpec } it( - "Fails if credential ID is not owned by the given user handle." + "Fails if credential ID is not owned by the user handle in the response." ) { val steps = finishAssertion( credentialRepository = credentialRepository, + userHandleForResponse = Some(nonOwner.userHandle), + userHandleForUser = owner.userHandle, usernameForRequest = None, + ) + val step: FinishAssertionSteps#Step6 = steps.begin.next + + step.validations shouldBe a[Failure[_]] + step.validations.failed.get shouldBe an[IllegalArgumentException] + step.tryNext shouldBe a[Failure[_]] + } + + it( + "Fails if credential ID is not owned by the user handle in the request." + ) { + val steps = finishAssertion( + credentialRepository = credentialRepository, + userHandleForRequest = Some(nonOwner.userHandle), + userHandleForResponse = None, userHandleForUser = owner.userHandle, - userHandleForResponse = Some(nonOwner.userHandle), + usernameForRequest = None, ) val step: FinishAssertionSteps#Step6 = steps.begin.next @@ -719,9 +744,25 @@ class RelyingPartyAssertionSpec it("Fails if neither username nor user handle is given.") { val steps = finishAssertion( credentialRepository = credentialRepository, + userHandleForRequest = None, + userHandleForResponse = None, + userHandleForUser = owner.userHandle, usernameForRequest = None, + ) + val step: FinishAssertionSteps#Step6 = steps.begin.next + + step.validations shouldBe a[Failure[_]] + step.validations.failed.get shouldBe an[IllegalArgumentException] + step.tryNext shouldBe a[Failure[_]] + } + + it("Fails if user handle in request does not agree with user handle in response.") { + val steps = finishAssertion( + credentialRepository = credentialRepository, + userHandleForRequest = Some(owner.userHandle), + userHandleForResponse = Some(nonOwner.userHandle), userHandleForUser = owner.userHandle, - userHandleForResponse = None, + usernameForRequest = None, ) val step: FinishAssertionSteps#Step6 = steps.begin.next @@ -730,12 +771,26 @@ class RelyingPartyAssertionSpec step.tryNext shouldBe a[Failure[_]] } - it("Succeeds if credential ID is owned by the given user handle.") { + it("Succeeds if credential ID is owned by the user handle in the response.") { val steps = finishAssertion( credentialRepository = credentialRepository, + userHandleForResponse = Some(owner.userHandle), + userHandleForUser = owner.userHandle, usernameForRequest = None, + ) + val step: FinishAssertionSteps#Step6 = steps.begin.next + + step.validations shouldBe a[Success[_]] + step.tryNext shouldBe a[Success[_]] + } + + it("Succeeds if credential ID is owned by the user handle in the request.") { + val steps = finishAssertion( + credentialRepository = credentialRepository, + userHandleForRequest = Some(owner.userHandle), + userHandleForResponse = None, userHandleForUser = owner.userHandle, - userHandleForResponse = Some(owner.userHandle), + usernameForRequest = None, ) val step: FinishAssertionSteps#Step6 = steps.begin.next @@ -1906,8 +1961,12 @@ class RelyingPartyAssertionSpec Try(steps.run) shouldBe a[Success[_]] step.result.get.isSuccess should be(true) - step.result.get.getCredentialId should equal(Defaults.credentialId) - step.result.get.getUserHandle should equal(Defaults.userHandle) + step.result.get.getCredential.getCredentialId should equal( + Defaults.credentialId + ) + step.result.get.getCredential.getUserHandle should equal( + Defaults.userHandle + ) step.result.get.getCredential.getCredentialId should equal( step.result.get.getCredentialId ) @@ -2001,8 +2060,8 @@ class RelyingPartyAssertionSpec ) result.isSuccess should be(true) - result.getUserHandle should equal(testData.userId.getId) - result.getCredentialId should equal(credId) + result.getCredential.getUserHandle should equal(testData.userId.getId) + result.getCredential.getCredentialId should equal(credId) } it("an Ed25519 key.") { @@ -2133,8 +2192,10 @@ class RelyingPartyAssertionSpec ) result.isSuccess should be(true) - result.getUserHandle should equal(registrationRequest.getUser.getId) - result.getCredentialId should equal(credId) + result.getCredential.getUserHandle should equal( + registrationRequest.getUser.getId + ) + result.getCredential.getCredentialId should equal(credId) } it("a generated Ed25519 key.") { @@ -2172,9 +2233,15 @@ class RelyingPartyAssertionSpec ) result.isSuccess should be(true) - result.getUserHandle should equal(registrationTestData.userId.getId) - result.getCredentialId should equal(registrationTestData.response.getId) - result.getCredentialId should equal(testData.response.getId) + result.getCredential.getUserHandle should equal( + registrationTestData.userId.getId + ) + result.getCredential.getCredentialId should equal( + registrationTestData.response.getId + ) + result.getCredential.getCredentialId should equal( + testData.response.getId + ) } describe("an RS1 key") { @@ -2215,11 +2282,15 @@ class RelyingPartyAssertionSpec ) result.isSuccess should be(true) - result.getUserHandle should equal(registrationTestData.userId.getId) - result.getCredentialId should equal( + result.getCredential.getUserHandle should equal( + registrationTestData.userId.getId + ) + result.getCredential.getCredentialId should equal( registrationTestData.response.getId ) - result.getCredentialId should equal(testData.response.getId) + result.getCredential.getCredentialId should equal( + testData.response.getId + ) } it("with basic attestation.") { @@ -2275,8 +2346,10 @@ class RelyingPartyAssertionSpec ) result.isSuccess should be(true) - result.getUserHandle should equal(testData.userId.getId) - result.getCredentialId should equal(testData.response.getId) + result.getCredential.getUserHandle should equal(testData.userId.getId) + result.getCredential.getCredentialId should equal( + testData.response.getId + ) } } } 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 94adc5a50..5a3f3244c 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 @@ -110,8 +110,12 @@ class RelyingPartyCeremoniesSpec ) assertionResult.isSuccess should be(true) - assertionResult.getCredentialId should equal(testData.assertion.get.id) - assertionResult.getUserHandle should equal(testData.user.getId) + assertionResult.getCredential.getCredentialId should equal( + testData.assertion.get.id + ) + assertionResult.getCredential.getUserHandle should equal( + testData.user.getId + ) assertionResult.getUsername should equal(testData.user.getName) assertionResult.getSignatureCount should be >= testData.attestation.authenticatorData.getSignatureCounter assertionResult.isSignatureCounterValid should be(true) @@ -163,9 +167,11 @@ class RelyingPartyCeremoniesSpec it("a YubiKey 5Ci FIPS.") { check(RealExamples.Yubikey5ciFips) } + it("a YubiKey Bio.") { check(RealExamples.YubikeyBio_5_5_4) check(RealExamples.YubikeyBio_5_5_5) + check(RealExamples.YubikeyBio_5_5_6) } it("an Apple iOS device.") { 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 a737f23b4..2519fa756 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 @@ -235,19 +235,13 @@ class RelyingPartyRegistrationSpec describe("3. Let response be credential.response.") { it("If response is not an instance of AuthenticatorAttestationResponse, abort the ceremony with a user-visible error.") { + val testData = RegistrationTestData.Packed.BasicAttestationEdDsa val frob = FinishRegistrationOptions .builder() - .request( - RegistrationTestData.Packed.BasicAttestationEdDsa.request - ) - val testData = - RegistrationTestData.Packed.BasicAttestationEdDsa.assertion.get - "frob.response(testData.response)" shouldNot compile - frob - .response( - RegistrationTestData.Packed.BasicAttestationEdDsa.response - ) - .build() should not be null + .request(testData.request) + "frob.response(testData.response)" should compile + "frob.response(testData.assertion.get.response)" shouldNot compile + frob.response(testData.response).build() should not be null } } @@ -1714,7 +1708,7 @@ class RelyingPartyRegistrationSpec ).getAuthenticatorData.getAttestedCredentialData.get.getCredentialPublicKey.getBytes ) .get(CBORObject.FromObject(3)) - .AsInt64 should equal(-7) + .AsInt64Value should equal(-7) new AttestationObject( testDataBase.attestationObject ).getAttestationStatement.get("alg").longValue should equal( @@ -1791,7 +1785,7 @@ class RelyingPartyRegistrationSpec attObj.getAuthenticatorData.getAttestedCredentialData.get.getCredentialPublicKey.getBytes ) .get(CBORObject.FromObject(3)) - .AsInt64 should equal(-257) + .AsInt64Value should equal(-257) attObj.getAttestationStatement .get("alg") .longValue should equal(-65535) @@ -2131,6 +2125,8 @@ class RelyingPartyRegistrationSpec def makeCred( authDataAndKeypair: Option[(ByteArray, KeyPair)] = None, + credKeyAlgorithm: COSEAlgorithmIdentifier = + TestAuthenticator.Defaults.keyAlgorithm, clientDataJson: Option[String] = None, subject: X500Name = emptySubject, rdn: Array[AttributeTypeAndValue] = @@ -2158,17 +2154,20 @@ class RelyingPartyRegistrationSpec ) = { val (authData, credentialKeypair) = authDataAndKeypair.getOrElse( - TestAuthenticator.createAuthenticatorData(keyAlgorithm = - COSEAlgorithmIdentifier.ES256 + TestAuthenticator.createAuthenticatorData( + credentialKeypair = Some( + TestAuthenticator.Defaults.defaultKeypair( + credKeyAlgorithm + ) + ), + keyAlgorithm = credKeyAlgorithm, ) ) TestAuthenticator.createCredential( authDataBytes = authData, credentialKeypair = credentialKeypair, - clientDataJson = clientDataJson.getOrElse( - TestAuthenticator.createClientData() - ), + clientDataJson = clientDataJson, attestationMaker = AttestationMaker.tpm( cert = AttestationSigner.ca( alg = COSEAlgorithmIdentifier.ES256, @@ -2727,7 +2726,11 @@ class RelyingPartyRegistrationSpec val (authData, keypair) = TestAuthenticator.createAuthenticatorData( aaguid = aaguid, - keyAlgorithm = COSEAlgorithmIdentifier.ES256, + credentialKeypair = Some( + TestAuthenticator.Defaults.defaultKeypair( + COSEAlgorithmIdentifier.ES256 + ) + ), ) val testData = (RegistrationTestData.from _).tupled( makeCred( @@ -2751,7 +2754,11 @@ class RelyingPartyRegistrationSpec val (authData, keypair) = TestAuthenticator.createAuthenticatorData( aaguid = aaguidInCred, - keyAlgorithm = COSEAlgorithmIdentifier.ES256, + credentialKeypair = Some( + TestAuthenticator.Defaults.defaultKeypair( + COSEAlgorithmIdentifier.ES256 + ) + ), ) val testData = (RegistrationTestData.from _).tupled( makeCred( @@ -2770,18 +2777,34 @@ class RelyingPartyRegistrationSpec describe("Other requirements:") { it("RSA keys must have the SIGN_ENCRYPT attribute.") { - forAll(Gen.chooseNum(0, Int.MaxValue.toLong * 2 + 1)) { - attributes: Long => + forAll( + Gen.chooseNum(0, Int.MaxValue.toLong * 2 + 1), + minSuccessful(5), + ) { attributes: Long => + val testData = (RegistrationTestData.from _).tupled( + makeCred( + credKeyAlgorithm = 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), + minSuccessful(5), + ) { symmetric: Int => + whenever(symmetric != TPM_ALG_NULL) { val testData = (RegistrationTestData.from _).tupled( makeCred( - authDataAndKeypair = Some( - TestAuthenticator - .createAuthenticatorData(keyAlgorithm = - COSEAlgorithmIdentifier.RS256 - ) - ), - attributes = - Some(attributes & ~Attributes.SIGN_ENCRYPT), + credKeyAlgorithm = COSEAlgorithmIdentifier.RS256, + symmetric = Some(symmetric), ) ) val step = init(testData) @@ -2789,72 +2812,62 @@ class RelyingPartyRegistrationSpec 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), - ) + it("""RSA keys must have "scheme" set to TPM_ALG_RSASSA or TPM_ALG_NULL""") { + forAll( + Gen.chooseNum(0, Short.MaxValue * 2 + 1), + minSuccessful(5), + ) { scheme: Int => + whenever( + scheme != TpmRsaScheme.RSASSA && scheme != TPM_ALG_NULL + ) { + val testData = (RegistrationTestData.from _).tupled( + makeCred( + credKeyAlgorithm = COSEAlgorithmIdentifier.RS256, + scheme = Some(scheme), ) - val step = init(testData) - testData.alg should be(COSEAlgorithmIdentifier.RS256) + ) + val step = init(testData) + testData.alg should be(COSEAlgorithmIdentifier.RS256) - step.validations shouldBe a[Failure[_]] - step.tryNext shouldBe a[Failure[_]] - } + 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) + it("ECC keys must have the SIGN_ENCRYPT attribute.") { + forAll( + Gen.chooseNum(0, Int.MaxValue.toLong * 2 + 1), + minSuccessful(5), + ) { attributes: Long => + val testData = (RegistrationTestData.from _).tupled( + makeCred( + credKeyAlgorithm = 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[_]] - } + 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 => + it("""ECC keys must have "symmetric" set to TPM_ALG_NULL""") { + forAll( + Gen.chooseNum(0, Short.MaxValue * 2 + 1), + minSuccessful(5), + ) { symmetric: Int => + whenever(symmetric != TPM_ALG_NULL) { val testData = (RegistrationTestData.from _).tupled( makeCred( - authDataAndKeypair = Some( - TestAuthenticator - .createAuthenticatorData(keyAlgorithm = - COSEAlgorithmIdentifier.ES256 - ) - ), - attributes = - Some(attributes & ~Attributes.SIGN_ENCRYPT), + credKeyAlgorithm = COSEAlgorithmIdentifier.ES256, + symmetric = Some(symmetric), ) ) val step = init(testData) @@ -2862,54 +2875,28 @@ class RelyingPartyRegistrationSpec 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), - ) + forAll( + Gen.chooseNum(0, Short.MaxValue * 2 + 1), + minSuccessful(5), + ) { scheme: Int => + whenever(scheme != TPM_ALG_NULL) { + val testData = (RegistrationTestData.from _).tupled( + makeCred( + credKeyAlgorithm = COSEAlgorithmIdentifier.ES256, + scheme = Some(scheme), ) - val step = init(testData) - testData.alg should be(COSEAlgorithmIdentifier.ES256) + ) + val step = init(testData) + testData.alg should be(COSEAlgorithmIdentifier.ES256) - step.validations shouldBe a[Failure[_]] - step.tryNext shouldBe a[Failure[_]] - } + step.validations shouldBe a[Failure[_]] + step.tryNext shouldBe a[Failure[_]] + } } } } @@ -3204,6 +3191,53 @@ class RelyingPartyRegistrationSpec step.tryNext shouldBe a[Success[_]] } + it("When the AAGUID in authenticator data is zero, the AAGUID in the attestation certificate is used instead, if possible.") { + val example = RealExamples.SecurityKeyNfc + val testData = example.asRegistrationTestData + testData.aaguid should equal( + ByteArray.fromHex("00000000000000000000000000000000") + ) + val certAaguid = new ByteArray( + CertificateParser + .parseFidoAaguidExtension( + CertificateParser.parseDer(example.attestationCert.getBytes) + ) + .get + ) + + val attestationTrustSource = new AttestationTrustSource { + override def findTrustRoots( + attestationCertificateChain: util.List[X509Certificate], + aaguid: Optional[ByteArray], + ): TrustRootsResult = { + TrustRootsResult + .builder() + .trustRoots( + if (aaguid == Optional.of(certAaguid)) { + Set(attestationRootCert).asJava + } else { + Set.empty[X509Certificate].asJava + } + ) + .build() + } + } + val steps = finishRegistration( + testData = testData, + attestationTrustSource = Some(attestationTrustSource), + ) + val step: FinishRegistrationSteps#Step20 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next + + step.validations shouldBe a[Success[_]] + step.getTrustRoots.toScala.map( + _.getTrustRoots.asScala + ) should equal( + Some(Set(attestationRootCert)) + ) + step.tryNext shouldBe a[Success[_]] + } + it( "If an attestation trust source is not set, no trust anchors are returned." ) { @@ -4015,22 +4049,31 @@ class RelyingPartyRegistrationSpec RegistrationTestData.defaultSettingsValidExamples.zipWithIndex .foreach { case (testData, i) => - it(s"Succeeds for example index ${i}.") { - val rp = { - val builder = RelyingParty - .builder() - .identity(testData.rpId) - .credentialRepository( - Helpers.CredentialRepository.empty - ) - builder.origins(Set(testData.clientData.getOrigin).asJava) - builder.build() - } + it(s"Succeeds for example index ${i} (${testData.alg}, ${testData.attestationStatementFormat}).") { + val rp = RelyingParty + .builder() + .identity(testData.rpId) + .credentialRepository( + Helpers.CredentialRepository.empty + ) + .origins(Set(testData.clientData.getOrigin).asJava) + .build() + + val request = rp + .startRegistration( + StartRegistrationOptions + .builder() + .user(testData.userId) + .build() + ) + .toBuilder + .challenge(testData.request.getChallenge) + .build() val result = rp.finishRegistration( FinishRegistrationOptions .builder() - .request(testData.request) + .request(request) .response(testData.response) .build() ) @@ -4067,6 +4110,24 @@ class RelyingPartyRegistrationSpec ) } + it("ES384.") { + pubKeyCredParams should contain( + PublicKeyCredentialParameters.ES384 + ) + pubKeyCredParams map (_.getAlg) should contain( + COSEAlgorithmIdentifier.ES384 + ) + } + + it("ES512.") { + pubKeyCredParams should contain( + PublicKeyCredentialParameters.ES512 + ) + pubKeyCredParams map (_.getAlg) should contain( + COSEAlgorithmIdentifier.ES512 + ) + } + it("EdDSA, when available.") { // The RelyingParty constructor call needs to be here inside the `it` call in order to have the right JCA provider environment val rp = RelyingParty @@ -4121,6 +4182,24 @@ class RelyingPartyRegistrationSpec COSEAlgorithmIdentifier.RS256 ) } + + it("RS384.") { + pubKeyCredParams should contain( + PublicKeyCredentialParameters.RS384 + ) + pubKeyCredParams map (_.getAlg) should contain( + COSEAlgorithmIdentifier.RS384 + ) + } + + it("RS512.") { + pubKeyCredParams should contain( + PublicKeyCredentialParameters.RS512 + ) + pubKeyCredParams map (_.getAlg) should contain( + COSEAlgorithmIdentifier.RS512 + ) + } } describe("do not include") { 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 b0893616f..7b491b189 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyStartOperationSpec.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyStartOperationSpec.scala @@ -24,6 +24,7 @@ package com.yubico.webauthn +import com.yubico.internal.util.JacksonCodecs import com.yubico.webauthn.Generators._ import com.yubico.webauthn.data.AssertionExtensionInputs import com.yubico.webauthn.data.AttestationConveyancePreference @@ -33,6 +34,7 @@ import com.yubico.webauthn.data.AuthenticatorTransport import com.yubico.webauthn.data.ByteArray import com.yubico.webauthn.data.Generators.Extensions.registrationExtensionInputs import com.yubico.webauthn.data.Generators._ +import com.yubico.webauthn.data.PublicKeyCredentialCreationOptions import com.yubico.webauthn.data.PublicKeyCredentialDescriptor import com.yubico.webauthn.data.PublicKeyCredentialParameters import com.yubico.webauthn.data.RegistrationExtensionInputs @@ -454,18 +456,37 @@ class RelyingPartyStartOperationSpec .build() ) + def jsonRequireResidentKey( + pkcco: PublicKeyCredentialCreationOptions + ): Option[Boolean] = + Option( + JacksonCodecs + .json() + .readTree(pkcco.toCredentialsCreateJson) + .get("publicKey") + .get("authenticatorSelection") + .get("requireResidentKey") + ).map(_.booleanValue) + pkccoDiscouraged.getAuthenticatorSelection.get.getResidentKey.toScala should be( Some(ResidentKeyRequirement.DISCOURAGED) ) + jsonRequireResidentKey(pkccoDiscouraged) should be(Some(false)) + pkccoPreferred.getAuthenticatorSelection.get.getResidentKey.toScala should be( Some(ResidentKeyRequirement.PREFERRED) ) + jsonRequireResidentKey(pkccoPreferred) should be(Some(false)) + pkccoRequired.getAuthenticatorSelection.get.getResidentKey.toScala should be( Some(ResidentKeyRequirement.REQUIRED) ) + jsonRequireResidentKey(pkccoRequired) should be(Some(true)) + pkccoUnspecified.getAuthenticatorSelection.get.getResidentKey.toScala should be( None ) + jsonRequireResidentKey(pkccoUnspecified) should be(None) } it("respects the authenticatorAttachment parameter.") { @@ -563,6 +584,34 @@ class RelyingPartyStartOperationSpec } } + it("passes username through to AssertionRequest.") { + forAll { username: String => + val testCaseUserId = userId.toBuilder.name(username).build() + val rp = relyingParty(userId = testCaseUserId) + val result = rp.startAssertion( + StartAssertionOptions + .builder() + .username(testCaseUserId.getName) + .build() + ) + result.getUsername.asScala should equal(Some(testCaseUserId.getName)) + } + } + + it("passes user handle through to AssertionRequest.") { + forAll { userHandle: ByteArray => + val testCaseUserId = userId.toBuilder.id(userHandle).build() + val rp = relyingParty(userId = testCaseUserId) + val result = rp.startAssertion( + StartAssertionOptions + .builder() + .userHandle(testCaseUserId.getId) + .build() + ) + result.getUserHandle.asScala should equal(Some(testCaseUserId.getId)) + } + } + it("includes transports in allowCredentials when available.") { forAll( Gen.nonEmptyContainerOf[Set, AuthenticatorTransport]( @@ -875,4 +924,95 @@ class RelyingPartyStartOperationSpec } } + describe("AssertionRequest") { + + it("resets username when userHandle is set.") { + forAll { (ar: AssertionRequest, userHandle: ByteArray) => + val result = ar.toBuilder.userHandle(userHandle).build() + result.getUsername.asScala shouldBe empty + } + + forAll { (ar: AssertionRequest, userHandle: ByteArray) => + val result = ar.toBuilder.userHandle(Some(userHandle).asJava).build() + result.getUsername.asScala shouldBe empty + } + } + + it("resets userHandle when username is set.") { + forAll { (ar: AssertionRequest, username: String) => + val result = ar.toBuilder.username(username).build() + result.getUserHandle.asScala shouldBe empty + } + + forAll { (ar: AssertionRequest, username: String) => + val result = ar.toBuilder.username(Some(username).asJava).build() + result.getUserHandle.asScala shouldBe empty + } + } + + it("does not reset username when userHandle is set to empty.") { + forAll { (ar: AssertionRequest, username: String) => + val result = ar.toBuilder + .username(username) + .userHandle(Optional.empty[ByteArray]) + .build() + result.getUsername.asScala should equal(Some(username)) + } + + forAll { (ar: AssertionRequest, username: String) => + val result = ar.toBuilder + .username(username) + .userHandle(null: ByteArray) + .build() + result.getUsername.asScala should equal(Some(username)) + } + } + + it("does not reset userHandle when username is set to empty.") { + forAll { (ar: AssertionRequest, userHandle: ByteArray) => + val result = ar.toBuilder + .userHandle(userHandle) + .username(Optional.empty[String]) + .build() + result.getUserHandle.asScala should equal(Some(userHandle)) + } + + forAll { (ar: AssertionRequest, userHandle: ByteArray) => + val result = ar.toBuilder + .userHandle(userHandle) + .username(null: String) + .build() + result.getUserHandle.asScala should equal(Some(userHandle)) + } + } + + it("allows unsetting username.") { + forAll { (ar: AssertionRequest, username: String) => + val preresult = ar.toBuilder.username(username).build() + preresult.getUsername.asScala should equal(Some(username)) + + val result1 = + preresult.toBuilder.username(Optional.empty[String]).build() + result1.getUsername.asScala shouldBe empty + + val result2 = preresult.toBuilder.username(null: String).build() + result2.getUsername.asScala shouldBe empty + } + } + + it("allows unsetting userHandle.") { + forAll { (ar: AssertionRequest, userHandle: ByteArray) => + val preresult = ar.toBuilder.userHandle(userHandle).build() + preresult.getUserHandle.asScala should equal(Some(userHandle)) + + val result1 = + preresult.toBuilder.userHandle(Optional.empty[ByteArray]).build() + result1.getUserHandle.asScala shouldBe empty + + val result2 = preresult.toBuilder.userHandle(null: ByteArray).build() + result2.getUserHandle.asScala shouldBe empty + } + } + } + } 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 3d70dfbc5..83f3b5606 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 @@ -125,6 +125,20 @@ object TestAuthenticator { ) val certValidFrom: Instant = Instant.parse("2018-09-06T17:42:00Z") val certValidTo: Instant = certValidFrom.plusSeconds(7 * 24 * 3600) + + private var defaultKeypairs: Map[COSEAlgorithmIdentifier, KeyPair] = + Map.empty + def defaultKeypair( + algorithm: COSEAlgorithmIdentifier = Defaults.keyAlgorithm + ): KeyPair = { + defaultKeypairs.get(algorithm) match { + case Some(keypair) => keypair + case None => + val keypair = generateKeypair(algorithm) + defaultKeypairs = defaultKeypairs + (algorithm -> keypair) + keypair + } + } } val RsaKeySizeBits = 2048 val Es256PrimeModulus: BigInteger = new BigInteger( @@ -137,6 +151,13 @@ object TestAuthenticator { ) private def jsonFactory: JsonNodeFactory = JsonNodeFactory.instance + private def jsonMap[V <: JsonNode]( + map: JsonNodeFactory => Map[String, V] + ): ObjectNode = + jsonFactory + .objectNode() + .setAll[ObjectNode](map.apply(jsonFactory).asJava) + private def toBytes(s: String): ByteArray = new ByteArray(s.getBytes("UTF-8")) def sha256(s: String): ByteArray = sha256(toBytes(s)) def sha256(b: ByteArray): ByteArray = @@ -146,24 +167,21 @@ object TestAuthenticator { val format: String def makeAttestationStatement( authDataBytes: ByteArray, - clientDataJson: String, + clientDataJson: ByteArray, ): JsonNode def certChain: List[(X509Certificate, PrivateKey)] = Nil def makeAttestationObjectBytes( authDataBytes: ByteArray, - clientDataJson: String, + clientDataJson: ByteArray, ): ByteArray = { - val f = JsonNodeFactory.instance - val attObj = f - .objectNode() - .setAll[ObjectNode]( - Map( - "authData" -> f.binaryNode(authDataBytes.getBytes), - "fmt" -> f.textNode(format), - "attStmt" -> makeAttestationStatement(authDataBytes, clientDataJson), - ).asJava + val attObj = jsonMap { f => + Map( + "authData" -> f.binaryNode(authDataBytes.getBytes), + "fmt" -> f.textNode(format), + "attStmt" -> makeAttestationStatement(authDataBytes, clientDataJson), ) + } new ByteArray(JacksonCodecs.cbor.writeValueAsBytes(attObj)) } } @@ -177,20 +195,22 @@ object TestAuthenticator { override def certChain = signer.certChain override def makeAttestationStatement( authDataBytes: ByteArray, - clientDataJson: String, + clientDataJson: ByteArray, ): JsonNode = makePackedAttestationStatement(authDataBytes, clientDataJson, signer) } + def fidoU2f(signer: AttestationSigner): AttestationMaker = new AttestationMaker { override val format = "fido-u2f" override def certChain = signer.certChain override def makeAttestationStatement( authDataBytes: ByteArray, - clientDataJson: String, + clientDataJson: ByteArray, ): JsonNode = makeU2fAttestationStatement(authDataBytes, clientDataJson, signer) } + def androidSafetynet( cert: AttestationCert, ctsProfileMatch: Boolean = true, @@ -200,7 +220,7 @@ object TestAuthenticator { override def certChain = cert.certChain override def makeAttestationStatement( authDataBytes: ByteArray, - clientDataJson: String, + clientDataJson: ByteArray, ): JsonNode = makeAndroidSafetynetAttestationStatement( authDataBytes, @@ -209,6 +229,7 @@ object TestAuthenticator { ctsProfileMatch = ctsProfileMatch, ) } + def apple( addNonceExtension: Boolean = true, nonceValue: Option[ByteArray] = None, @@ -227,7 +248,7 @@ object TestAuthenticator { override val format = "apple" override def makeAttestationStatement( authDataBytes: ByteArray, - clientDataJson: String, + clientDataJson: ByteArray, ): JsonNode = makeAppleAttestationStatement( caCert, @@ -243,6 +264,7 @@ object TestAuthenticator { caKey, ) } + def tpm( cert: AttestationCert, ver: Option[String] = Some("2.0"), @@ -260,7 +282,7 @@ object TestAuthenticator { override def certChain = cert.certChain override def makeAttestationStatement( authDataBytes: ByteArray, - clientDataJson: String, + clientDataJson: ByteArray, ): JsonNode = makeTpmAttestationStatement( authDataBytes, @@ -283,7 +305,7 @@ object TestAuthenticator { override def certChain = Nil override def makeAttestationStatement( authDataBytes: ByteArray, - clientDataJson: String, + clientDataJson: ByteArray, ): JsonNode = makeNoneAttestationStatement() } @@ -294,6 +316,7 @@ object TestAuthenticator { def cert: X509Certificate def certChain: List[(X509Certificate, PrivateKey)] } + case class SelfAttestation(keypair: KeyPair, alg: COSEAlgorithmIdentifier) extends AttestationSigner { override def key: PrivateKey = keypair.getPrivate @@ -302,6 +325,7 @@ object TestAuthenticator { } override def certChain = Nil } + case class AttestationCert( override val cert: X509Certificate, override val key: PrivateKey, @@ -313,6 +337,7 @@ object TestAuthenticator { keypair: (X509Certificate, PrivateKey), ) = this(keypair._1, keypair._2, alg, Nil) } + object AttestationSigner { def ca( alg: COSEAlgorithmIdentifier, @@ -419,41 +444,31 @@ object TestAuthenticator { 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() - - json.setAll( + ): String = + JacksonCodecs.json.writeValueAsString(clientData getOrElse { + jsonMap { + f => Map( - "challenge" -> jsonFactory.textNode(challenge.getBase64Url), - "origin" -> jsonFactory.textNode(origin), - "type" -> jsonFactory.textNode("webauthn.create"), - ).asJava - ) - - json.set( - "tokenBinding", { - val tokenBinding = jsonFactory.objectNode() - tokenBinding.set("status", jsonFactory.textNode(tokenBindingStatus)) - tokenBindingId foreach { id => - tokenBinding.set("id", jsonFactory.textNode(id)) - } - tokenBinding - }, - ) - - json - }) - - clientDataJson - } + "challenge" -> f.textNode(challenge.getBase64Url), + "origin" -> f.textNode(origin), + "type" -> f.textNode("webauthn.create"), + "tokenBinding" -> { + val tokenBinding = f.objectNode() + tokenBinding.set("status", f.textNode(tokenBindingStatus)) + tokenBindingId foreach { id => + tokenBinding.set("id", f.textNode(id)) + } + tokenBinding + }, + ) + } + }) def createCredential( authDataBytes: ByteArray, - clientDataJson: String, credentialKeypair: KeyPair, attestationMaker: AttestationMaker, + clientDataJson: Option[String] = None, clientExtensions: ClientRegistrationExtensionOutputs = ClientRegistrationExtensionOutputs.builder().build(), ): ( @@ -465,10 +480,15 @@ object TestAuthenticator { List[(X509Certificate, PrivateKey)], ) = { - val clientDataJsonBytes = toBytes(clientDataJson) + val clientDataJsonBytes = toBytes( + clientDataJson.getOrElse(createClientData()) + ) val attestationObjectBytes = - attestationMaker.makeAttestationObjectBytes(authDataBytes, clientDataJson) + attestationMaker.makeAttestationObjectBytes( + authDataBytes, + clientDataJsonBytes, + ) val response = AuthenticatorAttestationResponse .builder() @@ -510,7 +530,6 @@ object TestAuthenticator { createCredential( authDataBytes = authData, credentialKeypair = credentialKeypair, - clientDataJson = createClientData(), attestationMaker = attestationMaker, ) } @@ -532,7 +551,6 @@ object TestAuthenticator { val signer = SelfAttestation(keypair, keyAlgorithm) createCredential( authDataBytes = authData, - clientDataJson = createClientData(), credentialKeypair = keypair, attestationMaker = attestationMaker(signer), ) @@ -556,7 +574,7 @@ object TestAuthenticator { ) createCredential( authDataBytes = authData, - clientDataJson = createClientData(challenge = challenge), + clientDataJson = Some(createClientData(challenge = challenge)), credentialKeypair = keypair, attestationMaker = AttestationMaker.none(), ) @@ -611,28 +629,22 @@ object TestAuthenticator { val clientDataJson: String = JacksonCodecs.json.writeValueAsString(clientData getOrElse { - val json: ObjectNode = jsonFactory.objectNode() - - json.setAll( - Map( - "challenge" -> jsonFactory.textNode(challenge.getBase64Url), - "origin" -> jsonFactory.textNode(origin), - "type" -> jsonFactory.textNode("webauthn.get"), - ).asJava - ) - - json.set( - "tokenBinding", { - val tokenBinding = jsonFactory.objectNode() - tokenBinding.set("status", jsonFactory.textNode(tokenBindingStatus)) - tokenBindingId foreach { id => - tokenBinding.set("id", jsonFactory.textNode(id)) - } - tokenBinding - }, - ) - - json + jsonMap { + f => + Map( + "challenge" -> f.textNode(challenge.getBase64Url), + "origin" -> f.textNode(origin), + "type" -> f.textNode("webauthn.get"), + "tokenBinding" -> { + val tokenBinding = f.objectNode() + tokenBinding.set("status", f.textNode(tokenBindingStatus)) + tokenBindingId foreach { id => + tokenBinding.set("id", f.textNode(id)) + } + tokenBinding + }, + ) + } }) val clientDataJsonBytes = toBytes(clientDataJson) @@ -671,14 +683,14 @@ object TestAuthenticator { def makeU2fAttestationStatement( authDataBytes: ByteArray, - clientDataJson: String, + clientDataJson: ByteArray, signer: AttestationSigner, ): JsonNode = { val authData = new AuthenticatorData(authDataBytes) def makeSignedData( rpIdHash: ByteArray, - clientDataJson: String, + clientDataJson: ByteArray, credentialId: ByteArray, credentialPublicKeyRawBytes: ByteArray, ): ByteArray = { @@ -704,28 +716,25 @@ object TestAuthenticator { ), ) - val f = JsonNodeFactory.instance - f.objectNode() - .setAll( - Map( - "x5c" -> f.arrayNode().add(f.binaryNode(signer.cert.getEncoded)), - "sig" -> f.binaryNode( - sign( - signedData, - signer.key, - signer.alg, - ).getBytes - ), - ).asJava + jsonMap { f => + Map( + "x5c" -> f.arrayNode().add(f.binaryNode(signer.cert.getEncoded)), + "sig" -> f.binaryNode( + sign( + signedData, + signer.key, + signer.alg, + ).getBytes + ), ) + } } - def makeNoneAttestationStatement(): JsonNode = - JsonNodeFactory.instance.objectNode() + def makeNoneAttestationStatement(): JsonNode = jsonFactory.objectNode() def makePackedAttestationStatement( authDataBytes: ByteArray, - clientDataJson: String, + clientDataJson: ByteArray, signer: AttestationSigner, ): JsonNode = { val signedData = new ByteArray( @@ -737,66 +746,56 @@ object TestAuthenticator { case AttestationCert(_, key, alg, _) => sign(signedData, key, alg) } - val f = JsonNodeFactory.instance - f.objectNode() - .setAll( - ( + jsonMap { f => + Map( + "alg" -> f.numberNode(signer.alg.getId), + "sig" -> f.binaryNode(signature.getBytes), + ) ++ (signer match { + case _: SelfAttestation => Map.empty + case AttestationCert(cert, _, _, _) => Map( - "alg" -> f.numberNode(signer.alg.getId), - "sig" -> f.binaryNode(signature.getBytes), - ) ++ (signer match { - case _: SelfAttestation => Map.empty - case AttestationCert(cert, _, _, _) => - Map( - "x5c" -> f - .arrayNode() - .add(cert.getEncoded) - ) - }) - ).asJava - ) + "x5c" -> f + .arrayNode() + .add(cert.getEncoded) + ) + }) + } } def makeAndroidSafetynetAttestationStatement( authDataBytes: ByteArray, - clientDataJson: String, + clientDataJson: ByteArray, cert: AttestationCert, ctsProfileMatch: Boolean = true, ): JsonNode = { val nonce = Crypto.sha256(authDataBytes concat Crypto.sha256(clientDataJson)) - val f = JsonNodeFactory.instance - - val jwsHeader = f - .objectNode() - .setAll[ObjectNode]( - Map( - "alg" -> f.textNode("RS256"), - "x5c" -> f - .arrayNode() - .add(new ByteArray(cert.cert.getEncoded).getBase64), - ).asJava + val jwsHeader = jsonMap { f => + Map( + "alg" -> f.textNode("RS256"), + "x5c" -> f + .arrayNode() + .add(new ByteArray(cert.cert.getEncoded).getBase64), ) + } val jwsHeaderBase64 = new ByteArray( JacksonCodecs.json().writeValueAsBytes(jwsHeader) ).getBase64Url - val jwsPayload = f - .objectNode() - .setAll[ObjectNode]( - Map( - "nonce" -> f.textNode(nonce.getBase64), - "timestampMs" -> f.numberNode(Instant.now().toEpochMilli), - "apkPackageName" -> f.textNode("com.yubico.webauthn.test"), - "apkDigestSha256" -> f.textNode(Crypto.sha256("foo").getBase64), - "ctsProfileMatch" -> f.booleanNode(ctsProfileMatch), - "aplCertificateDigestSha256" -> f - .arrayNode() - .add(f.textNode(Crypto.sha256("foo").getBase64)), - "basicIntegrity" -> f.booleanNode(true), - ).asJava + val jwsPayload = jsonMap { f => + Map( + "nonce" -> f.textNode(nonce.getBase64), + "timestampMs" -> f.numberNode(Instant.now().toEpochMilli), + "apkPackageName" -> f.textNode("com.yubico.webauthn.test"), + "apkDigestSha256" -> f.textNode(Crypto.sha256("foo").getBase64), + "ctsProfileMatch" -> f.booleanNode(ctsProfileMatch), + "aplCertificateDigestSha256" -> f + .arrayNode() + .add(f.textNode(Crypto.sha256("foo").getBase64)), + "basicIntegrity" -> f.booleanNode(true), ) + } val jwsPayloadBase64 = new ByteArray( JacksonCodecs.json().writeValueAsBytes(jwsPayload) ).getBase64Url @@ -809,16 +808,14 @@ object TestAuthenticator { val jwsCompact = jwsSignedCompact + "." + jwsSignature.getBase64Url - val attStmt = f - .objectNode() - .setAll[ObjectNode]( - Map( - "ver" -> f.textNode("14799021"), - "response" -> f.binaryNode( - jwsCompact.getBytes(StandardCharsets.UTF_8) - ), - ).asJava + val attStmt = jsonMap { f => + Map( + "ver" -> f.textNode("14799021"), + "response" -> f.binaryNode( + jwsCompact.getBytes(StandardCharsets.UTF_8) + ), ) + } attStmt } @@ -827,15 +824,12 @@ object TestAuthenticator { caCert: X509Certificate, caKey: PrivateKey, authDataBytes: ByteArray, - clientDataJson: String, + clientDataJson: ByteArray, addNonceExtension: Boolean = true, nonceValue: Option[ByteArray] = None, certSubjectPublicKey: Option[PublicKey] = None, ): JsonNode = { - val clientDataJSON = new ByteArray( - clientDataJson.getBytes(StandardCharsets.UTF_8) - ) - val clientDataJsonHash = Crypto.sha256(clientDataJSON) + val clientDataJsonHash = Crypto.sha256(clientDataJson) val nonceToHash = authDataBytes.concat(clientDataJsonHash) val nonce = Crypto.sha256(nonceToHash) @@ -871,24 +865,22 @@ object TestAuthenticator { } else Nil, ) - val f = JsonNodeFactory.instance - f.objectNode() - .setAll( - Map( - "x5c" -> f - .arrayNode() - .addAll( - List(subjectCert, caCert) - .map(crt => f.binaryNode(crt.getEncoded)) - .asJava - ) - ).asJava + jsonMap { f => + Map( + "x5c" -> f + .arrayNode() + .addAll( + List(subjectCert, caCert) + .map(crt => f.binaryNode(crt.getEncoded)) + .asJava + ) ) + } } def makeTpmAttestationStatement( authDataBytes: ByteArray, - clientDataJson: String, + clientDataJson: ByteArray, cert: AttestationCert, ver: Option[String] = Some("2.0"), magic: ByteArray = TpmAttestationStatementVerifier.TPM_GENERATED_VALUE, @@ -917,7 +909,12 @@ object TestAuthenticator { (TpmAlgHash.SHA512, TpmAlgAsym.ECC) case COSEAlgorithmIdentifier.RS256 => (TpmAlgHash.SHA256, TpmAlgAsym.RSA) - case COSEAlgorithmIdentifier.RS1 => (TpmAlgHash.SHA1, TpmAlgAsym.RSA) + case COSEAlgorithmIdentifier.RS384 => + (TpmAlgHash.SHA384, TpmAlgAsym.RSA) + case COSEAlgorithmIdentifier.RS512 => + (TpmAlgHash.SHA512, TpmAlgAsym.RSA) + case COSEAlgorithmIdentifier.RS1 => (TpmAlgHash.SHA1, TpmAlgAsym.RSA) + case COSEAlgorithmIdentifier.EdDSA => ??? } val hashFunc = hashId match { case TpmAlgHash.SHA256 => Crypto.sha256(_: ByteArray) @@ -925,13 +922,10 @@ object TestAuthenticator { 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 extraData = hashFunc( + authDataBytes concat Crypto.sha256(clientDataJson) + ) + val (parameters, unique) = WebAuthnTestCodecs.getCoseKty(cosePubkey) match { case 3 => { // RSA val cose = CBORObject.DecodeFromBytes(cosePubkey.getBytes) @@ -972,6 +966,12 @@ object TestAuthenticator { case COSEAlgorithmIdentifier.ES256 => 0x0003 case COSEAlgorithmIdentifier.ES384 => 0x0004 case COSEAlgorithmIdentifier.ES512 => 0x0005 + case COSEAlgorithmIdentifier.RS1 | + COSEAlgorithmIdentifier.RS256 | + COSEAlgorithmIdentifier.RS384 | + COSEAlgorithmIdentifier.RS512 | + COSEAlgorithmIdentifier.EdDSA => + ??? })) ) .concat( @@ -1032,23 +1032,20 @@ object TestAuthenticator { 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 + jsonMap { f => + 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), ) + } } def makeAuthDataBytes( @@ -1124,8 +1121,9 @@ object TestAuthenticator { case COSEAlgorithmIdentifier.ES256 => generateEcKeypair("secp256r1") case COSEAlgorithmIdentifier.ES384 => generateEcKeypair("secp384r1") case COSEAlgorithmIdentifier.ES512 => generateEcKeypair("secp521r1") - case COSEAlgorithmIdentifier.RS256 => generateRsaKeypair() - case COSEAlgorithmIdentifier.RS1 => generateRsaKeypair() + case COSEAlgorithmIdentifier.RS256 | COSEAlgorithmIdentifier.RS384 | + COSEAlgorithmIdentifier.RS512 | COSEAlgorithmIdentifier.RS1 => + generateRsaKeypair() } def generateEcKeypair(curve: String = "secp256r1"): KeyPair = { 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 f70ab9ca1..c1efa2775 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 @@ -48,7 +48,7 @@ class WebAuthnCodecsSpec Arbitrary( for { ySign: Byte <- Gen.oneOf(0x02: Byte, 0x03: Byte) - rawBytes: Seq[Byte] <- Gen.listOfN[Byte](32, Arbitrary.arbitrary[Byte]) + rawBytes <- Gen.listOfN[Byte](32, Arbitrary.arbitrary[Byte]) key = Try( Util .decodePublicKey(new ByteArray((ySign +: rawBytes).toArray)) 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 7643a6d40..eb0202ebb 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 @@ -40,7 +40,8 @@ object WebAuthnTestCodecs { alg: COSEAlgorithmIdentifier, ): PrivateKey = alg match { - case COSEAlgorithmIdentifier.ES256 => + case COSEAlgorithmIdentifier.ES256 | COSEAlgorithmIdentifier.ES384 | + COSEAlgorithmIdentifier.ES512 => val keyFactory: KeyFactory = KeyFactory.getInstance("EC") val spec = new PKCS8EncodedKeySpec(encodedKey.getBytes) keyFactory.generatePrivate(spec) @@ -50,7 +51,8 @@ object WebAuthnTestCodecs { val spec = new PKCS8EncodedKeySpec(encodedKey.getBytes) keyFactory.generatePrivate(spec) - case COSEAlgorithmIdentifier.RS256 | COSEAlgorithmIdentifier.RS1 => + case COSEAlgorithmIdentifier.RS256 | COSEAlgorithmIdentifier.RS384 | + COSEAlgorithmIdentifier.RS512 | COSEAlgorithmIdentifier.RS1 => val keyFactory: KeyFactory = KeyFactory.getInstance("RSA") val spec = new PKCS8EncodedKeySpec(encodedKey.getBytes) keyFactory.generatePrivate(spec) 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 6a0ba9742..24d1fe2d0 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 @@ -30,6 +30,7 @@ import com.upokecenter.cbor.CBOREncodeOptions import com.upokecenter.cbor.CBORObject import com.yubico.internal.util.BinaryUtil import com.yubico.internal.util.JacksonCodecs +import com.yubico.scalacheck.gen.GenUtil.halfsized import com.yubico.scalacheck.gen.JacksonGenerators import com.yubico.scalacheck.gen.JacksonGenerators._ import com.yubico.scalacheck.gen.JavaGenerators._ @@ -81,38 +82,42 @@ object Generators { implicit val arbitraryAssertionRequest: Arbitrary[AssertionRequest] = Arbitrary( - for { - publicKeyCredentialRequestOptions <- - arbitrary[PublicKeyCredentialRequestOptions] - username <- arbitrary[Optional[String]] - } yield AssertionRequest - .builder() - .publicKeyCredentialRequestOptions(publicKeyCredentialRequestOptions) - .username(username) - .build() + halfsized( + for { + publicKeyCredentialRequestOptions <- + arbitrary[PublicKeyCredentialRequestOptions] + username <- arbitrary[Optional[String]] + } yield AssertionRequest + .builder() + .publicKeyCredentialRequestOptions(publicKeyCredentialRequestOptions) + .username(username) + .build() + ) ) implicit val arbitraryAttestedCredentialData : Arbitrary[AttestedCredentialData] = Arbitrary( - for { - aaguid <- byteArray(16) - credentialId <- arbitrary[ByteArray] - credentialPublicKey <- Gen.delay( - Gen.const( - TestAuthenticator - .generateEcKeypair() - .getPublic - .asInstanceOf[ECPublicKey] + halfsized( + for { + aaguid <- byteArray(16) + credentialId <- arbitrary[ByteArray] + credentialPublicKey <- Gen.delay( + Gen.const( + TestAuthenticator + .generateEcKeypair() + .getPublic + .asInstanceOf[ECPublicKey] + ) ) - ) - credentialPublicKeyCose = - WebAuthnTestCodecs.ecPublicKeyToCose(credentialPublicKey) - } yield AttestedCredentialData - .builder() - .aaguid(aaguid) - .credentialId(credentialId) - .credentialPublicKey(credentialPublicKeyCose) - .build() + credentialPublicKeyCose = + WebAuthnTestCodecs.ecPublicKeyToCose(credentialPublicKey) + } yield AttestedCredentialData + .builder() + .aaguid(aaguid) + .credentialId(credentialId) + .credentialPublicKey(credentialPublicKeyCose) + .build() + ) ) def attestedCredentialDataBytes: Gen[ByteArray] = for { @@ -143,7 +148,7 @@ object Generators { extensionOutputsGen: Gen[Option[CBORObject]] = Gen.option(Extensions.authenticatorAssertionExtensionOutputs()) ): Gen[ByteArray] = - for { + halfsized(for { authData <- authenticatorDataBytes(extensionOutputsGen) alg <- arbitrary[COSEAlgorithmIdentifier] sig <- arbitrary[ByteArray] @@ -172,13 +177,13 @@ object Generators { "attStmt" -> attStmt, ).asJava ) - } yield new ByteArray(JacksonCodecs.cbor().writeValueAsBytes(attObj)) + } yield new ByteArray(JacksonCodecs.cbor().writeValueAsBytes(attObj))) def fidoU2fAttestationObject( extensionOutputsGen: Gen[Option[CBORObject]] = Gen.option(Extensions.authenticatorAssertionExtensionOutputs()) ): Gen[ByteArray] = - for { + halfsized(for { authData <- authenticatorDataBytes(extensionOutputsGen) sig <- arbitrary[ByteArray] x5c <- arbitrary[List[ByteArray]] @@ -205,7 +210,7 @@ object Generators { "attStmt" -> attStmt, ).asJava ) - } yield new ByteArray(JacksonCodecs.cbor().writeValueAsBytes(attObj)) + } yield new ByteArray(JacksonCodecs.cbor().writeValueAsBytes(attObj))) val authenticatorDataFlagsByte: Gen[Byte] = for { value <- arbitrary[Byte] @@ -225,41 +230,47 @@ object Generators { extensionOutputsGen: Gen[Option[CBORObject]] = Gen.option(Extensions.authenticatorAssertionExtensionOutputs()) ): Gen[AuthenticatorAssertionResponse] = - for { - authenticatorData <- authenticatorDataBytes(extensionOutputsGen) - clientDataJson <- clientDataJsonBytes - signature <- arbitrary[ByteArray] - userHandle <- arbitrary[Option[ByteArray]] - } yield AuthenticatorAssertionResponse - .builder() - .authenticatorData(authenticatorData) - .clientDataJSON(clientDataJson) - .signature(signature) - .userHandle(userHandle.toJava) - .build() + halfsized( + for { + authenticatorData <- authenticatorDataBytes(extensionOutputsGen) + clientDataJson <- clientDataJsonBytes + signature <- arbitrary[ByteArray] + userHandle <- arbitrary[Option[ByteArray]] + } yield AuthenticatorAssertionResponse + .builder() + .authenticatorData(authenticatorData) + .clientDataJSON(clientDataJson) + .signature(signature) + .userHandle(userHandle.toJava) + .build() + ) implicit val arbitraryAuthenticatorAttestationResponse : Arbitrary[AuthenticatorAttestationResponse] = Arbitrary( - for { - attestationObject <- attestationObjectBytes() - clientDataJSON <- clientDataJsonBytes - } yield AuthenticatorAttestationResponse - .builder() - .attestationObject(attestationObject) - .clientDataJSON(clientDataJSON) - .build() + halfsized( + for { + attestationObject <- attestationObjectBytes() + clientDataJSON <- clientDataJsonBytes + } yield AuthenticatorAttestationResponse + .builder() + .attestationObject(attestationObject) + .clientDataJSON(clientDataJSON) + .build() + ) ) implicit val arbitraryAuthenticatorData: Arbitrary[AuthenticatorData] = Arbitrary( - authenticatorDataBytes(extensionsGen = - Gen.option( - Gen.oneOf( - Extensions.authenticatorRegistrationExtensionOutputs(), - Extensions.authenticatorAssertionExtensionOutputs(), + halfsized( + authenticatorDataBytes(extensionsGen = + Gen.option( + Gen.oneOf( + Extensions.authenticatorRegistrationExtensionOutputs(), + Extensions.authenticatorAssertionExtensionOutputs(), + ) ) - ) - ) map (new AuthenticatorData(_)) + ) map (new AuthenticatorData(_)) + ) ) def authenticatorDataBytes( @@ -271,39 +282,43 @@ object Generators { arbitrary[(Boolean, Boolean)].map({ case (be, bs) => (be, be && bs) }), signatureCountGen: Gen[ByteArray] = byteArray(4), ): Gen[ByteArray] = - for { - rpIdHash <- rpIdHashGen - signatureCount <- signatureCountGen - attestedCredentialDataBytes <- Gen.option(attestedCredentialDataBytes) - - extensions <- extensionsGen - extensionsBytes = extensions map { exts => - new ByteArray( - exts.EncodeToBytes(CBOREncodeOptions.DefaultCtap2Canonical) - ) - } + halfsized( + for { + rpIdHash <- rpIdHashGen + signatureCount <- signatureCountGen + attestedCredentialDataBytes <- Gen.option(attestedCredentialDataBytes) + + extensions <- extensionsGen + extensionsBytes = extensions map { exts => + new ByteArray( + exts.EncodeToBytes(CBOREncodeOptions.DefaultCtap2Canonical) + ) + } - flagsBase <- arbitrary[Byte] - upFlag <- upFlagGen - uvFlag <- uvFlagGen - (beFlag, bsFlag) <- backupFlagsGen - atFlag = attestedCredentialDataBytes.isDefined - edFlag = extensionsBytes.isDefined - flagsByte: Byte = setFlag(0x01, upFlag)( - setFlag(0x03, uvFlag)( - setFlag(0x40, atFlag)( - setFlag(BinaryUtil.singleFromHex("80"), edFlag)( - setFlag(0x08, beFlag)(setFlag(0x10, bsFlag)(flagsBase)) + flagsBase <- arbitrary[Byte] + upFlag <- upFlagGen + uvFlag <- uvFlagGen + (beFlag, bsFlag) <- backupFlagsGen + atFlag = attestedCredentialDataBytes.isDefined + edFlag = extensionsBytes.isDefined + flagsByte: Byte = setFlag(0x01, upFlag)( + setFlag(0x03, uvFlag)( + setFlag(0x40, atFlag)( + setFlag(BinaryUtil.singleFromHex("80"), edFlag)( + setFlag(0x08, beFlag)(setFlag(0x10, bsFlag)(flagsBase)) + ) ) ) ) + } yield new ByteArray( + rpIdHash.getBytes + :+ flagsByte + :++ signatureCount.getBytes + ++ attestedCredentialDataBytes + .map(_.getBytes) + .getOrElse(Array.empty) + ++ extensionsBytes.map(_.getBytes).getOrElse(Array.empty) ) - } yield new ByteArray( - rpIdHash.getBytes - :+ flagsByte - :++ signatureCount.getBytes - ++ attestedCredentialDataBytes.map(_.getBytes).getOrElse(Array.empty) - ++ extensionsBytes.map(_.getBytes).getOrElse(Array.empty) ) implicit val arbitraryAuthenticatorSelectionCriteria @@ -336,7 +351,7 @@ object Generators { def byteArray(minSize: Int, maxSize: Int): Gen[ByteArray] = for { - nums <- Gen.infiniteLazyList(arbitrary[Byte]).map(_.take(minSize)) + nums <- Gen.infiniteLazyList(arbitrary[Byte]) len <- Gen.chooseNum(minSize, maxSize) } yield new ByteArray(nums.take(len).toArray) @@ -360,8 +375,6 @@ object Generators { Set("appidExclude", "credProps", "largeBlob", "uvm") private val AuthenticationExtensionIds: Set[String] = Set("appid", "largeBlob", "uvm") - private val ExtensionIds: Set[String] = - RegistrationExtensionIds ++ AuthenticationExtensionIds private val ClientRegistrationExtensionOutputIds: Set[String] = RegistrationExtensionIds - "uvm" @@ -384,7 +397,7 @@ object Generators { for { appidExclude <- appidExcludeGen credProps <- credPropsGen - largeBlob <- largeBlobGen + largeBlob <- halfsized(largeBlobGen) uvm <- uvmGen } yield { val b = RegistrationExtensionInputs.builder() @@ -402,7 +415,7 @@ object Generators { ): Gen[ObjectNode] = for { base <- gen - extra <- genExtra + extra <- halfsized(genExtra) } yield { val result = extra result.setAll(JacksonCodecs.json().valueToTree[ObjectNode](base)) @@ -421,7 +434,7 @@ object Generators { for { appidExclude <- appidExcludeGen credProps <- credPropsGen - largeBlob <- largeBlobGen + largeBlob <- halfsized(largeBlobGen) } yield { val b = ClientRegistrationExtensionOutputs.builder() appidExclude.foreach(appidExclude => b.appidExclude(appidExclude)) @@ -445,12 +458,17 @@ object Generators { ) def authenticatorRegistrationExtensionOutputs( - uvmGen: Gen[Option[CBORObject]] = Gen.option(Uvm.authenticatorOutput) + uvmGen: Gen[Option[CBORObject]] = Gen.option(Uvm.authenticatorOutput), + includeUnknown: Boolean = true, ): Gen[CBORObject] = for { - uvm: Option[CBORObject] <- uvmGen + base <- + if (includeUnknown) + halfsized(unknownAuthenticatorRegistrationExtensionOutput) + else Gen.const(CBORObject.NewMap()) + uvm: Option[CBORObject] <- halfsized(uvmGen) } yield { - val result = CBORObject.NewMap() + val result = base uvm.foreach(result.set("uvm", _)) result } @@ -473,7 +491,7 @@ object Generators { ): Gen[AssertionExtensionInputs] = for { appid <- appidGen - largeBlob <- largeBlobGen + largeBlob <- halfsized(largeBlobGen) uvm <- uvmGen } yield { val b = AssertionExtensionInputs.builder() @@ -489,7 +507,7 @@ object Generators { ): Gen[ObjectNode] = for { base <- gen - extra <- genExtra + extra <- halfsized(genExtra) } yield { val result = extra result.setAll(JacksonCodecs.json().valueToTree[ObjectNode](base)) @@ -502,7 +520,7 @@ object Generators { ] = LargeBlob.largeBlobAuthenticationOutput ): Gen[ClientAssertionExtensionOutputs] = for { - largeBlob <- largeBlobGen + largeBlob <- halfsized(largeBlobGen) } yield { val b = ClientAssertionExtensionOutputs.builder() b.appid(true) @@ -511,12 +529,17 @@ object Generators { } def authenticatorAssertionExtensionOutputs( - uvmGen: Gen[Option[CBORObject]] = Gen.option(Uvm.authenticatorOutput) + uvmGen: Gen[Option[CBORObject]] = Gen.option(Uvm.authenticatorOutput), + includeUnknown: Boolean = true, ): Gen[CBORObject] = for { - uvm: Option[CBORObject] <- uvmGen + base <- + if (includeUnknown) + halfsized(unknownAuthenticatorAssertionExtensionOutput) + else Gen.const(CBORObject.NewMap()) + uvm: Option[CBORObject] <- halfsized(uvmGen) } yield { - val result = CBORObject.NewMap() + val result = base uvm.foreach(result.set("uvm", _)) result } @@ -627,21 +650,23 @@ object Generators { CBORObject, ) ] = - for { - inputs <- arbitrary[RegistrationExtensionInputs] - clientOutputs <- allClientRegistrationExtensionOutputs() - authenticatorOutputs <- allAuthenticatorRegistrationExtensionOutputs() - - requestedExtensionIds <- - Gen.someOf(inputs.getExtensionIds.asScala).map(_.toSet) - returnedExtensionIds <- Gen.oneOf( - Gen.const(requestedExtensionIds), - Gen.someOf(requestedExtensionIds).map(_.toSet), + halfsized( + for { + inputs <- arbitrary[RegistrationExtensionInputs] + clientOutputs <- allClientRegistrationExtensionOutputs() + authenticatorOutputs <- allAuthenticatorRegistrationExtensionOutputs() + + requestedExtensionIds <- + Gen.someOf(inputs.getExtensionIds.asScala).map(_.toSet) + returnedExtensionIds <- Gen.oneOf( + Gen.const(requestedExtensionIds), + Gen.someOf(requestedExtensionIds).map(_.toSet), + ) + } yield ( + filter(inputs, requestedExtensionIds), + filter(clientOutputs, returnedExtensionIds), + filter(authenticatorOutputs, returnedExtensionIds), ) - } yield ( - filter(inputs, requestedExtensionIds), - filter(clientOutputs, returnedExtensionIds), - filter(authenticatorOutputs, returnedExtensionIds), ) def unrequestedClientRegistrationExtensions: Gen[ @@ -651,27 +676,29 @@ object Generators { CBORObject, ) ] = - for { - inputs <- arbitrary[RegistrationExtensionInputs] - clientOutputs <- allClientRegistrationExtensionOutputs() - authenticatorOutputs <- allAuthenticatorRegistrationExtensionOutputs() + halfsized( + for { + inputs <- arbitrary[RegistrationExtensionInputs] + clientOutputs <- allClientRegistrationExtensionOutputs() + authenticatorOutputs <- allAuthenticatorRegistrationExtensionOutputs() - unrequestedClientExtensionIds: Set[String] <- - Gen.nonEmptyContainerOf[Set, String]( - Gen.oneOf(ClientRegistrationExtensionOutputIds) - ) - requestedExtensionIds: Set[String] <- - Gen - .someOf(inputs.getExtensionIds.asScala) - .map(_.toSet -- unrequestedClientExtensionIds) - returnedExtensionIds: Set[String] <- - Gen - .someOf(requestedExtensionIds) - .map(_.toSet ++ unrequestedClientExtensionIds) - } yield ( - filter(inputs, requestedExtensionIds), - filter(clientOutputs, returnedExtensionIds), - filter(authenticatorOutputs, requestedExtensionIds), + unrequestedClientExtensionIds: Set[String] <- + Gen.nonEmptyContainerOf[Set, String]( + Gen.oneOf(ClientRegistrationExtensionOutputIds) + ) + requestedExtensionIds: Set[String] <- + Gen + .someOf(inputs.getExtensionIds.asScala) + .map(_.toSet -- unrequestedClientExtensionIds) + returnedExtensionIds: Set[String] <- + Gen + .someOf(requestedExtensionIds) + .map(_.toSet ++ unrequestedClientExtensionIds) + } yield ( + filter(inputs, requestedExtensionIds), + filter(clientOutputs, returnedExtensionIds), + filter(authenticatorOutputs, requestedExtensionIds), + ) ) def unrequestedAuthenticatorRegistrationExtensions: Gen[ @@ -681,27 +708,29 @@ object Generators { CBORObject, ) ] = - for { - inputs <- arbitrary[RegistrationExtensionInputs] - clientOutputs <- allClientRegistrationExtensionOutputs() - authenticatorOutputs <- allAuthenticatorRegistrationExtensionOutputs() + halfsized( + for { + inputs <- arbitrary[RegistrationExtensionInputs] + clientOutputs <- allClientRegistrationExtensionOutputs() + authenticatorOutputs <- allAuthenticatorRegistrationExtensionOutputs() - unrequestedAuthenticatorExtensionIds: Set[String] <- - Gen.nonEmptyContainerOf[Set, String]( - Gen.oneOf(AuthenticatorRegistrationExtensionOutputIds) - ) - requestedExtensionIds: Set[String] <- - Gen - .someOf(inputs.getExtensionIds.asScala) - .map(_.toSet -- unrequestedAuthenticatorExtensionIds) - returnedExtensionIds: Set[String] <- - Gen - .someOf(requestedExtensionIds) - .map(_.toSet ++ unrequestedAuthenticatorExtensionIds) - } yield ( - filter(inputs, requestedExtensionIds), - filter(clientOutputs, requestedExtensionIds), - filter(authenticatorOutputs, returnedExtensionIds), + unrequestedAuthenticatorExtensionIds: Set[String] <- + Gen.nonEmptyContainerOf[Set, String]( + Gen.oneOf(AuthenticatorRegistrationExtensionOutputIds) + ) + requestedExtensionIds: Set[String] <- + Gen + .someOf(inputs.getExtensionIds.asScala) + .map(_.toSet -- unrequestedAuthenticatorExtensionIds) + returnedExtensionIds: Set[String] <- + Gen + .someOf(requestedExtensionIds) + .map(_.toSet ++ unrequestedAuthenticatorExtensionIds) + } yield ( + filter(inputs, requestedExtensionIds), + filter(clientOutputs, requestedExtensionIds), + filter(authenticatorOutputs, returnedExtensionIds), + ) ) def anyRegistrationExtensions: Gen[ @@ -711,113 +740,125 @@ object Generators { CBORObject, ) ] = - for { - inputs <- arbitrary[RegistrationExtensionInputs] - clientOutputs <- allClientRegistrationExtensionOutputs() - authenticatorOutputs <- allAuthenticatorRegistrationExtensionOutputs() - - requestedExtensionIds <- - Gen.someOf(RegistrationExtensionIds).map(_.toSet) - returnedClientExtensionIds <- - Gen.someOf(ClientRegistrationExtensionOutputIds).map(_.toSet) - returnedAuthenticatorExtensionIds <- - Gen.someOf(AuthenticatorRegistrationExtensionOutputIds).map(_.toSet) - } yield ( - filter(inputs, requestedExtensionIds), - filter(clientOutputs, returnedClientExtensionIds), - filter(authenticatorOutputs, returnedAuthenticatorExtensionIds), + halfsized( + for { + inputs <- arbitrary[RegistrationExtensionInputs] + clientOutputs <- allClientRegistrationExtensionOutputs() + authenticatorOutputs <- allAuthenticatorRegistrationExtensionOutputs() + + requestedExtensionIds <- + Gen.someOf(RegistrationExtensionIds).map(_.toSet) + returnedClientExtensionIds <- + Gen.someOf(ClientRegistrationExtensionOutputIds).map(_.toSet) + returnedAuthenticatorExtensionIds <- + Gen.someOf(AuthenticatorRegistrationExtensionOutputIds).map(_.toSet) + } yield ( + filter(inputs, requestedExtensionIds), + filter(clientOutputs, returnedClientExtensionIds), + filter(authenticatorOutputs, returnedAuthenticatorExtensionIds), + ) ) def subsetAssertionExtensions: Gen[ (AssertionExtensionInputs, ClientAssertionExtensionOutputs, CBORObject) ] = - for { - inputs <- arbitrary[AssertionExtensionInputs] - clientOutputs <- allClientAssertionExtensionOutputs() - authenticatorOutputs <- allAuthenticatorAssertionExtensionOutputs() - - requestedExtensionIds <- - Gen.someOf(inputs.getExtensionIds.asScala).map(_.toSet) - returnedExtensionIds <- Gen.oneOf( - Gen.const(requestedExtensionIds), - Gen.someOf(requestedExtensionIds).map(_.toSet), + halfsized( + for { + inputs <- arbitrary[AssertionExtensionInputs] + clientOutputs <- allClientAssertionExtensionOutputs() + authenticatorOutputs <- allAuthenticatorAssertionExtensionOutputs() + + requestedExtensionIds <- + Gen.someOf(inputs.getExtensionIds.asScala).map(_.toSet) + returnedExtensionIds <- Gen.oneOf( + Gen.const(requestedExtensionIds), + Gen.someOf(requestedExtensionIds).map(_.toSet), + ) + } yield ( + filter(inputs, requestedExtensionIds), + filter(clientOutputs, returnedExtensionIds), + filter(authenticatorOutputs, returnedExtensionIds), ) - } yield ( - filter(inputs, requestedExtensionIds), - filter(clientOutputs, returnedExtensionIds), - filter(authenticatorOutputs, returnedExtensionIds), ) def unrequestedClientAssertionExtensions: Gen[ (AssertionExtensionInputs, ClientAssertionExtensionOutputs, CBORObject) ] = - for { - inputs <- arbitrary[AssertionExtensionInputs] - clientOutputs <- allClientAssertionExtensionOutputs() - authenticatorOutputs <- allAuthenticatorAssertionExtensionOutputs() + halfsized( + for { + inputs <- arbitrary[AssertionExtensionInputs] + clientOutputs <- allClientAssertionExtensionOutputs() + authenticatorOutputs <- allAuthenticatorAssertionExtensionOutputs() - unrequestedClientExtensionIds: Set[String] <- - Gen.nonEmptyContainerOf[Set, String]( - Gen.oneOf(ClientAuthenticationExtensionOutputIds) - ) - requestedExtensionIds: Set[String] <- - Gen - .someOf(inputs.getExtensionIds.asScala) - .map(_.toSet -- unrequestedClientExtensionIds) - returnedExtensionIds: Set[String] <- - Gen - .someOf(requestedExtensionIds) - .map(_.toSet ++ unrequestedClientExtensionIds) - } yield ( - filter(inputs, requestedExtensionIds), - filter(clientOutputs, returnedExtensionIds), - filter(authenticatorOutputs, requestedExtensionIds), + unrequestedClientExtensionIds: Set[String] <- + Gen.nonEmptyContainerOf[Set, String]( + Gen.oneOf(ClientAuthenticationExtensionOutputIds) + ) + requestedExtensionIds: Set[String] <- + Gen + .someOf(inputs.getExtensionIds.asScala) + .map(_.toSet -- unrequestedClientExtensionIds) + returnedExtensionIds: Set[String] <- + Gen + .someOf(requestedExtensionIds) + .map(_.toSet ++ unrequestedClientExtensionIds) + } yield ( + filter(inputs, requestedExtensionIds), + filter(clientOutputs, returnedExtensionIds), + filter(authenticatorOutputs, requestedExtensionIds), + ) ) def unrequestedAuthenticatorAssertionExtensions: Gen[ (AssertionExtensionInputs, ClientAssertionExtensionOutputs, CBORObject) ] = - for { - inputs <- arbitrary[AssertionExtensionInputs] - clientOutputs <- allClientAssertionExtensionOutputs() - authenticatorOutputs <- allAuthenticatorAssertionExtensionOutputs() + halfsized( + for { + inputs <- arbitrary[AssertionExtensionInputs] + clientOutputs <- allClientAssertionExtensionOutputs() + authenticatorOutputs <- allAuthenticatorAssertionExtensionOutputs() - unrequestedAuthenticatorExtensionIds: Set[String] <- - Gen.nonEmptyContainerOf[Set, String]( - Gen.oneOf(AuthenticatorAuthenticationExtensionOutputIds) - ) - requestedExtensionIds: Set[String] <- - Gen - .someOf(inputs.getExtensionIds.asScala) - .map(_.toSet -- unrequestedAuthenticatorExtensionIds) - returnedExtensionIds: Set[String] <- - Gen - .someOf(requestedExtensionIds) - .map(_.toSet ++ unrequestedAuthenticatorExtensionIds) - } yield ( - filter(inputs, requestedExtensionIds), - filter(clientOutputs, requestedExtensionIds), - filter(authenticatorOutputs, returnedExtensionIds), + unrequestedAuthenticatorExtensionIds: Set[String] <- + Gen.nonEmptyContainerOf[Set, String]( + Gen.oneOf(AuthenticatorAuthenticationExtensionOutputIds) + ) + requestedExtensionIds: Set[String] <- + Gen + .someOf(inputs.getExtensionIds.asScala) + .map(_.toSet -- unrequestedAuthenticatorExtensionIds) + returnedExtensionIds: Set[String] <- + Gen + .someOf(requestedExtensionIds) + .map(_.toSet ++ unrequestedAuthenticatorExtensionIds) + } yield ( + filter(inputs, requestedExtensionIds), + filter(clientOutputs, requestedExtensionIds), + filter(authenticatorOutputs, returnedExtensionIds), + ) ) def anyAssertionExtensions: Gen[ (AssertionExtensionInputs, ClientAssertionExtensionOutputs, CBORObject) ] = - for { - inputs <- arbitrary[AssertionExtensionInputs] - clientOutputs <- allClientAssertionExtensionOutputs() - authenticatorOutputs <- allAuthenticatorAssertionExtensionOutputs() - - requestedExtensionIds <- - Gen.someOf(AuthenticationExtensionIds).map(_.toSet) - returnedClientExtensionIds <- - Gen.someOf(ClientAuthenticationExtensionOutputIds).map(_.toSet) - returnedAuthenticatorExtensionIds <- - Gen.someOf(AuthenticatorAuthenticationExtensionOutputIds).map(_.toSet) - } yield ( - filter(inputs, requestedExtensionIds), - filter(clientOutputs, returnedClientExtensionIds), - filter(authenticatorOutputs, returnedAuthenticatorExtensionIds), + halfsized( + for { + inputs <- arbitrary[AssertionExtensionInputs] + clientOutputs <- allClientAssertionExtensionOutputs() + authenticatorOutputs <- allAuthenticatorAssertionExtensionOutputs() + + requestedExtensionIds <- + Gen.someOf(AuthenticationExtensionIds).map(_.toSet) + returnedClientExtensionIds <- + Gen.someOf(ClientAuthenticationExtensionOutputIds).map(_.toSet) + returnedAuthenticatorExtensionIds <- + Gen + .someOf(AuthenticatorAuthenticationExtensionOutputIds) + .map(_.toSet) + } yield ( + filter(inputs, requestedExtensionIds), + filter(clientOutputs, returnedClientExtensionIds), + filter(authenticatorOutputs, returnedAuthenticatorExtensionIds), + ) ) object CredProps { @@ -842,22 +883,22 @@ object Generators { } yield new LargeBlobRegistrationOutput(supported) def largeBlobAuthenticationInput: Gen[LargeBlobAuthenticationInput] = - arbitrary[ByteArray] flatMap { write => + halfsized( Gen.oneOf( - LargeBlobAuthenticationInput.read(), - LargeBlobAuthenticationInput.write(write), + Gen.const(LargeBlobAuthenticationInput.read()), + arbitrary[ByteArray].map(LargeBlobAuthenticationInput.write), ) - } + ) def largeBlobAuthenticationOutput: Gen[LargeBlobAuthenticationOutput] = - for { + halfsized(for { blob <- arbitrary[ByteArray] written <- arbitrary[Boolean] result <- Gen.oneOf( new LargeBlobAuthenticationOutput(blob, null), new LargeBlobAuthenticationOutput(null, written), ) - } yield result + } yield result) } object Uvm { @@ -880,14 +921,9 @@ object Generators { ) def authenticatorOutput: Gen[CBORObject] = - for { - entry1 <- uvmEntry - entry23 <- Gen.listOfN(2, uvmEntry) - } yield { - CBORObject.FromObject( - Array(encodeUvmEntry(entry1)) ++ (entry23.map(encodeUvmEntry)) - ) - } + halfsized(for { + entries <- Gen.resize(3, Gen.nonEmptyListOf(uvmEntry)) + } yield CBORObject.FromObject(entries.map(encodeUvmEntry).toArray)) } } @@ -919,7 +955,7 @@ object Generators { implicit val arbitraryCollectedClientData: Arbitrary[CollectedClientData] = Arbitrary(clientDataJsonBytes map (new CollectedClientData(_))) def clientDataJsonBytes: Gen[ByteArray] = - for { + halfsized(for { jsonBase <- arbitrary[ObjectNode] challenge <- arbitrary[ByteArray] origin <- arbitrary[URL] @@ -965,7 +1001,7 @@ object Generators { json } - } yield new ByteArray(JacksonCodecs.json().writeValueAsBytes(json)) + } yield new ByteArray(JacksonCodecs.json().writeValueAsBytes(json))) implicit val arbitraryCOSEAlgorithmIdentifier : Arbitrary[COSEAlgorithmIdentifier] = Arbitrary( @@ -977,17 +1013,19 @@ object Generators { AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs, ]] = Arbitrary( - for { - id <- arbitrary[ByteArray] - (_, clientExtensionResults, authenticatorExtensionOutputs) <- - Extensions.anyAssertionExtensions - response <- arbitrary[AuthenticatorAssertionResponse] - } yield PublicKeyCredential - .builder() - .id(id) - .response(response) - .clientExtensionResults(clientExtensionResults) - .build() + halfsized( + for { + id <- arbitrary[ByteArray] + (_, clientExtensionResults, authenticatorExtensionOutputs) <- + Extensions.anyAssertionExtensions + response <- arbitrary[AuthenticatorAssertionResponse] + } yield PublicKeyCredential + .builder() + .id(id) + .response(response) + .clientExtensionResults(clientExtensionResults) + .build() + ) ) implicit val arbitraryPublicKeyCredentialWithAttestation @@ -995,92 +1033,102 @@ object Generators { AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs, ]] = Arbitrary( - for { - id <- arbitrary[ByteArray] - response <- arbitrary[AuthenticatorAttestationResponse] - clientExtensionResults <- arbitrary[ClientRegistrationExtensionOutputs] - } yield PublicKeyCredential - .builder() - .id(id) - .response(response) - .clientExtensionResults(clientExtensionResults) - .build() + halfsized( + for { + id <- arbitrary[ByteArray] + response <- arbitrary[AuthenticatorAttestationResponse] + clientExtensionResults <- arbitrary[ClientRegistrationExtensionOutputs] + } yield PublicKeyCredential + .builder() + .id(id) + .response(response) + .clientExtensionResults(clientExtensionResults) + .build() + ) ) implicit val arbitraryPublicKeyCredentialCreationOptions : Arbitrary[PublicKeyCredentialCreationOptions] = Arbitrary( - for { - attestation <- arbitrary[AttestationConveyancePreference] - authenticatorSelection <- - arbitrary[Optional[AuthenticatorSelectionCriteria]] - challenge <- arbitrary[ByteArray] - excludeCredentials <- - arbitrary[Optional[java.util.Set[PublicKeyCredentialDescriptor]]] - extensions <- arbitrary[RegistrationExtensionInputs] - pubKeyCredParams <- - arbitrary[java.util.List[PublicKeyCredentialParameters]] - rp <- arbitrary[RelyingPartyIdentity] - timeout <- arbitrary[Optional[java.lang.Long]] - user <- arbitrary[UserIdentity] - } yield PublicKeyCredentialCreationOptions - .builder() - .rp(rp) - .user(user) - .challenge(challenge) - .pubKeyCredParams(pubKeyCredParams) - .attestation(attestation) - .authenticatorSelection(authenticatorSelection) - .excludeCredentials(excludeCredentials) - .extensions(extensions) - .timeout(timeout) - .build() + halfsized( + for { + attestation <- arbitrary[AttestationConveyancePreference] + authenticatorSelection <- + arbitrary[Optional[AuthenticatorSelectionCriteria]] + challenge <- arbitrary[ByteArray] + excludeCredentials <- + arbitrary[Optional[java.util.Set[PublicKeyCredentialDescriptor]]] + extensions <- arbitrary[RegistrationExtensionInputs] + pubKeyCredParams <- + arbitrary[java.util.List[PublicKeyCredentialParameters]] + rp <- arbitrary[RelyingPartyIdentity] + timeout <- arbitrary[Optional[java.lang.Long]] + user <- arbitrary[UserIdentity] + } yield PublicKeyCredentialCreationOptions + .builder() + .rp(rp) + .user(user) + .challenge(challenge) + .pubKeyCredParams(pubKeyCredParams) + .attestation(attestation) + .authenticatorSelection(authenticatorSelection) + .excludeCredentials(excludeCredentials) + .extensions(extensions) + .timeout(timeout) + .build() + ) ) implicit val arbitraryPublicKeyCredentialDescriptor : Arbitrary[PublicKeyCredentialDescriptor] = Arbitrary( - for { - id <- arbitrary[ByteArray] - transports <- arbitrary[Optional[java.util.Set[AuthenticatorTransport]]] - tpe <- arbitrary[PublicKeyCredentialType] - } yield PublicKeyCredentialDescriptor - .builder() - .id(id) - .transports(transports) - .`type`(tpe) - .build() + halfsized( + for { + id <- arbitrary[ByteArray] + transports <- arbitrary[Optional[java.util.Set[AuthenticatorTransport]]] + tpe <- arbitrary[PublicKeyCredentialType] + } yield PublicKeyCredentialDescriptor + .builder() + .id(id) + .transports(transports) + .`type`(tpe) + .build() + ) ) implicit val arbitraryPublicKeyCredentialParameters : Arbitrary[PublicKeyCredentialParameters] = Arbitrary( - for { - alg <- arbitrary[COSEAlgorithmIdentifier] - tpe <- arbitrary[PublicKeyCredentialType] - } yield PublicKeyCredentialParameters - .builder() - .alg(alg) - .`type`(tpe) - .build() + halfsized( + for { + alg <- arbitrary[COSEAlgorithmIdentifier] + tpe <- arbitrary[PublicKeyCredentialType] + } yield PublicKeyCredentialParameters + .builder() + .alg(alg) + .`type`(tpe) + .build() + ) ) implicit val arbitraryPublicKeyCredentialRequestOptions : Arbitrary[PublicKeyCredentialRequestOptions] = Arbitrary( - for { - allowCredentials <- - arbitrary[Optional[java.util.List[PublicKeyCredentialDescriptor]]] - challenge <- arbitrary[ByteArray] - extensions <- arbitrary[AssertionExtensionInputs] - rpId <- arbitrary[Optional[String]] - timeout <- arbitrary[Optional[java.lang.Long]] - userVerification <- arbitrary[UserVerificationRequirement] - } yield PublicKeyCredentialRequestOptions - .builder() - .challenge(challenge) - .allowCredentials(allowCredentials) - .extensions(extensions) - .rpId(rpId) - .timeout(timeout) - .userVerification(userVerification) - .build() + halfsized( + for { + allowCredentials <- + arbitrary[Optional[java.util.List[PublicKeyCredentialDescriptor]]] + challenge <- arbitrary[ByteArray] + extensions <- arbitrary[AssertionExtensionInputs] + rpId <- arbitrary[Optional[String]] + timeout <- arbitrary[Optional[java.lang.Long]] + userVerification <- arbitrary[UserVerificationRequirement] + } yield PublicKeyCredentialRequestOptions + .builder() + .challenge(challenge) + .allowCredentials(allowCredentials) + .extensions(extensions) + .rpId(rpId) + .timeout(timeout) + .userVerification(userVerification) + .build() + ) ) implicit val arbitraryRegistrationExtensionInputs @@ -1090,14 +1138,16 @@ object Generators { implicit val arbitraryRelyingPartyIdentity: Arbitrary[RelyingPartyIdentity] = Arbitrary( - for { - id <- arbitrary[String] - name <- arbitrary[String] - } yield RelyingPartyIdentity - .builder() - .id(id) - .name(name) - .build() + halfsized( + for { + id <- arbitrary[String] + name <- arbitrary[String] + } yield RelyingPartyIdentity + .builder() + .id(id) + .name(name) + .build() + ) ) implicit val arbitraryTokenBindingInfo: Arbitrary[TokenBindingInfo] = @@ -1109,16 +1159,18 @@ object Generators { ) implicit val arbitraryUserIdentity: Arbitrary[UserIdentity] = Arbitrary( - for { - displayName <- arbitrary[String] - name <- arbitrary[String] - id <- arbitrary[ByteArray] - } yield UserIdentity - .builder() - .name(name) - .displayName(displayName) - .id(id) - .build() + halfsized( + for { + displayName <- arbitrary[String] + name <- arbitrary[String] + id <- arbitrary[ByteArray] + } yield UserIdentity + .builder() + .name(name) + .displayName(displayName) + .id(id) + .build() + ) ) } 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 0fd026f9b..0b2602b1c 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 @@ -61,32 +61,20 @@ class JsonIoSpec def test[A](tpe: TypeReference[A])(implicit a: Arbitrary[A]): Unit = { val cn = tpe.getType.getTypeName describe(s"${cn}") { - it("can be serialized to JSON.") { - forAll { value: A => + it("is identical after multiple serialization round-trips..") { + forAll(minSuccessful(10)) { value: A => val encoded: String = json.writeValueAsString(value) - encoded should not be empty - } - } - - it("can be deserialized from JSON.") { - forAll { value: A => - val encoded: String = json.writeValueAsString(value) val decoded: A = json.readValue(encoded, tpe) - decoded should equal(value) - } - } - it("is identical after multiple serialization round-trips..") { - forAll { value: A => - val encoded: String = json.writeValueAsString(value) - val decoded: A = json.readValue(encoded, tpe) val recoded: String = json.writeValueAsString(decoded) - - decoded should equal(value) recoded should equal(encoded) + + val redecoded: A = json.readValue(recoded, tpe) + redecoded should equal(value) } + } } } @@ -372,7 +360,7 @@ class JsonIoSpec val encoded = json.writeValueAsString(tree) println(authenticatorAttachment) val decoded = json.readValue(encoded, tpe) - decoded.getAuthenticatorAttachment.asScala should be(None) + decoded.getAuthenticatorAttachment.toScala should be(None) } forAll( @@ -388,7 +376,7 @@ class JsonIoSpec println(authenticatorAttachment) val decoded = json.readValue(encoded, tpe) - decoded.getAuthenticatorAttachment.asScala should equal( + decoded.getAuthenticatorAttachment.toScala should equal( Some(authenticatorAttachment) ) } @@ -402,7 +390,7 @@ class JsonIoSpec val encoded = json.writeValueAsString(tree) val decoded = json.readValue(encoded, tpe) - decoded.getAuthenticatorAttachment.asScala should be(None) + decoded.getAuthenticatorAttachment.toScala should be(None) } } diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/extension/uvm/Generators.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/extension/uvm/Generators.scala index d3f20199c..92869f232 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/extension/uvm/Generators.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/extension/uvm/Generators.scala @@ -2,15 +2,17 @@ package com.yubico.webauthn.extension.uvm import org.scalacheck.Gen +import scala.collection.immutable.ArraySeq + object Generators { def userVerificationMethod: Gen[UserVerificationMethod] = - Gen.oneOf(UserVerificationMethod.values) + Gen.oneOf(ArraySeq.unsafeWrapArray(UserVerificationMethod.values)) def keyProtectionType: Gen[KeyProtectionType] = - Gen.oneOf(KeyProtectionType.values) + Gen.oneOf(ArraySeq.unsafeWrapArray(KeyProtectionType.values)) def matcherProtectionType: Gen[MatcherProtectionType] = - Gen.oneOf(MatcherProtectionType.values) + Gen.oneOf(ArraySeq.unsafeWrapArray(MatcherProtectionType.values)) } 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 ac5b154e8..24dd52197 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 @@ -591,6 +591,36 @@ object RealExamples { ), ) + val YubikeyBio_5_5_6 = new Example( + RelyingPartyIdentity + .builder() + .id("demo.yubico.com") + .name("YubicoDemo") + .build(), + UserIdentity + .builder() + .name("Yubico demo user") + .displayName("Yubico demo user") + .id(ByteArray.fromBase64("KYljhyutCbO7mu5TI9Zt9ra11ScQvC+ArBpdYoAiEvg=")) + .build(), + AttestationExample( + base64UrlToString("eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiQnhoWTY4ZGczeHNNVmFRaWRqaW1BdyIsIm9yaWdpbiI6Imh0dHBzOi8vZGVtby55dWJpY28uY29tIiwiY3Jvc3NPcmlnaW4iOmZhbHNlfQ=="), + ByteArray.fromBase64("o2NmbXRmcGFja2VkZ2F0dFN0bXSjY2FsZyZjc2lnWEgwRgIhAMSgpu1ru29YJex9vN8Zmt7RJkvOj/DmD2Cnfz8nhVmLAiEA8qnz6llKsjWfZ1OYrR4AIS3JTIXsQgbmeK61pzuesYJjeDVjgVkC3DCCAtgwggHAoAMCAQICCQD/h2wtr3N5yDANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZdWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAwMDBaGA8yMDUwMDkwNDAwMDAwMFowbjELMAkGA1UEBhMCU0UxEjAQBgNVBAoMCVl1YmljbyBBQjEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjEnMCUGA1UEAwweWXViaWNvIFUyRiBFRSBTZXJpYWwgNzYyMDg3NDIzMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEJfEjoEgoP8V5bM+IfZlIn9k1wkGYxLXY1bLCv9fdXRWv5FtwcHdlZ9W1sLI+BFYLW+p3tIOx9kkeU6PyvuajmqOBgTB/MBMGCisGAQQBgsQKDQEEBQQDBQUGMCIGCSsGAQQBgsQKAgQVMS4zLjYuMS40LjEuNDE0ODIuMS45MBMGCysGAQQBguUcAgEBBAQDAgUgMCEGCysGAQQBguUcAQEEBBIEENhSLZ9XW0hmiKm6mfoC81swDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAQEAUrBpSduq0aZMG6nrwZizF+wx+aNzY7pRYbNC46ScrVBPNOdCi7iW6c/SjQOtEM4yWgaDjptsTssXrUDQkKFsnnw0SYMy/4U7YnR+j83wDa5idW5XvUCxbWd5B6g1wENaLrzpsLkGnKEiv52WSnMgavdP88ABROv/PefHdY0xR8jC+f6HwS8qlnWiBGsBB2NhqZchhx+nj7DeKUW1efkWbEitL9UMPOVsgiGnUIP2VhGTlDaP8X0skgxjoJ8B7SUBFGt98as5cKKjKTj6mlF69HEIXhYLPKeXZCMXRrpqu6aODRPOJZeWvNKgOtg8dOFTMTKOq0OOakGXyxLsb9HjiGhhdXRoRGF0YVifxGzvgq0bVGR3WR0Aiwh1nsPm0uy085R0v+ppaZJdA7fFAAAABNhSLZ9XW0hmiKm6mfoC81sAMIg/92bCZgLh2oUu6QF2XrSYZKh+qP1J3wf1SgOOkcMnF499E7JiLPi5YhY/308TfKQBAQMnIAYhWCCIP/dmwmYC4dqFLukBggD0oYvvkNUWXNzokKlsiK0/vaFrY3JlZFByb3RlY3QC"), + ), + AssertionExample( + id = ByteArray.fromBase64Url( + "iD_3ZsJmAuHahS7pAXZetJhkqH6o_UnfB_VKA46RwycXj30TsmIs-LliFj_fTxN8" + ), + clientData = + base64UrlToString("eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiX3RoYmVudXo3amZBcWJMZUxYVlFWQSIsIm9yaWdpbiI6Imh0dHBzOi8vZGVtby55dWJpY28uY29tIiwiY3Jvc3NPcmlnaW4iOmZhbHNlfQ=="), + authDataBytes = ByteArray.fromBase64( + "xGzvgq0bVGR3WR0Aiwh1nsPm0uy085R0v+ppaZJdA7cFAAAACA==" + ), + sig = + ByteArray.fromBase64("ZeXxnNYjBwh5Irn+W6VzRna/3XQrsvYhKVa+T8tv2eEw/UuALFoLHlBRkFQr73wgmLZ4ma2gEXocOnuUjVBZAw=="), + ), + ) + val CredPropsEmpty = AttestationExample( base64UrlToString("eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiYlZjNWxvY3dnV0ZvdlJ6M2RzWGkzcFc1cHgxZ3pGOFFIaFJmLU90REhuVSIsIm9yaWdpbiI6Imh0dHBzOi8vbG9jYWxob3N0Ojg0NDMiLCJjcm9zc09yaWdpbiI6ZmFsc2UsIm90aGVyX2tleXNfY2FuX2JlX2FkZGVkX2hlcmUiOiJkbyBub3QgY29tcGFyZSBjbGllbnREYXRhSlNPTiBhZ2FpbnN0IGEgdGVtcGxhdGUuIFNlZSBodHRwczovL2dvby5nbC95YWJQZXgifQ"), ByteArray.fromBase64Url("o2NmbXRmcGFja2VkZ2F0dFN0bXSjY2FsZyZjc2lnWEcwRQIgCTFl9y9YBafBiKkOnj59Cgypvz9hhPwpdsiFAmE8utcCIQC8bsfMEcI5-Di3Xj9CIWZ1PAGMjvxEiD1L2csJcgjoBmN4NWOBWQLwMIIC7DCCAdSgAwIBAgIJAN1TJeaFJ6cVMA0GCSqGSIb3DQEBCwUAMC4xLDAqBgNVBAMTI1l1YmljbyBVMkYgUm9vdCBDQSBTZXJpYWwgNDU3MjAwNjMxMCAXDTE0MDgwMTAwMDAwMFoYDzIwNTAwOTA0MDAwMDAwWjBvMQswCQYDVQQGEwJTRTESMBAGA1UECgwJWXViaWNvIEFCMSIwIAYDVQQLDBlBdXRoZW50aWNhdG9yIEF0dGVzdGF0aW9uMSgwJgYDVQQDDB9ZdWJpY28gVTJGIEVFIFNlcmlhbCAxNzEzNzIyMzMzMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEDeoY3vFmcuLvf1SL2oqIV5WaVs9VGyB4GPmtxdHY84v_-R2wtLKvAfjIH9eTIq3-Ev3-UQLipTY0Bb9Xn9Sp3KOBlDCBkTATBgorBgEEAYLECg0BBAUEAwUEAjAQBgkrBgEEAYLECgwEAwIBBDAiBgkrBgEEAYLECgIEFTEuMy42LjEuNC4xLjQxNDgyLjEuNzATBgsrBgEEAYLlHAIBAQQEAwIEMDAhBgsrBgEEAYLlHAEBBAQSBBDB-aC8HdJASrJ_jikEekP9MAwGA1UdEwEB_wQCMAAwDQYJKoZIhvcNAQELBQADggEBAGl5dmZIe5GOHFOAvVUaWFWyet89UCHWKmLBTXXfuoPwYqatxGhVqIeiV4nAuFF127294SzJcMgzycToui5_g8OUonTvs9xWF9yH23fXjGcBWoGErlF7DqkycOz2NtjPhGwEfBnE--0_KRc_IN6bu7u_XPXNwNmCLcg0reERI23NO_ZftcWebjRBCwY3p6l0ahalKmrgqOi7bhU1AjbHmiEvJgeBcpZphS87eikierMO5PmwvdbV3okNseEoaeoHDDQ7Av6RwCtKCXwYupRs6sULgUwo0fz2znURA-zSuTzK4iZ_hmQvRVJtQBPtfpwBEmNEdwwZ1A-VxfspsYzA7AVoYXV0aERhdGFYxEmWDeWIDoxodDQXD2R2YFuP5K65ooYyx5lc87qDHZdjQQAAAATB-aC8HdJASrJ_jikEekP9AEAJSmR-h-HuKqKK2uvaDSjTQrjbfukR_-71-SoVyEFkfLEc09nidnTryBiqZGARKeDhwvtog3_c3f8C3REXcI4spQECAyYgASFYIDUR5e5GusKylrCRkKq1U3jnp-fJ_l_CeykL_-5tj4juIlgg72ksmbxNptIfwrG1hiwbViIoWIphEt2819hHdziqSsc"), diff --git a/webauthn-server-demo/README b/webauthn-server-demo/README index 06bc0e86c..12b1f2a45 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:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.3.0/com/yubico/webauthn/CredentialRepository.html[`CredentialRepository`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.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:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.3.0/com/yubico/webauthn/RelyingParty.html[`RelyingParty`] + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.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:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.3.0/com/yubico/webauthn/CredentialRepository.html[`CredentialRepository`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.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.3.0/com/yubico/webauthn/attestation/AttestationTrustSource.html[`AttestationTrustSource`] +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.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`] diff --git a/webauthn-server-demo/src/main/java/com/yubico/webauthn/attestation/YubicoJsonMetadataService.java b/webauthn-server-demo/src/main/java/com/yubico/webauthn/attestation/YubicoJsonMetadataService.java index 62a7acd19..baf21282e 100644 --- a/webauthn-server-demo/src/main/java/com/yubico/webauthn/attestation/YubicoJsonMetadataService.java +++ b/webauthn-server-demo/src/main/java/com/yubico/webauthn/attestation/YubicoJsonMetadataService.java @@ -30,8 +30,8 @@ import com.google.common.collect.Maps; import com.yubico.internal.util.CertificateParser; import com.yubico.internal.util.CollectionUtil; +import com.yubico.internal.util.OptionalUtil; import com.yubico.webauthn.attestation.matcher.ExtensionMatcher; -import com.yubico.webauthn.attestation.matcher.FingerprintMatcher; import com.yubico.webauthn.data.ByteArray; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; @@ -55,9 +55,7 @@ public final class YubicoJsonMetadataService implements AttestationTrustSource { private static final String SELECTOR_PARAMETERS = "parameters"; private static final Map DEFAULT_DEVICE_MATCHERS = - ImmutableMap.of( - ExtensionMatcher.SELECTOR_TYPE, new ExtensionMatcher(), - FingerprintMatcher.SELECTOR_TYPE, new FingerprintMatcher()); + ImmutableMap.of(ExtensionMatcher.SELECTOR_TYPE, new ExtensionMatcher()); private final Collection metadataObjects; private final Map matchers; @@ -127,8 +125,7 @@ public Optional findMetadata(X509Certificate attestationCertificate .deviceProperties(deviceProps) .build()); }) - .filter(Optional::isPresent) - .map(Optional::get) + .flatMap(OptionalUtil::stream) .findAny(); } diff --git a/webauthn-server-demo/src/main/java/com/yubico/webauthn/attestation/matcher/ExtensionMatcher.java b/webauthn-server-demo/src/main/java/com/yubico/webauthn/attestation/matcher/ExtensionMatcher.java index 56c6fa415..1dd0aa3f1 100644 --- a/webauthn-server-demo/src/main/java/com/yubico/webauthn/attestation/matcher/ExtensionMatcher.java +++ b/webauthn-server-demo/src/main/java/com/yubico/webauthn/attestation/matcher/ExtensionMatcher.java @@ -69,9 +69,7 @@ public boolean matches(X509Certificate attestationCertificate, JsonNode paramete } } catch (IOException e) { log.error( - "Failed to parse extension value as ASN1: {}", - new ByteArray(extensionValue).getHex(), - e); + "Failed to parse extension value as ASN1: {}", new ByteArray(extensionValue), e); } } } diff --git a/webauthn-server-demo/src/main/java/com/yubico/webauthn/attestation/matcher/FingerprintMatcher.java b/webauthn-server-demo/src/main/java/com/yubico/webauthn/attestation/matcher/FingerprintMatcher.java deleted file mode 100644 index a057368c3..000000000 --- a/webauthn-server-demo/src/main/java/com/yubico/webauthn/attestation/matcher/FingerprintMatcher.java +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright (c) 2015-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.attestation.matcher; - -import com.fasterxml.jackson.databind.JsonNode; -import com.google.common.hash.Hashing; -import com.yubico.webauthn.attestation.DeviceMatcher; -import java.security.cert.CertificateEncodingException; -import java.security.cert.X509Certificate; - -public final class FingerprintMatcher implements DeviceMatcher { - public static final String SELECTOR_TYPE = "fingerprint"; - - private static final String FINGERPRINTS_KEY = "fingerprints"; - - @Override - public boolean matches(X509Certificate attestationCertificate, JsonNode parameters) { - JsonNode fingerprints = parameters.get(FINGERPRINTS_KEY); - if (fingerprints.isArray()) { - try { - String fingerprint = - Hashing.sha1().hashBytes(attestationCertificate.getEncoded()).toString().toLowerCase(); - for (JsonNode candidate : fingerprints) { - if (fingerprint.equals(candidate.asText().toLowerCase())) { - return true; - } - } - } catch (CertificateEncodingException e) { - // Fall through to return false. - } - } - return false; - } -} diff --git a/webauthn-server-demo/src/main/java/demo/webauthn/InMemoryRegistrationStorage.java b/webauthn-server-demo/src/main/java/demo/webauthn/InMemoryRegistrationStorage.java index 72c99132f..0cba71a9c 100644 --- a/webauthn-server-demo/src/main/java/demo/webauthn/InMemoryRegistrationStorage.java +++ b/webauthn-server-demo/src/main/java/demo/webauthn/InMemoryRegistrationStorage.java @@ -154,13 +154,14 @@ public Collection getRegistrationsByUserHandle(ByteArray public void updateSignatureCount(AssertionResult result) { CredentialRegistration registration = - getRegistrationByUsernameAndCredentialId(result.getUsername(), result.getCredentialId()) + getRegistrationByUsernameAndCredentialId( + result.getUsername(), result.getCredential().getCredentialId()) .orElseThrow( () -> new NoSuchElementException( String.format( "Credential \"%s\" is not registered to user \"%s\"", - result.getCredentialId(), result.getUsername()))); + result.getCredential().getCredentialId(), result.getUsername()))); Set regs = storage.getIfPresent(result.getUsername()); regs.remove(registration); 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 6fed76f0c..217c64a4c 100644 --- a/webauthn-server-demo/src/main/java/demo/webauthn/WebAuthnServer.java +++ b/webauthn-server-demo/src/main/java/demo/webauthn/WebAuthnServer.java @@ -464,7 +464,7 @@ public Either, SuccessfulAuthenticationResult> finishAuthentication response, userStorage.getRegistrationsByUsername(result.getUsername()), result.getUsername(), - sessions.createSession(result.getUserHandle()))); + sessions.createSession(result.getCredential().getUserHandle()))); } else { return Either.left(Collections.singletonList("Assertion failed: Invalid assertion.")); } diff --git a/webauthn-server-demo/src/main/java/demo/webauthn/data/CredentialRegistration.java b/webauthn-server-demo/src/main/java/demo/webauthn/data/CredentialRegistration.java index 54e32e9ca..e88aba83d 100644 --- a/webauthn-server-demo/src/main/java/demo/webauthn/data/CredentialRegistration.java +++ b/webauthn-server-demo/src/main/java/demo/webauthn/data/CredentialRegistration.java @@ -35,11 +35,11 @@ import java.util.SortedSet; import lombok.Builder; import lombok.Value; -import lombok.experimental.Wither; +import lombok.With; @Value @Builder -@Wither +@With public class CredentialRegistration { UserIdentity userIdentity; diff --git a/webauthn-server-demo/src/main/resources/metadata.json b/webauthn-server-demo/src/main/resources/metadata.json index bed561955..a654e2622 100644 --- a/webauthn-server-demo/src/main/resources/metadata.json +++ b/webauthn-server-demo/src/main/resources/metadata.json @@ -1,6 +1,6 @@ { "identifier": "2fb54029-7613-4f1d-94f1-fb876c14a6fe", - "version": 17, + "version": 19, "vendorInfo": { "url": "https://yubico.com", "imageUrl": "https://developers.yubico.com/U2F/Images/yubico.png", @@ -40,6 +40,46 @@ ] }, + { + "deviceId": "1.3.6.1.4.1.41482.1.1", + "displayName": "Security Key NFC by Yubico", + "transports": 12, + "deviceUrl": "https://support.yubico.com/support/solutions/articles/15000019469-security-key-nfc", + "imageUrl": "https://developers.yubico.com/U2F/Images/YK5NFC-CNFC.png", + "selectors": [ + { + "type": "x509Extension", + "parameters": { + "key": "1.3.6.1.4.1.45724.1.1.4", + "value": { + "type": "hex", + "value": "a4e9fc6d4cbe4758b8ba37598bb5bbaa" + } + } + } + ] + }, + + { + "deviceId": "1.3.6.1.4.1.41482.1.1", + "displayName": "Security Key NFC by Yubico - Enterprise Edition", + "transports": 12, + "deviceUrl": "https://support.yubico.com/support/solutions/articles/15000019469-security-key-nfc", + "imageUrl": "https://developers.yubico.com/U2F/Images/YK5NFC-CNFC.png", + "selectors": [ + { + "type": "x509Extension", + "parameters": { + "key": "1.3.6.1.4.1.45724.1.1.4", + "value": { + "type": "hex", + "value": "0bb43545fd2c418587ddfeb0b2916ace" + } + } + } + ] + }, + { "deviceId": "1.3.6.1.4.1.41482.1.1", "displayName": "Security Key by Yubico", @@ -178,7 +218,7 @@ "displayName": "YubiKey 5/5C NFC", "transports": 12, "deviceUrl": "https://support.yubico.com/support/solutions/articles/15000014174--yubikey-5-nfc", - "imageUrl": "https://developers.yubico.com/U2F/Images/YK5.png", + "imageUrl": "https://developers.yubico.com/U2F/Images/YK5NFC-CNFC.png", "selectors": [ { "type": "x509Extension", @@ -308,6 +348,8 @@ "deviceId": "1.3.6.1.4.1.41482.1.9", "displayName": "YubiKey Bio - FIDO Edition", "transports": 4, + "deviceUrl": "https://support.yubico.com/hc/en-us/articles/4407743521810-YubiKey-Bio-FIDO-Edition", + "imageUrl": "https://developers.yubico.com/U2F/Images/BIO.png", "selectors": [ { "type": "x509Extension", 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 a185d8c80..1542d51f4 100644 --- a/webauthn-server-demo/src/test/scala/demo/webauthn/WebAuthnServerSpec.scala +++ b/webauthn-server-demo/src/test/scala/demo/webauthn/WebAuthnServerSpec.scala @@ -251,7 +251,7 @@ class WebAuthnServerSpec def newServerWithAuthenticationRequest( testData: RegistrationTestData, - signatureCount: Option[Long] = None, + signatureCount: Option[Long], ) = { val assertionRequests: Cache[ByteArray, AssertionRequestWrapper] = newCache() diff --git a/yubico-util-scala/src/main/scala/com/yubico/scalacheck/gen/GenUtil.scala b/yubico-util-scala/src/main/scala/com/yubico/scalacheck/gen/GenUtil.scala new file mode 100644 index 000000000..a93978680 --- /dev/null +++ b/yubico-util-scala/src/main/scala/com/yubico/scalacheck/gen/GenUtil.scala @@ -0,0 +1,23 @@ +package com.yubico.scalacheck.gen + +import org.scalacheck.Gen + +object GenUtil { + + /** @return + * The generator `g` wrapped so that its size is at most `maxSize`. + */ + def maxSized[T](maxSize: Int, g: Gen[T]): Gen[T] = + Gen.sized(size => Gen.resize(Math.min(maxSize, size), g)) + + /** @return + * The generator `g` wrapped to reduce its size by half, but no lower than + * to 1 if the original size was 1 or greater. + */ + def halfsized[T](g: Gen[T]): Gen[T] = + Gen.sized(size => { + val s = if (size / 2 == 0 && size != 0) 1 else size / 2 + Gen.resize(s, g) + }) + +} 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 7d9d53d0a..75d7438f2 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 @@ -6,6 +6,8 @@ import org.scalatest.matchers.should.Matchers import java.security.Provider import java.security.Security +import java.security.Signature +import scala.util.Try trait TestWithEachProvider extends Matchers { this: AnyFunSpec => @@ -78,21 +80,27 @@ trait TestWithEachProvider extends Matchers { ): Unit = { val defaultProviders: List[Provider] = Security.getProviders.toList - // TODO: Uncomment this in the next major version - //it should behave like wrapItFunctionWithProviderContext("default", defaultProviders, registerTests) + if (Try(Signature.getInstance("EdDSA", "SunEC")).isSuccess) { + // Test with only stock providers for JDK >= 14 + it should behave like wrapItFunctionWithProviderContext( + "default", + defaultProviders, + registerTests, + ) + } else { + // JDK < 14 doesn't have EdDSA providers + it should behave like wrapItFunctionWithProviderContext( + "default and BouncyCastle", + defaultProviders.appended(new BouncyCastleProvider()), + registerTests, + ) + } it should behave like wrapItFunctionWithProviderContext( "BouncyCastle", List(new BouncyCastleProvider()), registerTests, ) - - // TODO: Delete this in the next major version - it should behave like wrapItFunctionWithProviderContext( - "default and BouncyCastle", - defaultProviders.appended(new BouncyCastleProvider()), - registerTests, - ) } } diff --git a/yubico-util/build.gradle.kts b/yubico-util/build.gradle.kts index 83d58d030..ba6974f9e 100644 --- a/yubico-util/build.gradle.kts +++ b/yubico-util/build.gradle.kts @@ -5,6 +5,7 @@ plugins { signing id("info.solidsoft.pitest") id("io.github.cosmicsilence.scalafix") + id("me.champeau.jmh") version "0.6.8" } description = "Yubico internal utilities" @@ -24,7 +25,6 @@ dependencies { 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") @@ -36,6 +36,13 @@ dependencies { testImplementation("org.scalatest:scalatest_2.13") testImplementation("org.scalatestplus:junit-4-13_2.13") testImplementation("org.scalatestplus:scalacheck-1-16_2.13") + + jmhImplementation(platform(project(":test-platform"))) + jmhRuntimeOnly("org.slf4j:slf4j-nop") +} + +configurations.jmhRuntimeClasspath { + exclude(module = "slf4j-test") } diff --git a/yubico-util/src/jmh/java/com/yubico/internal/util/benchmark/BinaryUtilBenchmark.java b/yubico-util/src/jmh/java/com/yubico/internal/util/benchmark/BinaryUtilBenchmark.java new file mode 100644 index 000000000..e50d1c7f2 --- /dev/null +++ b/yubico-util/src/jmh/java/com/yubico/internal/util/benchmark/BinaryUtilBenchmark.java @@ -0,0 +1,110 @@ +package com.yubico.internal.util.benchmark; + +import com.yubico.internal.util.BinaryUtil; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; + +@Warmup(iterations = 5, time = 100, timeUnit = TimeUnit.MILLISECONDS) +@Measurement(iterations = 10, time = 100, timeUnit = TimeUnit.MILLISECONDS) +public class BinaryUtilBenchmark { + + private static final class FromHexTests { + public static final List shortTests = + Arrays.asList( + "", "00", "01", "02", "03", "04", "05", "06", "07", "08", "09", "0a", "0b", "0c", "0d", + "0e", "0f", "10", "20", "30", "40", "50", "60", "70", "80", "90", "a0", "b0", "c0", + "d0", "e0", "f0", "ff"); + + public static final String long128 = + "05a32393cf6c4a162796e83360fe4cbc4e24bf0e89a0288d8594edda689e188379fb7d3d1e0dc7b6f5afaffcce40640c3a6bf197225f82039a568c3f232321cc3307edd7c10ff80f21fd5d1dd588054614db3a30d660e537143ee4604d5006a89226d9a0abd57e3108348d22f4dfd1c0ea3e2fb3d20f673f51b295809414c8bd"; + public static final String long256 = + "9a21145c579178e21973096ba131bb0af24b122350ed15b585eee634231fe8c0b16c1920bf76b6e100572c462856a1dcdaaa6d2023e895cd74f8db0e1189d5a264840ec7a7f59011b13725d2ede8fe4f813cdfa9e4cb67953f6f609cd694e82567585b88e72276170b69e7531cf2f7378e3ce0fc5ec7c28c00c179d5c7cc621007b93bb658dd9d07ee0d5a8307bc26c9fddc7daa50ec0d4e751dec29649b50051967aa3c360e78c6d1abb32d1143cd706a50b5e353a1fc1f690b2c5bcfd188813c7338ed231784ef1f67e08f1b08925718bb0bd3ef3f37365c4c344672915008d20d37e6a3d7a779f95d6be0fffd7cf0ba5fb7ea4da0ae0c997102621e3f4841"; + public static final String long512 = + "d6f686693781aad51a2cf40d1bfe4985f7dc094fbfa480023529b72602e376e9f51242176a16096ace8742807e6b2d5aae534762131fbd00b7adc9f6f8c70a29356f792cbf8b869a8265752c67658cd7566afa3c701889ba9e9b089200002ee69c7cf533d03d7077f31dc270de83e3a64a4102efdd5defb1e244ad2515b7f6043a7b34240dfd359ecc564aba8df30a9e41f36b9a7439a4b11304219dbfecd77cb30f69352276b816ac6ec5663b6fd72a7dbff03e9501b0dae97424abd6de101a150cdc446f7ad12ee578b62f1a468c8adcf717969b8c0580b453a920141a58ba2eaea32bf5a24fda5b4f7dabea985c0dec754d75683252f6a23623ce9248d7974075bd4eb6dc585ab516fa189cdcc987bde0548e4a06aac2682c3e41f23c53046933bcf62def3504d0f472066aa0c7ea9d2652aaa9e62d126a787292480617f09e8b75eef77bfe4fb0620366f406c83503f81f5778ca241137296ab9378a0201ddec60250330640fb9958cbb9c4e3cf0f0a78e0157882f393fce0bf9536cdae09390db232fc5d6902eb824c6ae5a8fd944898b9a0ead90e22e6b37d2c3e054ee50225f154fea740a38db7e857aeed6ca06fc7060d6a1e352969d26c6ee0966b855cb44ad22f5974c57992fca17d3d67c8132886c199bdff6eed11b3078e4805723742767af4d9f4ab04dcb176a2657adc7d28cc8b881fda344592d0a0f1ef34c"; + public static final String long1024 = + "8a454aaa25b4e1f93e28145579aecef9bcb7abcb593957db4993b018d871903ab9c33eeb3004addbe33abb411ff3d659d0806274001f2a5d15f371280fece5c531c232d751c9eb82b46d140aced518dcba0c10226130887036bba2b9cb8bd2523e9a26f92bd438ade5367cbb2e7982243dc81f3af5705507c664f1bad84bbfe719bea97e1c5c8973b05176fe0a9bc497a0497ce4c6e8decfda7be928c6114a3302bc64e90ca903f6c8b4d42042508d2af828a893492c8ef5d04a1f166426227fcce07412c9a66259c349a4139728b3c288f1b2d08660d7482e3151a028ecbf41a6a4eba848e67076fbbf3a9109ce2e7386a83c76dbcac34948bcfd6094f53cb8181cbb32981afd8018e88c40524002a5b8415ee81582daa50c4e85b4ea3501878ae5bed8ce4fe8af9f1ed0e45aefee1cd743521025db3d2f907a6552c9e79800e67af100b22f3e4ea051060bc2e65d9ceeb113d6af8c41847c54421f533a6ee1908ecd013a169a566349e8ea05a2e5d93d6903da28088e09416d119b47363a8c7b33657257ff99f1ea225a087ee774d0efa7d002dfeff0707972b8c4dd4e5628719f3ae75c145b038f44b0032542bb0c00564c0d6d75ffb3c1a6976a6f6c766be92396ba53d8ba46d296820cde9096f4b089aa9090062d9b9af9823b9fc2078c6d15fba1b024aa90bcc174c6269718dc56002c32bf4ad62a13583db2aac4a28bd131f94ab78478f8f8d7a5819b936f1eeb1a519f16368a2741bb04005e61e592b2e2f0ee35a81eae58c5417801dda5d44a7d3b2887907e4c708b9f11e5d449484e6532de6068c01af9d5f2e4ebd1873bd457e4afcedbec1d9459c8772c9db7cfb83beecdf68977fd5319eeb00666b49dacd5b947549bc757a07a8ee8404a41bf44c87dd553ff88f536471175abd92f7448f2ee9c7860ec6059a8d53e1a8490c734557a141167d3f37e871de27706417ac786f0edee32651631931c9d3156a9667d09dbf5a77a3f894500599ba5eeb7be531aa63b29d87e7439fda3e85d05ca1b4412036b7fbb44b684920c219d3edcf56ce40d30f877167a014de32c71a82e767d8478394edf172288772e8bdf8968c22c9dc27c89ba69e5c68f165be133f25567cf91e74cdf472b31d7f68b4b189522f47498c4089ce356b123e1a5d3a87e7faba6ca7ff8699bc137de6161c12c21916f6017903e762fb34a383f9e1a3705e1bcd6fb0307cf1434d1a86da69ba237488e8c64bcadf419ad6722d695e835b33a450ee71d4db1237a7b26d414ccd963ba4cd0b31c63e68fc953bb51b824c2776de68dd95d41be3bac154d1c3776f88f371cb8b1b8f489fb84e6bf0d0a6cb74e1a9280f1d04a3c845033cae8f75c612659a3733c0d094487b039a483bf6ab66f27e39b950b8bda0cd4d0aac83d149c59c804b1f1abff5ae4aa54e88df6e8106d3f"; + public static final String long2048 = + "77d3a29ca97179f409b407de4442f429a6f336ca638c5e702f192ddbdd92ebc127b5a9fcb0e71f211adcb5a55c06cb5b7be38e69bc68e23e6b9ab968e7bedd77516fb5fb4c59cbe8ad59ca163c2e7a829ec025b23e87724911f093a61dd23b86c8acb8c164c3567a89f9e32280c8f689997d790a6a22c1a2ea85a488587a889452a6c0764b205c8063db6c350521a8921034d058c33412a532b35f6cc42240150d5f1634a2918211a0de42097acde1eb3d562fc7c1294f2eb2ddd741832db6e7e77488389147164ff3c46f76ddfb181d080e5e8e0ffd9592bd45f6865470204aa4550c559f456e6c974bfe5dc435d8357971c63267f2a7bfe1649988b8273c2e9c8eb22a277197ce1477d21af24e6663de01aa11f439234968734312d355af88de4abffea68737af127cdc4b7f7a2d8b9789fb18469aff29f746fb981fca589b1992c03f477df26191cdc09c1251b94159ef19f0468a128899e1d6b8caefa0d6314627a8b594b062cfad0c5f557ded49e3b0a1b34f9f0eba37e513a6c7cf8fd45e22dcde01ef029f622bd1773c6cf882b82cab32fc8f37d485488a7acdda49781c0a4b53ee4ee10b2e8ccc6c452a5337586f168730b251e0edd2f9999f38dd0cbf5425ece00bb89b649068219ce4ebcca83b8d6630d2dcdd02f9d3aa6900f64e4ac999d9638977855d55ca3da12fdda22d988a0be6e9b450b927e92a4398fda8a1d9216616dc49c52f41044a01b4ce9113df65a50a3c4c0175b8a17e98b4a88e0b064e9b242fa6fa2e05e5894ee22ed82a66682348ca079e8fac16487eb2822ac67c77a1bb8d644dbf6b542c376dc19ad9f304ffc3f091eae942225340a89382b3d0f3d8edc9a8e8d4af813262c4fffb22ea84262e530645f91a0f2bb394f620dc9367ddd5649b524125456a84c1c8c64e6dbfa34f06bade952a92fe40bfa13e35a05e2aa7b2895576a5052af99a6d46b96dfe1e35a5c1723a1ed459828364af1ef5c6be4fb5f97e9b047030ae3908b27854564230878ee57e00c8288ee64249a1e1fc4a8536e32432163d355284db58ce0ef0a3e4e499c703081b86061790e2267c27fedf09a8446a1c8d8d68d5099502838547e9ec984e4713f0449a79d80cd15cd079478616e164915e780899666bf82dd508799ba5de00c3497c536b550888f355c4978f8f2ea2202c4ca7397d26a56e24d338e90af1b0458656462c4efebd47b3287c15e3ce99b10eac7680b0745659080dd7dac83f8ed1dace494c0018b1c671e8594d585574ed3bda263f7e616cbb970abd9ca54279d849ea9afe444ae0a658394d23f08dc8ad95956e40cef7741751a1ab775dd08e1d4c3834ab91a210392addd4647ecdfede03029d814967299b286add415a5c7078518f51a691a6302764f93c98471f9903a694e7b41e8c87a7985b0f23d9d4aa57ac6971777ff1f957058c41b51f4deb7225e3ef4cef334e06f90d43f2339ffc319fcb891cb64dd43d1cf1ccb10f25a9c717b4090168c4efb8e4539831fe48bd85df13849c48d9d26e448c369c51cd55de2e490c3aa2378725738b472251edf3e3e13e021783455d91ce2a66074b0f17e66a8c7fee8fa1df80f79b5ebcc0dc970fa7ed6f782d65182b349ff3c04a5e81472905d28efb6e1a003b9671a08416dcba67c18e02d9e8e9ea018d31dd981ffaa4e23c6656fa8ca05ec5c428e58ea9a530d863173e906bfec30be25dd61ad4f227d157bca31782ad26cd956c79016332a059e36fdb319591718778c047f129bf35136dab0012c2e5564a6cbd3d848d14de00316270160cae34d2255a7597573dc829ab2d11dff26c4c2442c6b37fa9b19a813af79e508ec32129355e47b062b7e392805faa47969b4ef520cc5c6b70a5eb57b11a0fec8b582b901bf1f70534037486e895205c05c3b9ce2eb6b0109bf74ae34d70820baf12eea1ef7da2dd2b0bd28c3beddfc08f56e4c16caf9aba86fa92d58fb85c17ddba36b867e63cdce69201ec9730a4058cbdac1a0a6e363f2e1601d5fb59474513902585af12334466d21ee5873aa8b352e1cf315f5c8fa3bd94210ac4866be795fbf789eefb38a799a796e5b466a32802dc51a6e80356af68c465e16bd86f26e12aaa41406707a6915f9f4449386f54b294d99e5b795fc3a651bc3b1ba365e835798ee7b99cfe98e2265a92abb2fd3067b3d90938d3a27f651eb82c6324181a0fecd3d7b847da04647130376b76603919dc4bc5bff3c51a7e8fff6ee9a4584f47f6352886bc7740ef536312848f0d40f444a3b4b4a611b058f174827007fede7df9da2f8759d8ef9d20d3444ef8d47b7477ee6316339d0b14f8545a1abd14902048173bae1cde1a455180a986a4428fd0e29c980266eba4b7c6fc80910dee6d43bd4163c3091121af38e173008add1d93ec00d9ea31fcb756000f6f59c78f6fb2d2c1a3e0644e81563e4ae678b5cbb9cc585f6f88c53ab866f423570764ab497ace4b89c7f5d9fefd49fbdcc2d3c9e35a369ae814821095b19ded47739107f219000dced7eff7a84a14e91e50d011b39ee1f2bd9c2a6b70686595e3e1d18bae9544d6157589352b0903ad22377f7155f8b0f0c67f12c3338732660fc113c81e65d93133523936727b3905c46eaec4dd9e3b9515871a7a551dfa0aee8d7391269e10153bf2dab86483aaf7aca5e86dbe501efd2aa8b5208fd55b4f0cdd53addb5db1f55593d3f698e63cab1c87868fe99f1fe91d149a3e7de7f2726bf7a1893f49291091a024d8c72d93da4fa051187d83e2ba755bb909157633ce5e4ceeeaaabd10c70be8a3401a15643e060dec472c605774ee6b4bdeb7ea264bedf33db21b18b5d876a818148da98a89a27447fbbf6aaa3cbd484f9c45b759d6d7c2eaf74d01"; + } + + private static final class ToHexTests { + public static final List shortTests = + FromHexTests.shortTests.stream().map(BinaryUtil::fromHex).collect(Collectors.toList()); + + public static final byte[] long128 = BinaryUtil.fromHex(FromHexTests.long128); + public static final byte[] long256 = BinaryUtil.fromHex(FromHexTests.long256); + public static final byte[] long512 = BinaryUtil.fromHex(FromHexTests.long512); + public static final byte[] long1024 = BinaryUtil.fromHex(FromHexTests.long1024); + public static final byte[] long2048 = BinaryUtil.fromHex(FromHexTests.long2048); + } + + @Benchmark + public void fromHexShort(Blackhole bh) { + for (String s : FromHexTests.shortTests) { + bh.consume(BinaryUtil.fromHex(s)); + } + } + + @Benchmark + public void fromHexLong128(Blackhole bh) { + bh.consume(BinaryUtil.fromHex(FromHexTests.long128)); + } + + @Benchmark + public void fromHexLong256(Blackhole bh) { + bh.consume(BinaryUtil.fromHex(FromHexTests.long256)); + } + + @Benchmark + public void fromHexLong512(Blackhole bh) { + bh.consume(BinaryUtil.fromHex(FromHexTests.long512)); + } + + @Benchmark + public void fromHexLong1024(Blackhole bh) { + bh.consume(BinaryUtil.fromHex(FromHexTests.long1024)); + } + + @Benchmark + public void fromHexLong2048(Blackhole bh) { + bh.consume(BinaryUtil.fromHex(FromHexTests.long2048)); + } + + @Benchmark + public void toHexShort(Blackhole bh) { + for (byte[] b : ToHexTests.shortTests) { + bh.consume(BinaryUtil.toHex(b)); + } + } + + @Benchmark + public void toHexLong128(Blackhole bh) { + bh.consume(BinaryUtil.toHex(ToHexTests.long128)); + } + + @Benchmark + public void toHexLong256(Blackhole bh) { + bh.consume(BinaryUtil.toHex(ToHexTests.long256)); + } + + @Benchmark + public void toHexLong512(Blackhole bh) { + bh.consume(BinaryUtil.toHex(ToHexTests.long512)); + } + + @Benchmark + public void toHexLong1024(Blackhole bh) { + bh.consume(BinaryUtil.toHex(ToHexTests.long1024)); + } + + @Benchmark + public void toHexLong2048(Blackhole bh) { + bh.consume(BinaryUtil.toHex(ToHexTests.long2048)); + } +} diff --git a/yubico-util/src/main/java/com/yubico/internal/util/BinaryUtil.java b/yubico-util/src/main/java/com/yubico/internal/util/BinaryUtil.java index ed7eb5b78..2f47aee3f 100644 --- a/yubico-util/src/main/java/com/yubico/internal/util/BinaryUtil.java +++ b/yubico-util/src/main/java/com/yubico/internal/util/BinaryUtil.java @@ -24,7 +24,6 @@ package com.yubico.internal.util; -import com.google.common.io.BaseEncoding; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; @@ -40,15 +39,33 @@ public static byte[] copy(byte[] bytes) { /** * @param bytes Bytes to encode */ - public static String toHex(byte[] bytes) { - return BaseEncoding.base16().encode(bytes).toLowerCase(); + public static String toHex(final byte[] bytes) { + final char[] digits = new char[bytes.length * 2]; + for (int i = 0; i < bytes.length; ++i) { + final int i2 = i * 2; + digits[i2] = Character.forDigit((bytes[i] >> 4) & 0x0f, 16); + digits[i2 + 1] = Character.forDigit(bytes[i] & 0x0f, 16); + } + return new String(digits); } /** * @param hex String of hexadecimal digits to decode as bytes. */ - public static byte[] fromHex(String hex) { - return BaseEncoding.base16().decode(hex.toUpperCase()); + public static byte[] fromHex(final String hex) { + if (hex.length() % 2 != 0) { + throw new IllegalArgumentException("Length of hex string is not even: " + hex); + } + + final byte[] result = new byte[hex.length() / 2]; + for (int i = 0; i < hex.length(); ++i) { + final int d = Character.digit(hex.charAt(i), 16); + if (d < 0) { + throw new IllegalArgumentException("Invalid hex digit at index " + i + " in: " + hex); + } + result[i / 2] |= d << (((i + 1) % 2) * 4); + } + return result; } /** @@ -57,7 +74,7 @@ public static byte[] fromHex(String hex) { * @param hex String of hexadecimal digits to decode as bytes. */ public static byte singleFromHex(String hex) { - ExceptionUtil.assure( + ExceptionUtil.assertTrue( hex.length() == 2, "Argument must be exactly 2 hexadecimal characters, was: %s", hex); return fromHex(hex)[0]; } @@ -111,8 +128,9 @@ public static long getUint32(byte[] bytes) { } public static byte[] encodeUint16(int value) { - ExceptionUtil.assure(value >= 0, "Argument must be non-negative, was: %d", value); - ExceptionUtil.assure(value < 65536, "Argument must be smaller than 2^16=65536, was: %d", value); + ExceptionUtil.assertTrue(value >= 0, "Argument must be non-negative, was: %d", value); + ExceptionUtil.assertTrue( + value < 65536, "Argument must be smaller than 2^16=65536, was: %d", value); ByteBuffer b = ByteBuffer.allocate(4); b.order(ByteOrder.BIG_ENDIAN); @@ -122,8 +140,8 @@ public static byte[] encodeUint16(int value) { } public static byte[] encodeUint32(long value) { - ExceptionUtil.assure(value >= 0, "Argument must be non-negative, was: %d", value); - ExceptionUtil.assure( + ExceptionUtil.assertTrue(value >= 0, "Argument must be non-negative, was: %d", value); + ExceptionUtil.assertTrue( value < 4294967296L, "Argument must be smaller than 2^32=4294967296, was: %d", value); ByteBuffer b = ByteBuffer.allocate(8); diff --git a/yubico-util/src/main/java/com/yubico/internal/util/CertificateParser.java b/yubico-util/src/main/java/com/yubico/internal/util/CertificateParser.java index bb03a8b32..1e1c72bfe 100755 --- a/yubico-util/src/main/java/com/yubico/internal/util/CertificateParser.java +++ b/yubico-util/src/main/java/com/yubico/internal/util/CertificateParser.java @@ -40,8 +40,6 @@ public class CertificateParser { public static final String ID_FIDO_GEN_CE_AAGUID = "1.3.6.1.4.1.45724.1.1.4"; - - // private static final Provider BC_PROVIDER = new BouncyCastleProvider(); private static final Base64.Decoder BASE64_DECODER = Base64.getDecoder(); private static final List FIXSIG = @@ -76,7 +74,7 @@ public static X509Certificate parseDer(InputStream is) throws CertificateExcepti (X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(is); // Some known certs have an incorrect "unused bits" value, which causes problems on newer // versions of BouncyCastle. - if (FIXSIG.contains(cert.getSubjectDN().getName())) { + if (FIXSIG.contains(cert.getSubjectX500Principal().getName())) { byte[] encoded = cert.getEncoded(); if (encoded.length >= UNUSED_BITS_BYTE_INDEX_FROM_END) { diff --git a/yubico-util/src/main/java/com/yubico/internal/util/ExceptionUtil.java b/yubico-util/src/main/java/com/yubico/internal/util/ExceptionUtil.java index dc61c11c2..9f5126384 100644 --- a/yubico-util/src/main/java/com/yubico/internal/util/ExceptionUtil.java +++ b/yubico-util/src/main/java/com/yubico/internal/util/ExceptionUtil.java @@ -36,7 +36,7 @@ public static RuntimeException wrapAndLog(Logger log, String message, Throwable return err; } - public static void assure( + public static void assertTrue( boolean condition, String failureMessageTemplate, Object... failureMessageArgs) { if (!condition) { throw new IllegalArgumentException(String.format(failureMessageTemplate, failureMessageArgs)); diff --git a/yubico-util/src/main/java/com/yubico/internal/util/OptionalUtil.java b/yubico-util/src/main/java/com/yubico/internal/util/OptionalUtil.java index 9a60ced41..6afb76ea5 100644 --- a/yubico-util/src/main/java/com/yubico/internal/util/OptionalUtil.java +++ b/yubico-util/src/main/java/com/yubico/internal/util/OptionalUtil.java @@ -3,6 +3,8 @@ import java.util.Optional; import java.util.function.BinaryOperator; import java.util.function.Supplier; +import java.util.stream.Stream; +import lombok.NonNull; import lombok.experimental.UtilityClass; /** Utilities for working with {@link Optional} values. */ @@ -21,6 +23,17 @@ public static Optional orElseOptional(Optional primary, Supplier Stream stream(@NonNull Optional o) { + return o.map(Stream::of).orElseGet(Stream::empty); + } + /** * If both a and b are present, return f(a, b). *