diff --git a/.github/workflows/release-verify-signatures.yml b/.github/workflows/release-verify-signatures.yml index fdb3d244c..fe85b844f 100644 --- a/.github/workflows/release-verify-signatures.yml +++ b/.github/workflows/release-verify-signatures.yml @@ -36,20 +36,16 @@ jobs: wget https://github.com/${GITHUB_REPOSITORY}/releases/download/${TAGNAME}/webauthn-server-attestation-${TAGNAME}.jar.asc wget https://github.com/${GITHUB_REPOSITORY}/releases/download/${TAGNAME}/webauthn-server-core-${TAGNAME}.jar.asc - wget https://github.com/${GITHUB_REPOSITORY}/releases/download/${TAGNAME}/webauthn-server-core-minimal-${TAGNAME}.jar.asc gpg --no-default-keyring --keyring yubico --verify webauthn-server-attestation-${TAGNAME}.jar.asc webauthn-server-attestation/build/libs/webauthn-server-attestation-${TAGNAME}.jar - gpg --no-default-keyring --keyring yubico --verify webauthn-server-core-${TAGNAME}.jar.asc webauthn-server-core-bundle/build/libs/webauthn-server-core-${TAGNAME}.jar - gpg --no-default-keyring --keyring yubico --verify webauthn-server-core-minimal-${TAGNAME}.jar.asc webauthn-server-core/build/libs/webauthn-server-core-minimal-${TAGNAME}.jar + gpg --no-default-keyring --keyring yubico --verify webauthn-server-core-${TAGNAME}.jar.asc webauthn-server-core/build/libs/webauthn-server-core-${TAGNAME}.jar - name: Verify signatures from Maven Central run: | export TAGNAME=${GITHUB_REF#refs/tags/} wget -O webauthn-server-core-${TAGNAME}.jar.mavencentral.asc https://repo1.maven.org/maven2/com/yubico/webauthn-server-core/${TAGNAME}/webauthn-server-core-${TAGNAME}.jar.asc - wget -O webauthn-server-core-minimal-${TAGNAME}.jar.mavencentral.asc https://repo1.maven.org/maven2/com/yubico/webauthn-server-core-minimal/${TAGNAME}/webauthn-server-core-minimal-${TAGNAME}.jar.asc wget -O webauthn-server-attestation-${TAGNAME}.jar.mavencentral.asc https://repo1.maven.org/maven2/com/yubico/webauthn-server-attestation/${TAGNAME}/webauthn-server-attestation-${TAGNAME}.jar.asc gpg --no-default-keyring --keyring yubico --verify webauthn-server-attestation-${TAGNAME}.jar.mavencentral.asc webauthn-server-attestation/build/libs/webauthn-server-attestation-${TAGNAME}.jar - gpg --no-default-keyring --keyring yubico --verify webauthn-server-core-${TAGNAME}.jar.mavencentral.asc webauthn-server-core-bundle/build/libs/webauthn-server-core-${TAGNAME}.jar - gpg --no-default-keyring --keyring yubico --verify webauthn-server-core-minimal-${TAGNAME}.jar.mavencentral.asc webauthn-server-core/build/libs/webauthn-server-core-minimal-${TAGNAME}.jar + gpg --no-default-keyring --keyring yubico --verify webauthn-server-core-${TAGNAME}.jar.mavencentral.asc webauthn-server-core/build/libs/webauthn-server-core-${TAGNAME}.jar diff --git a/NEWS b/NEWS index f994ea0e0..57dea19b5 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,85 @@ +== Version 2.0.0 == + +This release removes deprecated APIs and changes some defaults to better align +with the L2 version of the WebAuthn spec. It also adds a new major feature: +optional integration with the FIDO Metadata Service for retrieving authenticator +metadata and attestation trust roots. See below for details. + +`webauthn-server-core`: + +Breaking changes: + +* Deleted deprecated `icon` field in `RelyingPartyIdentity` and `UserIdentity`, + and its associated methods. +* Deleted deprecated `AuthenticatorSelectionCriteria` methods + `builder().requireResidentKey(boolean)` and `isRequireResidentKey()`. +* `RelyingParty` parameter `allowUnrequestedExtensions` removed. The library + will now always accept unrequested extensions. +* Class `ClientAssertionExtensionOutputs` now silently ignores unknown + extensions instead of rejecting them. +* `webauthn-server-core-minimal` module deleted. +* `webauthn-server-core` no longer depends on BouncyCastle and will no longer + attempt to automatically fall back to it. Therefore, EdDSA keys are no longer + supported by default in JDK 14 and earlier. The library will log warnings if + configured for algorithms with no JCA provider available, in which case the + dependent project may need to add additional dependencies and configure JCA + providers externally. +* Enum value `AttestationType.ECDAA` removed without replacement. +* Deleted methods `RegistrationResult.getWarnings()` and + `AssertionResult.getWarnings()` since they are now always empty. +* Framework for attestation metadata has been fully overhauled. See the + `webauthn-server-attestation` module documentation for the new ways to work + with attestation metadata: + ** Deleted method `RegistrationResult.getAttestationMetadata()`. + ** Interface `MetadataService` replaced with `AttestationTrustSource`, and + optional `RelyingParty` setting `.metadataService(MetadataService)` replaced + with `.attestationTrustSource(AttestationTrustSource)`. + ** Deleted types `Attestation` and `Transport`. + ** Deleted method `AuthenticatorTransport.fromU2fTransport`. +* `RelyingParty.finishRegistration()` now uses a JCA `CertPathValidator` to + validate attestation certificate paths, if an attestation trust source has + been configured. This requires a compatible JCA provider, but should already + be available in most environments. +* Classes in package `com.yubico.fido.metadata` moved to + `com.yubico.webauthn.extension.uvm` to avoid name clash with + `webauthn-server-attestation` module in JPMS. +* Changed return type of + `PublicKeyCredentialRequestOptions.getUserVerification()`, + `AuthenticatorSelectionCriteria.getUserVerification()` and + `AuthenticatorSelectionCriteria.getResidentKey()` to `Optional`, and changed + defaults for `userVerification` and `residentKey` to empty. This means we + won't inadvertently suppress warnings that browsers might issue in the browser + console if for example `userVerification` is not set explicitly. + +New features: + +* Method `getAaguid()` added to `RegistrationResult`. +* Method `getAttestationTrustPath()` added to `RegistrationResult`. +* Setting `.clock(Clock)` added to `RelyingParty`. It is used for attestation + path validation if an `attestationTrustSource` is configured. + + +`webauthn-server-attestation`: + +Breaking changes: + +* Types `AttestationResolver`, `CompositeAttestationResolver`, + `CompositeTrustResolver`, `DeviceMatcher`, `ExtensionMatcher`, + `FingerprintMatcher`, `MetadataObject`, `SimpleAttestationResolver`, + `SimpleTrustResolver`, `StandardMetadataService` and `TrustResolver` deleted + in favour of a new attestation metadata framework. Some of the functionality + is retained as the new `YubicoJsonMetadataService` class in the + `webauthn-server-demo` subproject in the library sources, but no longer + exposed in either library module. +* Library no longer contains a `/metadata.json` resource. + +New features: + +* New types `FidoMetadataService` and `FidoMetadataDownloader` which integrate + with the FIDO Metadata Service for retrieving authenticator metadata and + attestation trust roots. + + == Version 1.12.4 == Deprecated features: diff --git a/README b/README index 73a0938d6..ccb5cdb7c 100644 --- a/README +++ b/README @@ -14,6 +14,19 @@ for a server to support Web Authentication. This includes registering authenticators and authenticating registered authenticators. +[WARNING] +.*Psychic signatures in Java* +========== +In April 2022, link:https://neilmadden.blog/2022/04/19/psychic-signatures-in-java/[CVE-2022-21449] +was disclosed in Oracle's OpenJDK (and other JVMs derived from it) which can impact applications using java-webauthn-server. +The impact is that for the most common type of WebAuthn credential, invalid signatures are accepted as valid, +allowing authentication bypass for users with such a credential. +Please read link:https://openjdk.java.net/groups/vulnerability/advisories/2022-04-19[Oracle's advisory] +and make sure you are not using one of the impacted OpenJDK versions. +If you are, we urge you to upgrade your Java deployment to a version that is safe. +========== + + toc::[] @@ -25,7 +38,7 @@ Maven: com.yubico webauthn-server-core - 1.12.4 + 2.0.0 compile ---------- @@ -33,9 +46,14 @@ Maven: Gradle: ---------- -compile 'com.yubico:webauthn-server-core:1.12.4' +compile 'com.yubico:webauthn-server-core:2.0.0' ---------- +NOTE: You may need additional dependencies with JCA providers to support some signature algorithms. +In particular, OpenJDK 14 and earlier does not include providers for the EdDSA family of algorithms. +The library will log warnings if you try to configure it for algorithms with no JCA provider available. + + === Semantic versioning This library uses link:https://semver.org/[semantic versioning]. @@ -50,16 +68,11 @@ Breaking changes to these will NOT be reflected in version numbers. === Additional modules -In addition to the main `webauthn-server-core` module, there are also: - -- `webauthn-server-attestation`: A simple implementation of the - link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core-minimal/latest/com/yubico/webauthn/attestation/MetadataService.html[`MetadataService`] - interface, which by default comes preloaded with attestation metadata for Yubico devices. +In addition to the main `webauthn-server-core` module, there is also: -- `webauthn-server-core-minimal`: Alternative distribution of `webauthn-server-core`, - not dependent on BouncyCastle. - Using it means you may have to add your own JCA providers to support some signature algorithms. - In particular, OpenJDK 14 and earlier does not include providers for the EdDSA family of algorithms. +- `webauthn-server-attestation`: Integration with the https://fidoalliance.org/metadata/[FIDO Metadata Service] + for retrieving and selecting trust roots to use for verifying + https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#sctn-attestation[attestation statements]. == Features @@ -70,9 +83,8 @@ In addition to the main `webauthn-server-core` module, there are also: https://www.w3.org/TR/webauthn/#sctn-rp-operations[validation logic] on the response from the client - No mutable state or side effects - everything (except builders) is thread safe -- Optionally integrates with a "metadata service" to verify +- Optionally integrates with an "attestation trust source" to verify https://www.w3.org/TR/webauthn/#sctn-attestation[authenticator attestations] - and annotate responses with additional authenticator metadata - Reproducible builds: release signatures match fresh builds from source. See link:#reproducible-builds[Reproducible builds] below. @@ -93,6 +105,11 @@ link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server- for in-depth API documentation. +== Migrating from version `1.x` + +See link:doc/Migrating_from_v1.adoc[the migration guide]. + + == Getting started Using this library comes in two parts: the server side and the client side. @@ -557,6 +574,19 @@ credentials. . Finally, the application reports success and resumes its business logic. +== Using attestation + +WebAuthn supports +link:https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#sctn-attestation[authenticator attestation], +which provides a way for the web service +to request cryptographic proof of what authenticator the user is using. +Most services do not need this, and it is disabled by default. + +The link:webauthn-server-attestation[`webauthn-server-attestation` module] +provides optional additional features for working with attestation. +See the module documentation for more details. + + == Building Use the included diff --git a/build.gradle b/build.gradle index 493f2df33..fd0009c32 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.3.0' + classpath 'com.diffplug.spotless:spotless-plugin-gradle:6.5.1' classpath 'io.github.cosmicsilence:gradle-scalafix:0.1.13' } } @@ -40,7 +40,7 @@ if (publishEnabled) { } wrapper { - gradleVersion = '7.2' + gradleVersion = '7.3' } dependencies { @@ -49,6 +49,7 @@ dependencies { api('com.fasterxml.jackson.core:jackson-databind:[2.13.2.1,3)') api('com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:[2.13.2,3)') api('com.fasterxml.jackson.datatype:jackson-datatype-jdk8:[2.13.2,3)') + api('com.fasterxml.jackson.datatype:jackson-datatype-jsr310:[2.13.2,3)') api('com.fasterxml.jackson:jackson-bom') { version { strictly '[2.13.2.1,3)' @@ -72,6 +73,7 @@ dependencies { api('org.scalacheck:scalacheck_2.13:[1.14.0,2)') api('org.scalatest:scalatest_2.13:[3.0.8,3.1)') api('org.slf4j:slf4j-api:[1.7.25,2)') + api('uk.org.lidalia:slf4j-test:[1.1.0,2)') } } @@ -217,17 +219,6 @@ subprojects { project -> archiveClassifier = 'javadoc' from javadoc } - - // TODO: Revert this if statement in the next major release - if (project.projectDir.name != "webauthn-server-core-bundle") { - rootProject.tasks.assembleJavadoc { - dependsOn javadoc - inputs.dir javadoc.destinationDir - from(javadoc.destinationDir) { - into project.projectDir.name - } - } - } } if (project.hasProperty('publishMe') && project.publishMe) { diff --git a/doc/Migrating_from_v1.adoc b/doc/Migrating_from_v1.adoc new file mode 100644 index 000000000..d4cb5e6dc --- /dev/null +++ b/doc/Migrating_from_v1.adoc @@ -0,0 +1,351 @@ += v1.x to v2.0 migration guide + +The `2.0` release of the `webauthn-server-core` module +removes some deprecated features +and completely replaces the optional subsystem for attestation metadata. +This guide aims to help migrating between versions. + +If you find this migration guide to be incomplete, incorrect, +or otherwise difficult to follow, please +link:https://github.com/Yubico/java-webauthn-server/issues/new[let us know!] + +This is the migration guide for the core library. +The `webauthn-server-attestation` module has +link:../webauthn-server-attestation/doc/Migrating_from_v1.adoc[its own migration guide]. + +Here is a high-level outline of what needs to be updated: + +- Replace dependency on `webauthn-server-core-minimal` with + `webauthn-server-core`. +- If using JDK 14 or earlier, add a JCA provider for the `EdDSA` algorithms. +- Remove uses of removed features. +- Update uses of renamed and replaced features. +- Replace any implementations of `MetadataService` with + `AttestationTrustSource`. +- Rename imports of classes in `com.yubico.fido.metadata`. +- Update `getUserVerification()` and `getResidentKey()` calls + to expect `Optional` values. + + +== Replace dependency on `webauthn-server-core-minimal` + +If you were depending on the `webauthn-server-core-minimal` module, +update the dependency to `webauthn-server-core` instead. + +Maven example: + +[source,diff] +---------- + + com.yubico +- webauthn-server-core-minimal +- 1.12.2 ++ webauthn-server-core ++ 2.0.0 + compile + +---------- + +Gradle: + +[source,diff] +---------- +-compile 'com.yubico:webauthn-server-core-minimal:1.12.2' ++compile 'com.yubico:webauthn-server-core:2.0.0' +---------- + + +== Add JCA provider for EdDSA + +The library no longer depends explicitly on BouncyCastle for cryptography back-ends. +For applications running on JRE 15 or later this should not make a noticeable difference +and no action should be needed. +However, JRE 14 and earlier do not include EdDSA providers by default, +so you need to add a JCA provider yourself. +For example, you can use BouncyCastle. +First, add the dependency. + +Maven example: + +[source,xml] +---------- + + org.bouncycastle + bcprov-jdk15on + 1.70 + compile + +---------- + +Gradle: + +[source,groovy] +---------- +implementation 'org.bouncycastle:bcprov-jdk15on:1.70' +---------- + +Then set up the provider. This should be done before instantiating `RelyingParty`. + +Example: + +[source,java] +---------- +import org.bouncycastle.jce.provider.BouncyCastleProvider; + +Security.addProvider(new BouncyCastleProvider()); +---------- + + +== Remove uses of removed features + +Several fields, methods and settings have been removed: + +- The `icon` field in `RelyingPartyIdentity` and `UserIdentity`, + and its associated methods. + They were removed in WebAuthn Level 2 and have no replacement. ++ +Example: ++ +[source,diff] +---------- + RelyingPartyIdentity rpIdentity = RelyingPartyIdentity.builder() + .id("example.org") + .name("Example Service") +- .icon(new URL("https://example.org/favicon.ico")) + .build(); + + UserIdentity userIdentity = UserIdentity.builder() + .name("test@example.org") + .displayName("Test User") + .id(new ByteArray(new byte[] { 1, 2, 3, 4 })) +- .icon(new URL("https://example.org/user.png")) + .build(); +---------- + +- The setting `allowUnrequestedExtensions(boolean)` in `RelyingParty`. ++ +WebAuthn Level 2 now recommends that unrequested extensions should be allowed, +so this setting has been removed and is now always enabled. ++ +Example: ++ +[source,diff] +---------- + RelyingParty rp = RelyingParty + .builder() + .identity(rpIdentity) + .credentialRepository(credentialRepo) +- .allowUnrequestedExtensions(true) + .build() +---------- + +- Enum value `AttestationType.ECDAA`. ++ +This attestation type was removed from WebAuthn Level 2. +ECDAA support has not been implemented in this library, +so this value could in practice never be returned. ++ +Example: ++ +[source,diff] +---------- + RelyingParty rp = /* ... */; + RegistrationResult result = rp.finishRegistration(/* ... */); + switch (result.getAttestationType()) { +- case ECDAA: +- // Do something... +- break; +- + default: + // Do something else... + break; + } +---------- + +- Methods `RegistrationResult.getWarnings()` and `AssertionResult.getWarnings()`. ++ +These are now always empty. +Any warnings are instead logged via SLF4J +in the `com.yubico.webauthn` package and its subpackages. ++ +Example: ++ +[source,diff] +---------- + RelyingParty rp = /* ... */; + + RegistrationResult result = rp.finishRegistration(/* ... */); +-for (String warning : result.getWarnings()) { +- // Do something... +-} + + AssertionResult result = rp.finishAssertion(/* ... */); +-for (String warning : result.getWarnings()) { +- // Do something... +-} +---------- + +- Types `Attestation` and `Transport`, + methods `RegistrationResult.getAttestationMetadata()` + and `AuthenticatorTransport.fromU2fTransport()` + have been removed in an overhaul of the framework for attestation metadata. + The core library no longer exposes attestation metadata directly + in its result types, + instead each metadata source may provide its own interfaces + for retrieving and working with attestation metadata. + See for example the + link:../webauthn-server-attestation[`webauthn-server-attestation` module], + which provides the type `MetadataBlobPayloadEntry` as a replacement for `Attestation` + and reuses `AuthenticatorTransport` as a replacement for `Transport`. + + +== Update uses of renamed and replaced features + +- Methods `requireResidentKey(boolean)` and `isRequireResidentKey()` + in `AuthenticatorSelectionCriteria` have been replaced + by `residentKey(ResidentKeyRequirement)` and `getResidentKey()`, respectively. ++ +Replace `requireResidentKey(false)` +with `residentKey(ResidentKeyRequirement.DISCOURAGED)`. +Example: ++ +[source,diff] +---------- + RelyingParty rp = /* ... */; + PublicKeyCredentialCreationOptions pkcco = rp.startRegistration( + StartRegistrationOptions + .builder() + .user(userId) + .authenticatorSelection( + AuthenticatorSelectionCriteria + .builder() +- .requireResidentKey(false) ++ .residentKey(ResidentKeyRequirement.DISCOURAGED) + .build() + ) + .build() + ); +---------- ++ +Replace `requireResidentKey(true)` +with `residentKey(ResidentKeyRequirement.REQUIRED)`. +Example: ++ +[source,diff] +---------- + RelyingParty rp = /* ... */; + PublicKeyCredentialCreationOptions pkcco = rp.startRegistration( + StartRegistrationOptions + .builder() + .user(userId) + .authenticatorSelection( + AuthenticatorSelectionCriteria + .builder() +- .requireResidentKey(true) ++ .residentKey(ResidentKeyRequirement.REQUIRED) + .build() + ) + .build() + ); +---------- + + +== Replace implementations of `MetadataService` + +The `MetadataService` interface has been replaced with `AttestationTrustSource`. +The new interface has some key differences: + +- `MetadataService` implementations were expected to validate + the attestation certificate path. + `AttestationTrustSource` implementations are not; + instead they only need to retrieve the trust root certificates. + The `RelyingParty.finishRegistration` method will perform + certificate path validation internally + and report the result via `RegistrationResult.isAttestationTrusted()`. + The `AttestationTrustSource` may also return a `CertStore` + of untrusted certificates and CRLs that may be needed + for certificate path validation, + and/or disable certificate revocation checking for a particular query. + +- `MetadataService` implementations return attestation metadata. + `AttestationTrustSource` only returns + what's necessary for the certificate path validation. + Implementations may provide additional methods + for accessing attestation metadata, + but `RelyingParty` will not integrate them in the core result types. + +See the JavaDoc for `AttestationTrustSource` for details on how to implement it, +and see the `FidoMetadataService` class in the +link:../webauthn-server-attestation[`webauthn-server-attestation` module] +for a reference implementation. + +== Rename imports of classes in `com.yubico.fido.metadata` + +The `com.yubico.fido.metadata` package appears in both +the `webauthn-server-core` and `webauthn-server-attestation` modules. +This causes split package name clash in JPMS (Java Platform Module System), +so the classes in the core module have been moved +to the `com.yubico.webauthn.extension.uvm` package to avoid this name conflict. +Update any imports of these classes. + +Example: + +[source,diff] +---------- +-import com.yubico.fido.metadata.KeyProtectionType; +-import com.yubico.fido.metadata.MatcherProtectionType; +-import com.yubico.fido.metadata.UserVerificationMethod; ++import com.yubico.webauthn.extension.uvm.KeyProtectionType; ++import com.yubico.webauthn.extension.uvm.MatcherProtectionType; ++import com.yubico.webauthn.extension.uvm.UserVerificationMethod; +---------- + + +== Update `getUserVerification()` and `getResidentKey()` calls to expect `Optional` values + +The default `"preferred"` for `userVerification` has +link:https://github.com/w3c/webauthn/issues/1253[turned out to cause confusion]. +Therefore, browsers have started issuing console warnings +when `userVerification` is not set explicitly. +This library has mirrored the defaults for +`PublicKeyCredentialRequestOptions.userVerification` and +`AuthenticatorSelectionCriteria.userVerification`, +but this inadvertently suppresses any browser console warnings +since the library emits parameter objects with an explicit value set, +even if the value was not explicitly set at the library level. +The defaults have therefore been removed, +and the corresponding getters now return `Optional` values. +For consistency, the same change applies to +`AuthenticatorSelectionCriteria.residentKey` as well. + +The setters for these settings remain unchanged, +but if you use the getters you need to expect `Optional` values instead. + +Example: + +[source,diff] +---------- + PublicKeyCredentialCreationOptions pkcco = /* ... */; + if (pkcco + .getAuthenticatorSelectionCriteria() +- .map(AuthenticatorSelectionCriteria::getUserVerification) ++ .flatMap(AuthenticatorSelectionCriteria::getUserVerification) + .equals(Optional.of(UserVerificationRequirement.REQUIRED))) { + // Do something... + } + if (pkcco + .getAuthenticatorSelectionCriteria() +- .map(AuthenticatorSelectionCriteria::getResidentKey) ++ .flatMap(AuthenticatorSelectionCriteria::getResidentKey) + .equals(Optional.of(ResidentKeyRequirement.REQUIRED))) { + // Do something... + } + + PublicKeyCredentialRequestOptions pkcro = /* ... */; + if (pkcro + .getUserVerification() +- == UserVerificationRequirement.REQUIRED)) { ++ .equals(Optional.of(UserVerificationRequirement.REQUIRED))) { + // Do something... + } +---------- diff --git a/doc/development.md b/doc/development.md index bf5f4af57..9969f3ad8 100644 --- a/doc/development.md +++ b/doc/development.md @@ -1,20 +1,6 @@ Developer docs === -Inconsistent directory naming ---- - -In resolving [issue #97](https://github.com/Yubico/java-webauthn-server/issues/97), -we opted to split the `webauthn-server-core` module into one `webauthn-server-core` meta-module -and one `webauthn-server-core-minimal` module with the code and all dependencies except BouncyCastle. -However, to avoid file renames and since this is intended as a temporary change, -the source code for the `webauthn-server-core` module is hosted in the `webauthn-server-core-bundle/` subproject -and the `webauthn-server-core-minimal` module is hosted in `webauthn-server-core/`. - -We intend to eliminate the `webauthn-server-core-bundle` subproject in the next major version release, -and return the current `webauthn-server-core-minimal` module to the `webauthn-server-core` module name. -This naming inconsistency should be fixed along with this. - Code formatting --- diff --git a/doc/releasing.md b/doc/releasing.md index 8c20ba73e..0394948ba 100644 --- a/doc/releasing.md +++ b/doc/releasing.md @@ -60,10 +60,9 @@ Release candidate versions from ASCIIdoc to Markdown and remove line wraps. Include only changes/additions since the previous release or pre-release. - Attach the signature files from - `webauthn-server-attestation/build/libs/webauthn-server-attestation-X.Y.Z-RCN.jar.asc`, - `webauthn-server-core/build/libs/webauthn-server-core-minimal-X.Y.Z-RCN.jar.asc` + `webauthn-server-attestation/build/libs/webauthn-server-attestation-X.Y.Z-RCN.jar.asc` and - `webauthn-server-core-bundle/build/libs/webauthn-server-core-X.Y.Z-RCN.jar.asc`. + `webauthn-server-core/build/libs/webauthn-server-core-X.Y.Z-RCN.jar.asc`. - Note which JDK version was used to build the artifacts. @@ -146,9 +145,8 @@ Release versions from ASCIIdoc to Markdown and remove line wraps. Include all changes since the previous release (not just changes since the previous pre-release). - Attach the signature files from - `webauthn-server-attestation/build/libs/webauthn-server-attestation-X.Y.Z.jar.asc`, - `webauthn-server-core/build/libs/webauthn-server-core-minimal-X.Y.Z.jar.asc` + `webauthn-server-attestation/build/libs/webauthn-server-attestation-X.Y.Z.jar.asc` and - `webauthn-server-core-bundle/build/libs/webauthn-server-core-X.Y.Z.jar.asc`. + `webauthn-server-core/build/libs/webauthn-server-core-X.Y.Z.jar.asc`. - Note which JDK version was used to build the artifacts. diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ffed3a254..e750102e0 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/settings.gradle b/settings.gradle index ebf5b7f85..efdcc3775 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,15 +1,11 @@ rootProject.name = 'webauthn-server-parent' include ':webauthn-server-attestation' include ':webauthn-server-core' -include ':webauthn-server-core-bundle' include ':webauthn-server-demo' include ':yubico-util' include ':yubico-util-scala' include ':test-dependent-projects:java-dep-webauthn-server-attestation' include ':test-dependent-projects:java-dep-webauthn-server-core' -include ':test-dependent-projects:java-dep-webauthn-server-core-minimal' +include ':test-dependent-projects:java-dep-webauthn-server-core-and-bouncycastle' include ':test-dependent-projects:java-dep-yubico-util' - -project(':webauthn-server-core').name = 'webauthn-server-core-minimal' -project(':webauthn-server-core-bundle').name = 'webauthn-server-core' diff --git a/test-dependent-projects/java-dep-webauthn-server-attestation/src/main/java/com/yubico/test/compilability/ThisShouldCompile.java b/test-dependent-projects/java-dep-webauthn-server-attestation/src/main/java/com/yubico/test/compilability/ThisShouldCompile.java deleted file mode 100644 index 2287b9f41..000000000 --- a/test-dependent-projects/java-dep-webauthn-server-attestation/src/main/java/com/yubico/test/compilability/ThisShouldCompile.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.yubico.test.compilability; - -import com.yubico.webauthn.attestation.AttestationResolver; -import java.security.cert.X509Certificate; -import java.util.List; -import java.util.Optional; - -public class ThisShouldCompile { - - public AttestationResolver getResolver() { - return new AttestationResolver() { - @Override - public Optional resolve( - X509Certificate attestationCertificate, List certificateChain) { - return Optional.empty(); - } - - @Override - public com.yubico.webauthn.attestation.Attestation untrustedFromCertificate( - X509Certificate attestationCertificate) { - return null; - } - }; - } -} diff --git a/test-dependent-projects/java-dep-webauthn-server-attestation/src/test/java/com/yubico/webauthn/attestation/ManifestInfoTest.java b/test-dependent-projects/java-dep-webauthn-server-attestation/src/test/java/com/yubico/webauthn/attestation/ManifestInfoTest.java index f910d3c3c..7af52d43b 100644 --- a/test-dependent-projects/java-dep-webauthn-server-attestation/src/test/java/com/yubico/webauthn/attestation/ManifestInfoTest.java +++ b/test-dependent-projects/java-dep-webauthn-server-attestation/src/test/java/com/yubico/webauthn/attestation/ManifestInfoTest.java @@ -3,6 +3,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; +import com.yubico.fido.metadata.FidoMetadataService; import java.io.IOException; import java.net.URL; import java.util.Enumeration; @@ -14,7 +15,7 @@ public class ManifestInfoTest { private static String lookup(String key) throws IOException { final Enumeration resources = - AttestationResolver.class.getClassLoader().getResources("META-INF/MANIFEST.MF"); + FidoMetadataService.class.getClassLoader().getResources("META-INF/MANIFEST.MF"); while (resources.hasMoreElements()) { final URL resource = resources.nextElement(); diff --git a/test-dependent-projects/java-dep-webauthn-server-core-minimal/build.gradle.kts b/test-dependent-projects/java-dep-webauthn-server-core-and-bouncycastle/build.gradle.kts similarity index 51% rename from test-dependent-projects/java-dep-webauthn-server-core-minimal/build.gradle.kts rename to test-dependent-projects/java-dep-webauthn-server-core-and-bouncycastle/build.gradle.kts index 37512414a..f558ba389 100644 --- a/test-dependent-projects/java-dep-webauthn-server-core-minimal/build.gradle.kts +++ b/test-dependent-projects/java-dep-webauthn-server-core-and-bouncycastle/build.gradle.kts @@ -2,18 +2,21 @@ plugins { `java-library` } -val coreTestsOutput = project(":webauthn-server-core-minimal").extensions.getByType(SourceSetContainer::class).test.get().output +val coreTestsOutput = project(":webauthn-server-core").extensions.getByType(SourceSetContainer::class).test.get().output dependencies { - implementation(project(":webauthn-server-core-minimal")) + implementation(project(":webauthn-server-core")) + implementation("org.bouncycastle:bcprov-jdk15on:[1.62,2)") testImplementation(coreTestsOutput) testImplementation("junit:junit:4.12") testImplementation("org.mockito:mockito-core:[2.27.0,3)") - // Runtime-only internal dependency of webauthn-server-core-minimal + // Runtime-only internal dependency of webauthn-server-core testImplementation("com.augustcellars.cose:cose-java:[1.0.0,2)") + testRuntimeOnly("ch.qos.logback:logback-classic:[1.2.3,2)") + // Transitive dependencies from coreTestOutput testImplementation("org.scala-lang:scala-library:[2.13.1,3)") } diff --git a/test-dependent-projects/java-dep-webauthn-server-core-and-bouncycastle/src/test/java/com/yubico/webauthn/BouncyCastleProviderPresenceTest.java b/test-dependent-projects/java-dep-webauthn-server-core-and-bouncycastle/src/test/java/com/yubico/webauthn/BouncyCastleProviderPresenceTest.java new file mode 100644 index 000000000..e38997392 --- /dev/null +++ b/test-dependent-projects/java-dep-webauthn-server-core-and-bouncycastle/src/test/java/com/yubico/webauthn/BouncyCastleProviderPresenceTest.java @@ -0,0 +1,108 @@ +package com.yubico.webauthn; + +import static org.junit.Assert.assertTrue; + +import COSE.CoseException; +import com.yubico.webauthn.data.AttestationObject; +import com.yubico.webauthn.data.RelyingPartyIdentity; +import java.io.IOException; +import java.security.NoSuchAlgorithmException; +import java.security.Provider; +import java.security.Security; +import java.security.spec.InvalidKeySpecException; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +/** + * Test that the BouncyCastle provider is not loaded by default. + * + *

Motivation: https://github.com/Yubico/java-webauthn-server/issues/97 + */ +public class BouncyCastleProviderPresenceTest { + + private List providersBefore; + + @Before + public void setUp() { + providersBefore = Stream.of(Security.getProviders()).collect(Collectors.toList()); + } + + @After + public void tearDown() { + for (Provider prov : Security.getProviders()) { + Security.removeProvider(prov.getName()); + } + providersBefore.forEach(Security::addProvider); + } + + private static boolean isNamedBouncyCastle(Provider prov) { + return prov.getName().equals("BC") || prov.getClass().getCanonicalName().contains("bouncy"); + } + + @Test + public void bouncyCastleProviderIsInClasspath() { + new BouncyCastleProvider(); + } + + @Test + public void bouncyCastleProviderIsNotLoadedByDefault() { + assertTrue( + Arrays.stream(Security.getProviders()) + .noneMatch(BouncyCastleProviderPresenceTest::isNamedBouncyCastle)); + } + + @Test + public void bouncyCastleProviderIsNotLoadedAfterInstantiatingRelyingParty() { + RelyingParty.builder() + .identity(RelyingPartyIdentity.builder().id("foo").name("foo").build()) + .credentialRepository(Mockito.mock(CredentialRepository.class)) + .build(); + + assertTrue( + Arrays.stream(Security.getProviders()) + .noneMatch(BouncyCastleProviderPresenceTest::isNamedBouncyCastle)); + } + + @Test + public void bouncyCastleProviderIsNotLoadedAfterAttemptingToLoadEddsaKey() + throws IOException, CoseException, InvalidKeySpecException { + try { + WebAuthnCodecs.importCosePublicKey( + new AttestationObject( + RegistrationTestData.Packed$.MODULE$.BasicAttestationEdDsa().attestationObject()) + .getAuthenticatorData() + .getAttestedCredentialData() + .get() + .getCredentialPublicKey()); + } catch (NoSuchAlgorithmException e) { + // OK + } + + assertTrue( + Arrays.stream(Security.getProviders()) + .noneMatch(BouncyCastleProviderPresenceTest::isNamedBouncyCastle)); + } + + @Test(expected = NoSuchAlgorithmException.class) + public void doesNotFallBackToBouncyCastleAutomatically() + throws IOException, CoseException, InvalidKeySpecException, NoSuchAlgorithmException { + for (Provider prov : Security.getProviders()) { + Security.removeProvider(prov.getName()); + } + + WebAuthnCodecs.importCosePublicKey( + new AttestationObject( + RegistrationTestData.Packed$.MODULE$.BasicAttestationEdDsa().attestationObject()) + .getAuthenticatorData() + .getAttestedCredentialData() + .get() + .getCredentialPublicKey()); + } +} diff --git a/test-dependent-projects/java-dep-webauthn-server-core-and-bouncycastle/src/test/java/com/yubico/webauthn/CryptoAlgorithmsTest.java b/test-dependent-projects/java-dep-webauthn-server-core-and-bouncycastle/src/test/java/com/yubico/webauthn/CryptoAlgorithmsTest.java new file mode 100644 index 000000000..78201c6a2 --- /dev/null +++ b/test-dependent-projects/java-dep-webauthn-server-core-and-bouncycastle/src/test/java/com/yubico/webauthn/CryptoAlgorithmsTest.java @@ -0,0 +1,91 @@ +package com.yubico.webauthn; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import COSE.CoseException; +import com.yubico.webauthn.data.AttestationObject; +import com.yubico.webauthn.data.RelyingPartyIdentity; +import java.io.IOException; +import java.security.NoSuchAlgorithmException; +import java.security.Provider; +import java.security.PublicKey; +import java.security.Security; +import java.security.spec.InvalidKeySpecException; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +public class CryptoAlgorithmsTest { + + private List providersBefore; + + @Before + public void setUp() { + providersBefore = Stream.of(Security.getProviders()).collect(Collectors.toList()); + + Security.addProvider(new BouncyCastleProvider()); + + RelyingParty.builder() + .identity(RelyingPartyIdentity.builder().id("foo").name("foo").build()) + .credentialRepository(Mockito.mock(CredentialRepository.class)) + .build(); + } + + @After + public void tearDown() { + for (Provider prov : Security.getProviders()) { + Security.removeProvider(prov.getName()); + } + providersBefore.forEach(Security::addProvider); + } + + @Test + public void importRsa() + throws IOException, CoseException, NoSuchAlgorithmException, InvalidKeySpecException { + PublicKey key = + WebAuthnCodecs.importCosePublicKey( + new AttestationObject( + RegistrationTestData.Packed$.MODULE$.BasicAttestationRsa().attestationObject()) + .getAuthenticatorData() + .getAttestedCredentialData() + .get() + .getCredentialPublicKey()); + assertEquals(key.getAlgorithm(), "RSA"); + } + + @Test + public void importEcdsa() + throws IOException, CoseException, NoSuchAlgorithmException, InvalidKeySpecException { + PublicKey key = + WebAuthnCodecs.importCosePublicKey( + new AttestationObject( + RegistrationTestData.Packed$.MODULE$.BasicAttestation().attestationObject()) + .getAuthenticatorData() + .getAttestedCredentialData() + .get() + .getCredentialPublicKey()); + assertEquals(key.getAlgorithm(), "EC"); + } + + @Test + public void importEddsa() + throws IOException, CoseException, NoSuchAlgorithmException, InvalidKeySpecException { + PublicKey key = + WebAuthnCodecs.importCosePublicKey( + new AttestationObject( + RegistrationTestData.Packed$.MODULE$ + .BasicAttestationEdDsa() + .attestationObject()) + .getAuthenticatorData() + .getAttestedCredentialData() + .get() + .getCredentialPublicKey()); + assertTrue("EdDSA".equals(key.getAlgorithm()) || "Ed25519".equals(key.getAlgorithm())); + } +} diff --git a/test-dependent-projects/java-dep-webauthn-server-core/build.gradle.kts b/test-dependent-projects/java-dep-webauthn-server-core/build.gradle.kts index 41166dddf..1e8977835 100644 --- a/test-dependent-projects/java-dep-webauthn-server-core/build.gradle.kts +++ b/test-dependent-projects/java-dep-webauthn-server-core/build.gradle.kts @@ -2,22 +2,18 @@ plugins { `java-library` } -val coreTestsOutput = project(":webauthn-server-core-minimal").extensions.getByType(SourceSetContainer::class).test.get().output +val coreTestsOutput = project(":webauthn-server-core").extensions.getByType(SourceSetContainer::class).test.get().output dependencies { implementation(project(":webauthn-server-core")) - testCompileOnly("org.bouncycastle:bcprov-jdk15on:[1.62,2)") - testImplementation(coreTestsOutput) testImplementation("junit:junit:4.12") testImplementation("org.mockito:mockito-core:[2.27.0,3)") - // Runtime-only internal dependency of webauthn-server-core-minimal + // Runtime-only internal dependency of webauthn-server-core testImplementation("com.augustcellars.cose:cose-java:[1.0.0,2)") - testRuntimeOnly("ch.qos.logback:logback-classic:[1.2.3,2)") - // Transitive dependencies from coreTestOutput testImplementation("org.scala-lang:scala-library:[2.13.1,3)") } diff --git a/test-dependent-projects/java-dep-webauthn-server-core/src/main/java/com/yubico/test/compilability/ThisShouldCompile.java b/test-dependent-projects/java-dep-webauthn-server-core/src/main/java/com/yubico/test/compilability/ThisShouldCompile.java index 5201a6409..bc6da998b 100644 --- a/test-dependent-projects/java-dep-webauthn-server-core/src/main/java/com/yubico/test/compilability/ThisShouldCompile.java +++ b/test-dependent-projects/java-dep-webauthn-server-core/src/main/java/com/yubico/test/compilability/ThisShouldCompile.java @@ -55,7 +55,7 @@ public ByteArray getByteArray() { public PublicKeyCredentialType getPublicKeyCredentialType() { PublicKeyCredentialType a = PublicKeyCredentialType.PUBLIC_KEY; - String b = a.toJsonString(); + String b = a.getId(); return a; } } diff --git a/test-dependent-projects/java-dep-webauthn-server-core-minimal/src/test/java/com/yubico/webauthn/BouncyCastleProviderPresenceTest.java b/test-dependent-projects/java-dep-webauthn-server-core/src/test/java/com/yubico/webauthn/BouncyCastleProviderPresenceTest.java similarity index 69% rename from test-dependent-projects/java-dep-webauthn-server-core-minimal/src/test/java/com/yubico/webauthn/BouncyCastleProviderPresenceTest.java rename to test-dependent-projects/java-dep-webauthn-server-core/src/test/java/com/yubico/webauthn/BouncyCastleProviderPresenceTest.java index e3dc68d5a..6ce756bbc 100644 --- a/test-dependent-projects/java-dep-webauthn-server-core-minimal/src/test/java/com/yubico/webauthn/BouncyCastleProviderPresenceTest.java +++ b/test-dependent-projects/java-dep-webauthn-server-core/src/test/java/com/yubico/webauthn/BouncyCastleProviderPresenceTest.java @@ -7,6 +7,7 @@ import com.yubico.webauthn.data.RelyingPartyIdentity; import java.io.IOException; import java.security.NoSuchAlgorithmException; +import java.security.Provider; import java.security.Security; import java.security.spec.InvalidKeySpecException; import java.util.Arrays; @@ -14,13 +15,16 @@ import org.mockito.Mockito; /** - * Test that the BouncyCastle provider is not loaded by default when depending on the - * webauthn-server-core-minimal package. + * Test that the BouncyCastle provider is not loaded by default. * *

Motivation: https://github.com/Yubico/java-webauthn-server/issues/97 */ public class BouncyCastleProviderPresenceTest { + private static boolean isNamedBouncyCastle(Provider prov) { + return prov.getName().equals("BC") || prov.getClass().getCanonicalName().contains("bouncy"); + } + @Test(expected = ClassNotFoundException.class) public void bouncyCastleProviderIsNotInClasspath() throws ClassNotFoundException { Class.forName("org.bouncycastle.jce.provider.BouncyCastleProvider"); @@ -30,13 +34,11 @@ public void bouncyCastleProviderIsNotInClasspath() throws ClassNotFoundException public void bouncyCastleProviderIsNotLoadedByDefault() { assertTrue( Arrays.stream(Security.getProviders()) - .noneMatch(prov -> prov.getName().toLowerCase().contains("bouncy"))); + .noneMatch(BouncyCastleProviderPresenceTest::isNamedBouncyCastle)); } @Test public void bouncyCastleProviderIsNotLoadedAfterInstantiatingRelyingParty() { - // The RelyingParty constructor has the possible side-effect of loading the BouncyCastle - // provider RelyingParty.builder() .identity(RelyingPartyIdentity.builder().id("foo").name("foo").build()) .credentialRepository(Mockito.mock(CredentialRepository.class)) @@ -44,15 +46,12 @@ public void bouncyCastleProviderIsNotLoadedAfterInstantiatingRelyingParty() { assertTrue( Arrays.stream(Security.getProviders()) - .noneMatch( - prov -> - prov.getName().equals("BC") - || prov.getClass().getCanonicalName().contains("bouncy"))); + .noneMatch(BouncyCastleProviderPresenceTest::isNamedBouncyCastle)); } @Test public void bouncyCastleProviderIsNotLoadedAfterAttemptingToLoadEddsaKey() - throws IOException, CoseException, NoSuchAlgorithmException, InvalidKeySpecException { + throws IOException, CoseException, InvalidKeySpecException { try { WebAuthnCodecs.importCosePublicKey( new AttestationObject( @@ -67,9 +66,6 @@ public void bouncyCastleProviderIsNotLoadedAfterAttemptingToLoadEddsaKey() assertTrue( Arrays.stream(Security.getProviders()) - .noneMatch( - prov -> - prov.getName().equals("BC") - || prov.getClass().getCanonicalName().contains("bouncy"))); + .noneMatch(BouncyCastleProviderPresenceTest::isNamedBouncyCastle)); } } diff --git a/test-dependent-projects/java-dep-webauthn-server-core/src/test/java/com/yubico/webauthn/CryptoAlgorithmsTest.java b/test-dependent-projects/java-dep-webauthn-server-core/src/test/java/com/yubico/webauthn/CryptoAlgorithmsTest.java index d3b38f338..f35ce43ae 100644 --- a/test-dependent-projects/java-dep-webauthn-server-core/src/test/java/com/yubico/webauthn/CryptoAlgorithmsTest.java +++ b/test-dependent-projects/java-dep-webauthn-server-core/src/test/java/com/yubico/webauthn/CryptoAlgorithmsTest.java @@ -1,7 +1,6 @@ package com.yubico.webauthn; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; import COSE.CoseException; import com.yubico.webauthn.data.AttestationObject; @@ -71,20 +70,4 @@ public void importEcdsa() .getCredentialPublicKey()); assertEquals(key.getAlgorithm(), "EC"); } - - @Test - public void importEddsa() - throws IOException, CoseException, NoSuchAlgorithmException, InvalidKeySpecException { - PublicKey key = - WebAuthnCodecs.importCosePublicKey( - new AttestationObject( - RegistrationTestData.Packed$.MODULE$ - .BasicAttestationEdDsa() - .attestationObject()) - .getAuthenticatorData() - .getAttestedCredentialData() - .get() - .getCredentialPublicKey()); - assertTrue("EdDSA".equals(key.getAlgorithm()) || "Ed25519".equals(key.getAlgorithm())); - } } diff --git a/webauthn-server-attestation/README.adoc b/webauthn-server-attestation/README.adoc new file mode 100644 index 000000000..5eee6ddc0 --- /dev/null +++ b/webauthn-server-attestation/README.adoc @@ -0,0 +1,314 @@ += webauthn-server-attestation +:toc: +:toc-placement: macro +:toc-title: + +An optional module which extends link:../[`webauthn-server-core`] +with a trust root source for verifying +https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#sctn-attestation[attestation statements], +by interfacing with the https://fidoalliance.org/metadata/[FIDO Metadata Service]. + + +toc::[] + +== Features + +This module does four things: + +- Download, verify and cache metadata BLOBs from the FIDO Metadata Service. +- Re-download the metadata BLOB when out of date or invalid. +- Provide utilities for selecting trusted metadata entries and authenticators. +- Integrate with the + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core-minimal/latest/com/yubico/webauthn/RelyingParty.html[`RelyingParty`] + class in the base library, to provide trust root certificates + for verifying attestation statements during credential registrations. + +Notable *non-features* include: + +- *Scheduled BLOB downloads.* ++ +The `FidoMetadataDownloader` +class will attempt to download a new BLOB only when its `loadCachedBlob()` is executed, +and then only if the cache is empty or if the cached BLOB is invalid or out of date. +`FidoMetadataService` +will never re-download a new BLOB once instantiated. ++ +You should use some external scheduling mechanism to re-run `loadCachedBlob()` periodically +and rebuild new `FidoMetadataService` instances with the updated metadata contents. +You can do this with minimal disruption since the `FidoMetadataService` and `RelyingParty` +classes keep no internal mutable state. + +- *Revocation of already-registered credentials* ++ +The FIDO Metadata Service may from time to time report security issues with particular authenticator models. +The `FidoMetadataService` class can be configured with a filter for which authenticators to trust, +and untrusted authenticators can be rejected during registration by setting `.allowUntrustedAttestation(false)` on `RelyingParty`, +but this will not affect any credentials already registered. + + +== Before you start + +It is important to be aware that *requiring attestation is an invasive policy*, +especially when used to restrict users' choice of authenticator. +For some applications this is necessary; for most it is not. +Similarly, *attestation does not automatically make your users more secure*. +Attestation gives you information, but you have to know what to do with that information +in order to get a security benefit from it; it is a powerful tool but does very little on its own. +This library can help retrieve and verify additional information about an authenticator, +and enforce some very basic policy based on it, +but it is your responsibility to further leverage that information into improved security. + +When in doubt, err towards being more permissive, because _using WebAuthn is more secure than not using WebAuthn_. +It may still be useful to request and store attestation information for future reference - +for example, to warn users if security issues are discovered in their authenticators - +but we recommend that you do not _require_ a trusted attestation unless you have specific reason to do so. + + +== Migrating from version `1.x` + +See link:doc/Migrating_from_v1.adoc[the migration guide]. + + +== Getting started + +Using this module consists of 4 major steps: + + 1. Create a + `FidoMetadataDownloader` + instance to download and cache metadata BLOBs, + and a + `FidoMetadataService` + instance to make use of the downloaded BLOB. + See the JavaDoc for these classes for details on how to construct them. ++ +[WARNING] +===== +Unlike other classes in this module and the core library, +`FidoMetadataDownloader` is NOT THREAD SAFE since its `loadCachedBlob()` method reads and writes caches. +`FidoMetadataService`, on the other hand, is thread safe, +and `FidoMetadataDownloader` instances can be reused for subsequent `loadCachedBlob()` calls +as long as only one `loadCachedBlob()` call executes at a time. +===== ++ +[source,java] +---------- +FidoMetadataDownloader downloader = FidoMetadataDownloader.builder() + .expectLegalHeader("Lorem ipsum dolor sit amet") + .useDefaultTrustRoot() + .useTrustRootCacheFile(new File("/var/cache/webauthn-server/fido-mds-trust-root.bin")) + .useDefaultBlob() + .useBlobCacheFile(new File("/var/cache/webauthn-server/fido-mds-blob.bin")) + .build(); + +FidoMetadataService mds = FidoMetadataService.builder() + .useBlob(downloader.loadCachedBlob()) + .build(); +---------- + + 2. Set the `FidoMetadataService` as the `attestationTrustSource` on your + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core-minimal/latest/com/yubico/webauthn/RelyingParty.html[`RelyingParty`] + instance, + and set `attestationConveyancePreference(AttestationConveyancePreference.DIRECT)` on `RelyingParty` + to request an attestation statement for new registrations. + Optionally also set `.allowUntrustedAttestation(false)` on `RelyingParty` to require trusted attestation for new registrations. ++ +[source,java] +---------- +RelyingParty rp = RelyingParty.builder() + .identity(/* ... */) + .credentialRepository(/* ... */) + .attestationTrustSource(mds) + .attestationConveyancePreference(AttestationConveyancePreference.DIRECT) + .allowUntrustedAttestation(true) // Optional step: set to true (default) or false + .build(); +---------- + + 3. After performing registrations, inspect the `isAttestationTrusted()` result in + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core-minimal/latest/com/yubico/webauthn/RelyingParty.html[`RelyingParty`] + 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. ++ +[source,java] +---------- +RelyingParty rp = /* ... */; +RegistrationResult result = rp.finishRegistration(/* ... */); + +if (result.isAttestationTrusted()) { + // Do something... +} else { + // Do something else... +} +---------- + + 4. If needed, use the `findEntries` methods of `FidoMetadataService` to retrieve additional authenticator metadata for new registrations. ++ +[source,java] +---------- +RelyingParty rp = /* ... */; +RegistrationResult result = rp.finishRegistration(/* ... */); + +Set metadata = mds.findEntries(result); +---------- + +By default, `FidoMetadataDownloader` will probably use the SUN provider for the `PKIX` certificate path validation algorithm. +This requires the `com.sun.security.enableCRLDP` system property set to `true` in order to verify the BLOB signature. +For example, this can be done on the JVM command line using a `-Dcom.sun.security.enableCRLDP=true` option. +See the https://docs.oracle.com/javase/9/security/java-pki-programmers-guide.htm#JSSEC-GUID-EB250086-0AC1-4D60-AE2A-FC7461374746[Java PKI Programmers Guide] +for details. + + +== Selecting trusted authenticators + +The +`FidoMetadataService` +class can be configured with filters for which authenticators to trust. +When the `FidoMetadataService` is used as the `.attestationTrustSource()` in `RelyingParty`, +this will be reflected in the `.isAttestationTrusted()` result in +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core-minimal/latest/com/yubico/webauthn/RegistrationResult.html[`RegistrationResult`]. +Any authenticators not trusted will also be rejected for new registrations if you set `.allowUntrustedAttestation(false)` on `RelyingParty`. + +The filter has two stages: a "prefilter" which selects metadata entries to include in the data source, +and a registration-time filter which decides whether to associate a metadata entry +with a particular authenticator. +The prefilter executes only once (per metadata entry): +when the `FidoMetadataService` instance is constructed. +The registration-time filter takes effect during credential registration +and in the `findEntries()` methods of `FidoMetadataService`. +The following figure illustrates where each filter appears in the data flows: + +[source] +---------- + +----------+ + | FIDO MDS | + +----------+ + | + | Metadata BLOB + | ++--------------------------------------------------------------------------+ +| | FidoMetadataService | +| v =================== | +| +-----------+ | +| | Prefilter | | +| +-----------+ | +| | | +| | Selected metadata entries | +| v Matching | +| +-----------------------------+ metadata +-------------------+ | +| | Search by AAGUID & | entries | Registration-time | | +| | Attestation certificate key |------------------->| filter | | +| +-----------------------------+ +-------------------+ | +| ^ (1) ^ (2) | (1) (2) | | +| | (internal) | findEntries() | | | ++--------------------------------------------------------------------------+ + | | | | + | `-------------------------|--. | + | Get trust roots | | v + | Matched | | Matched + +-----------------------------------+ trust roots | | metadata entries + | RelyingParty.finishRegistration() |<----------------' | + +-----------------------------------+ | + ^ | | + | | Verify signature | + | PublicKeyCredential | Validate contents | Retrieve matching + | | Evaluate trust | metadata entries + | v | + +-------------+ +-----------------------------------+ + | Registering | | RegistrationResult | + | user | | - getAaguid(): ByteArray | + +-------------+ | - getAttestationTrustPath(): List | + | - isAttestationTrusted(): boolean | + | - getPublicKeyCose(): ByteArray | + +-----------------------------------+ +---------- + +The default prefilter excludes any authenticator with any `REVOKED` +link:https://fidoalliance.org/specs/mds/fido-metadata-service-v3.0-ps-20210518.html#dom-metadatablobpayloadentry-statusreports[status report] +entry, +and the default registration-time filter excludes any authenticator +with a matching `ATTESTATION_KEY_COMPROMISE` status report entry. +To customize the filters, configure the `.prefilter(Predicate)` and `.filter(Predicate)` settings +in the `FidoMetadataService` builder. +The filters are predicate functions; +each metadata entry will be trusted if and only if the prefilter predicate returns `true` for that entry. +Similarly during registration or metadata lookup, the authenticator will be matched with each metadata entry +only if the registration-time filter returns `true` for that pair of authenticator and metadata entry. +You can also use the `FidoMetadataService.Filters.allOf()` combinator to merge several predicates into one. + +[NOTE] +===== +Setting a custom filter will replace the default filter. +This is true for both the prefilter and the registration-time filter. +If you want to maintain the default filter in addition to the new behaviour, +you must include the default condition in the new filter. +For example, you can use `FidoMetadataService.Filters.allOf()` to combine a predefined filter with a custom one. +The default filters are available via static functions in `FidoMetadataService.Filters`. +===== + + +=== A note on "allow-lists" vs "deny-lists" + +The filtering functionality described above essentially expresses an "allow-list" policy. +Any metadata entry that satisfies the filters is eligible as a trust root; +any attestation statement that can be verified by one of those trust roots is trusted, +and any that cannot is not trusted. +There is no complementary "deny-list" option to reject some specific authenticators +and implicitly trust everything else even with unknown trust roots. +This is because you cannot use such a deny list to enforce an attestation policy. + +If unknown attestation trust roots were permitted, +then a deny list could be easily circumvented by making up an attestation that is not on the deny list. +Since it will have an unknown trust root, it would then be implicitly trusted. +This is why any enforceable attestation policy must disallow unknown trust roots. + +Note that unknown and untrusted attestation is allowed by default, +but can be disallowed by explicitly configuring `RelyingParty` with `.allowUntrustedAttestation(false)`. + + +== Alignment with FIDO MDS spec + +The FIDO Metadata Service specification defines +link:https://fidoalliance.org/specs/mds/fido-metadata-service-v3.0-ps-20210518.html#metadata-blob-object-processing-rules[processing rules for servers]. +The library implements these as closely as possible, but with some slight departures from the spec: + +* Processing rules steps 1-7 are implemented as specified, by the `FidoMetadataDownloader` class. + All "SHOULD" clauses are also respected, with some caveats: + + ** Step 3 states "The `nextUpdate` field of the Metadata BLOB specifies a date when the download SHOULD occur at latest". + `FidoMetadataDownloader` does not automatically re-download the BLOB. + Instead, each time its `.loadCachedBlob()` method is executed it checks whether a new BLOB should be downloaded. ++ +If no BLOB exists in cache, or the cached BLOB is invalid, or if the current date is greater than or equal to `nextUpdate`, +then a new BLOB is downloaded. +If the new BLOB is valid, has a correct signature, and has a `no` field greater than the cached BLOB, +then the new BLOB replaces the cached one; +otherwise, the new BLOB is discarded and the cached one is kept until the next execution of `.loadCachedBlob()`. + +* Metadata entries are not stored or cached individually, instead the BLOB is cached as a whole. + In processing rules step 8, neither `FidoMetadataDownloader` nor `FidoMetadataService` + 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. + +There are also some other requirements throughout the spec, which may not be obvious: + +* The + link:https://fidoalliance.org/specs/mds/fido-metadata-service-v3.0-ps-20210518.html#info-statuses[AuthenticatorStatus section] + states that "The Relying party MUST reject the Metadata Statement if the `authenticatorVersion` has not increased" + in an `UPDATE_AVAILABLE` status report. + Thus, `FidoMetadataService` silently ignores any `MetadataBLOBPayloadEntry` + whose `metadataStatement.authenticatorVersion` is present and not greater than or equal to + the `authenticatorVersion` in the respective status report. + Again, no comparison is made between metadata entries from different BLOB versions. + +* The + link:https://fidoalliance.org/specs/mds/fido-metadata-service-v3.0-ps-20210518.html#info-statuses[AuthenticatorStatus section] + states that "FIDO Servers MUST silently ignore all unknown AuthenticatorStatus values". + Thus any unknown status valus will be parsed as `AuthenticatorStatus.UNKNOWN`, + and `MetadataBLOBPayloadEntry` will silently ignore any status report with that status. + + +== Overriding certificate path validation + +The `FidoMetadataDownloader` class uses `CertPathValidator.getInstance("PKIX")` to retrieve a `CertPathValidator` instance. +If you need to override any aspect of certificate path validation, +such as CRL retrieval or OCSP, you may provide a custom `CertPathValidator` provider for the `"PKIX"` algorithm. diff --git a/webauthn-server-attestation/build.gradle b/webauthn-server-attestation/build.gradle index df8dfff19..7fe728b33 100644 --- a/webauthn-server-attestation/build.gradle +++ b/webauthn-server-attestation/build.gradle @@ -3,6 +3,7 @@ plugins { id 'scala' id 'maven-publish' id 'signing' + id 'info.solidsoft.pitest' id 'io.github.cosmicsilence.scalafix' } @@ -13,13 +14,25 @@ project.ext.publishMe = true sourceCompatibility = 1.8 targetCompatibility = 1.8 -evaluationDependsOn(':webauthn-server-core-minimal') +evaluationDependsOn(':webauthn-server-core') + +sourceSets { + integrationTest { + compileClasspath += sourceSets.main.output + runtimeClasspath += sourceSets.main.output + } +} + +configurations { + integrationTestImplementation.extendsFrom testImplementation + integrationTestRuntimeOnly.extendsFrom testRuntimeOnly +} dependencies { api(platform(rootProject)) api( - project(':webauthn-server-core-minimal'), + project(':webauthn-server-core'), ) implementation( @@ -31,21 +44,38 @@ dependencies { ) testImplementation( - project(':webauthn-server-core-minimal').sourceSets.test.output, + project(':webauthn-server-core').sourceSets.test.output, project(':yubico-util-scala'), + 'com.fasterxml.jackson.datatype:jackson-datatype-jdk8', 'junit:junit', + 'org.bouncycastle:bcpkix-jdk15on', + 'org.eclipse.jetty:jetty-server:[9.4.9.v20180320,10)', 'org.mockito:mockito-core', 'org.scala-lang:scala-library', 'org.scalacheck:scalacheck_2.13', 'org.scalatest:scalatest_2.13', + 'uk.org.lidalia:slf4j-test', ) - testRuntimeOnly( - // Transitive dependency from :webauthn-server-core:test - 'org.bouncycastle:bcpkix-jdk15on', - ) + testImplementation('org.slf4j:slf4j-api') { + version { + strictly '[1.7.25,1.8-a)' // Pre-1.8 version required by slf4j-test + } + } } +tasks.register('integrationTest', Test) { + description = 'Runs integration tests.' + group = 'verification' + + testClassesDirs = sourceSets.integrationTest.output.classesDirs + classpath = sourceSets.integrationTest.runtimeClasspath + shouldRunAfter test + check.dependsOn it + + // Required for processing CRL distribution points extension + systemProperty 'com.sun.security.enableCRLDP', 'true' +} jar { manifest { @@ -59,3 +89,17 @@ jar { } } +pitest { + pitestVersion = '1.4.11' + + timestampedReports = false + outputFormats = ['XML', 'HTML'] + + avoidCallsTo = [ + 'java.util.logging', + 'org.apache.log4j', + 'org.slf4j', + 'org.apache.commons.logging', + 'com.google.common.io.Closeables', + ] +} diff --git a/webauthn-server-attestation/doc/Migrating_from_v1.adoc b/webauthn-server-attestation/doc/Migrating_from_v1.adoc new file mode 100644 index 000000000..cf0f035b7 --- /dev/null +++ b/webauthn-server-attestation/doc/Migrating_from_v1.adoc @@ -0,0 +1,148 @@ += v1.x to v2.0 migration guide + +The `2.0` release of the `webauthn-server-attestation` module +makes lots of breaking changes compared to the `1.x` versions. +This guide aims to help migrating between versions. + +If you find this migration guide to be incomplete, incorrect, +or otherwise difficult to follow, please +link:https://github.com/Yubico/java-webauthn-server/issues/new[let us know!] + +Here is a high-level outline of what needs to be updated: + +- Replace uses of `StandardMetadataService` and its related classes + with `FidoMetadataService` and `FidoMetadataDownloader`. +- Update the name of the `RelyingParty` integration point + from `metadataService` to `attestationTrustSource`. +- `RegistrationResult` no longer includes attestation metadata, + instead you'll need to retrieve it separately after a successful registration. +- Replace uses of the `Attestation` result type with `MetadataBLOBPayloadEntry`. + + +== Replace `StandardMetadataService` + +`StandardMetadataService` and its constituent classes have been removed +in favour of `FidoMetadataService` and `FidoMetadataDownloader`. +See the link:../#getting-started[Getting started] documentation +for details on how to configure and construct them. + +Example `1.x` code: + +[source,java] +---------- +MetadataService metadataService = + new StandardMetadataService( + StandardMetadataService.createDefaultAttestationResolver( + StandardMetadataService.createDefaultTrustResolver() + )); +---------- + +Example `2.0` code: + +[source,java] +---------- +FidoMetadataService metadataService = FidoMetadataService.builder() + .useBlob(FidoMetadataDownloader.builder() + .expectLegalHeader("Retrieval and use of this BLOB indicates acceptance of the appropriate agreement located at https://fidoalliance.org/metadata/metadata-legal-terms/") + .useDefaultTrustRoot() + .useTrustRootCacheFile(new File("fido-mds-trust-root-cache.bin")) + .useDefaultBlob() + .useBlobCacheFile(new File("fido-mds-blob-cache.bin")) + .build() + .loadBlob() + ) + .build(); +---------- + +You may also need to add external logic to occasionally re-run `loadBlob()` +and reconstruct the `FidoMetadataService`, +as `FidoMetadataService` will not automatically update the BLOB on its own. + + +== Update `RelyingParty` integration point + +`FidoMetadataService` integrates with `RelyingParty` in much the same way as `StandardMetadataService`, +although the name of the setting has changed. + +Example `1.x` code: + +[source,diff] +---------- + RelyingParty rp = RelyingParty.builder() + .identity(rpIdentity) + .credentialRepository(credentialRepo) + .attestationConveyancePreference(AttestationConveyancePreference.DIRECT) +- .metadataService(metadataService)) + .allowUntrustedAttestation(true) + .build(); +---------- + +Example `2.0` code: + +[source,diff] +---------- + RelyingParty rp = RelyingParty.builder() + .identity(rpIdentity) + .credentialRepository(credentialRepo) + .attestationConveyancePreference(AttestationConveyancePreference.DIRECT) ++ .attestationTrustSource(metadataService) + .allowUntrustedAttestation(true) + .build(); +---------- + + +== Retrieve attestation metadata separately + +In `1.x`, `RegistrationResult` could include an `Attestation` object with attestation metadata, +if a metadata service was configured and the authenticator matched anything in the metadata service. +In order to keep `RelyingParty` and the new `AttestationTrustSource` interface +decoupled from any particular format of attestation metadata, this result field has been removed. +Instead, use the `findEntries` methods of `FidoMetadataService` +to retrieve attestation metadata after a successful registration, if needed. + +Example `1.x` code: + +[source,java] +---------- +RegistrationResult result = rp.finishRegistration(/* ... */); +Optional authenticatorName = result.getAttestationMetadata() + .flatMap(Attestation::getDeviceProperties) + .map(deviceProps -> deviceProps.get("description")); +---------- + +Example `2.0` code: + +[source,java] +---------- +FidoMetadataService mds = /* ... */; +RegistrationResult result = rp.finishRegistration(/* ... */); +Optional authenticatorName = mds.findEntries(result) + .stream() + .findAny() + .flatMap(MetadataBLOBPayloadEntry::getMetadataStatement) + .flatMap(MetadataStatement::getDescription); +---------- + + +== Replace `Attestation` with `MetadataBLOBPayloadEntry` + +This ties in with the previous step, and much of it will likely be done already. +However if your front-end accesses and/or displays contents of an `Attestation` object, +it will need to be updated to work with `MetadataBLOBPayloadEntry` or similar types instead. + + +Example `1.x` code: + +[source,diff] +---------- + var registrationResult = fetch(/* ... */).then(response => response.json()); +-var authenticatorName = registrationResult.attestationMetadata?.deviceProperties?.description; +---------- + +Example `2.0` code: + +[source,diff] +---------- + var registrationResult = fetch(/* ... */).then(response => response.json()); ++var authenticatorName = registrationResult.attestationMetadata?.metadataStatement?.description; +---------- diff --git a/webauthn-server-attestation/src/integrationTest/scala/com/yubico/fido/metadata/FidoMetadataDownloaderIntegrationTest.scala b/webauthn-server-attestation/src/integrationTest/scala/com/yubico/fido/metadata/FidoMetadataDownloaderIntegrationTest.scala new file mode 100644 index 000000000..26100a559 --- /dev/null +++ b/webauthn-server-attestation/src/integrationTest/scala/com/yubico/fido/metadata/FidoMetadataDownloaderIntegrationTest.scala @@ -0,0 +1,44 @@ +package com.yubico.fido.metadata + +import org.junit.runner.RunWith +import org.scalatest.BeforeAndAfter +import org.scalatest.FunSpec +import org.scalatest.Matchers +import org.scalatest.tags.Network +import org.scalatest.tags.Slow +import org.scalatestplus.junit.JUnitRunner + +import java.util.Optional +import scala.util.Success +import scala.util.Try + +@Slow +@Network +@RunWith(classOf[JUnitRunner]) +class FidoMetadataDownloaderIntegrationTest + extends FunSpec + with Matchers + with BeforeAndAfter { + + describe("FidoMetadataDownloader with default settings") { + val downloader = + FidoMetadataDownloader + .builder() + .expectLegalHeader( + "Retrieval and use of this BLOB indicates acceptance of the appropriate agreement located at https://fidoalliance.org/metadata/metadata-legal-terms/" + ) + .useDefaultTrustRoot() + .useTrustRootCache(() => Optional.empty(), _ => {}) + .useDefaultBlob() + .useBlobCache(() => Optional.empty(), _ => {}) + .build() + + it("downloads and verifies the root cert and BLOB successfully.") { + // This test requires the system property com.sun.security.enableCRLDP=true + val blob = Try(downloader.loadCachedBlob) + blob shouldBe a[Success[_]] + blob.get should not be null + } + } + +} 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 new file mode 100644 index 000000000..399c8ceb8 --- /dev/null +++ b/webauthn-server-attestation/src/integrationTest/scala/com/yubico/fido/metadata/FidoMetadataServiceIntegrationTest.scala @@ -0,0 +1,242 @@ +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_NFC +import com.yubico.fido.metadata.AttachmentHint.ATTACHMENT_HINT_WIRED +import com.yubico.fido.metadata.AttachmentHint.ATTACHMENT_HINT_WIRELESS +import com.yubico.internal.util.CertificateParser +import com.yubico.webauthn.data.AttestationObject +import com.yubico.webauthn.test.RealExamples +import org.junit.runner.RunWith +import org.scalatest.BeforeAndAfter +import org.scalatest.FunSpec +import org.scalatest.Matchers +import org.scalatest.tags.Network +import org.scalatest.tags.Slow +import org.scalatestplus.junit.JUnitRunner + +import java.io.IOException +import java.security.cert.X509Certificate +import java.util +import java.util.Optional +import scala.jdk.CollectionConverters.IteratorHasAsScala +import scala.jdk.CollectionConverters.SetHasAsScala +import scala.jdk.OptionConverters.RichOption +import scala.jdk.OptionConverters.RichOptional +import scala.util.Try + +@Slow +@Network +@RunWith(classOf[JUnitRunner]) +class FidoMetadataServiceIntegrationTest + extends FunSpec + with Matchers + with BeforeAndAfter { + + describe("FidoMetadataService") { + + describe("downloaded with default settings") { + val downloader = FidoMetadataDownloader + .builder() + .expectLegalHeader( + "Retrieval and use of this BLOB indicates acceptance of the appropriate agreement located at https://fidoalliance.org/metadata/metadata-legal-terms/" + ) + .useDefaultTrustRoot() + .useTrustRootCache(() => Optional.empty(), _ => {}) + .useDefaultBlob() + .useBlobCache(() => Optional.empty(), _ => {}) + .build() + val fidoMds = + Try( + FidoMetadataService + .builder() + .useBlob(downloader.loadCachedBlob()) + .build() + ) + + val attachmentHintsUsb = + Set(ATTACHMENT_HINT_EXTERNAL, ATTACHMENT_HINT_WIRED) + val attachmentHintsNfc = + attachmentHintsUsb ++ Set(ATTACHMENT_HINT_WIRELESS, ATTACHMENT_HINT_NFC) + + describe("by AAGUID") { + describe("correctly identifies") {} + } + + describe("correctly identifies") { + def check( + expectedDescriptionRegex: String, + testData: RealExamples.Example, + 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 entries = fidoMds.get + .findEntries( + getAttestationTrustPath( + testData.attestation.attestationObject + ).get, + Some( + new AAGUID( + testData.attestation.attestationObject.getAuthenticatorData.getAttestedCredentialData.get.getAaguid + ) + ).toJava, + ) + .asScala + entries should not be empty + val metadataStatements = + entries.flatMap(_.getMetadataStatement.toScala) + + val descriptions = + metadataStatements.flatMap(_.getDescription.toScala).toSet + for { desc <- descriptions } { + desc should (fullyMatch regex expectedDescriptionRegex) + } + + metadataStatements + .flatMap(_.getAttachmentHint.toScala.map(_.asScala)) + .flatten + .toSet should equal(attachmentHints) + } + + ignore("a YubiKey NEO.") { // TODO: Investigate why this fails + check("YubiKey NEO", RealExamples.YubiKeyNeo, attachmentHintsNfc) + } + + it("a YubiKey 4.") { + check( + "YK4 Series Key by Yubico", + RealExamples.YubiKey4, + attachmentHintsUsb, + ) + } + + it("a YubiKey 5 NFC.") { + check( + "YubiKey 5 Series with NFC", + RealExamples.YubiKey5, + attachmentHintsNfc, + ) + } + + it("an early YubiKey 5 NFC.") { + check( + "YubiKey 5 Series with NFC", + RealExamples.YubiKey5Nfc, + attachmentHintsNfc, + ) + } + + it("a newer YubiKey 5 NFC.") { + check( + "YubiKey 5 Series with NFC", + RealExamples.YubiKey5NfcPost5cNfc, + attachmentHintsNfc, + ) + } + + it("a YubiKey 5C NFC.") { + check( + "YubiKey 5 Series with NFC", + RealExamples.YubiKey5cNfc, + attachmentHintsNfc, + ) + } + + it("a YubiKey 5 Nano.") { + check( + "YubiKey ?5 Series", + RealExamples.YubiKey5Nano, + attachmentHintsUsb, + ) + } + + it("a YubiKey 5Ci.") { + check("YubiKey 5Ci", RealExamples.YubiKey5Ci, attachmentHintsUsb) + } + + ignore("a Security Key by Yubico.") { // TODO: Investigate why this fails + check( + "Security Key by Yubico", + RealExamples.SecurityKey, + attachmentHintsUsb, + ) + } + + it("a Security Key 2 by Yubico.") { + check( + "Security Key by Yubico", + RealExamples.SecurityKey2, + attachmentHintsUsb, + ) + } + + ignore("a Security Key NFC by Yubico.") { // TODO: Investigate why this fails + check( + "Security Key NFC by Yubico", + RealExamples.SecurityKeyNfc, + attachmentHintsNfc, + ) + } + + it("a YubiKey 5.4 NFC FIPS.") { + check( + "YubiKey 5 FIPS Series with NFC", + RealExamples.YubikeyFips5Nfc, + attachmentHintsNfc, + ) + } + + it("a YubiKey 5.4 Ci FIPS.") { + check( + "YubiKey 5Ci FIPS", + RealExamples.Yubikey5ciFips, + attachmentHintsUsb, + ) + } + + it("a YubiKey Bio.") { + check( + "YubiKey Bio Series", + RealExamples.YubikeyBio_5_5_5, + attachmentHintsUsb, + ) + } + } + } + } +} 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 new file mode 100644 index 000000000..3ef4524cf --- /dev/null +++ b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/AAGUID.java @@ -0,0 +1,126 @@ +package com.yubico.fido.metadata; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import com.yubico.internal.util.ExceptionUtil; +import com.yubico.webauthn.data.ByteArray; +import com.yubico.webauthn.data.exception.HexException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.ToString; +import lombok.Value; + +/** + * Some authenticators have an AAGUID, which is a 128-bit identifier that indicates the type (e.g. + * make and model) of the authenticator. The AAGUID MUST be chosen by the manufacturer to be + * identical across all substantially identical authenticators made by that manufacturer, and + * different (with probability 1-2-128 or greater) from the AAGUIDs of all other types of + * authenticators. + * + *

The AAGUID is represented as a string (e.g. "7a98c250-6808-11cf-b73b-00aa00b677a7") consisting + * of 5 hex strings separated by a dash ("-"), see [RFC4122]. + * + * @see FIDO + * Metadata Statement §3.1. Authenticator Attestation GUID (AAGUID) typedef + * @see RFC 4122: A Universally Unique IDentifier + * (UUID) URN Namespace + */ +@Value +@Getter(AccessLevel.NONE) +@ToString(includeFieldNames = false, onlyExplicitlyIncluded = true) +public class AAGUID { + + private static final Pattern AAGUID_PATTERN = + Pattern.compile( + "^([0-9a-fA-F]{8})-?([0-9a-fA-F]{4})-?([0-9a-fA-F]{4})-?([0-9a-fA-F]{4})-?([0-9a-fA-F]{12})$"); + + private static final ByteArray ZERO = + new ByteArray(new byte[] {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}); + + ByteArray value; + + /** + * Construct an AAGUID from its raw binary representation. + * + *

This is the inverse of {@link #asBytes()}. + * + * @param value a {@link ByteArray} of length exactly 16. + */ + public AAGUID(ByteArray value) { + ExceptionUtil.assure( + value.size() == 16, + "AAGUID as bytes must be exactly 16 bytes long, was %d: %s", + value.size(), + value); + this.value = value; + } + + /** + * The 16-byte binary representation of this AAGUID, for example + * 7a98c250680811cfb73b00aa00b677a7 when hex-encoded. + * + *

This is the inverse of {@link #AAGUID(ByteArray)}. + */ + public ByteArray asBytes() { + return value; + } + + /** + * The 32-character hexadecimal representation of this AAGUID, for example + * "7a98c250680811cfb73b00aa00b677a7". + */ + public String asHexString() { + return value.getHex(); + } + + /** + * The 36-character string representation of this AAGUID, for example + * "7a98c250-6808-11cf-b73b-00aa00b677a7". + */ + @JsonValue + @ToString.Include + public String asGuidString() { + final String hex = value.getHex(); + return String.format( + "%s-%s-%s-%s-%s", + hex.substring(0, 8), + hex.substring(8, 8 + 4), + hex.substring(8 + 4, 8 + 4 + 4), + hex.substring(8 + 4 + 4, 8 + 4 + 4 + 4), + hex.substring(8 + 4 + 4 + 4, 8 + 4 + 4 + 4 + 12)); + } + + /** + * true if and only if this {@link AAGUID} consists of all zeroes. This typically + * indicates that an authenticator has no AAGUID, or that the AAGUID has been redacted. + */ + public boolean isZero() { + return ZERO.equals(value); + } + + private static ByteArray parse(String value) { + Matcher matcher = AAGUID_PATTERN.matcher(value); + if (matcher.find()) { + try { + return ByteArray.fromHex(matcher.group(1)) + .concat(ByteArray.fromHex(matcher.group(2))) + .concat(ByteArray.fromHex(matcher.group(3))) + .concat(ByteArray.fromHex(matcher.group(4))) + .concat(ByteArray.fromHex(matcher.group(5))); + } catch (HexException e) { + throw new RuntimeException( + "This exception should be impossible, please file a bug report.", e); + } + } else { + throw new IllegalArgumentException("Value does not match AAGUID pattern: " + value); + } + } + + @JsonCreator + private static AAGUID fromString(String aaguid) { + return new AAGUID(parse(aaguid)); + } +} diff --git a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/AAID.java b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/AAID.java new file mode 100644 index 000000000..686fd6eb2 --- /dev/null +++ b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/AAID.java @@ -0,0 +1,73 @@ +package com.yubico.fido.metadata; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import java.util.regex.Pattern; +import lombok.Value; + +/** + * Each UAF authenticator MUST have an AAID to identify UAF enabled authenticator models globally. + * The AAID MUST uniquely identify a specific authenticator model within the range of all + * UAF-enabled authenticator models made by all authenticator vendors, where authenticators of a + * specific model must share identical security characteristics within the model (see Security + * Considerations). + * + *

The AAID is a string with format "V#M", where + * + *

    + *
  • # is a separator + *
  • V indicates the authenticator Vendor Code. This code consists of 4 hexadecimal + * digits. + *
  • M indicates the authenticator Model Code. This code consists of 4 hexadecimal + * digits. + *
+ * + * @see FIDO + * UAF Protocol Specification §3.1.4 Authenticator Attestation ID (AAID) typedef + */ +@Value +public class AAID { + + private static final Pattern AAID_PATTERN = Pattern.compile("^[0-9a-fA-F]{4}#[0-9a-fA-F]{4}$"); + + /** + * The underlying string value of this AAID. + * + *

The AAID is a string with format "V#M", where + * + *

    + *
  • # is a separator + *
  • V indicates the authenticator Vendor Code. This code consists of 4 + * hexadecimal digits. + *
  • M indicates the authenticator Model Code. This code consists of 4 + * hexadecimal digits. + *
+ * + * @see Authenticator + * Attestation ID (AAID) typedef + */ + @JsonValue String value; + + /** + * Construct an {@link AAID} from its String representation. + * + *

This is the inverse of {@link #getValue()}. + * + * @param value a {@link String} conforming to the rules specified in the {@link AAID} type. + */ + @JsonCreator + public AAID(String value) { + this.value = validate(value); + } + + private String validate(String value) { + if (AAID_PATTERN.matcher(value).matches()) { + return value; + } else { + throw new IllegalArgumentException( + String.format("Value does not satisfy AAID format: %s", value)); + } + } +} diff --git a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/AlternativeDescriptions.java b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/AlternativeDescriptions.java new file mode 100644 index 000000000..8dd82dc67 --- /dev/null +++ b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/AlternativeDescriptions.java @@ -0,0 +1,49 @@ +package com.yubico.fido.metadata; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import java.util.Map; +import java.util.Optional; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Value; + +/** + * See: + * https://fidoalliance.org/specs/mds/fido-metadata-statement-v3.0-ps-20210518.html#alternativedescriptions-dictionary + * + * @see FIDO + * Metadata Statement §3.11. AlternativeDescriptions dictionary + */ +@Value +@AllArgsConstructor(onConstructor_ = {@JsonCreator}) +public class AlternativeDescriptions { + + @JsonValue + @Getter(AccessLevel.NONE) + Map values; + + /** + * Get a map entry in accordance with the rules defined in AlternativeDescriptions + * dictionary. + * + * @see AlternativeDescriptions + * dictionary. + */ + public Optional get(String languageCode) { + if (values.containsKey(languageCode)) { + return Optional.of(values.get(languageCode)); + } else { + final String[] splits = languageCode.split("-"); + if (splits.length > 1 && values.containsKey(splits[0])) { + return Optional.of(values.get(splits[0])); + } else { + return Optional.empty(); + } + } + } +} diff --git a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/AttachmentHint.java b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/AttachmentHint.java new file mode 100644 index 000000000..6cbd675e7 --- /dev/null +++ b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/AttachmentHint.java @@ -0,0 +1,142 @@ +package com.yubico.fido.metadata; + +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * The ATTACHMENT_HINT constants are flags in a bit field represented as a 32 bit long. They + * describe the method FIDO authenticators use to communicate with the FIDO User Device. These + * constants are reported and queried through the UAF Discovery APIs [UAFAppAPIAndTransport], and + * used to form Authenticator policies in UAF protocol messages. Because the connection state and + * topology of an authenticator may be transient, these values are only hints that can be used by + * server-supplied policy to guide the user experience, e.g. to prefer a device that is connected + * and ready for authenticating or confirming a low-value transaction, rather than one that is more + * secure but requires more user effort. Each constant has a case-sensitive string representation + * (in quotes), which is used in the authoritative metadata for FIDO authenticators. Note + * + *

These flags are not a mandatory part of authenticator metadata and, when present, only + * indicate possible states that may be reported during authenticator discovery. + * + * @see FIDO + * Registry of Predefined Values §3.4 Authenticator Attachment Hints + */ +public enum AttachmentHint { + + /** + * This flag MAY be set to indicate that the authenticator is permanently attached to the FIDO + * User Device. + * + *

A device such as a smartphone may have authenticator functionality that is able to be used + * both locally and remotely. In such a case, the FIDO client MUST filter and exclusively report + * only the relevant bit during Discovery and when performing policy matching. + * + *

This flag cannot be combined with any other {@link AttachmentHint} flags. + * + * @see FIDO + * Registry of Predefined Values §3.4 Authenticator Attachment Hints + */ + ATTACHMENT_HINT_INTERNAL(0x0001, "internal"), + + /** + * This flag MAY be set to indicate, for a hardware-based authenticator, that it is removable or + * remote from the FIDO User Device. + * + *

A device such as a smartphone may have authenticator functionality that is able to be used + * both locally and remotely. In such a case, the FIDO UAF Client MUST filter and exclusively + * report only the relevant bit during discovery and when performing policy matching. This flag + * MUST be combined with one or more other {@link AttachmentHint} flag(s). + * + * @see FIDO + * Registry of Predefined Values §3.4 Authenticator Attachment Hints + */ + ATTACHMENT_HINT_EXTERNAL(0x0002, "external"), + + /** + * This flag MAY be set to indicate that an external authenticator currently has an exclusive + * wired connection, e.g. through USB, Firewire or similar, to the FIDO User Device. + * + * @see FIDO + * Registry of Predefined Values §3.4 Authenticator Attachment Hints + */ + ATTACHMENT_HINT_WIRED(0x0004, "wired"), + + /** + * This flag MAY be set to indicate that an external authenticator communicates with the FIDO User + * Device through a personal area or otherwise non-routed wireless protocol, such as Bluetooth or + * NFC. + * + * @see FIDO + * Registry of Predefined Values §3.4 Authenticator Attachment Hints + */ + ATTACHMENT_HINT_WIRELESS(0x0008, "wireless"), + + /** + * This flag MAY be set to indicate that an external authenticator is able to communicate by NFC + * to the FIDO User Device. As part of authenticator metadata, or when reporting characteristics + * through discovery, if this flag is set, the {@link #ATTACHMENT_HINT_WIRELESS} flag SHOULD also + * be set as well. + * + * @see FIDO + * Registry of Predefined Values §3.4 Authenticator Attachment Hints + */ + ATTACHMENT_HINT_NFC(0x0010, "nfc"), + + /** + * This flag MAY be set to indicate that an external authenticator is able to communicate using + * Bluetooth with the FIDO User Device. As part of authenticator metadata, or when reporting + * characteristics through discovery, if this flag is set, the {@link #ATTACHMENT_HINT_WIRELESS} + * flag SHOULD also be set. + * + * @see FIDO + * Registry of Predefined Values §3.4 Authenticator Attachment Hints + */ + ATTACHMENT_HINT_BLUETOOTH(0x0020, "bluetooth"), + + /** + * This flag MAY be set to indicate that the authenticator is connected to the FIDO User Device + * over a non-exclusive network (e.g. over a TCP/IP LAN or WAN, as opposed to a PAN or + * point-to-point connection). + * + * @see FIDO + * Registry of Predefined Values §3.4 Authenticator Attachment Hints + */ + ATTACHMENT_HINT_NETWORK(0x0040, "network"), + + /** + * This flag MAY be set to indicate that an external authenticator is in a "ready" state. This + * flag is set by the ASM at its discretion. + * + * @see FIDO + * Registry of Predefined Values §3.4 Authenticator Attachment Hints + */ + ATTACHMENT_HINT_READY(0x0080, "ready"), + + /** + * This flag MAY be set to indicate that an external authenticator is able to communicate using + * WiFi Direct with the FIDO User Device. As part of authenticator metadata and when reporting + * characteristics through discovery, if this flag is set, the {@link #ATTACHMENT_HINT_WIRELESS} + * flag SHOULD also be set. + * + * @see FIDO + * Registry of Predefined Values §3.4 Authenticator Attachment Hints + */ + ATTACHMENT_HINT_WIFI_DIRECT(0x0100, "wifi_direct"); + + private final int value; + + @JsonValue private final String name; + + AttachmentHint(int value, String name) { + this.value = value; + this.name = name; + } +} diff --git a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/AuthenticationAlgorithm.java b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/AuthenticationAlgorithm.java new file mode 100644 index 000000000..166645ea9 --- /dev/null +++ b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/AuthenticationAlgorithm.java @@ -0,0 +1,152 @@ +package com.yubico.fido.metadata; + +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * The ALG_SIGN constants are 16 bit long integers indicating the specific signature + * algorithm and encoding. + * + *

Each constant has a case-sensitive string representation (in quotes), which is used in the + * authoritative metadata for FIDO authenticators. + * + * @see FIDO + * Registry of Predefined Values §3.6.1 Authentication Algorithms + */ +public enum AuthenticationAlgorithm { + + /** + * @see FIDO + * Registry of Predefined Values §3.6.1 Authentication Algorithms + */ + ALG_SIGN_SECP256R1_ECDSA_SHA256_RAW(0x0001, "secp256r1_ecdsa_sha256_raw"), + + /** + * @see FIDO + * Registry of Predefined Values §3.6.1 Authentication Algorithms + */ + ALG_SIGN_SECP256R1_ECDSA_SHA256_DER(0x0002, "secp256r1_ecdsa_sha256_der"), + + /** + * @see FIDO + * Registry of Predefined Values §3.6.1 Authentication Algorithms + */ + ALG_SIGN_RSASSA_PSS_SHA256_RAW(0x0003, "rsassa_pss_sha256_raw"), + + /** + * @see FIDO + * Registry of Predefined Values §3.6.1 Authentication Algorithms + */ + ALG_SIGN_RSASSA_PSS_SHA256_DER(0x0004, "rsassa_pss_sha256_der"), + + /** + * @see FIDO + * Registry of Predefined Values §3.6.1 Authentication Algorithms + */ + ALG_SIGN_SECP256K1_ECDSA_SHA256_RAW(0x0005, "secp256k1_ecdsa_sha256_raw"), + + /** + * @see FIDO + * Registry of Predefined Values §3.6.1 Authentication Algorithms + */ + ALG_SIGN_SECP256K1_ECDSA_SHA256_DER(0x0006, "secp256k1_ecdsa_sha256_der"), + + /** + * @see FIDO + * Registry of Predefined Values §3.6.1 Authentication Algorithms + */ + ALG_SIGN_RSA_EMSA_PKCS1_SHA256_RAW(0x0008, "rsa_emsa_pkcs1_sha256_raw"), + + /** + * @see FIDO + * Registry of Predefined Values §3.6.1 Authentication Algorithms + */ + ALG_SIGN_RSA_EMSA_PKCS1_SHA256_DER(0x0009, "rsa_emsa_pkcs1_sha256_der"), + + /** + * @see FIDO + * Registry of Predefined Values §3.6.1 Authentication Algorithms + */ + ALG_SIGN_RSASSA_PSS_SHA384_RAW(0x000A, "rsassa_pss_sha384_raw"), + + /** + * @see FIDO + * Registry of Predefined Values §3.6.1 Authentication Algorithms + */ + ALG_SIGN_RSASSA_PSS_SHA512_RAW(0x000B, "rsassa_pss_sha512_raw"), + + /** + * @see FIDO + * Registry of Predefined Values §3.6.1 Authentication Algorithms + */ + ALG_SIGN_RSASSA_PKCSV15_SHA256_RAW(0x000C, "rsassa_pkcsv15_sha256_raw"), + + /** + * @see FIDO + * Registry of Predefined Values §3.6.1 Authentication Algorithms + */ + ALG_SIGN_RSASSA_PKCSV15_SHA384_RAW(0x000D, "rsassa_pkcsv15_sha384_raw"), + + /** + * @see FIDO + * Registry of Predefined Values §3.6.1 Authentication Algorithms + */ + ALG_SIGN_RSASSA_PKCSV15_SHA512_RAW(0x000E, "rsassa_pkcsv15_sha512_raw"), + + /** + * @see FIDO + * Registry of Predefined Values §3.6.1 Authentication Algorithms + */ + ALG_SIGN_RSASSA_PKCSV15_SHA1_RAW(0x000F, "rsassa_pkcsv15_sha1_raw"), + + /** + * @see FIDO + * Registry of Predefined Values §3.6.1 Authentication Algorithms + */ + ALG_SIGN_SECP384R1_ECDSA_SHA384_RAW(0x0010, "secp384r1_ecdsa_sha384_raw"), + + /** + * @see FIDO + * Registry of Predefined Values §3.6.1 Authentication Algorithms + */ + ALG_SIGN_SECP521R1_ECDSA_SHA512_RAW(0x0011, "secp521r1_ecdsa_sha512_raw"), + + /** + * @see FIDO + * Registry of Predefined Values §3.6.1 Authentication Algorithms + */ + ALG_SIGN_ED25519_EDDSA_SHA512_RAW(0x0012, "ed25519_eddsa_sha512_raw"), + + /** + * @see FIDO + * Registry of Predefined Values §3.6.1 Authentication Algorithms + */ + ALG_SIGN_ED448_EDDSA_SHA512_RAW(0x0013, "ed448_eddsa_sha512_raw"); + + private final int value; + + @JsonValue private final String name; + + AuthenticationAlgorithm(int value, String name) { + this.value = value; + this.name = name; + } +} diff --git a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/AuthenticatorAttestationType.java b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/AuthenticatorAttestationType.java new file mode 100644 index 000000000..6612fbfdd --- /dev/null +++ b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/AuthenticatorAttestationType.java @@ -0,0 +1,92 @@ +package com.yubico.fido.metadata; + +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * The ATTESTATION constants are 16 bit long integers indicating the specific attestation that + * authenticator supports. + * + *

Each constant has a case-sensitive string representation (in quotes), which is used in the + * authoritative metadata for FIDO authenticators. * + * + * @see FIDO + * Registry of Predefined Values §3.7 Authenticator Attestation Types + */ +public enum AuthenticatorAttestationType { + + /** + * Indicates full basic attestation, based on an attestation private key shared among a class of + * authenticators (e.g. same model). Authenticators must provide its attestation signature during + * the registration process for the same reason. The attestation trust anchor is shared with FIDO + * Servers out of band (as part of the Metadata). This sharing process should be done according to + * [FIDOMetadataService]. + * + * @see FIDO + * Registry of Predefined Values §3.7 Authenticator Attestation Types + */ + ATTESTATION_BASIC_FULL(0x3E07, "basic_full"), + + /** + * Just syntactically a Basic Attestation. The attestation object self-signed, i.e. it is signed + * using the UAuth.priv key, i.e. the key corresponding to the UAuth.pub key included in the + * attestation object. As a consequence it does not provide a cryptographic proof of the security + * characteristics. But it is the best thing we can do if the authenticator is not able to have an + * attestation private key. + * + * @see FIDO + * Registry of Predefined Values §3.7 Authenticator Attestation Types + */ + ATTESTATION_BASIC_SURROGATE(0x3E08, "basic_surrogate"), + + /** + * Indicates use of elliptic curve based direct anonymous attestation as defined in [FIDOEcdaaAlgorithm]. + * Support for this attestation type is optional at this time. It might be required by FIDO + * Certification. + * + * @see FIDO + * Registry of Predefined Values §3.7 Authenticator Attestation Types + */ + ATTESTATION_ECDAA(0x3E09, "ecdaa"), + + /** + * Indicates PrivacyCA attestation as defined in [TCG-CMCProfile-AIKCertEnroll]. + * Support for this attestation type is optional at this time. It might be required by FIDO + * Certification. + * + * @see FIDO + * Registry of Predefined Values §3.7 Authenticator Attestation Types + */ + ATTESTATION_ATTCA(0x3E0A, "attca"), + + /** + * In this case, the authenticator uses an Anonymization CA which dynamically generates + * per-credential attestation certificates such that the attestation statements presented to + * Relying Parties do not provide uniquely identifiable information, e.g., that might be used for + * tracking purposes. The applicable [WebAuthn] + * attestation formats "fmt" are Google SafetyNet Attestation "android-safetynet", Android + * Keystore Attestation "android-key", Apple Anonymous Attestation "apple", and Apple Application + * Attestation "apple-appattest". + */ + ATTESTATION_ANONCA(0x3E0C, "anonca"), + + /** Indicates absence of attestation. */ + ATTESTATION_NONE(0x3E0B, "none"); + + private final int value; + + @JsonValue private final String name; + + AuthenticatorAttestationType(int value, String name) { + this.value = value; + this.name = name; + } +} diff --git a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/AuthenticatorGetInfo.java b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/AuthenticatorGetInfo.java new file mode 100644 index 000000000..78cefbd64 --- /dev/null +++ b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/AuthenticatorGetInfo.java @@ -0,0 +1,381 @@ +package com.yubico.fido.metadata; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.core.JacksonException; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.yubico.webauthn.data.AuthenticatorTransport; +import com.yubico.webauthn.data.PublicKeyCredentialParameters; +import com.yubico.webauthn.extension.uvm.UserVerificationMethod; +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import lombok.Builder; +import lombok.NonNull; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; + +/** + * This dictionary describes supported versions, extensions, AAGUID of the device and its + * capabilities. + * + *

See: Client + * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) + * + * @see FIDO + * Metadata Statement §3.12. AuthenticatorGetInfo dictionary + * @see Client + * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) + */ +@Value +@Builder(toBuilder = true) +@Jacksonized +@JsonIgnoreProperties({ + "maxAuthenticatorConfigLength", + "defaultCredProtect" +}) // Present in example but not defined +public class AuthenticatorGetInfo { + + /** + * @see Client + * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) + */ + @NonNull Set versions; + + /** + * @see Client + * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) + */ + Set extensions; + + /** + * @see Client + * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) + */ + AAGUID aaguid; + + /** + * @see Client + * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) + */ + SupportedCtapOptions options; + + /** + * @see Client + * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) + */ + Integer maxMsgSize; + + /** + * @see Client + * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) + */ + Set pinUvAuthProtocols; + + /** + * @see Client + * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) + */ + Integer maxCredentialCountInList; + + /** + * @see Client + * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) + */ + Integer maxCredentialIdLength; + + /** + * @see Client + * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) + */ + Set transports; + + /** + * @see Client + * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) + */ + List algorithms; + + /** + * @see Client + * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) + */ + Integer maxSerializedLargeBlobArray; + + /** + * @see Client + * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) + */ + Boolean forcePINChange; + + /** + * @see Client + * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) + */ + Integer minPINLength; + + /** + * @see Client + * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) + */ + Integer firmwareVersion; + + /** + * @see Client + * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) + */ + Integer maxCredBlobLength; + + /** + * @see Client + * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) + */ + Integer maxRPIDsForSetMinPINLength; + + /** + * @see Client + * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) + */ + Integer preferredPlatformUvAttempts; + + @JsonDeserialize(using = SetFromIntJsonDeserializer.class) + @JsonSerialize(contentUsing = IntFromSetJsonSerializer.class) + Set uvModality; + + Map certifications; + Integer remainingDiscoverableCredentials; + Set vendorPrototypeConfigCommands; + + /** + * @see Client + * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) + */ + public Optional> getExtensions() { + return Optional.ofNullable(extensions); + } + + /** + * @see Client + * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) + */ + public Optional getAaguid() { + return Optional.ofNullable(aaguid); + } + + /** + * @see Client + * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) + */ + public Optional getOptions() { + return Optional.ofNullable(options); + } + + /** + * @see Client + * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) + */ + public Optional getMaxMsgSize() { + return Optional.ofNullable(maxMsgSize); + } + + /** + * @see Client + * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) + */ + public Optional> getPinUvAuthProtocols() { + return Optional.ofNullable(pinUvAuthProtocols); + } + + /** + * @see Client + * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) + */ + public Optional getMaxCredentialCountInList() { + return Optional.ofNullable(maxCredentialCountInList); + } + + /** + * @see Client + * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) + */ + public Optional getMaxCredentialIdLength() { + return Optional.ofNullable(maxCredentialIdLength); + } + + /** + * @see Client + * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) + */ + public Optional> getTransports() { + return Optional.ofNullable(transports); + } + + /** + * @see Client + * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) + */ + public Optional> getAlgorithms() { + return Optional.ofNullable(algorithms); + } + + /** + * @see Client + * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) + */ + public Optional getMaxSerializedLargeBlobArray() { + return Optional.ofNullable(maxSerializedLargeBlobArray); + } + + /** + * @see Client + * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) + */ + public Optional getForcePINChange() { + return Optional.ofNullable(forcePINChange); + } + + /** + * @see Client + * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) + */ + public Optional getMinPINLength() { + return Optional.ofNullable(minPINLength); + } + + /** + * @see Client + * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) + */ + public Optional getFirmwareVersion() { + return Optional.ofNullable(firmwareVersion); + } + + /** + * @see Client + * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) + */ + public Optional getMaxCredBlobLength() { + return Optional.ofNullable(maxCredBlobLength); + } + + /** + * @see Client + * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) + */ + public Optional getMaxRPIDsForSetMinPINLength() { + return Optional.ofNullable(maxRPIDsForSetMinPINLength); + } + + /** + * @see Client + * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) + */ + public Optional getPreferredPlatformUvAttempts() { + return Optional.ofNullable(preferredPlatformUvAttempts); + } + + /** + * @see Client + * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) + */ + public Optional> getUvModality() { + return Optional.ofNullable(uvModality); + } + + /** + * @see Client + * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) + */ + public Optional> getCertifications() { + return Optional.ofNullable(certifications); + } + + /** + * @see Client + * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) + */ + public Optional getRemainingDiscoverableCredentials() { + return Optional.ofNullable(remainingDiscoverableCredentials); + } + + /** + * @see Client + * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) + */ + public Optional> getVendorPrototypeConfigCommands() { + return Optional.ofNullable(vendorPrototypeConfigCommands); + } + + private static class SetFromIntJsonDeserializer + extends JsonDeserializer> { + @Override + public Set deserialize(JsonParser p, DeserializationContext ctxt) + throws IOException, JacksonException { + final int bitset = p.getNumberValue().intValue(); + return Arrays.stream(UserVerificationMethod.values()) + .filter(uvm -> (uvm.getValue() & bitset) != 0) + .collect(Collectors.toSet()); + } + } + + private static class IntFromSetJsonSerializer + extends JsonSerializer> { + @Override + public void serialize( + Set value, JsonGenerator gen, SerializerProvider serializers) + throws IOException { + gen.writeNumber( + value.stream().reduce(0, (acc, next) -> acc | next.getValue(), (a, b) -> a | b)); + } + } +} diff --git a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/AuthenticatorStatus.java b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/AuthenticatorStatus.java new file mode 100644 index 000000000..49c66572c --- /dev/null +++ b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/AuthenticatorStatus.java @@ -0,0 +1,189 @@ +package com.yubico.fido.metadata; + +import com.fasterxml.jackson.annotation.JsonEnumDefaultValue; + +/** + * This enumeration describes the status of an authenticator model as identified by its AAID/AAGUID + * or attestationCertificateKeyIdentifiers and potentially some additional information (such as a + * specific attestation key). + * + * @see FIDO + * Metadata Service §3.1.4. AuthenticatorStatus enum + */ +public enum AuthenticatorStatus { + /** (NOT DEFINED IN SPEC) Placeholder for any unknown {@link AuthenticatorStatus} value. */ + @JsonEnumDefaultValue + UNKNOWN(0), + + /** + * This authenticator is not FIDO certified. + * + * @see FIDO + * Metadata Service §3.1.4. AuthenticatorStatus enum + */ + NOT_FIDO_CERTIFIED(0), + + /** + * This authenticator has passed FIDO functional certification. This certification scheme is + * phased out and will be replaced by {@link #FIDO_CERTIFIED_L1}. + * + * @see FIDO + * Metadata Service §3.1.4. AuthenticatorStatus enum + */ + FIDO_CERTIFIED(10), + + /** + * Indicates that malware is able to bypass the user verification. This means that the + * authenticator could be used without the user’s consent and potentially even without the user’s + * knowledge. + * + * @see FIDO + * Metadata Service §3.1.4. AuthenticatorStatus enum + */ + USER_VERIFICATION_BYPASS(0), + + /** + * Indicates that an attestation key for this authenticator is known to be compromised. The + * relying party SHOULD check the certificate field and use it to identify the compromised + * authenticator batch. If the certificate field is not set, the relying party should reject all + * new registrations of the compromised authenticator. The Authenticator manufacturer should set + * the date to the date when compromise has occurred. + * + * @see FIDO + * Metadata Service §3.1.4. AuthenticatorStatus enum + */ + ATTESTATION_KEY_COMPROMISE(0), + + /** + * This authenticator has identified weaknesses that allow registered keys to be compromised and + * should not be trusted. This would include both, e.g. weak entropy that causes predictable keys + * to be generated or side channels that allow keys or signatures to be forged, guessed or + * extracted. + * + * @see FIDO + * Metadata Service §3.1.4. AuthenticatorStatus enum + */ + USER_KEY_REMOTE_COMPROMISE(0), + + /** + * This authenticator has known weaknesses in its key protection mechanism(s) that allow user keys + * to be extracted by an adversary in physical possession of the device. + * + * @see FIDO + * Metadata Service §3.1.4. AuthenticatorStatus enum + */ + USER_KEY_PHYSICAL_COMPROMISE(0), + + /** + * A software or firmware update is available for the device. The Authenticator manufacturer + * should set the url to the URL where users can obtain an update and the date the update was + * published. When this status code is used, then the field authenticatorVersion in the + * authenticator Metadata Statement [FIDOMetadataStatement] + * MUST be updated, if the update fixes severe security issues, e.g. the ones reported by + * preceding StatusReport entries with status code {@link #USER_VERIFICATION_BYPASS}, {@link + * #ATTESTATION_KEY_COMPROMISE}, {@link #USER_KEY_REMOTE_COMPROMISE}, {@link + * #USER_KEY_PHYSICAL_COMPROMISE}, {@link #REVOKED}. The Relying party MUST reject the Metadata + * Statement if the authenticatorVersion has not increased + * + * @see FIDO + * Metadata Service §3.1.4. AuthenticatorStatus enum + */ + UPDATE_AVAILABLE(0), + + /** + * The FIDO Alliance has determined that this authenticator should not be trusted for any reason. + * For example if it is known to be a fraudulent product or contain a deliberate backdoor. Relying + * parties SHOULD reject any future registration of this authenticator model. + * + * @see FIDO + * Metadata Service §3.1.4. AuthenticatorStatus enum + */ + REVOKED(0), + + /** + * The authenticator vendor has completed and submitted the self-certification checklist to the + * FIDO Alliance. If this completed checklist is publicly available, the URL will be specified in + * url. + * + * @see FIDO + * Metadata Service §3.1.4. AuthenticatorStatus enum + */ + SELF_ASSERTION_SUBMITTED(0), + + /** + * The authenticator has passed FIDO Authenticator certification at level 1. This level is the + * more strict successor of {@link #FIDO_CERTIFIED}. + * + * @see FIDO + * Metadata Service §3.1.4. AuthenticatorStatus enum + */ + FIDO_CERTIFIED_L1(10), + + /** + * The authenticator has passed FIDO Authenticator certification at level 1+. This level is the + * more than level 1. + * + * @see FIDO + * Metadata Service §3.1.4. AuthenticatorStatus enum + */ + FIDO_CERTIFIED_L1plus(11), + + /** + * The authenticator has passed FIDO Authenticator certification at level 2. This level is more + * strict than level 1+. + * + * @see FIDO + * Metadata Service §3.1.4. AuthenticatorStatus enum + */ + FIDO_CERTIFIED_L2(20), + + /** + * The authenticator has passed FIDO Authenticator certification at level 2+. This level is more + * strict than level 2. + * + * @see FIDO + * Metadata Service §3.1.4. AuthenticatorStatus enum + */ + FIDO_CERTIFIED_L2plus(21), + + /** + * The authenticator has passed FIDO Authenticator certification at level 3. This level is more + * strict than level 2+. + * + * @see FIDO + * Metadata Service §3.1.4. AuthenticatorStatus enum + */ + FIDO_CERTIFIED_L3(30), + + /** + * The authenticator has passed FIDO Authenticator certification at level 3+. This level is more + * strict than level 3. + * + * @see FIDO + * Metadata Service §3.1.4. AuthenticatorStatus enum + */ + FIDO_CERTIFIED_L3plus(31); + + int certificationLevel; + + AuthenticatorStatus(int certificationLevel) { + this.certificationLevel = certificationLevel; + } +} diff --git a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/BiometricAccuracyDescriptor.java b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/BiometricAccuracyDescriptor.java new file mode 100644 index 000000000..c16f7a23a --- /dev/null +++ b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/BiometricAccuracyDescriptor.java @@ -0,0 +1,75 @@ +package com.yubico.fido.metadata; + +import java.util.Optional; +import lombok.Builder; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; + +/** + * The BiometricAccuracyDescriptor describes relevant accuracy/complexity aspects in the case of a + * biometric user verification method, see [FIDOBiometricsRequirements]. + * + *

At least one of the values MUST be set. If the vendor doesn’t want to specify such values, + * then {@link VerificationMethodDescriptor#getBaDesc()} MUST be omitted. + * + * @see FIDO + * Metadata Statement §3.3. BiometricAccuracyDescriptor dictionary + */ +@Value +@Builder(toBuilder = true) +@Jacksonized +public class BiometricAccuracyDescriptor { + + Double selfAttestedFRR; + Double selfAttestedFAR; + Integer maxTemplates; + Integer maxRetries; + Integer blockSlowdown; + + /** + * @see FIDO + * Metadata Statement §3.3. BiometricAccuracyDescriptor dictionary + */ + public Optional getSelfAttestedFRR() { + return Optional.ofNullable(selfAttestedFRR); + } + + /** + * @see FIDO + * Metadata Statement §3.3. BiometricAccuracyDescriptor dictionary + */ + public Optional getSelfAttestedFAR() { + return Optional.ofNullable(selfAttestedFAR); + } + + /** + * @see FIDO + * Metadata Statement §3.3. BiometricAccuracyDescriptor dictionary + */ + public Optional getMaxTemplates() { + return Optional.ofNullable(maxTemplates); + } + + /** + * @see FIDO + * Metadata Statement §3.3. BiometricAccuracyDescriptor dictionary + */ + public Optional getMaxRetries() { + return Optional.ofNullable(maxRetries); + } + + /** + * @see FIDO + * Metadata Statement §3.3. BiometricAccuracyDescriptor dictionary + */ + public Optional getBlockSlowdown() { + return Optional.ofNullable(blockSlowdown); + } +} diff --git a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/BiometricStatusReport.java b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/BiometricStatusReport.java new file mode 100644 index 000000000..8ff687969 --- /dev/null +++ b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/BiometricStatusReport.java @@ -0,0 +1,87 @@ +package com.yubico.fido.metadata; + +import com.yubico.webauthn.extension.uvm.UserVerificationMethod; +import java.time.LocalDate; +import java.util.Optional; +import lombok.Builder; +import lombok.NonNull; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; + +/** + * Contains the current BiometricStatusReport of one of the authenticator’s biometric component. + * + * @see FIDO + * Metadata Service §3.1.2. BiometricStatusReport dictionary + */ +@Value +@Builder(toBuilder = true) +@Jacksonized +public class BiometricStatusReport { + + /** + * @see FIDO + * Metadata Service §3.1.2. BiometricStatusReport dictionary + */ + int certLevel; + + /** + * @see FIDO + * Metadata Service §3.1.2. BiometricStatusReport dictionary + */ + @NonNull UserVerificationMethod modality; + + LocalDate effectiveDate; + String certificationDescriptor; + String certificateNumber; + String certificationPolicyVersion; + String certificationRequirementsVersion; + + /** + * @see FIDO + * Metadata Service §3.1.2. BiometricStatusReport dictionary + */ + public Optional getEffectiveDate() { + return Optional.ofNullable(effectiveDate); + } + + /** + * @see FIDO + * Metadata Service §3.1.2. BiometricStatusReport dictionary + */ + public Optional getCertificationDescriptor() { + return Optional.ofNullable(certificationDescriptor); + } + + /** + * @see FIDO + * Metadata Service §3.1.2. BiometricStatusReport dictionary + */ + public Optional getCertificateNumber() { + return Optional.ofNullable(certificateNumber); + } + + /** + * @see FIDO + * Metadata Service §3.1.2. BiometricStatusReport dictionary + */ + public Optional getCertificationPolicyVersion() { + return Optional.ofNullable(certificationPolicyVersion); + } + + /** + * @see FIDO + * Metadata Service §3.1.2. BiometricStatusReport dictionary + */ + public Optional getCertificationRequirementsVersion() { + return Optional.ofNullable(certificationRequirementsVersion); + } +} diff --git a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/CertFromBase64Converter.java b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/CertFromBase64Converter.java new file mode 100644 index 000000000..32c83e78b --- /dev/null +++ b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/CertFromBase64Converter.java @@ -0,0 +1,31 @@ +package com.yubico.fido.metadata; + +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.type.TypeFactory; +import com.fasterxml.jackson.databind.util.Converter; +import com.yubico.internal.util.CertificateParser; +import com.yubico.webauthn.data.ByteArray; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +class CertFromBase64Converter implements Converter { + @Override + public X509Certificate convert(String value) { + try { + return CertificateParser.parseDer( + ByteArray.fromBase64(value.replaceAll("\\s+", "")).getBytes()); + } catch (CertificateException e) { + throw new RuntimeException(e); + } + } + + @Override + public JavaType getInputType(TypeFactory typeFactory) { + return typeFactory.constructType(String.class); + } + + @Override + public JavaType getOutputType(TypeFactory typeFactory) { + return typeFactory.constructType(X509Certificate.class); + } +} diff --git a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/CertToBase64Converter.java b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/CertToBase64Converter.java new file mode 100644 index 000000000..a80e746e0 --- /dev/null +++ b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/CertToBase64Converter.java @@ -0,0 +1,29 @@ +package com.yubico.fido.metadata; + +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.type.TypeFactory; +import com.fasterxml.jackson.databind.util.Converter; +import com.yubico.webauthn.data.ByteArray; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; + +class CertToBase64Converter implements Converter { + @Override + public String convert(X509Certificate value) { + try { + return new ByteArray(value.getEncoded()).getBase64(); + } catch (CertificateEncodingException e) { + throw new RuntimeException(e); + } + } + + @Override + public JavaType getInputType(TypeFactory typeFactory) { + return typeFactory.constructType(X509Certificate.class); + } + + @Override + public JavaType getOutputType(TypeFactory typeFactory) { + return typeFactory.constructType(String.class); + } +} diff --git a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/CodeAccuracyDescriptor.java b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/CodeAccuracyDescriptor.java new file mode 100644 index 000000000..30908626c --- /dev/null +++ b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/CodeAccuracyDescriptor.java @@ -0,0 +1,55 @@ +package com.yubico.fido.metadata; + +import java.util.Optional; +import lombok.Builder; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; + +/** + * The CodeAccuracyDescriptor describes the relevant accuracy/complexity aspects of passcode user + * verification methods. + * + * @see FIDO + * Metadata Statement §3.2. CodeAccuracyDescriptor dictionary + */ +@Value +@Builder(toBuilder = true) +@Jacksonized +public class CodeAccuracyDescriptor { + + /** + * @see FIDO + * Metadata Statement §3.2. CodeAccuracyDescriptor dictionary + */ + int base; + + /** + * @see FIDO + * Metadata Statement §3.2. CodeAccuracyDescriptor dictionary + */ + int minLength; + + Integer maxRetries; + Integer blockSlowdown; + + /** + * @see FIDO + * Metadata Statement §3.2. CodeAccuracyDescriptor dictionary + */ + public Optional getMaxRetries() { + return Optional.ofNullable(maxRetries); + } + + /** + * @see FIDO + * Metadata Statement §3.2. CodeAccuracyDescriptor dictionary + */ + public Optional getBlockSlowdown() { + return Optional.ofNullable(blockSlowdown); + } +} diff --git a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/CtapCertificationId.java b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/CtapCertificationId.java new file mode 100644 index 000000000..0357fcb81 --- /dev/null +++ b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/CtapCertificationId.java @@ -0,0 +1,65 @@ +package com.yubico.fido.metadata; + +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * The {@link AuthenticatorGetInfo#getCertifications()} member provides a hint to the platform with + * additional information about certifications that the authenticator has received. Certification + * programs may revoke certification of specific devices at any time. Relying partys are responsible + * for validating attestations and AAGUID via appropriate methods. Platforms may alter their + * behaviour based on these hints such as selecting a PIN protocol or credProtect level. + * + * @see Client + * to Authenticator Protocol (CTAP) §7.3. Authenticator Certifications + */ +public enum CtapCertificationId { + + /** + * @see Client + * to Authenticator Protocol (CTAP) §7.3. Authenticator Certifications + */ + FIPS_CMVP_2("FIPS-CMVP-2"), + + /** + * @see Client + * to Authenticator Protocol (CTAP) §7.3. Authenticator Certifications + */ + FIPS_CMVP_3("FIPS-CMVP-3"), + + /** + * @see Client + * to Authenticator Protocol (CTAP) §7.3. Authenticator Certifications + */ + FIPS_CMVP_2_PHY("FIPS-CMVP-2-PHY"), + + /** + * @see Client + * to Authenticator Protocol (CTAP) §7.3. Authenticator Certifications + */ + FIPS_CMVP_3_PHY("FIPS-CMVP-3-PHY"), + + /** + * @see Client + * to Authenticator Protocol (CTAP) §7.3. Authenticator Certifications + */ + CC_EAL("CC-EAL"), + + /** + * @see Client + * to Authenticator Protocol (CTAP) §7.3. Authenticator Certifications + */ + FIDO("FIDO"); + + @JsonValue private String id; + + CtapCertificationId(String id) { + this.id = id; + } +} diff --git a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/CtapPinUvAuthProtocolVersion.java b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/CtapPinUvAuthProtocolVersion.java new file mode 100644 index 000000000..254c2a823 --- /dev/null +++ b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/CtapPinUvAuthProtocolVersion.java @@ -0,0 +1,37 @@ +package com.yubico.fido.metadata; + +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * Enumeration of valid PIN/UV auth protocol version identifiers. + * + * @see Client + * to Authenticator Protocol (CTAP) §6.5. authenticatorClientPIN (0x06) + */ +public enum CtapPinUvAuthProtocolVersion { + + /** + * Represents PIN/UV Auth Protocol One. + * + * @see Client + * to Authenticator Protocol (CTAP) §6.5.6. PIN/UV Auth Protocol One + */ + ONE(1), + + /** + * Represents PIN/UV Auth Protocol Two. + * + * @see Client + * to Authenticator Protocol (CTAP) §6.5.7. PIN/UV Auth Protocol Two + */ + TWO(2); + + @JsonValue private int value; + + CtapPinUvAuthProtocolVersion(int value) { + this.value = value; + } +} diff --git a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/CtapVersion.java b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/CtapVersion.java new file mode 100644 index 000000000..978beee13 --- /dev/null +++ b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/CtapVersion.java @@ -0,0 +1,39 @@ +package com.yubico.fido.metadata; + +/** + * Enumeration of CTAP versions. + * + * @see Client + * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) + */ +public enum CtapVersion { + + /** + * @see Client + * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) + */ + U2F_V2, + + /** + * @see Client + * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) + */ + FIDO_2_0, + + /** + * @see Client + * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) + */ + FIDO_2_1_PRE, + + /** + * @see Client + * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) + */ + FIDO_2_1; +} diff --git a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/DisplayPNGCharacteristicsDescriptor.java b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/DisplayPNGCharacteristicsDescriptor.java new file mode 100644 index 000000000..b1dff5599 --- /dev/null +++ b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/DisplayPNGCharacteristicsDescriptor.java @@ -0,0 +1,78 @@ +package com.yubico.fido.metadata; + +import java.util.List; +import lombok.Builder; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; + +/** + * The DisplayPNGCharacteristicsDescriptor describes a PNG image characteristics as defined in the + * PNG [PNG] + * spec for IHDR (image header) and PLTE (palette table). + * + * @see FIDO + * Metadata Statement §3.8. DisplayPNGCharacteristicsDescriptor dictionary + */ +@Value +@Builder(toBuilder = true) +@Jacksonized +public class DisplayPNGCharacteristicsDescriptor { + + /** + * @see FIDO + * Metadata Statement §3.8. DisplayPNGCharacteristicsDescriptor dictionary + */ + long width; + + /** + * @see FIDO + * Metadata Statement §3.8. DisplayPNGCharacteristicsDescriptor dictionary + */ + long height; + + /** + * @see FIDO + * Metadata Statement §3.8. DisplayPNGCharacteristicsDescriptor dictionary + */ + short bitDepth; + + /** + * @see FIDO + * Metadata Statement §3.8. DisplayPNGCharacteristicsDescriptor dictionary + */ + short colorType; + + /** + * @see FIDO + * Metadata Statement §3.8. DisplayPNGCharacteristicsDescriptor dictionary + */ + short compression; + + /** + * @see FIDO + * Metadata Statement §3.8. DisplayPNGCharacteristicsDescriptor dictionary + */ + short filter; + + /** + * @see FIDO + * Metadata Statement §3.8. DisplayPNGCharacteristicsDescriptor dictionary + */ + short interlace; + + /** + * @see FIDO + * Metadata Statement §3.8. DisplayPNGCharacteristicsDescriptor dictionary + */ + List plte; +} diff --git a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/ExtensionDescriptor.java b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/ExtensionDescriptor.java new file mode 100644 index 000000000..e2efdab4f --- /dev/null +++ b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/ExtensionDescriptor.java @@ -0,0 +1,49 @@ +package com.yubico.fido.metadata; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.NonNull; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; + +/** + * This descriptor contains an extension supported by the authenticator. + * + * @see FIDO + * Metadata Statement §3.10. ExtensionDescriptor dictionary + */ +@Value +@Builder(toBuilder = true) +@Jacksonized +public class ExtensionDescriptor { + + /** + * @see FIDO + * Metadata Statement §3.10. ExtensionDescriptor dictionary + */ + @NonNull String id; + + /** + * @see FIDO + * Metadata Statement §3.10. ExtensionDescriptor dictionary + */ + Integer tag; + + /** + * @see FIDO + * Metadata Statement §3.10. ExtensionDescriptor dictionary + */ + String data; + + /** + * @see FIDO + * Metadata Statement §3.10. ExtensionDescriptor dictionary + */ + @JsonProperty("fail_if_unknown") + boolean failIfUnknown; +} diff --git a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/FidoMetadataDownloader.java b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/FidoMetadataDownloader.java new file mode 100644 index 000000000..d9261251c --- /dev/null +++ b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/FidoMetadataDownloader.java @@ -0,0 +1,991 @@ +// Copyright (c) 2015-2021, Yubico AB +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package com.yubico.fido.metadata; + +import com.fasterxml.jackson.core.Base64Variants; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.yubico.fido.metadata.FidoMetadataDownloaderException.Reason; +import com.yubico.internal.util.BinaryUtil; +import com.yubico.internal.util.CertificateParser; +import com.yubico.webauthn.data.ByteArray; +import com.yubico.webauthn.data.exception.Base64UrlException; +import com.yubico.webauthn.data.exception.HexException; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLConnection; +import java.nio.charset.StandardCharsets; +import java.security.DigestException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.Signature; +import java.security.SignatureException; +import java.security.cert.CRL; +import java.security.cert.CertPath; +import java.security.cert.CertPathValidator; +import java.security.cert.CertPathValidatorException; +import java.security.cert.CertStore; +import java.security.cert.CertStoreParameters; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.CollectionCertStoreParameters; +import java.security.cert.PKIXParameters; +import java.security.cert.TrustAnchor; +import java.security.cert.X509Certificate; +import java.time.Clock; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Optional; +import java.util.Scanner; +import java.util.Set; +import java.util.UUID; +import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManagerFactory; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +/** + * Utility for downloading, caching and verifying Fido Metadata Service BLOBs and associated + * certificates. + * + *

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

Use the {@link #builder() builder} to configure settings, then use the {@link + * #loadCachedBlob()} method to load the metadata BLOB. + */ +@Slf4j +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public final class FidoMetadataDownloader { + + @NonNull private final Set expectedLegalHeaders; + private final X509Certificate trustRootCertificate; + private final URL trustRootUrl; + private final Set trustRootSha256; + private final File trustRootCacheFile; + private final Supplier> trustRootCacheSupplier; + private final Consumer trustRootCacheConsumer; + private final String blobJwt; + private final URL blobUrl; + private final File blobCacheFile; + private final Supplier> blobCacheSupplier; + private final Consumer blobCacheConsumer; + private final CertStore certStore; + @NonNull private final Clock clock; + private final KeyStore httpsTrustStore; + + /** + * Begin configuring a {@link FidoMetadataDownloader} instance. See the {@link + * FidoMetadataDownloaderBuilder.Step1 Step1} type. + * + * @see FidoMetadataDownloaderBuilder.Step1 + */ + public static FidoMetadataDownloaderBuilder.Step1 builder() { + return new FidoMetadataDownloaderBuilder.Step1(); + } + + @RequiredArgsConstructor(access = AccessLevel.PRIVATE) + public static class FidoMetadataDownloaderBuilder { + @NonNull private final Set expectedLegalHeaders; + private final X509Certificate trustRootCertificate; + private final URL trustRootUrl; + private final Set trustRootSha256; + private final File trustRootCacheFile; + private final Supplier> trustRootCacheSupplier; + private final Consumer trustRootCacheConsumer; + private final String blobJwt; + private final URL blobUrl; + private final File blobCacheFile; + private final Supplier> blobCacheSupplier; + private final Consumer blobCacheConsumer; + + private CertStore certStore = null; + @NonNull private Clock clock = Clock.systemUTC(); + private KeyStore httpsTrustStore = null; + + public FidoMetadataDownloader build() { + return new FidoMetadataDownloader( + expectedLegalHeaders, + trustRootCertificate, + trustRootUrl, + trustRootSha256, + trustRootCacheFile, + trustRootCacheSupplier, + trustRootCacheConsumer, + blobJwt, + blobUrl, + blobCacheFile, + blobCacheSupplier, + blobCacheConsumer, + certStore, + clock, + httpsTrustStore); + } + + /** + * Step 1: Set the legal header to expect from the FIDO Metadata Service. + * + *

By using the FIDO Metadata Service, you will be subject to its terms of service. This step + * serves two purposes: + * + *

    + *
  1. To remind you and any code reviewers that you need to read those terms of service + * before using this feature. + *
  2. To help you detect if the legal header changes, so you can take appropriate action. + *
+ * + *

See {@link Step1#expectLegalHeader(String...)}. + * + * @see Step1#expectLegalHeader(String...) + */ + @AllArgsConstructor(access = AccessLevel.PRIVATE) + public static class Step1 { + + /** + * Set legal headers expected in the metadata BLOB. + * + *

By using the FIDO Metadata Service, you will be subject to its terms of service. This + * builder step serves two purposes: + * + *

    + *
  1. To remind you and any code reviewers that you need to read those terms of service + * before using this feature. + *
  2. To help you detect if the legal header changes, so you can take appropriate action. + *
+ * + *

If the legal header in the downloaded BLOB does not equal any of the + * expectedLegalHeaders, an {@link UnexpectedLegalHeader} exception will be thrown in + * the finalizing builder step. + * + *

Note that this library makes no guarantee that a change to the FIDO Metadata Service + * terms of service will also cause a change to the legal header in the BLOB. + * + *

At the time of this library release, the current legal header is + * "Retrieval and use of this BLOB indicates acceptance of the appropriate agreement located at https://fidoalliance.org/metadata/metadata-legal-terms/" + * . + * + * @param expectedLegalHeaders the set of BLOB legal headers you expect in the metadata BLOB + * payload. + */ + public Step2 expectLegalHeader(@NonNull String... expectedLegalHeaders) { + return new Step2(Stream.of(expectedLegalHeaders).collect(Collectors.toSet())); + } + } + + /** + * Step 2: Configure how to retrieve the FIDO Metadata Service trust root certificate when + * necessary. + * + *

This step offers three mutually exclusive options: + * + *

    + *
  1. Use the default download URL and certificate hash. This is the main intended use case. + * See {@link #useDefaultTrustRoot()}. + *
  2. Use a custom download URL and certificate hash. This is for future-proofing in case the + * trust root certificate changes and there is no new release of this library. See {@link + * #downloadTrustRoot(URL, Set)}. + *
  3. Use a pre-retrieved trust root certificate. It is up to you to perform any integrity + * checks and cache it as desired. See {@link #useTrustRoot(X509Certificate)}. + *
+ */ + @AllArgsConstructor(access = AccessLevel.PRIVATE) + public static class Step2 { + + @NonNull private final Set expectedLegalHeaders; + + /** + * Download the trust root certificate from a hard-coded URL and verify it against a + * hard-coded SHA-256 hash. + * + *

This is an alias of: + * + *

+       * downloadTrustRoot(
+       *   new URL("https://secure.globalsign.com/cacert/root-r3.crt"),
+       *   Collections.singleton(ByteArray.fromHex("cbb522d7b7f127ad6a0113865bdf1cd4102e7d0759af635a7cf4720dc963c53b"))
+       * )
+       * 
+ * + * This is the current FIDO Metadata Service trust root certificate at the time of this + * library release. + * + * @see #downloadTrustRoot(URL, Set) + */ + public Step3 useDefaultTrustRoot() { + try { + return downloadTrustRoot( + new URL("https://secure.globalsign.com/cacert/root-r3.crt"), + Collections.singleton( + ByteArray.fromHex( + "cbb522d7b7f127ad6a0113865bdf1cd4102e7d0759af635a7cf4720dc963c53b"))); + } catch (MalformedURLException e) { + throw new RuntimeException( + "Bad hard-coded trust root certificate URL. Please file a bug report.", e); + } catch (HexException e) { + throw new RuntimeException( + "Bad hard-coded trust root certificate hash. Please file a bug report.", e); + } + } + + /** + * Download the trust root certificate from the given HTTPS url and verify its + * SHA-256 hash against acceptedCertSha256. + * + *

The certificate will be downloaded if it does not exist in the cache, or if the cached + * certificate is not currently valid. + * + *

If the cert is downloaded, it is also written to the cache {@link File} or {@link + * Consumer} configured in the {@link Step3 next step}. + * + * @param url the HTTP URL to download. It MUST use the https: scheme. + * @param acceptedCertSha256 a set of SHA-256 hashes to verify the downloaded certificate + * against. The downloaded certificate MUST match at least one of these hashes. + * @throws IllegalArgumentException if url is not a HTTPS URL. + */ + public Step3 downloadTrustRoot(@NonNull URL url, @NonNull Set acceptedCertSha256) { + if (!"https".equals(url.getProtocol())) { + throw new IllegalArgumentException("Trust certificate download URL must be a HTTPS URL."); + } + return new Step3(this, null, url, acceptedCertSha256); + } + + /** + * Use the given trust root certificate. It is the caller's responsibility to perform any + * integrity checks and/or caching logic. + * + * @param trustRootCertificate the certificate to use as the FIDO Metadata Service trust root. + */ + public Step4 useTrustRoot(@NonNull X509Certificate trustRootCertificate) { + return new Step4(new Step3(this, trustRootCertificate, null, null), null, null, null); + } + } + + /** + * Step 3: Configure how to cache the trust root certificate. + * + *

This step offers two mutually exclusive options: + * + *

    + *
  1. Cache the trust root certificate in a {@link File}. See {@link + * Step3#useTrustRootCacheFile(File)}. + *
  2. Cache the trust root certificate using a {@link Supplier} to read the cache and a + * {@link Consumer} to write the cache. See {@link Step3#useTrustRootCache(Supplier, + * Consumer)}. + *
+ */ + @AllArgsConstructor(access = AccessLevel.PRIVATE) + public static class Step3 { + @NonNull private final Step2 step2; + private final X509Certificate trustRootCertificate; + private final URL trustRootUrl; + private final Set trustRootSha256; + + /** + * Cache the trust root certificate in the file cacheFile. + * + *

If cacheFile exists, is a normal file, is readable, matches one of the + * SHA-256 hashes configured in the previous step, and contains a currently valid X.509 + * certificate, then it will be used as the trust root for the FIDO Metadata Service blob. + * + *

Otherwise, the trust root certificate will be downloaded and written to this file. + */ + public Step4 useTrustRootCacheFile(@NonNull File cacheFile) { + return new Step4(this, cacheFile, null, null); + } + + /** + * Cache the trust root certificate using a {@link Supplier} to read the cache, and using a + * {@link Consumer} to write the cache. + * + *

If getCachedTrustRootCert returns non-empty, the value matches one of the + * SHA-256 hashes configured in the previous step, and is a currently valid X.509 certificate, + * then it will be used as the trust root for the FIDO Metadata Service blob. + * + *

Otherwise, the trust root certificate will be downloaded and written to + * writeCachedTrustRootCert. + * + * @param getCachedTrustRootCert a {@link Supplier} that fetches the cached trust root + * certificate if it exists. The returned value, if any, should be the trust root + * certificate in X.509 DER format. + * @param writeCachedTrustRootCert a {@link Consumer} that accepts the trust root certificate + * in X.509 DER format and writes it to the cache. + */ + public Step4 useTrustRootCache( + @NonNull Supplier> getCachedTrustRootCert, + @NonNull Consumer writeCachedTrustRootCert) { + return new Step4(this, null, getCachedTrustRootCert, writeCachedTrustRootCert); + } + } + + /** + * Step 4: Configure how to fetch the FIDO Metadata Service metadata BLOB. + * + *

This step offers three mutually exclusive options: + * + *

    + *
  1. Use the default download URL. This is the main intended use case. See {@link + * #useDefaultBlob()}. + *
  2. Use a custom download URL. This is for future-proofing in case the BLOB download URL + * changes and there is no new release of this library. See {@link #downloadBlob(URL)}. + *
  3. Use a pre-retrieved BLOB. The signature will still be verified, but it is up to you to + * renew it when appropriate and perform any caching as desired. See {@link + * #useBlob(String)}. + *
+ */ + @AllArgsConstructor(access = AccessLevel.PRIVATE) + public static class Step4 { + @NonNull private final Step3 step3; + private final File trustRootCacheFile; + private final Supplier> trustRootCacheSupplier; + private final Consumer trustRootCacheConsumer; + + /** + * Download the metadata BLOB from a hard-coded URL. + * + *

This is an alias of downloadBlob(new URL("https://mds.fidoalliance.org/")). + * + *

This is the current FIDO Metadata Service BLOB download URL at the time of this library + * release. + * + * @see #downloadBlob(URL) + */ + public Step5 useDefaultBlob() { + try { + return downloadBlob(new URL("https://mds.fidoalliance.org/")); + } catch (MalformedURLException e) { + throw new RuntimeException( + "Bad hard-coded trust root certificate URL. Please file a bug report.", e); + } + } + + /** + * Download the metadata BLOB from the given HTTPS url. + * + *

The BLOB will be downloaded if it does not exist in the cache, or if the + * nextUpdate property of the cached BLOB is the current date or earlier. + * + *

If the BLOB is downloaded, it is also written to the cache {@link File} or {@link + * Consumer} configured in the previous step. + * + * @param url the HTTP URL to download. It MUST use the https: scheme. + */ + public Step5 downloadBlob(@NonNull URL url) { + return new Step5(this, null, url); + } + + /** + * Use the given metadata BLOB; never download it. + * + *

The blob signature and trust chain will still be verified, but it is the caller's + * responsibility to renew the metadata BLOB according to the FIDO + * Metadata Service specification. + * + * @param blobJwt the Metadata BLOB in JWT format as defined in FIDO + * Metadata Service §3.1.7. Metadata BLOB. The byte array should not be + * Base64-decoded. + * @see FIDO + * Metadata Service §3.1.7. Metadata BLOB + * @see FIDO + * Metadata Service §3.2. Metadata BLOB object processing rules + */ + public FidoMetadataDownloaderBuilder useBlob(@NonNull String blobJwt) { + return finishRequiredSteps(new Step5(this, blobJwt, null), null, null, null); + } + } + + /** + * Step 5: Configure how to cache the metadata BLOB. + * + *

This step offers two mutually exclusive options: + * + *

    + *
  1. Cache the metadata BLOB in a {@link File}. See {@link Step5#useBlobCacheFile(File)}. + *
  2. Cache the metadata BLOB using a {@link Supplier} to read the cache and a {@link + * Consumer} to write the cache. See {@link Step5#useBlobCache(Supplier, Consumer)}. + *
+ */ + @AllArgsConstructor(access = AccessLevel.PRIVATE) + public static class Step5 { + @NonNull private final Step4 step4; + private final String blobJwt; + private final URL blobUrl; + + /** + * Cache metadata BLOB in the file cacheFile. + * + *

If cacheFile exists, is a normal file, is readable, and is not out of date, + * then it will be used as the FIDO Metadata Service BLOB. + * + *

Otherwise, the metadata BLOB will be downloaded and written to this file. + * + * @param cacheFile a {@link File} which may or may not exist. If it exists, it should contain + * the metadata BLOB in JWS compact serialization format [RFC7515]. + */ + public FidoMetadataDownloaderBuilder useBlobCacheFile(@NonNull File cacheFile) { + return finishRequiredSteps(this, cacheFile, null, null); + } + + /** + * Cache the metadata BLOB using a {@link Supplier} to read the cache, and using a {@link + * Consumer} to write the cache. + * + *

If getCachedBlob returns non-empty and the content is not out of date, then + * it will be used as the FIDO Metadata Service BLOB. + * + *

Otherwise, the metadata BLOB will be downloaded and written to writeCachedBlob + * . + * + * @param getCachedBlob a {@link Supplier} that fetches the cached metadata BLOB if it exists. + * The returned value, if any, should be in JWS compact serialization format [RFC7515]. + * @param writeCachedBlob a {@link Consumer} that accepts the metadata BLOB in JWS compact + * serialization format [RFC7515] and + * writes it to the cache. + */ + public FidoMetadataDownloaderBuilder useBlobCache( + @NonNull Supplier> getCachedBlob, + @NonNull Consumer writeCachedBlob) { + return finishRequiredSteps(this, null, getCachedBlob, writeCachedBlob); + } + } + + private static FidoMetadataDownloaderBuilder finishRequiredSteps( + FidoMetadataDownloaderBuilder.Step5 step5, + File blobCacheFile, + Supplier> blobCacheSupplier, + Consumer blobCacheConsumer) { + return new FidoMetadataDownloaderBuilder( + step5.step4.step3.step2.expectedLegalHeaders, + step5.step4.step3.trustRootCertificate, + step5.step4.step3.trustRootUrl, + step5.step4.step3.trustRootSha256, + step5.step4.trustRootCacheFile, + step5.step4.trustRootCacheSupplier, + step5.step4.trustRootCacheConsumer, + step5.blobJwt, + step5.blobUrl, + blobCacheFile, + blobCacheSupplier, + blobCacheConsumer); + } + + /** + * Use clock as the source of the current time for some application-level logic. + * + *

This is primarily intended for testing. + * + *

The default is {@link Clock#systemUTC()}. + * + * @param clock a {@link Clock} which the finished {@link FidoMetadataDownloader} will use to + * tell the time. + */ + public FidoMetadataDownloaderBuilder clock(@NonNull Clock clock) { + this.clock = clock; + return this; + } + + /** + * Use the provided CRLs. + * + *

CRLs will also be downloaded from distribution points if the + * com.sun.security.enableCRLDP system property is set to true (assuming the + * use of the {@link CertPathValidator} implementation from the SUN provider). + * + * @throws InvalidAlgorithmParameterException if {@link CertStore#getInstance(String, + * CertStoreParameters)} does. + * @throws NoSuchAlgorithmException if a "Collection" type {@link CertStore} + * provider is not available. + * @see #useCrls(CertStore) + */ + public FidoMetadataDownloaderBuilder useCrls(@NonNull Collection crls) + throws InvalidAlgorithmParameterException, NoSuchAlgorithmException { + return useCrls(CertStore.getInstance("Collection", new CollectionCertStoreParameters(crls))); + } + + /** + * Use CRLs in the provided {@link CertStore}. + * + *

CRLs will also be downloaded from distribution points if the + * com.sun.security.enableCRLDP system property is set to true (assuming the + * use of the {@link CertPathValidator} implementation from the SUN provider). + * + * @see #useCrls(Collection) + */ + public FidoMetadataDownloaderBuilder useCrls(CertStore certStore) { + this.certStore = certStore; + return this; + } + + /** + * Use the provided {@link X509Certificate}s as trust roots for HTTPS downloads. + * + *

This is primarily useful when setting {@link Step2#downloadTrustRoot(URL, Set) + * downloadTrustRoot} and/or {@link Step4#downloadBlob(URL) downloadBlob} to download from + * custom servers instead of the defaults. + * + *

If provided, these will be used for downloading + * + *

    + *
  • the trust root certificate for the BLOB signature chain, and + *
  • the metadata BLOB. + *
+ * + * If not set, the system default certificate store will be used. + */ + public FidoMetadataDownloaderBuilder trustHttpsCerts(@NonNull X509Certificate... certificates) { + final KeyStore trustStore; + try { + trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); + trustStore.load(null); + } catch (KeyStoreException + | IOException + | NoSuchAlgorithmException + | CertificateException e) { + throw new RuntimeException( + "Failed to instantiate or initialize KeyStore. This should not be possible, please file a bug report.", + e); + } + for (X509Certificate cert : certificates) { + try { + trustStore.setCertificateEntry(UUID.randomUUID().toString(), cert); + } catch (KeyStoreException e) { + throw new RuntimeException( + "Failed to import HTTPS cert into KeyStore. This should not be possible, please file a bug report.", + e); + } + } + this.httpsTrustStore = trustStore; + + return this; + } + } + + /** + * Load the metadata BLOB from cache, or download a fresh one if necessary. + * + *

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

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

    + *
  1. Download the trust root certificate, if necessary: if the cache is empty, the cache fails + * to load, or the cached cert is not valid at the current time (as determined by the {@link + * FidoMetadataDownloaderBuilder#clock(Clock) clock} setting). + *
  2. If downloaded, cache the trust root certificate using the configured {@link File} or + * {@link Consumer} (see {@link FidoMetadataDownloaderBuilder.Step3}) + *
  3. Download the metadata BLOB, if necessary: if the cache is empty, the cache fails to load, + * or the "nextUpdate" property in the cached BLOB is the current date (as + * determined by the {@link FidoMetadataDownloaderBuilder#clock(Clock) clock} setting) or + * earlier. + *
  4. Check the "no" property of the downloaded BLOB, if any, and compare it with + * the "no" of the cached BLOB, if any. The one with a greater "no" + * overrides the other, even if its "nextUpdate" is in the past. + *
  5. If a BLOB with a newer "no" was downloaded, verify that the value of its + * "legalHeader" appears in the configured {@link + * FidoMetadataDownloaderBuilder.Step1#expectLegalHeader(String...) expectLegalHeader} + * setting. If not, throw an {@link UnexpectedLegalHeader} exception containing the cached + * BLOB, if any, and the downloaded BLOB. + *
  6. If a BLOB with a newer "no" was downloaded and had an expected + * "legalHeader", cache the new BLOB using the configured {@link File} or {@link + * Consumer} (see {@link FidoMetadataDownloaderBuilder.Step5}). + *
+ * + * No internal mutable state is maintained between invocations of loadBlob(); each + * invocation will reload/rewrite caches, perform downloads and check the "legalHeader" + * as necessary. You may therefore reuse a {@link FidoMetadataDownloader} instance and, + * for example, call loadBlob() periodically to refresh the BLOB when appropriate. + * Each call will return a new {@link MetadataBLOB} instance; ones already returned will not be + * updated by subsequent loadBlob() calls. + * + * @return the successfully retrieved and validated metadata BLOB. + * @throws Base64UrlException if the metadata BLOB is not a well-formed JWT in compact + * serialization. + * @throws CertPathValidatorException if the downloaded or explicitly configured BLOB fails + * certificate path validation. + * @throws CertificateException if the trust root certificate was downloaded and passed the + * SHA-256 integrity check, but does not contain a currently valid X.509 DER certificate; or + * if the BLOB signing certificate chain fails to parse. + * @throws DigestException if the trust root certificate was downloaded but failed the SHA-256 + * integrity check. + * @throws FidoMetadataDownloaderException if the explicitly configured BLOB (if any) has a bad + * signature. + * @throws IOException if any of the following fails: downloading the trust root certificate, + * downloading the BLOB, reading or writing any cache file (if any), or parsing the BLOB + * contents. + * @throws InvalidAlgorithmParameterException if certificate path validation fails. + * @throws InvalidKeyException if signature verification fails. + * @throws NoSuchAlgorithmException if signature verification fails, or if the SHA-256 algorithm + * is not available. + * @throws SignatureException if signature verification fails. + * @throws UnexpectedLegalHeader if the downloaded BLOB (if any) contains a "legalHeader" + * value not configured in {@link + * FidoMetadataDownloaderBuilder.Step1#expectLegalHeader(String...) + * expectLegalHeader(String...)} but is otherwise valid. The downloaded BLOB will not be + * written to cache in this case. + */ + public MetadataBLOB loadCachedBlob() + throws CertPathValidatorException, InvalidAlgorithmParameterException, Base64UrlException, + CertificateException, IOException, NoSuchAlgorithmException, SignatureException, + InvalidKeyException, UnexpectedLegalHeader, DigestException, + FidoMetadataDownloaderException { + X509Certificate trustRoot = retrieveTrustRootCert(); + return retrieveBlob(trustRoot); + } + + /** + * @throws CertificateException if the trust root certificate was downloaded and passed the + * SHA-256 integrity check, but does not contain a currently valid X.509 DER certificate. + * @throws DigestException if the trust root certificate was downloaded but failed the SHA-256 + * integrity check. + * @throws IOException if the trust root certificate download failed, or if reading or writing the + * cache file (if any) failed. + * @throws NoSuchAlgorithmException if the SHA-256 algorithm is not available. + */ + private X509Certificate retrieveTrustRootCert() + throws CertificateException, DigestException, IOException, NoSuchAlgorithmException { + + if (trustRootCertificate != null) { + return trustRootCertificate; + + } else { + final Optional cachedContents; + if (trustRootCacheFile != null) { + cachedContents = readCacheFile(trustRootCacheFile); + } else { + cachedContents = trustRootCacheSupplier.get(); + } + + X509Certificate cert = null; + if (cachedContents.isPresent()) { + try { + final X509Certificate cachedCert = + CertificateParser.parseDer(cachedContents.get().getBytes()); + cachedCert.checkValidity(Date.from(clock.instant())); + cert = cachedCert; + } catch (CertificateException e) { + // Fall through + } + } + + if (cert == null) { + final ByteArray downloaded = verifyHash(download(trustRootUrl), trustRootSha256); + if (downloaded == null) { + throw new DigestException( + "Downloaded trust root certificate matches none of the acceptable hashes."); + } + + cert = CertificateParser.parseDer(downloaded.getBytes()); + cert.checkValidity(Date.from(clock.instant())); + + if (trustRootCacheFile != null) { + new FileOutputStream(trustRootCacheFile).write(downloaded.getBytes()); + } + + if (trustRootCacheConsumer != null) { + trustRootCacheConsumer.accept(downloaded); + } + } + + return cert; + } + } + + /** + * @throws Base64UrlException if the metadata BLOB is not a well-formed JWT in compact + * serialization. + * @throws CertPathValidatorException if the downloaded or explicitly configured BLOB fails + * certificate path validation. + * @throws CertificateException if the BLOB signing certificate chain fails to parse. + * @throws IOException if any of the following fails: downloading the BLOB, reading or writing the + * cache file (if any), or parsing the BLOB contents. + * @throws InvalidAlgorithmParameterException if certificate path validation fails. + * @throws InvalidKeyException if signature verification fails. + * @throws UnexpectedLegalHeader if the downloaded BLOB (if any) contains a "legalHeader" + * value not configured in {@link + * FidoMetadataDownloaderBuilder.Step1#expectLegalHeader(String...) + * expectLegalHeader(String...)} but is otherwise valid. The downloaded BLOB will not be + * written to cache in this case. + * @throws NoSuchAlgorithmException if signature verification fails. + * @throws SignatureException if signature verification fails. + * @throws FidoMetadataDownloaderException if the explicitly configured BLOB (if any) has a bad + * signature. + */ + private MetadataBLOB retrieveBlob(X509Certificate trustRootCertificate) + throws Base64UrlException, CertPathValidatorException, CertificateException, IOException, + InvalidAlgorithmParameterException, InvalidKeyException, UnexpectedLegalHeader, + NoSuchAlgorithmException, SignatureException, FidoMetadataDownloaderException { + if (blobJwt != null) { + return parseAndVerifyBlob( + new ByteArray(blobJwt.getBytes(StandardCharsets.UTF_8)), trustRootCertificate); + + } else { + + final Optional cachedContents; + if (blobCacheFile != null) { + cachedContents = readCacheFile(blobCacheFile); + } else { + cachedContents = blobCacheSupplier.get(); + } + + final MetadataBLOB cachedBlob = + cachedContents + .map( + cached -> { + try { + return parseAndVerifyBlob(cached, trustRootCertificate); + } catch (Exception e) { + return null; + } + }) + .orElse(null); + + if (cachedBlob != null + && cachedBlob + .getPayload() + .getNextUpdate() + .atStartOfDay() + .atZone(clock.getZone()) + .isAfter(clock.instant().atZone(clock.getZone()))) { + return cachedBlob; + + } else { + final ByteArray downloaded = download(blobUrl); + try { + final MetadataBLOB downloadedBlob = parseAndVerifyBlob(downloaded, trustRootCertificate); + + if (cachedBlob == null + || downloadedBlob.getPayload().getNo() > cachedBlob.getPayload().getNo()) { + if (expectedLegalHeaders.contains(downloadedBlob.getPayload().getLegalHeader())) { + if (blobCacheFile != null) { + new FileOutputStream(blobCacheFile).write(downloaded.getBytes()); + } + + if (blobCacheConsumer != null) { + blobCacheConsumer.accept(downloaded); + } + + return downloadedBlob; + } else { + throw new UnexpectedLegalHeader(cachedBlob, downloadedBlob); + } + + } else { + return cachedBlob; + } + } catch (FidoMetadataDownloaderException e) { + if (e.getReason() == FidoMetadataDownloaderException.Reason.BAD_SIGNATURE + && cachedBlob != null) { + return cachedBlob; + } else { + throw e; + } + } + } + } + } + + private Optional readCacheFile(File cacheFile) throws IOException { + if (cacheFile.exists() && cacheFile.canRead() && cacheFile.isFile()) { + try { + return Optional.of(readAll(new FileInputStream(cacheFile))); + } catch (FileNotFoundException e) { + throw new RuntimeException( + "This exception should be impossible, please file a bug report.", e); + } + } else { + return Optional.empty(); + } + } + + private ByteArray download(URL url) throws IOException { + URLConnection conn = url.openConnection(); + + if (conn instanceof HttpsURLConnection) { + HttpsURLConnection httpsConn = (HttpsURLConnection) conn; + if (httpsTrustStore != null) { + try { + TrustManagerFactory trustMan = + TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustMan.init(httpsTrustStore); + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, trustMan.getTrustManagers(), null); + + httpsConn.setSSLSocketFactory(sslContext.getSocketFactory()); + } catch (NoSuchAlgorithmException | KeyStoreException | KeyManagementException e) { + throw new RuntimeException( + "Failed to initialize HTTPS trust store. This should be impossible, please file a bug report.", + e); + } + } + httpsConn.setRequestMethod("GET"); + } + + return readAll(conn.getInputStream()); + } + + private MetadataBLOB parseAndVerifyBlob(ByteArray jwt, X509Certificate trustRootCertificate) + throws CertPathValidatorException, InvalidAlgorithmParameterException, CertificateException, + IOException, NoSuchAlgorithmException, SignatureException, InvalidKeyException, + Base64UrlException, FidoMetadataDownloaderException { + Scanner s = new Scanner(new ByteArrayInputStream(jwt.getBytes())).useDelimiter("\\."); + final ByteArray header = ByteArray.fromBase64Url(s.next()); + final ByteArray payload = ByteArray.fromBase64Url(s.next()); + final ByteArray signature = ByteArray.fromBase64Url(s.next()); + return verifyBlob(header, payload, signature, trustRootCertificate); + } + + private MetadataBLOB verifyBlob( + ByteArray jwtHeader, + ByteArray jwtPayload, + ByteArray jwtSignature, + X509Certificate trustRootCertificate) + throws IOException, CertificateException, NoSuchAlgorithmException, InvalidKeyException, + SignatureException, CertPathValidatorException, InvalidAlgorithmParameterException, + FidoMetadataDownloaderException { + final ObjectMapper headerJsonMapper = + com.yubico.internal.util.JacksonCodecs.json() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true) + .setBase64Variant(Base64Variants.MIME_NO_LINEFEEDS); + final MetadataBLOBHeader header = + headerJsonMapper.readValue(jwtHeader.getBytes(), MetadataBLOBHeader.class); + + final List certChain; + if (header.getX5u().isPresent()) { + final URL x5u = header.getX5u().get(); + if (blobUrl != null + && (!(x5u.getHost().equals(blobUrl.getHost()) + && x5u.getProtocol().equals(blobUrl.getProtocol()) + && x5u.getPort() == blobUrl.getPort()))) { + throw new IllegalArgumentException( + String.format( + "x5u in BLOB header must have same origin as the URL the BLOB was downloaded from. Expected origin of: %s ; found: %s", + blobUrl, x5u)); + } + List certs = new ArrayList<>(); + for (String pem : + new String(download(x5u).getBytes(), StandardCharsets.UTF_8) + .trim() + .split("\\n+-----END CERTIFICATE-----\\n+-----BEGIN CERTIFICATE-----\\n+")) { + X509Certificate x509Certificate = CertificateParser.parsePem(pem); + certs.add(x509Certificate); + } + certChain = certs; + } else if (header.getX5c().isPresent()) { + certChain = header.getX5c().get(); + } else { + certChain = Collections.singletonList(trustRootCertificate); + } + + final X509Certificate leafCert = certChain.get(0); + + final Signature signature; + switch (header.getAlg()) { + case "RS256": + signature = Signature.getInstance("SHA256withRSA"); + break; + + case "ES256": + signature = Signature.getInstance("SHA256withECDSA"); + break; + + default: + throw new UnsupportedOperationException( + "Unimplemented JWT verification algorithm: " + header.getAlg()); + } + + signature.initVerify(leafCert.getPublicKey()); + signature.update( + (jwtHeader.getBase64Url() + "." + jwtPayload.getBase64Url()) + .getBytes(StandardCharsets.UTF_8)); + if (!signature.verify(jwtSignature.getBytes())) { + throw new FidoMetadataDownloaderException(Reason.BAD_SIGNATURE); + } + + final CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); + final CertPathValidator cpv = CertPathValidator.getInstance("PKIX"); + final CertPath blobCertPath = certFactory.generateCertPath(certChain); + final PKIXParameters pathParams = + new PKIXParameters(Collections.singleton(new TrustAnchor(trustRootCertificate, null))); + if (certStore != null) { + pathParams.addCertStore(certStore); + } + pathParams.setDate(Date.from(clock.instant())); + cpv.validate(blobCertPath, pathParams); + + return new MetadataBLOB( + header, + JacksonCodecs.jsonWithDefaultEnums() + .readValue(jwtPayload.getBytes(), MetadataBLOBPayload.class)); + } + + private static ByteArray readAll(InputStream is) throws IOException { + return new ByteArray(BinaryUtil.readAll(is)); + } + + /** + * @return contents if its SHA-256 hash matches any element of + * acceptedCertSha256, otherwise null. + */ + private static ByteArray verifyHash(ByteArray contents, Set acceptedCertSha256) + throws NoSuchAlgorithmException { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + final ByteArray hash = new ByteArray(digest.digest(contents.getBytes())); + if (acceptedCertSha256.stream().anyMatch(acceptableHash -> acceptableHash.equals(hash))) { + return contents; + } else { + return null; + } + } +} 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 new file mode 100644 index 000000000..59f7d3711 --- /dev/null +++ b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/FidoMetadataDownloaderException.java @@ -0,0 +1,39 @@ +package com.yubico.fido.metadata; + +import lombok.NonNull; +import lombok.Value; + +@Value +public class FidoMetadataDownloaderException extends Exception { + + public enum Reason { + BAD_SIGNATURE("Bad JWT signature."); + + private final String message; + + Reason(String message) { + this.message = message; + } + } + + @NonNull + /** 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; + + FidoMetadataDownloaderException(Reason reason, Throwable cause) { + this.reason = reason; + this.cause = cause; + } + + FidoMetadataDownloaderException(Reason reason) { + this(reason, null); + } + + @Override + public String getMessage() { + return reason.message; + } +} 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 new file mode 100644 index 000000000..388c515d7 --- /dev/null +++ b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/FidoMetadataService.java @@ -0,0 +1,619 @@ +// Copyright (c) 2015-2021, Yubico AB +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package com.yubico.fido.metadata; + +import com.yubico.fido.metadata.FidoMetadataService.Filters.AuthenticatorToBeFiltered; +import com.yubico.internal.util.CertificateParser; +import com.yubico.webauthn.RegistrationResult; +import com.yubico.webauthn.RelyingParty; +import com.yubico.webauthn.RelyingParty.RelyingPartyBuilder; +import com.yubico.webauthn.attestation.AttestationTrustSource; +import com.yubico.webauthn.data.ByteArray; +import com.yubico.webauthn.data.exception.Base64UrlException; +import java.io.IOException; +import java.security.DigestException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.SignatureException; +import java.security.cert.CertPathValidatorException; +import java.security.cert.CertStore; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.Value; +import lombok.extern.slf4j.Slf4j; + +/** + * Utility for filtering and querying Fido + * Metadata Service BLOB entries. + * + *

This class implements {@link AttestationTrustSource}, so it can be configured as the {@link + * RelyingPartyBuilder#attestationTrustSource(AttestationTrustSource) attestationTrustSource} + * setting in {@link RelyingParty}. + * + *

The metadata service may be configured with a two stages of filters to select trusted + * authenticators. The first stage is the {@link FidoMetadataServiceBuilder#prefilter(Predicate) + * prefilter} setting, which is executed once when the {@link FidoMetadataService} instance is + * constructed. The second stage is the {@link FidoMetadataServiceBuilder#filter(Predicate) filter} + * setting, which is executed whenever metadata or trust roots are to be looked up for a given + * authenticator. Any metadata entry that satisfies both filters will be considered trusted. + * + *

Use the {@link #builder() builder} to configure settings, then use the {@link + * #findEntries(List, AAGUID)} method or its overloads to retrieve metadata entries. + */ +@Slf4j +public final class FidoMetadataService implements AttestationTrustSource { + + private final HashMap> + prefilteredEntriesByCertificateKeyIdentifier; + private final HashMap> prefilteredEntriesByAaguid; + private final HashSet prefilteredUnindexedEntries; + + private final Predicate filter; + private final CertStore certStore; + + private FidoMetadataService( + @NonNull MetadataBLOBPayload blob, + @NonNull Predicate prefilter, + @NonNull Predicate filter, + CertStore certStore) { + final List prefilteredEntries = + blob.getEntries().stream() + .filter(FidoMetadataService::ignoreInvalidUpdateAvailableAuthenticatorVersion) + .filter(prefilter) + .collect(Collectors.toList()); + + this.prefilteredEntriesByCertificateKeyIdentifier = buildCkiMap(prefilteredEntries); + this.prefilteredEntriesByAaguid = buildAaguidMap(prefilteredEntries); + + this.prefilteredUnindexedEntries = new HashSet<>(prefilteredEntries); + for (HashSet byAaguid : prefilteredEntriesByAaguid.values()) { + prefilteredUnindexedEntries.removeAll(byAaguid); + } + for (HashSet byCski : + prefilteredEntriesByCertificateKeyIdentifier.values()) { + prefilteredUnindexedEntries.removeAll(byCski); + } + + this.filter = filter; + this.certStore = certStore; + } + + private static boolean ignoreInvalidUpdateAvailableAuthenticatorVersion( + MetadataBLOBPayloadEntry metadataBLOBPayloadEntry) { + return metadataBLOBPayloadEntry + .getMetadataStatement() + .map(MetadataStatement::getAuthenticatorVersion) + .map( + authenticatorVersion -> + metadataBLOBPayloadEntry.getStatusReports().stream() + .filter( + statusReport -> + AuthenticatorStatus.UPDATE_AVAILABLE.equals(statusReport.getStatus())) + .noneMatch( + statusReport -> + statusReport + .getAuthenticatorVersion() + .map(av -> av > authenticatorVersion) + .orElse(false))) + .orElse(true); + } + + private static HashMap> buildCkiMap( + @NonNull List entries) { + + return entries.stream() + .collect( + HashMap::new, + (result, metadataBLOBPayloadEntry) -> { + for (String acki : + metadataBLOBPayloadEntry.getAttestationCertificateKeyIdentifiers()) { + result.computeIfAbsent(acki, o -> new HashSet<>()).add(metadataBLOBPayloadEntry); + } + for (String acki : + metadataBLOBPayloadEntry + .getMetadataStatement() + .map(MetadataStatement::getAttestationCertificateKeyIdentifiers) + .orElseGet(Collections::emptySet)) { + result.computeIfAbsent(acki, o -> new HashSet<>()).add(metadataBLOBPayloadEntry); + } + }, + (mapA, mapB) -> { + for (Map.Entry> e : mapB.entrySet()) { + mapA.merge( + e.getKey(), + e.getValue(), + (entriesA, entriesB) -> { + entriesA.addAll(entriesB); + return entriesA; + }); + } + }); + } + + private static HashMap> buildAaguidMap( + @NonNull List entries) { + + return entries.stream() + .collect( + HashMap::new, + (result, metadataBLOBPayloadEntry) -> { + final Consumer appendToAaguidEntry = + aaguid -> + result + .computeIfAbsent(aaguid, o -> new HashSet<>()) + .add(metadataBLOBPayloadEntry); + metadataBLOBPayloadEntry + .getAaguid() + .filter(aaguid -> !aaguid.isZero()) + .ifPresent(appendToAaguidEntry); + metadataBLOBPayloadEntry + .getMetadataStatement() + .flatMap(MetadataStatement::getAaguid) + .filter(aaguid -> !aaguid.isZero()) + .ifPresent(appendToAaguidEntry); + }, + (mapA, mapB) -> { + for (Map.Entry> e : mapB.entrySet()) { + mapA.merge( + e.getKey(), + e.getValue(), + (entriesA, entriesB) -> { + entriesA.addAll(entriesB); + return entriesA; + }); + } + }); + } + + public static FidoMetadataServiceBuilder.Step1 builder() { + return new FidoMetadataServiceBuilder.Step1(); + } + + @RequiredArgsConstructor(access = AccessLevel.PRIVATE) + public static class FidoMetadataServiceBuilder { + @NonNull private final MetadataBLOBPayload blob; + + private Predicate prefilter = Filters.notRevoked(); + private Predicate filter = Filters.noAttestationKeyCompromise(); + private CertStore certStore = null; + + public static class Step1 { + /** + * Use payload of the given blob as the data source. + * + *

The {@link FidoMetadataDownloader#loadCachedBlob()} method returns a value suitable for + * use here. + * + *

This is an alias of useBlob(blob.getPayload(). + * + * @see FidoMetadataDownloader#loadCachedBlob() + * @see #useBlob(MetadataBLOBPayload) + */ + public FidoMetadataServiceBuilder useBlob(@NonNull MetadataBLOB blob) { + return useBlob(blob.getPayload()); + } + + /** + * Use the given blobPayload as the data source. + * + *

The {@link FidoMetadataDownloader#loadCachedBlob()} method returns a value whose {@link + * MetadataBLOB#getPayload() .getPayload()} result is suitable for use here. + * + * @see FidoMetadataDownloader#loadCachedBlob() + * @see #useBlob(MetadataBLOB) + */ + public FidoMetadataServiceBuilder useBlob(@NonNull MetadataBLOBPayload blobPayload) { + return new FidoMetadataServiceBuilder(blobPayload); + } + } + + /** + * Set a first-stage filter for which metadata entries to include in the data source. + * + *

This prefilter is executed once for each metadata entry during initial construction of a + * {@link FidoMetadataService} instance. + * + *

The default is {@link Filters#notRevoked() Filters.notRevoked()}. Setting a different + * filter overrides this default; to preserve the "not revoked" condition in addition to the new + * filter, you must explicitly include the condition in the few filter. For example, by using + * {@link Filters#allOf(Predicate[]) Filters.allOf(Predicate...)}. + * + * @param prefilter a {@link Predicate} which returns true for metadata entries to + * include in the data source. + * @see #filter + * @see Filters#allOf(Predicate[]) + */ + public FidoMetadataServiceBuilder prefilter( + @NonNull Predicate prefilter) { + this.prefilter = prefilter; + return this; + } + + /** + * Set a filter for which metadata entries to allow for a given authenticator during credential + * registration and metadata lookup. + * + *

This filter is executed during each execution of {@link #findEntries(List, AAGUID)}, its + * overloads, and {@link #findTrustRoots(List, Optional)}. + * + *

The default is {@link Filters#noAttestationKeyCompromise() + * Filters.noAttestationKeyCompromise()}. Setting a different filter overrides this default; to + * preserve this condition in addition to the new filter, you must explicitly include the + * condition in the few filter. For example, by using {@link Filters#allOf(Predicate[]) + * Filters.allOf(Predicate...)}. + * + *

Note: Returning true in the filter predicate does not automatically make the + * authenticator trusted, as its attestation certificate must also correctly chain to a trusted + * attestation root. Rather, returning true in the filter predicate allows the + * corresponding metadata entry to be used for further trust assessment for that authenticator, + * while returning false eliminates the metadata entry (and thus any associated + * trust roots) for the ongoing query. + * + * @param filter a {@link Predicate} which returns true for metadata entries to + * allow for the corresponding authenticator during credential registration and metadata + * lookup. + * @see #prefilter(Predicate) + * @see AuthenticatorToBeFiltered + * @see Filters#allOf(Predicate[]) + */ + public FidoMetadataServiceBuilder filter( + @NonNull Predicate filter) { + this.filter = filter; + return this; + } + + /** + * Set a {@link CertStore} of additional CRLs and/or intermediate certificates to use while + * validating attestation certificate paths. + * + *

This setting is most likely useful for tests. + * + * @param certStore a {@link CertStore} of additional CRLs and/or intermediate certificates to + * use while validating attestation certificate paths. + */ + public FidoMetadataServiceBuilder certStore(@NonNull CertStore certStore) { + this.certStore = certStore; + return this; + } + + public FidoMetadataService build() + throws CertPathValidatorException, InvalidAlgorithmParameterException, Base64UrlException, + DigestException, FidoMetadataDownloaderException, CertificateException, + UnexpectedLegalHeader, IOException, NoSuchAlgorithmException, SignatureException, + InvalidKeyException { + return new FidoMetadataService(blob, prefilter, filter, certStore); + } + } + + /** + * Preconfigured filters and utilities for combining filters. See the {@link + * FidoMetadataServiceBuilder#prefilter(Predicate) filter} setting. + * + * @see FidoMetadataServiceBuilder#prefilter(Predicate) + */ + public static class Filters { + + /** + * Combine a set of filters into a filter that requires inputs to satisfy ALL of those filters. + * + *

If filters is empty, then all inputs will satisfy the resulting filter. + * + * @param filters A set of filters. + * @return A filter which only accepts inputs that satisfy ALL of the given + * filters. + */ + public static Predicate allOf(Predicate... filters) { + return (entry) -> Stream.of(filters).allMatch(filter -> filter.test(entry)); + } + + /** + * Include any metadata entry whose {@link MetadataBLOBPayloadEntry#getStatusReports() + * statusReports} array contains no entry with {@link AuthenticatorStatus#REVOKED REVOKED} + * status. + * + * @see AuthenticatorStatus#REVOKED + */ + public static Predicate notRevoked() { + return (entry) -> + entry.getStatusReports().stream() + .noneMatch( + statusReport -> AuthenticatorStatus.REVOKED.equals(statusReport.getStatus())); + } + + /** + * Accept any authenticator whose matched metadata entry does NOT indicate a compromised + * attestation key. + * + *

A metadata entry indicates a compromised attestation key if any of its {@link + * MetadataBLOBPayloadEntry#getStatusReports() statusReports} entries has {@link + * AuthenticatorStatus#ATTESTATION_KEY_COMPROMISE ATTESTATION_KEY_COMPROMISE} status and either + * an empty {@link StatusReport#getCertificate() certificate} field or a {@link + * StatusReport#getCertificate() certificate} whose public key appears in the authenticator's + * {@link AuthenticatorToBeFiltered#getAttestationCertificateChain() attestation certificate + * chain}. + * + * @see AuthenticatorStatus#ATTESTATION_KEY_COMPROMISE + */ + public static Predicate noAttestationKeyCompromise() { + return (params) -> + params.getMetadataEntry().getStatusReports().stream() + .filter( + statusReport -> + AuthenticatorStatus.ATTESTATION_KEY_COMPROMISE.equals( + statusReport.getStatus())) + .noneMatch( + statusReport -> + !statusReport.getCertificate().isPresent() + || (params.getAttestationCertificateChain().stream() + .anyMatch( + cert -> + Arrays.equals( + statusReport + .getCertificate() + .get() + .getPublicKey() + .getEncoded(), + cert.getPublicKey().getEncoded())))); + } + + /** + * This class encapsulates parameters for filtering authenticators in the {@link + * FidoMetadataServiceBuilder#filter(Predicate) filter} setting of {@link FidoMetadataService}. + */ + @Value + @AllArgsConstructor(access = AccessLevel.PRIVATE) + public static class AuthenticatorToBeFiltered { + + /** + * The attestation certificate chain from the attestation + * statement from an authenticator about ot be registered. + */ + @NonNull List attestationCertificateChain; + + /** + * A metadata BLOB entry that matches the {@link #getAttestationCertificateChain()} and {@link + * #getAaguid()} in this same {@link AuthenticatorToBeFiltered} object. + */ + @NonNull MetadataBLOBPayloadEntry metadataEntry; + + AAGUID aaguid; + + /** + * The AAGUID from the attested + * credential data of a credential about ot be registered. + */ + public Optional getAaguid() { + return Optional.ofNullable(aaguid); + } + } + } + + /** + * Look up metadata entries matching a given attestation certificate chain or AAGUID. + * + * @param attestationCertificateChain an attestation certificate chain, presumably from a WebAuthn + * attestation statement. + * @param aaguid the AAGUID of the authenticator to look up, if available. + * @return All metadata entries which satisfy ALL of the following: + *

    + *
  • It satisfies the {@link FidoMetadataServiceBuilder#prefilter(Predicate) prefilter}. + *
  • It satisfies AT LEAST ONE of the following: + *
      + *
    • 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 + * MetadataBLOBPayloadEntry#getMetadataStatement() metadata statement}, if any, in + * the metadata entry. + *
    • The certificate subject key identifier of any certificate in + * attestationCertificateChain matches any element of {@link + * MetadataBLOBPayloadEntry#getAttestationCertificateKeyIdentifiers() + * attestationCertificateKeyIdentifiers} in the metadata entry. + *
    • The certificate subject key identifier of any certificate in + * attestationCertificateChain matches any element of {@link + * MetadataStatement#getAttestationCertificateKeyIdentifiers() + * attestationCertificateKeyIdentifiers} in the {@link + * MetadataBLOBPayloadEntry#getMetadataStatement() metadata statement}, if any, in + * the metadata entry. + *
    + *
  • It satisfies the {@link FidoMetadataServiceBuilder#filter(Predicate) filter} together + * with attestationCertificateChain and aaguid. + *
+ * + * @see #findEntries(List) + * @see #findEntries(List, AAGUID) + */ + public Set findEntries( + @NonNull List attestationCertificateChain, + @NonNull Optional aaguid) { + + final Set certSubjectKeyIdentifiers = + attestationCertificateChain.stream() + .map( + cert -> { + try { + return new ByteArray(CertificateParser.computeSubjectKeyIdentifier(cert)) + .getHex(); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException( + "SHA-1 hash algorithm is not available in JCA context.", e); + } + }) + .collect(Collectors.toSet()); + + final Optional nonzeroAaguid = aaguid.filter(a -> !a.isZero()); + + log.debug( + "findEntries(certSubjectKeyIdentifiers = {}, aaguid = {})", + certSubjectKeyIdentifiers, + aaguid); + + if (!nonzeroAaguid.isPresent()) { + log.debug("findEntries: ignoring zero AAGUID"); + } + + final Set result = + Stream.concat( + nonzeroAaguid + .map(prefilteredEntriesByAaguid::get) + .map(Collection::stream) + .orElseGet(Stream::empty), + certSubjectKeyIdentifiers.stream() + .flatMap( + cski -> + Optional.ofNullable( + prefilteredEntriesByCertificateKeyIdentifier.get(cski)) + .map(Collection::stream) + .orElseGet(Stream::empty))) + .filter( + metadataBLOBPayloadEntry -> + this.filter.test( + new AuthenticatorToBeFiltered( + attestationCertificateChain, + metadataBLOBPayloadEntry, + aaguid.orElse(null)))) + .collect(Collectors.toSet()); + + log.debug( + "findEntries(certSubjectKeyIdentifiers = {}, aaguid = {}) => {} matches", + certSubjectKeyIdentifiers, + aaguid, + result.size()); + return result; + } + + /** + * Alias of findEntries(attestationCertificateChain, Optional.empty()). + * + * @see #findEntries(List, Optional) + */ + public Set findEntries( + @NonNull List attestationCertificateChain) { + return findEntries(attestationCertificateChain, Optional.empty()); + } + + /** + * Alias of findEntries(attestationCertificateChain, Optional.of(aaguid)). + * + * @see #findEntries(List, Optional) + */ + public Set findEntries( + @NonNull List attestationCertificateChain, @NonNull AAGUID aaguid) { + return findEntries(attestationCertificateChain, Optional.of(aaguid)); + } + + /** + * Find metadata entries matching the credential represented by registrationResult. + * + *

This is an alias of: + * + *

+   * registrationResult.getAttestationTrustPath()
+   *   .map(atp -> this.findEntries(atp, new AAGUID(registrationResult.getAaguid())))
+   *   .orElseGet(Collections::emptySet)
+   * 
+ * + * @see #findEntries(List, Optional) + */ + public Set findEntries(@NonNull RegistrationResult registrationResult) { + return registrationResult + .getAttestationTrustPath() + .map(atp -> findEntries(atp, new AAGUID(registrationResult.getAaguid()))) + .orElseGet(Collections::emptySet); + } + + /** + * Find metadata entries matching the given AAGUID. + * + * @see #findEntries(List, Optional) + */ + public Set findEntries(@NonNull AAGUID aaguid) { + return findEntries(Collections.emptyList(), aaguid); + } + + /** + * Retrieve metadata entries matching the given filter. + * + *

Note: The result MAY include fewer results than the number of times the filter + * returned true, because of possible duplication in the underlying data store. + * + * @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 + * FidoMetadataServiceBuilder#prefilter(Predicate) prefilter} AND for which the filter + * returns true. + * @see #findEntries(List, Optional) + */ + public Set findEntries( + @NonNull Predicate filter) { + return Stream.concat( + Stream.concat( + prefilteredEntriesByAaguid.values().stream().flatMap(Collection::stream), + prefilteredEntriesByCertificateKeyIdentifier.values().stream() + .flatMap(Collection::stream)), + prefilteredUnindexedEntries.stream()) + .filter(filter) + .collect(Collectors.toSet()); + } + + @Override + public TrustRootsResult findTrustRoots( + List attestationCertificateChain, Optional aaguid) { + return TrustRootsResult.builder() + .trustRoots( + findEntries(attestationCertificateChain, aaguid.map(AAGUID::new)).stream() + .map(MetadataBLOBPayloadEntry::getMetadataStatement) + .filter(Optional::isPresent) + .map(Optional::get) + .flatMap( + metadataStatement -> + metadataStatement.getAttestationRootCertificates().stream()) + .collect(Collectors.toSet())) + .certStore(certStore) + .enableRevocationChecking(false) + .build(); + } +} diff --git a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/JacksonCodecs.java b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/JacksonCodecs.java new file mode 100644 index 000000000..2d16b1820 --- /dev/null +++ b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/JacksonCodecs.java @@ -0,0 +1,12 @@ +package com.yubico.fido.metadata; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; + +class JacksonCodecs { + + static ObjectMapper jsonWithDefaultEnums() { + return com.yubico.internal.util.JacksonCodecs.json() + .configure(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE, true); + } +} diff --git a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/MetadataBLOB.java b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/MetadataBLOB.java new file mode 100644 index 000000000..93e457b98 --- /dev/null +++ b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/MetadataBLOB.java @@ -0,0 +1,37 @@ +package com.yubico.fido.metadata; + +import lombok.Value; + +/** + * The header and payload of a FIDO Metadata Service BLOB. + * + *

This does not include the JWT signature. + * + * @see FIDO + * Metadata Service §3.1.7. Metadata BLOB + */ +@Value +public class MetadataBLOB { + + /** + * The JWT header of the FIDO Metadata Service BLOB. + * + * @see FIDO + * Metadata Service §3.1.7. Metadata BLOB + */ + MetadataBLOBHeader header; + + /** + * The payload of the Metadata Service BLOB. + * + * @see FIDO + * Metadata Service §3.1.7. Metadata BLOB + * @see FIDO + * Metadata Service §3.1.6. Metadata BLOB Payload dictionary + */ + MetadataBLOBPayload payload; +} diff --git a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/MetadataBLOBHeader.java b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/MetadataBLOBHeader.java new file mode 100644 index 000000000..116095dc7 --- /dev/null +++ b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/MetadataBLOBHeader.java @@ -0,0 +1,92 @@ +package com.yubico.fido.metadata; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import java.net.URL; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.Optional; +import lombok.Builder; +import lombok.NonNull; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; + +/** + * The metadata BLOB is a JSON Web Token (see [JWT] + * and [JWS]). + * + *

This type represents the contents of the JWT header. + * + * @see FIDO + * Metadata Service §3.1.7. Metadata BLOB + * @see RFC 7519: JSON Web Token (JWT) + */ +@Value +@Builder(toBuilder = true) +@Jacksonized +public class MetadataBLOBHeader { + + /** + * @see RFC 7519 §5.1. "typ" + * (Type) Header Parameter + */ + String typ; + + /** + * @see RFC 7515 §4.1.1. + * "alg" (Algorithm) Header Parameter + */ + @NonNull String alg; + + /** + * @see RFC 7515 §4.1.5. + * "x5u" (X.509 URL) Header Parameter + */ + URL x5u; + + /** + * @see RFC 7515 §4.1.6. + * "x5c" (X.509 Certificate Chain) Header Parameter + */ + @JsonDeserialize(contentConverter = CertFromBase64Converter.class) + @JsonSerialize(contentConverter = CertToBase64Converter.class) + List x5c; + + private MetadataBLOBHeader(String typ, @NonNull String alg, URL x5u, List x5c) { + this.typ = typ; + this.alg = alg; + this.x5u = x5u; + this.x5c = x5c; + + if (typ != null && !typ.equals("JWT")) { + throw new IllegalArgumentException("Unsupported JWT type: " + typ); + } + } + + /** + * @see RFC 7519 §5.1. "typ" + * (Type) Header Parameter + */ + public Optional getTyp() { + return Optional.ofNullable(typ); + } + + /** + * @see RFC 7515 §4.1.5. + * "x5u" (X.509 URL) Header Parameter + */ + public Optional getX5u() { + return Optional.ofNullable(x5u); + } + + /** + * @see RFC 7515 §4.1.6. + * "x5c" (X.509 Certificate Chain) Header Parameter + */ + public Optional> getX5c() { + return Optional.ofNullable(x5c); + } +} diff --git a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/MetadataBLOBPayload.java b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/MetadataBLOBPayload.java new file mode 100644 index 000000000..605e4964a --- /dev/null +++ b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/MetadataBLOBPayload.java @@ -0,0 +1,68 @@ +package com.yubico.fido.metadata; + +import java.time.LocalDate; +import java.util.Set; +import lombok.Builder; +import lombok.NonNull; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; + +/** + * The metadata BLOB is a JSON Web Token (see [JWT] + * and [JWS]). + * + *

This type represents the contents of the JWT payload. + * + * @see FIDO + * Metadata Service §3.1.7. Metadata BLOB + * @see FIDO + * Metadata Service §3.1.6. Metadata BLOB Payload dictionary + */ +@Value +@Builder(toBuilder = true) +@Jacksonized +public class MetadataBLOBPayload { + + /** + * The legalHeader, which MUST be in each BLOB, is an indication of the acceptance of the relevant + * legal agreement for using the MDS. + * + * @see FIDO + * Metadata Service §3.1.6. Metadata BLOB Payload dictionary + */ + String legalHeader; + + /** + * The serial number of this Metadata BLOB Payload. Serial numbers MUST be consecutive and + * strictly monotonic, i.e. the successor BLOB will have a no value exactly + * incremented by one. + * + * @see FIDO + * Metadata Service §3.1.6. Metadata BLOB Payload dictionary + */ + int no; + + /** + * ISO-8601 formatted date when the next update will be provided at latest. + * + * @see FIDO + * Metadata Service §3.1.6. Metadata BLOB Payload dictionary + */ + @NonNull LocalDate nextUpdate; + + /** + * Zero or more {@link MetadataBLOBPayloadEntry} objects. + * + * @see FIDO + * Metadata Service §3.1.6. Metadata BLOB Payload dictionary + */ + @NonNull Set entries; +} diff --git a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/MetadataBLOBPayloadEntry.java b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/MetadataBLOBPayloadEntry.java new file mode 100644 index 000000000..56d4346b7 --- /dev/null +++ b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/MetadataBLOBPayloadEntry.java @@ -0,0 +1,172 @@ +package com.yubico.fido.metadata; + +import com.yubico.internal.util.CollectionUtil; +import com.yubico.webauthn.data.ByteArray; +import java.net.URL; +import java.time.LocalDate; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import lombok.Builder; +import lombok.NonNull; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; + +/** + * An element of {@link MetadataBLOBPayload#getEntries() entries} in a {@link MetadataBLOBPayload}. + * + * @see FIDO + * Metadata Service §3.1.1. Metadata BLOB Payload Entry dictionary + */ +@Value +@Builder(toBuilder = true) +@Jacksonized +public class MetadataBLOBPayloadEntry { + + /** + * @see FIDO + * Metadata Service §3.1.1. Metadata BLOB Payload Entry dictionary + */ + AAID aaid; + + /** + * @see FIDO + * Metadata Service §3.1.1. Metadata BLOB Payload Entry dictionary + */ + AAGUID aaguid; + + /** + * @see FIDO + * Metadata Service §3.1.1. Metadata BLOB Payload Entry dictionary + */ + Set attestationCertificateKeyIdentifiers; + + /** + * @see FIDO + * Metadata Service §3.1.1. Metadata BLOB Payload Entry dictionary + */ + MetadataStatement metadataStatement; + + /** + * @see FIDO + * Metadata Service §3.1.1. Metadata BLOB Payload Entry dictionary + */ + List biometricStatusReports; + + /** + * @see FIDO + * Metadata Service §3.1.1. Metadata BLOB Payload Entry dictionary + */ + @NonNull List statusReports; + + /** + * @see FIDO + * Metadata Service §3.1.1. Metadata BLOB Payload Entry dictionary + */ + @NonNull LocalDate timeOfLastStatusChange; + + /** + * @see FIDO + * Metadata Service §3.1.1. Metadata BLOB Payload Entry dictionary + */ + URL rogueListURL; + + /** + * @see FIDO + * Metadata Service §3.1.1. Metadata BLOB Payload Entry dictionary + */ + ByteArray rogueListHash; + + private MetadataBLOBPayloadEntry( + AAID aaid, + AAGUID aaguid, + Set attestationCertificateKeyIdentifiers, + MetadataStatement metadataStatement, + List biometricStatusReports, + @NonNull List statusReports, + @NonNull LocalDate timeOfLastStatusChange, + URL rogueListURL, + ByteArray rogueListHash) { + this.aaid = aaid; + this.aaguid = aaguid; + this.attestationCertificateKeyIdentifiers = + CollectionUtil.immutableSetOrEmpty(attestationCertificateKeyIdentifiers); + this.metadataStatement = metadataStatement; + this.biometricStatusReports = CollectionUtil.immutableListOrEmpty(biometricStatusReports); + this.statusReports = + Collections.unmodifiableList( + statusReports.stream() + .filter( + statusReport -> !statusReport.getStatus().equals(AuthenticatorStatus.UNKNOWN)) + .collect(Collectors.toList())); + this.timeOfLastStatusChange = timeOfLastStatusChange; + this.rogueListURL = rogueListURL; + this.rogueListHash = rogueListHash; + } + + /** + * @see FIDO + * Metadata Service §3.1.1. Metadata BLOB Payload Entry dictionary + */ + public Optional getAaid() { + return Optional.ofNullable(this.aaid); + } + + /** + * @see FIDO + * Metadata Service §3.1.1. Metadata BLOB Payload Entry dictionary + */ + public Optional getAaguid() { + return Optional.ofNullable(this.aaguid); + } + + /** + * @see FIDO + * Metadata Service §3.1.1. Metadata BLOB Payload Entry dictionary + */ + public Optional getMetadataStatement() { + return Optional.ofNullable(this.metadataStatement); + } + + /** + * @see FIDO + * Metadata Service §3.1.1. Metadata BLOB Payload Entry dictionary + */ + public Optional getTimeOfLastStatusChange() { + return Optional.of(this.timeOfLastStatusChange); + } + + /** + * @see FIDO + * Metadata Service §3.1.1. Metadata BLOB Payload Entry dictionary + */ + public Optional getRogueListURL() { + return Optional.ofNullable(this.rogueListURL); + } + + /** + * @see FIDO + * Metadata Service §3.1.1. Metadata BLOB Payload Entry dictionary + */ + public Optional getRogueListHash() { + return Optional.ofNullable(this.rogueListHash); + } +} diff --git a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/MetadataStatement.java b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/MetadataStatement.java new file mode 100644 index 000000000..51910fe5c --- /dev/null +++ b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/MetadataStatement.java @@ -0,0 +1,410 @@ +package com.yubico.fido.metadata; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.yubico.internal.util.CollectionUtil; +import com.yubico.webauthn.extension.uvm.KeyProtectionType; +import com.yubico.webauthn.extension.uvm.MatcherProtectionType; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import lombok.Builder; +import lombok.NonNull; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; + +/** + * Relying Parties can learn a subset of verifiable information for authenticators certified by the + * FIDO Alliance with an Authenticator Metadata statement. The Metadata statement can be acquired + * from the Metadata BLOB that is hosted on the Metadata Service [FIDOMetadataService]. + * + *

This class does not include the field ecdaaTrustAnchors since ECDAA is deprecated + * in WebAuthn Level 2. + * + * @see FIDO + * Metadata Statement + */ +@Value +@Builder(toBuilder = true) +@Jacksonized +public class MetadataStatement { + + /** + * @see FIDO + * Metadata Statement + */ + String legalHeader; + + /** + * @see FIDO + * Metadata Statement + */ + AAID aaid; + + /** + * @see FIDO + * Metadata Statement + */ + AAGUID aaguid; + + /** + * @see FIDO + * Metadata Statement + */ + Set attestationCertificateKeyIdentifiers; + + /** + * @see FIDO + * Metadata Statement + */ + String description; + + /** + * @see FIDO + * Metadata Statement + */ + AlternativeDescriptions alternativeDescriptions; + + /** + * @see FIDO + * Metadata Statement + */ + long authenticatorVersion; + + /** + * @see FIDO + * Metadata Statement + */ + @NonNull ProtocolFamily protocolFamily; + + /** + * @see FIDO + * Metadata Statement + */ + int schema; + + /** + * @see FIDO + * Metadata Statement + */ + @NonNull Set upv; + + /** + * @see FIDO + * Metadata Statement + */ + @NonNull Set authenticationAlgorithms; + + /** + * @see FIDO + * Metadata Statement + */ + @NonNull Set publicKeyAlgAndEncodings; + + /** + * @see FIDO + * Metadata Statement + */ + @NonNull Set attestationTypes; + + /** + * @see FIDO + * Metadata Statement + */ + @NonNull Set> userVerificationDetails; + + /** + * @see FIDO + * Metadata Statement + */ + @NonNull Set keyProtection; + + /** + * @see FIDO + * Metadata Statement + */ + Boolean isKeyRestricted; + + /** + * @see FIDO + * Metadata Statement + */ + Boolean isFreshUserVerificationRequired; + + /** + * @see FIDO + * Metadata Statement + */ + @NonNull Set matcherProtection; + + /** + * @see FIDO + * Metadata Statement + */ + Integer cryptoStrength; + + /** + * @see FIDO + * Metadata Statement + */ + Set attachmentHint; + + /** + * @see FIDO + * Metadata Statement + */ + @NonNull Set tcDisplay; + + /** + * @see FIDO + * Metadata Statement + */ + String tcDisplayContentType; + + /** + * @see FIDO + * Metadata Statement + */ + List tcDisplayPNGCharacteristics; + + /** + * @see FIDO + * Metadata Statement + */ + @NonNull + @JsonDeserialize(contentConverter = CertFromBase64Converter.class) + @JsonSerialize(contentConverter = CertToBase64Converter.class) + Set attestationRootCertificates; + + /** + * @see FIDO + * Metadata Statement + */ + String icon; + + /** + * @see FIDO + * Metadata Statement + */ + Set supportedExtensions; + + /** + * @see FIDO + * Metadata Statement + */ + AuthenticatorGetInfo authenticatorGetInfo; + + public MetadataStatement( + String legalHeader, + AAID aaid, + AAGUID aaguid, + Set attestationCertificateKeyIdentifiers, + String description, + AlternativeDescriptions alternativeDescriptions, + long authenticatorVersion, + @NonNull ProtocolFamily protocolFamily, + int schema, + @NonNull Set upv, + @NonNull Set authenticationAlgorithms, + @NonNull Set publicKeyAlgAndEncodings, + @NonNull Set attestationTypes, + @NonNull Set> userVerificationDetails, + @NonNull Set keyProtection, + Boolean isKeyRestricted, + Boolean isFreshUserVerificationRequired, + @NonNull Set matcherProtection, + Integer cryptoStrength, + Set attachmentHint, + @NonNull Set tcDisplay, + String tcDisplayContentType, + List tcDisplayPNGCharacteristics, + @NonNull Set attestationRootCertificates, + String icon, + Set supportedExtensions, + AuthenticatorGetInfo authenticatorGetInfo) { + this.legalHeader = legalHeader; + this.aaid = aaid; + this.aaguid = aaguid; + this.attestationCertificateKeyIdentifiers = + CollectionUtil.immutableSetOrEmpty(attestationCertificateKeyIdentifiers); + this.description = description; + this.alternativeDescriptions = alternativeDescriptions; + this.authenticatorVersion = authenticatorVersion; + this.protocolFamily = protocolFamily; + this.schema = schema; + this.upv = upv; + this.authenticationAlgorithms = authenticationAlgorithms; + this.publicKeyAlgAndEncodings = publicKeyAlgAndEncodings; + this.attestationTypes = attestationTypes; + this.userVerificationDetails = userVerificationDetails; + this.keyProtection = keyProtection; + this.isKeyRestricted = isKeyRestricted; + this.isFreshUserVerificationRequired = isFreshUserVerificationRequired; + this.matcherProtection = matcherProtection; + this.cryptoStrength = cryptoStrength; + this.attachmentHint = attachmentHint; + this.tcDisplay = tcDisplay; + this.tcDisplayContentType = tcDisplayContentType; + this.tcDisplayPNGCharacteristics = tcDisplayPNGCharacteristics; + this.attestationRootCertificates = attestationRootCertificates; + this.icon = icon; + this.supportedExtensions = supportedExtensions; + this.authenticatorGetInfo = authenticatorGetInfo; + } + + /** + * @see FIDO + * Metadata Statement + */ + public Optional getLegalHeader() { + return Optional.ofNullable(this.legalHeader); + } + + /** + * @see FIDO + * Metadata Statement + */ + public Optional getAaid() { + return Optional.ofNullable(this.aaid); + } + + /** + * @see FIDO + * Metadata Statement + */ + public Optional getAaguid() { + return Optional.ofNullable(this.aaguid); + } + + /** + * @see FIDO + * Metadata Statement + */ + public Optional getDescription() { + return Optional.ofNullable(this.description); + } + + /** + * @see FIDO + * Metadata Statement + */ + public Optional getAlternativeDescriptions() { + return Optional.ofNullable(this.alternativeDescriptions); + } + + /** + * @see FIDO + * Metadata Statement + */ + public Optional getIsKeyRestricted() { + return Optional.ofNullable(this.isKeyRestricted); + } + + /** + * @see FIDO + * Metadata Statement + */ + public Optional getIsFreshUserVerificationRequired() { + return Optional.ofNullable(this.isFreshUserVerificationRequired); + } + + /** + * @see FIDO + * Metadata Statement + */ + public Optional getCryptoStrength() { + return Optional.ofNullable(this.cryptoStrength); + } + + /** + * @see FIDO + * Metadata Statement + */ + public Optional> getAttachmentHint() { + return Optional.ofNullable(this.attachmentHint); + } + + /** + * @see FIDO + * Metadata Statement + */ + public Optional getTcDisplayContentType() { + return Optional.ofNullable(this.tcDisplayContentType); + } + + /** + * @see FIDO + * Metadata Statement + */ + public Optional> getTcDisplayPNGCharacteristics() { + return Optional.ofNullable(this.tcDisplayPNGCharacteristics); + } + + /** + * @see FIDO + * Metadata Statement + */ + public Optional getIcon() { + return Optional.ofNullable(this.icon); + } + + /** + * @see FIDO + * Metadata Statement + */ + public Optional> getSupportedExtensions() { + return Optional.ofNullable(this.supportedExtensions); + } + + /** + * @see FIDO + * Metadata Statement + */ + public Optional getAuthenticatorGetInfo() { + return Optional.ofNullable(this.authenticatorGetInfo); + } +} diff --git a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/PatternAccuracyDescriptor.java b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/PatternAccuracyDescriptor.java new file mode 100644 index 000000000..3b094c514 --- /dev/null +++ b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/PatternAccuracyDescriptor.java @@ -0,0 +1,59 @@ +package com.yubico.fido.metadata; + +import java.util.Optional; +import lombok.Builder; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; + +/** + * The {@link PatternAccuracyDescriptor} describes relevant accuracy/complexity aspects in the case + * that a pattern is used as the user verification method. + * + * @see FIDO + * Metadata Statement §3.4. PatternAccuracyDescriptor dictionary + */ +@Value +@Builder(toBuilder = true) +@Jacksonized +public class PatternAccuracyDescriptor { + + /** + * @see FIDO + * Metadata Statement §3.4. PatternAccuracyDescriptor dictionary + */ + long minComplexity; + + /** + * @see FIDO + * Metadata Statement §3.4. PatternAccuracyDescriptor dictionary + */ + Integer maxRetries; + + /** + * @see FIDO + * Metadata Statement §3.4. PatternAccuracyDescriptor dictionary + */ + Integer blockSlowdown; + + /** + * @see FIDO + * Metadata Statement §3.4. PatternAccuracyDescriptor dictionary + */ + public Optional getMaxRetries() { + return Optional.ofNullable(maxRetries); + } + + /** + * @see FIDO + * Metadata Statement §3.4. PatternAccuracyDescriptor dictionary + */ + public Optional getBlockSlowdown() { + return Optional.ofNullable(blockSlowdown); + } +} diff --git a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/ProtocolFamily.java b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/ProtocolFamily.java new file mode 100644 index 000000000..a41a4b104 --- /dev/null +++ b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/ProtocolFamily.java @@ -0,0 +1,40 @@ +package com.yubico.fido.metadata; + +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * Enumeration of valid values for {@link MetadataStatement#getProtocolFamily()}. + * + * @see FIDO + * Metadata Statement §4. Metadata Keys + */ +public enum ProtocolFamily { + + /** + * @see FIDO + * Metadata Statement §4. Metadata Keys + */ + UAF("uaf"), + + /** + * @see FIDO + * Metadata Statement §4. Metadata Keys + */ + U2F("u2f"), + + /** + * @see FIDO + * Metadata Statement §4. Metadata Keys + */ + FIDO2("fido2"); + + @JsonValue private final String value; + + ProtocolFamily(String value) { + this.value = value; + } +} diff --git a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/PublicKeyRepresentationFormat.java b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/PublicKeyRepresentationFormat.java new file mode 100644 index 000000000..6e0bd2ee3 --- /dev/null +++ b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/PublicKeyRepresentationFormat.java @@ -0,0 +1,61 @@ +package com.yubico.fido.metadata; + +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * The ALG_KEY constants are 16 bit long integers indicating the specific Public Key algorithm and + * encoding. + * + *

Each constant has a case-sensitive string representation (in quotes), which is used in the + * authoritative metadata for FIDO authenticators. + * + * @see FIDO + * Registry of Predefined Values §3.6.2 Public Key Representation Formats + */ +public enum PublicKeyRepresentationFormat { + + /** + * @see FIDO + * Registry of Predefined Values §3.6.2 Public Key Representation Formats + */ + ALG_KEY_ECC_X962_RAW(0x0100, "ecc_x962_raw"), + + /** + * @see FIDO + * Registry of Predefined Values §3.6.2 Public Key Representation Formats + */ + ALG_KEY_ECC_X962_DER(0x0101, "ecc_x962_der"), + + /** + * @see FIDO + * Registry of Predefined Values §3.6.2 Public Key Representation Formats + */ + ALG_KEY_RSA_2048_RAW(0x0102, "rsa_2048_raw"), + + /** + * @see FIDO + * Registry of Predefined Values §3.6.2 Public Key Representation Formats + */ + ALG_KEY_RSA_2048_DER(0x0103, "rsa_2048_der"), + + /** + * @see FIDO + * Registry of Predefined Values §3.6.2 Public Key Representation Formats + */ + ALG_KEY_COSE(0x0104, "cose"); + + private final int value; + + @JsonValue private final String name; + + PublicKeyRepresentationFormat(int value, String name) { + this.value = value; + this.name = name; + } +} diff --git a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/RgbPaletteEntry.java b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/RgbPaletteEntry.java new file mode 100644 index 000000000..e5f5cbeb5 --- /dev/null +++ b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/RgbPaletteEntry.java @@ -0,0 +1,28 @@ +package com.yubico.fido.metadata; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +/** + * The rgbPaletteEntry is an RGB three-sample tuple palette entry. + * + *

FIDO + * Metadata Statement §3.7. rgbPaletteEntry dictionary + */ +@Value +public class RgbPaletteEntry { + + int r; + int g; + int b; + + @JsonCreator + public RgbPaletteEntry( + @JsonProperty("r") int r, @JsonProperty("g") int g, @JsonProperty("b") int b) { + this.r = r; + this.g = g; + this.b = b; + } +} diff --git a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/StatusReport.java b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/StatusReport.java new file mode 100644 index 000000000..c0d852eb5 --- /dev/null +++ b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/StatusReport.java @@ -0,0 +1,189 @@ +package com.yubico.fido.metadata; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import java.net.MalformedURLException; +import java.net.URL; +import java.security.cert.X509Certificate; +import java.time.LocalDate; +import java.util.Optional; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NonNull; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; + +/** + * Contains an {@link AuthenticatorStatus} and additional data associated with it, if any. + * + * @see FIDO + * Metadata Service §3.1.3. StatusReport dictionary + */ +@Value +@Builder +@Jacksonized +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class StatusReport { + + /** + * @see FIDO + * Metadata Service §3.1.3. StatusReport dictionary + */ + @NonNull AuthenticatorStatus status; + + /** + * @see FIDO + * Metadata Service §3.1.3. StatusReport dictionary + */ + LocalDate effectiveDate; + + /** + * @see FIDO + * Metadata Service §3.1.3. StatusReport dictionary + */ + Long authenticatorVersion; + + /** + * @see FIDO + * Metadata Service §3.1.3. StatusReport dictionary + */ + @JsonDeserialize(converter = CertFromBase64Converter.class) + @JsonSerialize(converter = CertToBase64Converter.class) + X509Certificate certificate; + + /** + * @see FIDO + * Metadata Service §3.1.3. StatusReport dictionary + */ + @JsonProperty("url") + @Getter(AccessLevel.NONE) + String url; + + /** + * @see FIDO + * Metadata Service §3.1.3. StatusReport dictionary + */ + String certificationDescriptor; + + /** + * @see FIDO + * Metadata Service §3.1.3. StatusReport dictionary + */ + String certificateNumber; + + /** + * @see FIDO + * Metadata Service §3.1.3. StatusReport dictionary + */ + String certificationPolicyVersion; + + /** + * @see FIDO + * Metadata Service §3.1.3. StatusReport dictionary + */ + String certificationRequirementsVersion; + + /** + * @see FIDO + * Metadata Service §3.1.3. StatusReport dictionary + */ + public Optional getEffectiveDate() { + return Optional.ofNullable(effectiveDate); + } + + /** + * @see FIDO + * Metadata Service §3.1.3. StatusReport dictionary + */ + public Optional getAuthenticatorVersion() { + return Optional.ofNullable(authenticatorVersion); + } + + /** + * @see FIDO + * Metadata Service §3.1.3. StatusReport dictionary + */ + @JsonIgnore + public Optional getCertificate() { + return Optional.ofNullable(this.certificate); + } + + /** + * Attempt to parse the {@link #getUrlAsString() url} property, if any, as a {@link URL}. + * + * @return A present value if and only if {@link #getUrlAsString()} is present and a valid URL. + */ + public Optional getUrl() { + try { + return Optional.of(new URL(url)); + } catch (MalformedURLException e) { + return Optional.empty(); + } + } + + /** + * Get the raw url property of this {@link StatusReport} object. This may or may not + * be a valid URL. + * + * @see FIDO + * Metadata Service §3.1.3. StatusReport dictionary + */ + @JsonIgnore + public Optional getUrlAsString() { + return Optional.ofNullable(this.url); + } + + /** + * @see FIDO + * Metadata Service §3.1.3. StatusReport dictionary + */ + public Optional getCertificationDescriptor() { + return Optional.ofNullable(this.certificationDescriptor); + } + + /** + * @see FIDO + * Metadata Service §3.1.3. StatusReport dictionary + */ + public Optional getCertificateNumber() { + return Optional.ofNullable(this.certificateNumber); + } + + /** + * @see FIDO + * Metadata Service §3.1.3. StatusReport dictionary + */ + public Optional getCertificationPolicyVersion() { + return Optional.ofNullable(this.certificationPolicyVersion); + } + + /** + * @see FIDO + * Metadata Service §3.1.3. StatusReport dictionary + */ + public Optional getCertificationRequirementsVersion() { + return Optional.ofNullable(this.certificationRequirementsVersion); + } +} diff --git a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/SupportedCtapOptions.java b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/SupportedCtapOptions.java new file mode 100644 index 000000000..cda8898e9 --- /dev/null +++ b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/SupportedCtapOptions.java @@ -0,0 +1,160 @@ +package com.yubico.fido.metadata; + +import com.fasterxml.jackson.annotation.JsonAlias; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; + +/** + * A fixed-keys map of CTAP2 option names to Boolean values representing whether an authenticator + * supports the respective option. + * + * @see Client + * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) + */ +@Value +@Builder +@Jacksonized +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class SupportedCtapOptions { + + /** + * @see Client + * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) + */ + @Builder.Default boolean plat = false; + + /** + * @see Client + * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) + */ + @Builder.Default boolean rk = false; + + /** + * @see Client + * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) + */ + @Builder.Default boolean clientPin = false; + + /** + * @see Client + * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) + */ + @Builder.Default boolean up = false; + + /** + * @see Client + * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) + */ + @Builder.Default boolean uv = false; + + /** + * @see Client + * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) + */ + @JsonAlias("uvToken") + @Builder.Default + boolean pinUvAuthToken = false; + + /** + * @see Client + * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) + */ + @Builder.Default boolean noMcGaPermissionsWithClientPin = false; + + /** + * @see Client + * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) + */ + @Builder.Default boolean largeBlobs = false; + + /** + * @see Client + * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) + */ + @Builder.Default boolean ep = false; + + /** + * @see Client + * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) + */ + @Builder.Default boolean bioEnroll = false; + + /** + * @see Client + * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) + */ + @Builder.Default boolean userVerificationMgmtPreview = false; + + /** + * @see Client + * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) + */ + @Builder.Default boolean uvBioEnroll = false; + + /** + * @see Client + * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) + */ + @JsonAlias("config") + @Builder.Default + boolean authnrCfg = false; + + /** + * @see Client + * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) + */ + @Builder.Default boolean uvAcfg = false; + + /** + * @see Client + * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) + */ + @Builder.Default boolean credMgmt = false; + + /** + * @see Client + * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) + */ + @Builder.Default boolean credentialMgmtPreview = false; + + /** + * @see Client + * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) + */ + @Builder.Default boolean setMinPINLength = false; + + /** + * @see Client + * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) + */ + @Builder.Default boolean makeCredUvNotRqd = false; + + /** + * @see Client + * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04) + */ + @Builder.Default boolean alwaysUv = false; +} diff --git a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/TransactionConfirmationDisplayType.java b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/TransactionConfirmationDisplayType.java new file mode 100644 index 000000000..e62a5fe44 --- /dev/null +++ b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/TransactionConfirmationDisplayType.java @@ -0,0 +1,64 @@ +package com.yubico.fido.metadata; + +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * The TRANSACTION_CONFIRMATION_DISPLAY constants are flags in a bit field represented as a 16 bit + * long integer. They describe the availability and implementation of a transaction confirmation + * display capability required for the transaction confirmation operation. These constants are + * reported and queried through the UAF Discovery APIs and used to form authenticator policies in + * UAF protocol messages. Each constant has a case-sensitive string representation (in quotes), + * which is used in the authoritative metadata for FIDO authenticators. Refer to [UAFAuthnrCommands] + * for more details on the security aspects of TransactionConfirmation Display. + * + * @see FIDO + * Registry of Predefined Values §3.5 Transaction Confirmation Display Types + */ +public enum TransactionConfirmationDisplayType { + + /** + * @see FIDO + * Registry of Predefined Values §3.5 Transaction Confirmation Display Types + */ + TRANSACTION_CONFIRMATION_DISPLAY_ANY(0x0001, "any"), + + /** + * @see FIDO + * Registry of Predefined Values §3.5 Transaction Confirmation Display Types + */ + TRANSACTION_CONFIRMATION_DISPLAY_PRIVILEGED_SOFTWARE(0x0002, "privileged_software"), + + /** + * @see FIDO + * Registry of Predefined Values §3.5 Transaction Confirmation Display Types + */ + TRANSACTION_CONFIRMATION_DISPLAY_TEE(0x0004, "tee"), + + /** + * @see FIDO + * Registry of Predefined Values §3.5 Transaction Confirmation Display Types + */ + TRANSACTION_CONFIRMATION_DISPLAY_HARDWARE(0x0008, "hardware"), + + /** + * @see FIDO + * Registry of Predefined Values §3.5 Transaction Confirmation Display Types + */ + TRANSACTION_CONFIRMATION_DISPLAY_REMOTE(0x0010, "remote"); + + private final int value; + + @JsonValue private final String name; + + TransactionConfirmationDisplayType(int value, String name) { + this.value = value; + this.name = name; + } +} diff --git a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/UnexpectedLegalHeader.java b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/UnexpectedLegalHeader.java new file mode 100644 index 000000000..b28a94349 --- /dev/null +++ b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/UnexpectedLegalHeader.java @@ -0,0 +1,39 @@ +package com.yubico.fido.metadata; + +import java.util.Optional; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NonNull; + +/** + * A FIDO Metadata Service metadata BLOB was successfully downloaded and validated, but contained an + * unexpected legal header. + * + *

This exception contains the offending downloaded metadata BLOB as well as the cached metadata + * BLOB, if any (see {@link #getCachedBlob()}). This enables applications to gracefully fall back to + * the cached blob when possible, while notifying maintainers that action is required for the new + * legal header. + */ +@AllArgsConstructor(access = AccessLevel.PACKAGE) +public class UnexpectedLegalHeader extends Exception { + + /** The cached metadata BLOB, if any, which is assumed to have an expected legal header. */ + private final MetadataBLOB cachedBlob; + + /** + * The newly downloaded metadata BLOB, which has an unexpected legal header. + * + *

The unexpected legal header can be retrieved via the {@link MetadataBLOB#getPayload() + * getPayload()}.{@link MetadataBLOBPayload#getLegalHeader() getLegalHeader()} methods. + * + * @see MetadataBLOB#getPayload() + * @see MetadataBLOBPayload#getLegalHeader() + */ + @Getter @NonNull private final MetadataBLOB downloadedBlob; + + /** The cached metadata BLOB, if any. */ + public Optional getCachedBlob() { + return Optional.ofNullable(cachedBlob); + } +} diff --git a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/VerificationMethodDescriptor.java b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/VerificationMethodDescriptor.java new file mode 100644 index 000000000..8a8016645 --- /dev/null +++ b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/VerificationMethodDescriptor.java @@ -0,0 +1,48 @@ +package com.yubico.fido.metadata; + +import com.yubico.webauthn.extension.uvm.UserVerificationMethod; +import lombok.Builder; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; + +/** + * A descriptor for a specific base user verification method as implemented by the + * authenticator. + * + * @see FIDO + * Metadata Statement §3.5. VerificationMethodDescriptor dictionary + */ +@Value +@Builder(toBuilder = true) +@Jacksonized +public class VerificationMethodDescriptor { + + /** + * @see FIDO + * Metadata Statement §3.5. VerificationMethodDescriptor dictionary + */ + UserVerificationMethod userVerificationMethod; + + /** + * @see FIDO + * Metadata Statement §3.5. VerificationMethodDescriptor dictionary + */ + CodeAccuracyDescriptor caDesc; + + /** + * @see FIDO + * Metadata Statement §3.5. VerificationMethodDescriptor dictionary + */ + BiometricAccuracyDescriptor baDesc; + + /** + * @see FIDO + * Metadata Statement §3.5. VerificationMethodDescriptor dictionary + */ + PatternAccuracyDescriptor paDesc; +} diff --git a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/Version.java b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/Version.java new file mode 100644 index 000000000..d5f05a58f --- /dev/null +++ b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/Version.java @@ -0,0 +1,41 @@ +package com.yubico.fido.metadata; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +/** + * Represents a generic version with major and minor fields. + * + * @see FIDO + * UAF Protocol Specification §3.1.1 Version Interface + */ +@Value +public class Version { + + /** + * @see FIDO + * UAF Protocol Specification §3.1.1 Version Interface + */ + int major; + + /** + * @see FIDO + * UAF Protocol Specification §3.1.1 Version Interface + */ + int minor; + + /** + * @see FIDO + * UAF Protocol Specification §3.1.1 Version Interface + */ + @JsonCreator + public Version(@JsonProperty("major") int major, @JsonProperty("minor") int minor) { + this.major = major; + this.minor = minor; + } +} diff --git a/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/AttestationResolver.java b/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/AttestationResolver.java deleted file mode 100644 index 69a08efad..000000000 --- a/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/AttestationResolver.java +++ /dev/null @@ -1,43 +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; - -import java.security.cert.X509Certificate; -import java.util.Collections; -import java.util.List; -import java.util.Optional; - -public interface AttestationResolver { - - /** Alias of resolve(attestationCertificate, Collections.emptyList()). */ - default Optional resolve(X509Certificate attestationCertificate) { - return resolve(attestationCertificate, Collections.emptyList()); - } - - Optional resolve( - X509Certificate attestationCertificate, List certificateChain); - - Attestation untrustedFromCertificate(X509Certificate attestationCertificate); -} diff --git a/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/StandardMetadataService.java b/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/StandardMetadataService.java deleted file mode 100644 index 501b09eb5..000000000 --- a/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/StandardMetadataService.java +++ /dev/null @@ -1,134 +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; - -import com.google.common.cache.Cache; -import com.google.common.cache.CacheBuilder; -import com.google.common.hash.Hashing; -import com.yubico.internal.util.ExceptionUtil; -import com.yubico.webauthn.attestation.resolver.SimpleAttestationResolver; -import com.yubico.webauthn.attestation.resolver.SimpleTrustResolver; -import java.security.cert.CertificateEncodingException; -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.ExecutionException; -import lombok.NonNull; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public final class StandardMetadataService implements MetadataService { - private static final Logger logger = LoggerFactory.getLogger(StandardMetadataService.class); - - private final Attestation unknownAttestation = Attestation.empty(); - private final AttestationResolver attestationResolver; - private final Cache cache; - - private StandardMetadataService( - @NonNull AttestationResolver attestationResolver, @NonNull Cache cache) { - this.attestationResolver = attestationResolver; - this.cache = cache; - } - - public StandardMetadataService(AttestationResolver attestationResolver) { - this(attestationResolver, CacheBuilder.newBuilder().build()); - } - - public StandardMetadataService() throws CertificateException { - this(createDefaultAttestationResolver()); - } - - public static TrustResolver createDefaultTrustResolver() throws CertificateException { - return SimpleTrustResolver.fromMetadata(Collections.singleton(MetadataObject.readDefault())); - } - - public static AttestationResolver createDefaultAttestationResolver(TrustResolver trustResolver) - throws CertificateException { - return new SimpleAttestationResolver( - Collections.singleton(MetadataObject.readDefault()), trustResolver); - } - - public static AttestationResolver createDefaultAttestationResolver() throws CertificateException { - return createDefaultAttestationResolver(createDefaultTrustResolver()); - } - - public Attestation getCachedAttestation(String attestationCertificateFingerprint) { - return cache.getIfPresent(attestationCertificateFingerprint); - } - - /** - * Attempt to look up attestation for a chain of certificates - * - *

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

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

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

If the certificate chain is empty, an untrusted empty attestation is returned. - * - * @param attestationCertificateChain a certificate chain, where each certificate in the list - * should be signed by the following certificate. - * @throws CertificateEncodingException if computation of the fingerprint fails for any element of - * attestationCertificateChain that needs to be inspected - * @return An attestation as described above. - */ - @Override - public Attestation getAttestation(@NonNull List attestationCertificateChain) - throws CertificateEncodingException { - if (attestationCertificateChain.isEmpty()) { - return unknownAttestation; - } - - X509Certificate attestationCertificate = attestationCertificateChain.get(0); - List certificateChain = - attestationCertificateChain.subList(1, attestationCertificateChain.size()); - - try { - final String fingerprint = - Hashing.sha1().hashBytes(attestationCertificate.getEncoded()).toString(); - return cache.get( - fingerprint, - () -> - attestationResolver - .resolve(attestationCertificate, certificateChain) - .orElseGet( - () -> attestationResolver.untrustedFromCertificate(attestationCertificate))); - } catch (ExecutionException e) { - throw ExceptionUtil.wrapAndLog( - logger, - "Failed to look up attestation information for certificate: " + attestationCertificate, - e); - } - } -} diff --git a/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/TrustResolver.java b/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/TrustResolver.java deleted file mode 100644 index 803d879a2..000000000 --- a/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/TrustResolver.java +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (c) 2018, Yubico AB -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, this -// list of conditions and the following disclaimer. -// -// 2. Redistributions in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -package com.yubico.webauthn.attestation; - -import java.security.cert.X509Certificate; -import java.util.Collections; -import java.util.List; -import java.util.Optional; - -public interface TrustResolver { - - /** - * Alias of resolveTrustAnchor(attestationCertificate, Collections.emptyList()). - * - * @see #resolveTrustAnchor(X509Certificate, List) - */ - default Optional resolveTrustAnchor(X509Certificate attestationCertificate) { - return resolveTrustAnchor(attestationCertificate, Collections.emptyList()); - } - - /** - * Resolve a trusted root anchor for the given attestation certificate and certificate chain - * - * @param attestationCertificate The attestation certificate - * @param caCertificateChain Zero or more certificates, of which the first has signed - * attestationCertificate and each of the remaining certificates has signed the - * certificate preceding it. - * @return A trusted root certificate from which there is a signature path to - * attestationCertificate, if one exists. - */ - Optional resolveTrustAnchor( - X509Certificate attestationCertificate, List caCertificateChain); -} diff --git a/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/resolver/CompositeAttestationResolver.java b/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/resolver/CompositeAttestationResolver.java deleted file mode 100644 index 85ff4a367..000000000 --- a/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/resolver/CompositeAttestationResolver.java +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (c) 2018, Yubico AB -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, this -// list of conditions and the following disclaimer. -// -// 2. Redistributions in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -package com.yubico.webauthn.attestation.resolver; - -import com.yubico.internal.util.CollectionUtil; -import com.yubico.webauthn.attestation.Attestation; -import com.yubico.webauthn.attestation.AttestationResolver; -import java.security.cert.X509Certificate; -import java.util.List; -import java.util.Optional; - -/** - * An {@link AttestationResolver} whose {@link #resolve(X509Certificate, List)} method calls {@link - * AttestationResolver#resolve(X509Certificate, List)} on each of the subordinate {@link - * AttestationResolver}s in turn, and returns the first non-null result. - */ -public final class CompositeAttestationResolver implements AttestationResolver { - - private final List resolvers; - - public CompositeAttestationResolver(List resolvers) { - this.resolvers = CollectionUtil.immutableList(resolvers); - } - - @Override - public Optional resolve( - X509Certificate attestationCertificate, List certificateChain) { - for (AttestationResolver resolver : resolvers) { - Optional result = resolver.resolve(attestationCertificate, certificateChain); - if (result.isPresent()) { - return result; - } - } - return Optional.empty(); - } - - /** Delegates to the first subordinate resolver, or throws an exception if there is none. */ - @Override - public Attestation untrustedFromCertificate(X509Certificate attestationCertificate) { - if (resolvers.isEmpty()) { - throw new UnsupportedOperationException("Cannot do this without any sub-resolver."); - } else { - return resolvers.get(0).untrustedFromCertificate(attestationCertificate); - } - } -} diff --git a/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/resolver/CompositeTrustResolver.java b/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/resolver/CompositeTrustResolver.java deleted file mode 100644 index 4578f29ea..000000000 --- a/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/resolver/CompositeTrustResolver.java +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright (c) 2018, Yubico AB -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, this -// list of conditions and the following disclaimer. -// -// 2. Redistributions in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -package com.yubico.webauthn.attestation.resolver; - -import com.yubico.internal.util.CollectionUtil; -import com.yubico.webauthn.attestation.TrustResolver; -import java.security.cert.X509Certificate; -import java.util.List; -import java.util.Optional; - -/** - * A {@link TrustResolver} whose {@link #resolveTrustAnchor(X509Certificate, List)} method calls - * {@link TrustResolver#resolveTrustAnchor(X509Certificate, List)} on each of the subordinate {@link - * TrustResolver}s in turn, and returns the first non-null result. - */ -public final class CompositeTrustResolver implements TrustResolver { - - private final List resolvers; - - public CompositeTrustResolver(List resolvers) { - this.resolvers = CollectionUtil.immutableList(resolvers); - } - - @Override - public Optional resolveTrustAnchor( - X509Certificate attestationCertificate, List certificateChain) { - for (TrustResolver resolver : resolvers) { - Optional result = - resolver.resolveTrustAnchor(attestationCertificate, certificateChain); - if (result.isPresent()) { - return result; - } - } - return Optional.empty(); - } -} diff --git a/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/resolver/SimpleTrustResolver.java b/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/resolver/SimpleTrustResolver.java deleted file mode 100644 index e7552a7cb..000000000 --- a/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/resolver/SimpleTrustResolver.java +++ /dev/null @@ -1,142 +0,0 @@ -// Copyright (c) 2018, Yubico AB -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, this -// list of conditions and the following disclaimer. -// -// 2. Redistributions in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -package com.yubico.webauthn.attestation.resolver; - -import com.google.common.collect.ArrayListMultimap; -import com.google.common.collect.Multimap; -import com.yubico.internal.util.CertificateParser; -import com.yubico.internal.util.JacksonCodecs; -import com.yubico.webauthn.attestation.MetadataObject; -import com.yubico.webauthn.attestation.TrustResolver; -import java.io.IOException; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.security.NoSuchProviderException; -import java.security.SignatureException; -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Optional; -import java.util.Set; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Assesses whether an argument certificate can be trusted, and if so, by what trusted root - * certificate. - */ -public final class SimpleTrustResolver implements TrustResolver { - - private static final Logger logger = LoggerFactory.getLogger(SimpleTrustResolver.class); - - private final Multimap trustedCerts = ArrayListMultimap.create(); - - public SimpleTrustResolver(Iterable trustedCertificates) { - for (X509Certificate cert : trustedCertificates) { - trustedCerts.put(cert.getSubjectDN().getName(), cert); - } - } - - public static SimpleTrustResolver fromMetadata(Iterable metadataObjects) - throws CertificateException { - Set certs = new HashSet<>(); - for (MetadataObject metadata : metadataObjects) { - for (String encodedCert : metadata.getTrustedCertificates()) { - certs.add(CertificateParser.parsePem(encodedCert)); - } - } - return new SimpleTrustResolver(certs); - } - - public static SimpleTrustResolver fromMetadataJson(String metadataObjectJson) - throws IOException, CertificateException { - return fromMetadata( - Collections.singleton( - JacksonCodecs.json().readValue(metadataObjectJson, MetadataObject.class))); - } - - @Override - public Optional resolveTrustAnchor( - X509Certificate attestationCertificate, List caCertificateChain) { - final List certChain = new ArrayList<>(); - certChain.add(attestationCertificate); - certChain.addAll(caCertificateChain); - - X509Certificate lastTriedCert = null; - - for (X509Certificate untrustedCert : certChain) { - if (lastTriedCert != null) { - logger.trace( - "No trusted certificate has signed certificate [{}] - trying next element in certificate chain.", - lastTriedCert); - - try { - lastTriedCert.verify(untrustedCert.getPublicKey()); - } catch (CertificateException - | NoSuchAlgorithmException - | InvalidKeyException - | NoSuchProviderException e) { - logger.error( - "Failed to verify that certificate [{}] was signed by [{}]", - lastTriedCert, - untrustedCert, - e); - throw new RuntimeException("Resolve failed", e); - } catch (SignatureException e) { - logger.debug( - "Certificate chain broken - certificate [{}] was not signed by certificate [{}]", - lastTriedCert, - untrustedCert); - return Optional.empty(); - } - } - - final String issuer = untrustedCert.getIssuerDN().getName(); - for (X509Certificate trustedCert : trustedCerts.get(issuer)) { - try { - untrustedCert.verify(trustedCert.getPublicKey()); - logger.debug("Found signature from trusted certificate [{}]", trustedCert); - return Optional.of(trustedCert); - } catch (CertificateException - | NoSuchAlgorithmException - | InvalidKeyException - | NoSuchProviderException e) { - logger.error("Resolve failed", e); - throw new RuntimeException("Resolve failed", e); - } catch (SignatureException e) { - // Not signed by the trusted cert - } - } - - lastTriedCert = untrustedCert; - } - - logger.debug("No trusted certificate has signed certificate chain {}", certChain); - return Optional.empty(); - } -} diff --git a/webauthn-server-attestation/src/test/java/com/yubico/webauthn/attestation/MetadataObjectTest.java b/webauthn-server-attestation/src/test/java/com/yubico/webauthn/attestation/MetadataObjectTest.java deleted file mode 100644 index 445dced2c..000000000 --- a/webauthn-server-attestation/src/test/java/com/yubico/webauthn/attestation/MetadataObjectTest.java +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright (c) 2018, Yubico AB -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, this -// list of conditions and the following disclaimer. -// -// 2. Redistributions in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -package com.yubico.webauthn.attestation; - -import static org.junit.Assert.assertEquals; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.yubico.internal.util.JacksonCodecs; -import org.junit.Test; - -public class MetadataObjectTest { - public static final String JSON = - "{" - + "\"identifier\":\"foobar\"," - + "\"version\":1," - + "\"vendorInfo\":{\"name\":\"Yubico\",\"url\":\"https://yubico.com\",\"imageUrl\":\"https://developers.yubico.com/U2F/Images/yubico.png\"}," - + "\"trustedCertificates\":[\"-----BEGIN CERTIFICATE-----\\nMIIDHjCCAgagAwIBAgIEG1BT9zANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZ\\ndWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAw\\nMDBaGA8yMDUwMDkwNDAwMDAwMFowLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290\\nIENBIFNlcmlhbCA0NTcyMDA2MzEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK\\nAoIBAQC/jwYuhBVlqaiYWEMsrWFisgJ+PtM91eSrpI4TK7U53mwCIawSDHy8vUmk\\n5N2KAj9abvT9NP5SMS1hQi3usxoYGonXQgfO6ZXyUA9a+KAkqdFnBnlyugSeCOep\\n8EdZFfsaRFtMjkwz5Gcz2Py4vIYvCdMHPtwaz0bVuzneueIEz6TnQjE63Rdt2zbw\\nnebwTG5ZybeWSwbzy+BJ34ZHcUhPAY89yJQXuE0IzMZFcEBbPNRbWECRKgjq//qT\\n9nmDOFVlSRCt2wiqPSzluwn+v+suQEBsUjTGMEd25tKXXTkNW21wIWbxeSyUoTXw\\nLvGS6xlwQSgNpk2qXYwf8iXg7VWZAgMBAAGjQjBAMB0GA1UdDgQWBBQgIvz0bNGJ\\nhjgpToksyKpP9xv9oDAPBgNVHRMECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIBBjAN\\nBgkqhkiG9w0BAQsFAAOCAQEAjvjuOMDSa+JXFCLyBKsycXtBVZsJ4Ue3LbaEsPY4\\nMYN/hIQ5ZM5p7EjfcnMG4CtYkNsfNHc0AhBLdq45rnT87q/6O3vUEtNMafbhU6kt\\nhX7Y+9XFN9NpmYxr+ekVY5xOxi8h9JDIgoMP4VB1uS0aunL1IGqrNooL9mmFnL2k\\nLVVee6/VR6C5+KSTCMCWppMuJIZII2v9o4dkoZ8Y7QRjQlLfYzd3qGtKbw7xaF1U\\nsG/5xUb/Btwb2X2g4InpiB/yt/3CpQXpiWX/K4mBvUKiGn05ZsqeY1gx4g0xLBqc\\nU9psmyPzK+Vsgw2jeRQ5JlKDyqE0hebfC1tvFu0CCrJFcw==\\n-----END CERTIFICATE-----\"]," - + "\"devices\":[{" - + "\"deviceId\":\"1.3.6.1.4.1.41482.1.2\"," - + "\"deviceUrl\":\"https://www.yubico.com/products/yubikey-hardware/yubikey-neo/\"," - + "\"displayName\":\"YubiKey NEO/NEO-n\"," - + "\"imageUrl\":\"https://developers.yubico.com/U2F/Images/NEO.png\"," - + "\"selectors\":[{" - + "\"type\":\"x509Extension\"," - + "\"parameters\":{" - + "\"key\":\"1.3.6.1.4.1.41482.1.2\"" - + "}" - + "}]" - + "}]" - + "}"; - - private final ObjectMapper objectMapper = JacksonCodecs.json(); - - @Test - public void testToAndFromJson() throws Exception { - MetadataObject metadata = objectMapper.readValue(JSON, MetadataObject.class); - ObjectMapper objectMapper = new ObjectMapper(); - MetadataObject metadata2 = - objectMapper.readValue(objectMapper.writeValueAsString(metadata), MetadataObject.class); - - assertEquals("foobar", metadata.getIdentifier()); - assertEquals(1, metadata.getVersion()); - assertEquals(1, metadata.getTrustedCertificates().size()); - - assertEquals("Yubico", metadata.getVendorInfo().get("name")); - assertEquals("1.3.6.1.4.1.41482.1.2", metadata.getDevices().get(0).get("deviceId").asText()); - - assertEquals(metadata, metadata2); - assertEquals(JSON, objectMapper.writeValueAsString(metadata)); - } -} diff --git a/webauthn-server-attestation/src/test/java/com/yubico/webauthn/attestation/StandardMetadataServiceTest.java b/webauthn-server-attestation/src/test/java/com/yubico/webauthn/attestation/StandardMetadataServiceTest.java deleted file mode 100644 index df57edccf..000000000 --- a/webauthn-server-attestation/src/test/java/com/yubico/webauthn/attestation/StandardMetadataServiceTest.java +++ /dev/null @@ -1,111 +0,0 @@ -// Copyright (c) 2018, Yubico AB -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, this -// list of conditions and the following disclaimer. -// -// 2. Redistributions in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -package com.yubico.webauthn.attestation; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; - -import com.google.common.hash.Hashing; -import com.yubico.internal.util.CertificateParser; -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; -import java.util.Collections; -import java.util.EnumSet; -import java.util.Optional; -import org.junit.Test; - -public class StandardMetadataServiceTest { - private static final String ATTESTATION_CERT = - "MIICGzCCAQWgAwIBAgIEdaP2dTALBgkqhkiG9w0BAQswLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290IENBIFNlcmlhbCA0NTcyMDA2MzEwIBcNMTQwODAxMDAwMDAwWhgPMjA1MDA5MDQwMDAwMDBaMCoxKDAmBgNVBAMMH1l1YmljbyBVMkYgRUUgU2VyaWFsIDE5NzM2Nzk3MzMwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQZo35Damtpl81YdmcbhEuXKAr7xDcQzAy5n3ftAAhtBbu8EeGU4ynfSgLonckqX6J2uXLBppTNE3v2bt+Yf8MLoxIwEDAOBgorBgEEAYLECgECBAAwCwYJKoZIhvcNAQELA4IBAQG9LbiNPgs0sQYOHAJcg+lMk+HCsiWRlYVnbT4I/5lnqU907vY17XYAORd432bU3Nnhsbkvjz76kQJGXeNAF4DPANGGlz8JU+LNEVE2PWPGgEM0GXgB7mZN5Sinfy1AoOdO+3c3bfdJQuXlUxHbo+nDpxxKpzq9gr++RbokF1+0JBkMbaA/qLYL4WdhY5NvaOyMvYpO3sBxlzn6FcP67hlotGH1wU7qhCeh+uur7zDeAWVh7c4QtJOXHkLJQfV3Z7ZMvhkIA6jZJAX99hisABU/SSa5DtgX7AfsHwa04h69AAAWDUzSk3HgOXbUd1FaSOPdlVFkG2N2JllFHykyO3zO"; - private static final String ATTESTATION_CERT2 = - "MIICLzCCARmgAwIBAgIEQvUaTTALBgkqhkiG9w0BAQswLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290IENBIFNlcmlhbCA0NTcyMDA2MzEwIBcNMTQwODAxMDAwMDAwWhgPMjA1MDA5MDQwMDAwMDBaMCoxKDAmBgNVBAMMH1l1YmljbyBVMkYgRUUgU2VyaWFsIDExMjMzNTkzMDkwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQphQ+PJYiZjZEVHtrx5QGE3/LE1+OytZPTwzrpWBKywji/3qmg22mwmVFl32PO269TxY+yVN4jbfVf5uX0EWJWoyYwJDAiBgkrBgEEAYLECgIEFTEuMy42LjEuNC4xLjQxNDgyLjEuNDALBgkqhkiG9w0BAQsDggEBALSc3YwTRbLwXhePj/imdBOhWiqh6ssS2ONgp5tphJCHR5Agjg2VstLBRsJzyJnLgy7bGZ0QbPOyh/J0hsvgBfvjByXOu1AwCW+tcoJ+pfxESojDLDn8hrFph6eWZoCtBsWMDh6vMqPENeP6grEAECWx4fTpBL9Bm7F+0Rp/d1/l66g4IhF/ZvuRFhY+BUK94BfivuBHpEkMwxKENTas7VkxvlVstUvPqhPHGYOq7RdF1D/THsbNY8+tgCTgvTziEG+bfDeY6zIz5h7bxb1rpajNVTpUDWtVYL7/w44e1KCoErqdS+kEbmmkmm7KvDE8kuyg42Fmb5DTMsbY2jxMlMU="; - private static final String ATTESTATION_CERT_WITH_TRANSPORTS = - "MIICIjCCAQygAwIBAgIEIHHwozALBgkqhkiG9w0BAQswDzENMAsGA1UEAxMEdGVzdDAeFw0xNTA4MTEwOTAwMzNaFw0xNjA4MTAwOTAwMzNaMCkxJzAlBgNVBAMTHll1YmljbyBVMkYgRUUgU2VyaWFsIDU0NDMzODA4MzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABPdFG1pBjBBQVhLrD39Qg1vKjuR2kRdBZnwLI/zgzztQpf4ffpkrkB/3E0TXj5zg8gN9sgMkX48geBe+tBEpvMmjOzA5MCIGCSsGAQQBgsQKAgQVMS4zLjYuMS40LjEuNDE0ODIuMS4yMBMGCysGAQQBguUcAgEBBAQDAgQwMAsGCSqGSIb3DQEBCwOCAQEAb3YpnmHHduNuWEXlLqlnww9034ZeZaojhPAYSLR8d5NPk9gc0hkjQKmIaaBM7DsaHbcHMKpXoMGTQSC++NCZTcKvZ0Lt12mp5HRnM1NNBPol8Hte5fLmvW4tQ9EzLl4gkz7LSlORxTuwTbae1eQqNdxdeB+0ilMFCEUc+3NGCNM0RWd+sP5+gzMXBDQAI1Sc9XaPIg8t3du5JChAl1ifpu/uERZ2WQgtxeBDO6z1Xoa5qz4svf5oURjPZjxS0WUKht48Z2rIjk5lZzERSaY3RrX3UtrnZEIzCmInXOrcRPeAD4ZutpiwuHe62ABsjuMRnKbATbOUiLdknNyPYYQz2g=="; - - @Test - public void testGetAttestation_x509extension_key() throws Exception { - StandardMetadataService service = new StandardMetadataService(); - - X509Certificate attestationCert = CertificateParser.parsePem(ATTESTATION_CERT); - Attestation attestation = service.getAttestation(Collections.singletonList(attestationCert)); - - assertTrue(attestation.isTrusted()); - assertEquals("Yubico", attestation.getVendorProperties().get().get("name")); - assertEquals("1.3.6.1.4.1.41482.1.2", attestation.getDeviceProperties().get().get("deviceId")); - } - - @Test - public void testGetAttestation_x509extension_key_value() throws Exception { - StandardMetadataService service = new StandardMetadataService(); - - X509Certificate attestationCert = CertificateParser.parsePem(ATTESTATION_CERT2); - Attestation attestation = service.getAttestation(Collections.singletonList(attestationCert)); - - assertTrue(attestation.isTrusted()); - assertEquals("Yubico", attestation.getVendorProperties().get().get("name")); - assertEquals("1.3.6.1.4.1.41482.1.4", attestation.getDeviceProperties().get().get("deviceId")); - } - - @Test - public void testGetTransportsFromCertificate() throws CertificateException { - StandardMetadataService service = new StandardMetadataService(); - - X509Certificate attestationCert = CertificateParser.parsePem(ATTESTATION_CERT_WITH_TRANSPORTS); - Attestation attestation = service.getAttestation(Collections.singletonList(attestationCert)); - - assertEquals( - Optional.of(EnumSet.of(Transport.USB, Transport.NFC)), attestation.getTransports()); - } - - @Test - public void testGetTransportsFromMetadata() throws CertificateException { - StandardMetadataService service = new StandardMetadataService(); - - X509Certificate attestationCert = CertificateParser.parsePem(ATTESTATION_CERT2); - Attestation attestation = service.getAttestation(Collections.singletonList(attestationCert)); - - assertEquals(Optional.of(EnumSet.of(Transport.USB)), attestation.getTransports()); - } - - @Test - public void getCachedAttestationReturnsCertIfPresent() throws Exception { - StandardMetadataService service = new StandardMetadataService(); - - final X509Certificate attestationCert = CertificateParser.parsePem(ATTESTATION_CERT); - final String certFingerprint = - Hashing.sha1().hashBytes(attestationCert.getEncoded()).toString(); - - assertNull(service.getCachedAttestation(certFingerprint)); - - service.getAttestation(Collections.singletonList(attestationCert)); - - Attestation attestation = service.getCachedAttestation(certFingerprint); - - assertTrue(attestation.isTrusted()); - assertEquals("Yubico", attestation.getVendorProperties().get().get("name")); - assertEquals("1.3.6.1.4.1.41482.1.2", attestation.getDeviceProperties().get().get("deviceId")); - } -} diff --git a/webauthn-server-attestation/src/test/java/com/yubico/webauthn/attestation/matcher/FingerprintMatcherTest.java b/webauthn-server-attestation/src/test/java/com/yubico/webauthn/attestation/matcher/FingerprintMatcherTest.java deleted file mode 100644 index 3e9fea7cf..000000000 --- a/webauthn-server-attestation/src/test/java/com/yubico/webauthn/attestation/matcher/FingerprintMatcherTest.java +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright (c) 2018, Yubico AB -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, this -// list of conditions and the following disclaimer. -// -// 2. Redistributions in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -package com.yubico.webauthn.attestation.matcher; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.BooleanNode; -import com.fasterxml.jackson.databind.node.JsonNodeFactory; -import com.fasterxml.jackson.databind.node.TextNode; -import com.google.common.hash.Hashing; -import com.yubico.internal.util.CertificateParser; -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; -import org.junit.Test; - -public class FingerprintMatcherTest { - - private static final String ATTESTATION_CERT = - "MIICGzCCAQWgAwIBAgIEdaP2dTALBgkqhkiG9w0BAQswLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290IENBIFNlcmlhbCA0NTcyMDA2MzEwIBcNMTQwODAxMDAwMDAwWhgPMjA1MDA5MDQwMDAwMDBaMCoxKDAmBgNVBAMMH1l1YmljbyBVMkYgRUUgU2VyaWFsIDE5NzM2Nzk3MzMwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQZo35Damtpl81YdmcbhEuXKAr7xDcQzAy5n3ftAAhtBbu8EeGU4ynfSgLonckqX6J2uXLBppTNE3v2bt+Yf8MLoxIwEDAOBgorBgEEAYLECgECBAAwCwYJKoZIhvcNAQELA4IBAQG9LbiNPgs0sQYOHAJcg+lMk+HCsiWRlYVnbT4I/5lnqU907vY17XYAORd432bU3Nnhsbkvjz76kQJGXeNAF4DPANGGlz8JU+LNEVE2PWPGgEM0GXgB7mZN5Sinfy1AoOdO+3c3bfdJQuXlUxHbo+nDpxxKpzq9gr++RbokF1+0JBkMbaA/qLYL4WdhY5NvaOyMvYpO3sBxlzn6FcP67hlotGH1wU7qhCeh+uur7zDeAWVh7c4QtJOXHkLJQfV3Z7ZMvhkIA6jZJAX99hisABU/SSa5DtgX7AfsHwa04h69AAAWDUzSk3HgOXbUd1FaSOPdlVFkG2N2JllFHykyO3zO"; - - @Test - public void matchesIsFalseForNonArrayFingerprints() { - JsonNode parameters = mock(JsonNode.class); - when(parameters.get("fingerprints")).thenReturn(BooleanNode.TRUE); - - assertFalse(new FingerprintMatcher().matches(mock(X509Certificate.class), parameters)); - } - - @Test - public void matchesIsFalseIfNoFingerprintMatches() throws CertificateException { - final X509Certificate cert = CertificateParser.parsePem(ATTESTATION_CERT); - - ArrayNode fingerprints = new ArrayNode(JsonNodeFactory.instance); - fingerprints.add(new TextNode("foo")); - fingerprints.add(new TextNode("bar")); - - JsonNode parameters = mock(JsonNode.class); - when(parameters.get("fingerprints")).thenReturn(fingerprints); - - assertFalse(new FingerprintMatcher().matches(cert, parameters)); - } - - @Test - public void matchesIsTrueIfSomeFingerprintMatches() throws CertificateException { - final X509Certificate cert = CertificateParser.parsePem(ATTESTATION_CERT); - final String fingerprint = Hashing.sha1().hashBytes(cert.getEncoded()).toString().toLowerCase(); - - ArrayNode fingerprints = new ArrayNode(JsonNodeFactory.instance); - fingerprints.add(new TextNode("foo")); - fingerprints.add(new TextNode(fingerprint)); - - JsonNode parameters = mock(JsonNode.class); - when(parameters.get("fingerprints")).thenReturn(fingerprints); - - assertTrue(new FingerprintMatcher().matches(cert, parameters)); - } -} diff --git a/webauthn-server-attestation/src/test/java/com/yubico/webauthn/attestation/resolver/SimpleAttestationResolverTest.java b/webauthn-server-attestation/src/test/java/com/yubico/webauthn/attestation/resolver/SimpleAttestationResolverTest.java deleted file mode 100644 index 8f74df544..000000000 --- a/webauthn-server-attestation/src/test/java/com/yubico/webauthn/attestation/resolver/SimpleAttestationResolverTest.java +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright (c) 2018, Yubico AB -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, this -// list of conditions and the following disclaimer. -// -// 2. Redistributions in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -package com.yubico.webauthn.attestation.resolver; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; - -import com.yubico.internal.util.CertificateParser; -import com.yubico.internal.util.JacksonCodecs; -import com.yubico.webauthn.attestation.Attestation; -import com.yubico.webauthn.attestation.MetadataObject; -import java.io.IOException; -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; -import java.util.Collections; -import java.util.Optional; -import org.junit.Test; - -public class SimpleAttestationResolverTest { - - private static final String METADATA_JSON = - "{\"identifier\":\"foobar\",\"version\":1,\"trustedCertificates\":[\"-----BEGIN CERTIFICATE-----\\nMIIDHjCCAgagAwIBAgIEG1BT9zANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZ\\ndWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAw\\nMDBaGA8yMDUwMDkwNDAwMDAwMFowLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290\\nIENBIFNlcmlhbCA0NTcyMDA2MzEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK\\nAoIBAQC/jwYuhBVlqaiYWEMsrWFisgJ+PtM91eSrpI4TK7U53mwCIawSDHy8vUmk\\n5N2KAj9abvT9NP5SMS1hQi3usxoYGonXQgfO6ZXyUA9a+KAkqdFnBnlyugSeCOep\\n8EdZFfsaRFtMjkwz5Gcz2Py4vIYvCdMHPtwaz0bVuzneueIEz6TnQjE63Rdt2zbw\\nnebwTG5ZybeWSwbzy+BJ34ZHcUhPAY89yJQXuE0IzMZFcEBbPNRbWECRKgjq//qT\\n9nmDOFVlSRCt2wiqPSzluwn+v+suQEBsUjTGMEd25tKXXTkNW21wIWbxeSyUoTXw\\nLvGS6xlwQSgNpk2qXYwf8iXg7VWZAgMBAAGjQjBAMB0GA1UdDgQWBBQgIvz0bNGJ\\nhjgpToksyKpP9xv9oDAPBgNVHRMECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIBBjAN\\nBgkqhkiG9w0BAQsFAAOCAQEAjvjuOMDSa+JXFCLyBKsycXtBVZsJ4Ue3LbaEsPY4\\nMYN/hIQ5ZM5p7EjfcnMG4CtYkNsfNHc0AhBLdq45rnT87q/6O3vUEtNMafbhU6kt\\nhX7Y+9XFN9NpmYxr+ekVY5xOxi8h9JDIgoMP4VB1uS0aunL1IGqrNooL9mmFnL2k\\nLVVee6/VR6C5+KSTCMCWppMuJIZII2v9o4dkoZ8Y7QRjQlLfYzd3qGtKbw7xaF1U\\nsG/5xUb/Btwb2X2g4InpiB/yt/3CpQXpiWX/K4mBvUKiGn05ZsqeY1gx4g0xLBqc\\nU9psmyPzK+Vsgw2jeRQ5JlKDyqE0hebfC1tvFu0CCrJFcw==\\n-----END CERTIFICATE-----\"],\"vendorInfo\":{\"name\":\"Yubico\",\"url\":\"https://yubico.com\",\"imageUrl\":\"https://developers.yubico.com/U2F/Images/yubico.png\"},\"devices\":[{\"displayName\":\"YubiKey NEO/NEO-n\",\"deviceId\":\"1.3.6.1.4.1.41482.1.2\",\"deviceUrl\":\"https://www.yubico.com/products/yubikey-hardware/yubikey-neo/\",\"imageUrl\":\"https://developers.yubico.com/U2F/Images/NEO.png\",\"selectors\":[{\"type\":\"x509Extension\",\"parameters\":{\"key\":\"1.3.6.1.4.1.41482.1.2\"}}]}] }"; - private static final String ATTESTATION_CERT = - "MIICGzCCAQWgAwIBAgIEdaP2dTALBgkqhkiG9w0BAQswLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290IENBIFNlcmlhbCA0NTcyMDA2MzEwIBcNMTQwODAxMDAwMDAwWhgPMjA1MDA5MDQwMDAwMDBaMCoxKDAmBgNVBAMMH1l1YmljbyBVMkYgRUUgU2VyaWFsIDE5NzM2Nzk3MzMwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQZo35Damtpl81YdmcbhEuXKAr7xDcQzAy5n3ftAAhtBbu8EeGU4ynfSgLonckqX6J2uXLBppTNE3v2bt+Yf8MLoxIwEDAOBgorBgEEAYLECgECBAAwCwYJKoZIhvcNAQELA4IBAQG9LbiNPgs0sQYOHAJcg+lMk+HCsiWRlYVnbT4I/5lnqU907vY17XYAORd432bU3Nnhsbkvjz76kQJGXeNAF4DPANGGlz8JU+LNEVE2PWPGgEM0GXgB7mZN5Sinfy1AoOdO+3c3bfdJQuXlUxHbo+nDpxxKpzq9gr++RbokF1+0JBkMbaA/qLYL4WdhY5NvaOyMvYpO3sBxlzn6FcP67hlotGH1wU7qhCeh+uur7zDeAWVh7c4QtJOXHkLJQfV3Z7ZMvhkIA6jZJAX99hisABU/SSa5DtgX7AfsHwa04h69AAAWDUzSk3HgOXbUd1FaSOPdlVFkG2N2JllFHykyO3zO"; - - private final MetadataObject metadata = - JacksonCodecs.json().readValue(METADATA_JSON, MetadataObject.class); - private final X509Certificate attestationCertificate = - CertificateParser.parseDer(ATTESTATION_CERT); - - public SimpleAttestationResolverTest() throws IOException, CertificateException {} - - private static SimpleAttestationResolver createAttestationResolver(MetadataObject metadata) - throws CertificateException { - return new SimpleAttestationResolver( - Collections.singleton(metadata), - SimpleTrustResolver.fromMetadata(Collections.singleton(metadata))); - } - - @Test - public void testResolve() throws Exception { - final SimpleAttestationResolver resolver = createAttestationResolver(metadata); - Attestation metadata = resolver.resolve(attestationCertificate).orElse(null); - - assertNotNull(metadata); - assertEquals("foobar", metadata.getMetadataIdentifier().get()); - } - - @Test - public void resolveReturnsEmptyOnUntrustedSignature() throws Exception { - final SimpleAttestationResolver resolver = - new SimpleAttestationResolver( - Collections.singletonList(metadata), - SimpleTrustResolver.fromMetadata(Collections.emptyList())); - - assertEquals(Optional.empty(), resolver.resolve(attestationCertificate)); - } -} diff --git a/webauthn-server-attestation/src/test/java/com/yubico/webauthn/attestation/resolver/SimpleTrustResolverTest.java b/webauthn-server-attestation/src/test/java/com/yubico/webauthn/attestation/resolver/SimpleTrustResolverTest.java deleted file mode 100644 index e2e4570ad..000000000 --- a/webauthn-server-attestation/src/test/java/com/yubico/webauthn/attestation/resolver/SimpleTrustResolverTest.java +++ /dev/null @@ -1,108 +0,0 @@ -// Copyright (c) 2018, Yubico AB -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, this -// list of conditions and the following disclaimer. -// -// 2. Redistributions in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -package com.yubico.webauthn.attestation.resolver; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import com.yubico.internal.util.CertificateParser; -import java.io.IOException; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.security.NoSuchProviderException; -import java.security.Principal; -import java.security.SignatureException; -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; -import java.util.Optional; -import org.junit.Test; -import org.mockito.ArgumentMatchers; - -public class SimpleTrustResolverTest { - - private static final String METADATA_JSON = - "{\"identifier\":\"foobar\",\"version\":1,\"trustedCertificates\":[\"-----BEGIN CERTIFICATE-----\\nMIIDHjCCAgagAwIBAgIEG1BT9zANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZ\\ndWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAw\\nMDBaGA8yMDUwMDkwNDAwMDAwMFowLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290\\nIENBIFNlcmlhbCA0NTcyMDA2MzEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK\\nAoIBAQC/jwYuhBVlqaiYWEMsrWFisgJ+PtM91eSrpI4TK7U53mwCIawSDHy8vUmk\\n5N2KAj9abvT9NP5SMS1hQi3usxoYGonXQgfO6ZXyUA9a+KAkqdFnBnlyugSeCOep\\n8EdZFfsaRFtMjkwz5Gcz2Py4vIYvCdMHPtwaz0bVuzneueIEz6TnQjE63Rdt2zbw\\nnebwTG5ZybeWSwbzy+BJ34ZHcUhPAY89yJQXuE0IzMZFcEBbPNRbWECRKgjq//qT\\n9nmDOFVlSRCt2wiqPSzluwn+v+suQEBsUjTGMEd25tKXXTkNW21wIWbxeSyUoTXw\\nLvGS6xlwQSgNpk2qXYwf8iXg7VWZAgMBAAGjQjBAMB0GA1UdDgQWBBQgIvz0bNGJ\\nhjgpToksyKpP9xv9oDAPBgNVHRMECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIBBjAN\\nBgkqhkiG9w0BAQsFAAOCAQEAjvjuOMDSa+JXFCLyBKsycXtBVZsJ4Ue3LbaEsPY4\\nMYN/hIQ5ZM5p7EjfcnMG4CtYkNsfNHc0AhBLdq45rnT87q/6O3vUEtNMafbhU6kt\\nhX7Y+9XFN9NpmYxr+ekVY5xOxi8h9JDIgoMP4VB1uS0aunL1IGqrNooL9mmFnL2k\\nLVVee6/VR6C5+KSTCMCWppMuJIZII2v9o4dkoZ8Y7QRjQlLfYzd3qGtKbw7xaF1U\\nsG/5xUb/Btwb2X2g4InpiB/yt/3CpQXpiWX/K4mBvUKiGn05ZsqeY1gx4g0xLBqc\\nU9psmyPzK+Vsgw2jeRQ5JlKDyqE0hebfC1tvFu0CCrJFcw==\\n-----END CERTIFICATE-----\"],\"vendorInfo\":{\"name\":\"Yubico\",\"url\":\"https://yubico.com\",\"imageUrl\":\"https://developers.yubico.com/U2F/Images/yubico.png\"},\"devices\":[{\"displayName\":\"YubiKey NEO/NEO-n\",\"deviceId\":\"1.3.6.1.4.1.41482.1.2\",\"deviceUrl\":\"https://www.yubico.com/products/yubikey-hardware/yubikey-neo/\",\"imageUrl\":\"https://developers.yubico.com/U2F/Images/NEO.png\",\"selectors\":[{\"type\":\"x509Extension\",\"parameters\":{\"key\":\"1.3.6.1.4.1.41482.1.2\"}}]}] }"; - private static final String ATTESTATION_CERT = - "MIICGzCCAQWgAwIBAgIEdaP2dTALBgkqhkiG9w0BAQswLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290IENBIFNlcmlhbCA0NTcyMDA2MzEwIBcNMTQwODAxMDAwMDAwWhgPMjA1MDA5MDQwMDAwMDBaMCoxKDAmBgNVBAMMH1l1YmljbyBVMkYgRUUgU2VyaWFsIDE5NzM2Nzk3MzMwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQZo35Damtpl81YdmcbhEuXKAr7xDcQzAy5n3ftAAhtBbu8EeGU4ynfSgLonckqX6J2uXLBppTNE3v2bt+Yf8MLoxIwEDAOBgorBgEEAYLECgECBAAwCwYJKoZIhvcNAQELA4IBAQG9LbiNPgs0sQYOHAJcg+lMk+HCsiWRlYVnbT4I/5lnqU907vY17XYAORd432bU3Nnhsbkvjz76kQJGXeNAF4DPANGGlz8JU+LNEVE2PWPGgEM0GXgB7mZN5Sinfy1AoOdO+3c3bfdJQuXlUxHbo+nDpxxKpzq9gr++RbokF1+0JBkMbaA/qLYL4WdhY5NvaOyMvYpO3sBxlzn6FcP67hlotGH1wU7qhCeh+uur7zDeAWVh7c4QtJOXHkLJQfV3Z7ZMvhkIA6jZJAX99hisABU/SSa5DtgX7AfsHwa04h69AAAWDUzSk3HgOXbUd1FaSOPdlVFkG2N2JllFHykyO3zO"; - - private final SimpleTrustResolver resolver = SimpleTrustResolver.fromMetadataJson(METADATA_JSON); - - public SimpleTrustResolverTest() throws IOException, CertificateException {} - - @Test - public void testResolve() throws Exception { - X509Certificate certificate = CertificateParser.parseDer(ATTESTATION_CERT); - - Optional trustAnchor = resolver.resolveTrustAnchor(certificate); - - assertTrue(trustAnchor.isPresent()); - assertEquals( - "CN=Yubico U2F Root CA Serial 457200631", trustAnchor.get().getSubjectDN().getName()); - } - - @Test - public void resolveReturnsEmptyOnUntrustedSignature() throws Exception { - X509Certificate cert = mock(X509Certificate.class); - doThrow(new SignatureException("Forced failure")).when(cert).verify(ArgumentMatchers.any()); - Principal issuerDN = mock(Principal.class); - when(issuerDN.getName()).thenReturn("CN=Yubico U2F Root CA Serial 457200631"); - when(cert.getIssuerDN()).thenReturn(issuerDN); - - assertEquals(Optional.empty(), resolver.resolveTrustAnchor(cert)); - } - - private void resolveThrowsExceptionOnUnexpectedError(Exception thrownException) throws Exception { - X509Certificate cert = mock(X509Certificate.class); - doThrow(thrownException).when(cert).verify(ArgumentMatchers.any()); - Principal issuerDN = mock(Principal.class); - when(issuerDN.getName()).thenReturn("CN=Yubico U2F Root CA Serial 457200631"); - when(cert.getIssuerDN()).thenReturn(issuerDN); - - resolver.resolveTrustAnchor(cert); - } - - @Test(expected = RuntimeException.class) - public void resolveThrowsExceptionOnCertificateException() throws Exception { - resolveThrowsExceptionOnUnexpectedError(new CertificateException("Forced failure")); - } - - @Test(expected = RuntimeException.class) - public void resolveThrowsExceptionOnNoSuchAlgorithmException() throws Exception { - resolveThrowsExceptionOnUnexpectedError(new NoSuchAlgorithmException("Forced failure")); - } - - @Test(expected = RuntimeException.class) - public void resolveThrowsExceptionOnInvalidKeyException() throws Exception { - resolveThrowsExceptionOnUnexpectedError(new InvalidKeyException("Forced failure")); - } - - @Test(expected = RuntimeException.class) - public void resolveThrowsExceptionOnNoSuchProviderException() throws Exception { - resolveThrowsExceptionOnUnexpectedError(new NoSuchProviderException("Forced failure")); - } -} diff --git a/webauthn-server-attestation/src/test/resources/slf4jtest.properties b/webauthn-server-attestation/src/test/resources/slf4jtest.properties new file mode 100644 index 000000000..eacb68e5f --- /dev/null +++ b/webauthn-server-attestation/src/test/resources/slf4jtest.properties @@ -0,0 +1 @@ +print.level=DEBUG diff --git a/webauthn-server-attestation/src/test/scala/com/yubico/fido/metadata/FidoMds3Examples.scala b/webauthn-server-attestation/src/test/scala/com/yubico/fido/metadata/FidoMds3Examples.scala new file mode 100644 index 000000000..2fe8fb519 --- /dev/null +++ b/webauthn-server-attestation/src/test/scala/com/yubico/fido/metadata/FidoMds3Examples.scala @@ -0,0 +1,469 @@ +package com.yubico.fido.metadata + +object FidoMds3Examples { + + /** Example 1 + * + * @see + * FIDO + * Metadata Service §3.1. Metadata BLOB Format + */ + val BlobPayloadJson: String = + """{ + | "no": 1234, + | "nextUpdate": "2014-03-31", + | "entries": [ + | { + | "aaid": "1234#5678", + | "metadataStatement": "Metadata Statement object as defined in Metadata Statement spec.", + | "statusReports": [ + | { + | "status": "FIDO_CERTIFIED", + | "effectiveDate": "2014-01-04" + | } + | ], + | "timeOfLastStatusChange": "2014-01-04" + | }, + | { + | "attestationCertificateKeyIdentifiers": [ + | "7c0903708b87115b0b422def3138c3c864e44573" + | ], + | "metadataStatement": "Metadata Statement object as defined in Metadata Statement spec.", + | "statusReports": [ + | { + | "status": "FIDO_CERTIFIED", + | "effectiveDate": "2014-01-07" + | }, + | { + | "status": "UPDATE_AVAILABLE", + | "effectiveDate": "2014-02-19", + | "url": "https://example.com/update1234" + | } + | ], + | "timeOfLastStatusChange": "2014-02-19" + | } + | ] + |}""".stripMargin + + /** Example: Encoded Metadata BLOB + * + * @see + * FIDO + * Metadata Service §3.1.7.1. Metadata BLOB Examples + */ + val BlobPayloadBase64url: String = + """ + |ewoJImxlZ2FsSGVhZGVyIjogIlJldHJpZXZhbCBhbmQgdXNlIG9mIHRoaXMgQkxPQiBpbmRpY2F0ZXMg + |YWNjZXB0YW5jZSBvZiB0aGUgYXBwcm9wcmlhdGUgYWdyZWVtZW50IGxvY2F0ZWQgYXQgaHR0cHM6Ly9m + |aWRvYWxsaWFuY2Uub3JnL21ldGFkYXRhL21ldGFkYXRhLWxlZ2FsLXRlcm1zLyIsCgkibm8iOiAxNSwK + |CSJuZXh0VXBkYXRlIjogIjIwMjAtMDMtMzAiLAoJImVudHJpZXMiOiBbewoJCQkiYWFpZCI6ICIxMjM0 + |IzU2NzgiLAoJCQkibWV0YWRhdGFTdGF0ZW1lbnQiOiB7CgkJCQkibGVnYWxIZWFkZXIiOiAiaHR0cHM6 + |Ly9maWRvYWxsaWFuY2Uub3JnL21ldGFkYXRhL21ldGFkYXRhLXN0YXRlbWVudC1sZWdhbC1oZWFkZXIv + |IiwKCQkJCSJkZXNjcmlwdGlvbiI6ICJGSURPIEFsbGlhbmNlIFNhbXBsZSBVQUYgQXV0aGVudGljYXRv + |ciIsCgkJCQkiYWFpZCI6ICIxMjM0IzU2NzgiLAoJCQkJImFsdGVybmF0aXZlRGVzY3JpcHRpb25zIjog + |ewoJCQkJCSJydS1SVSI6ICLQn9GA0LjQvNC10YAgVUFGINCw0YPRgtC10L3RgtC40YTQuNC60LDRgtC- + |0YDQsCDQvtGCIEZJRE8gQWxsaWFuY2UiLAoJCQkJCSJmci1GUiI6ICJFeGVtcGxlIFVBRiBhdXRoZW50 + |aWNhdG9yIGRlIEZJRE8gQWxsaWFuY2UiCgkJCQl9LAoJCQkJImF1dGhlbnRpY2F0b3JWZXJzaW9uIjog + |MiwKCQkJCSJwcm90b2NvbEZhbWlseSI6ICJ1YWYiLAoJCQkJInNjaGVtYSI6IDMsCgkJCQkidXB2Ijog + |W3sKCQkJCQkJIm1ham9yIjogMSwKCQkJCQkJIm1pbm9yIjogMAoJCQkJCX0sCgkJCQkJewoJCQkJCQki + |bWFqb3IiOiAxLAoJCQkJCQkibWlub3IiOiAxCgkJCQkJfQoJCQkJXSwKCQkJCSJhdXRoZW50aWNhdGlv + |bkFsZ29yaXRobXMiOiBbInNlY3AyNTZyMV9lY2RzYV9zaGEyNTZfcmF3Il0sCgkJCQkicHVibGljS2V5 + |QWxnQW5kRW5jb2RpbmdzIjogWyJlY2NfeDk2Ml9yYXciXSwKCQkJCSJhdHRlc3RhdGlvblR5cGVzIjog + |WyJiYXNpY19mdWxsIl0sCgkJCQkidXNlclZlcmlmaWNhdGlvbkRldGFpbHMiOiBbCgkJCQkJW3sKCQkJ + |CQkJInVzZXJWZXJpZmljYXRpb25NZXRob2QiOiAiZmluZ2VycHJpbnRfaW50ZXJuYWwiLAoJCQkJCQki + |YmFEZXNjIjogewoJCQkJCQkJInNlbGZBdHRlc3RlZEZBUiI6IDAuMDAwMDIsCgkJCQkJCQkibWF4UmV0 + |cmllcyI6IDUsCgkJCQkJCQkiYmxvY2tTbG93ZG93biI6IDMwLAoJCQkJCQkJIm1heFRlbXBsYXRlcyI6 + |IDUKCQkJCQkJfQoJCQkJCX1dCgkJCQldLAoJCQkJImtleVByb3RlY3Rpb24iOiBbImhhcmR3YXJlIiwg + |InRlZSJdLAoJCQkJImlzS2V5UmVzdHJpY3RlZCI6IHRydWUsCgkJCQkibWF0Y2hlclByb3RlY3Rpb24i + |OiBbInRlZSJdLAoJCQkJImNyeXB0b1N0cmVuZ3RoIjogMTI4LAoJCQkJImF0dGFjaG1lbnRIaW50Ijog + |WyJpbnRlcm5hbCJdLAoJCQkJInRjRGlzcGxheSI6IFsiYW55IiwgInRlZSJdLAoJCQkJInRjRGlzcGxh + |eUNvbnRlbnRUeXBlIjogImltYWdlL3BuZyIsCgkJCQkidGNEaXNwbGF5UE5HQ2hhcmFjdGVyaXN0aWNz + |IjogW3sKCQkJCQkid2lkdGgiOiAzMjAsCgkJCQkJImhlaWdodCI6IDQ4MCwKCQkJCQkiYml0RGVwdGgi + |OiAxNiwKCQkJCQkiY29sb3JUeXBlIjogMiwKCQkJCQkiY29tcHJlc3Npb24iOiAwLAoJCQkJCSJmaWx0 + |ZXIiOiAwLAoJCQkJCSJpbnRlcmxhY2UiOiAwCgkJCQl9XSwKCQkJCSJhdHRlc3RhdGlvblJvb3RDZXJ0 + |aWZpY2F0ZXMiOiBbCgkJCQkJIk1JSUNQVENDQWVPZ0F3SUJBZ0lKQU91ZXh2VTNPeTJ3TUFvR0NDcUdT + |TTQ5QkFNQ01Ic3hJREFlQmdOVkJBTU1GMU5oYlhCc1pTQkJkSFJsYzNSaGRHbHZiaUJTYjI5ME1SWXdG + |QVlEVlFRS0RBMUdTVVJQSUVGc2JHbGhibU5sTVJFd0R3WURWUVFMREFoVlFVWWdWRmRITERFU01CQUdB + |MVVFQnd3SlVHRnNieUJCYkhSdk1Rc3dDUVlEVlFRSURBSkRRVEVMTUFrR0ExVUVCaE1DVlZNd0hoY05N + |VFF3TmpFNE1UTXpNek15V2hjTk5ERXhNVEF6TVRNek16TXlXakI3TVNBd0hnWURWUVFEREJkVFlXMXdi + |R1VnUVhSMFpYTjBZWFJwYjI0Z1VtOXZkREVXTUJRR0ExVUVDZ3dOUmtsRVR5QkJiR3hwWVc1alpURVJN + |QThHQTFVRUN3d0lWVUZHSUZSWFJ5d3hFakFRQmdOVkJBY01DVkJoYkc4Z1FXeDBiekVMTUFrR0ExVUVD + |QXdDUTBFeEN6QUpCZ05WQkFZVEFsVlRNRmt3RXdZSEtvWkl6ajBDQVFZSUtvWkl6ajBEQVFjRFFnQUVI + |OGh2MkQwSFhhNTkvQm1wUTdSWmVoTC9GTUd6RmQxUUJnOXZBVXBPWjNham51UTk0UFI3YU16SDMzblVT + |QnI4ZkhZRHJxT0JiNThweEdxSEpSeVgvNk5RTUU0d0hRWURWUjBPQkJZRUZQb0hBM0NMaHhGYkMwSXQ3 + |ekU0dzhoazVFSi9NQjhHQTFVZEl3UVlNQmFBRlBvSEEzQ0xoeEZiQzBJdDd6RTR3OGhrNUVKL01Bd0dB + |MVVkRXdRRk1BTUJBZjh3Q2dZSUtvWkl6ajBFQXdJRFNBQXdSUUloQUowNlFTWHQ5aWhJYkVLWUtJanNQ + |a3JpVmRMSWd0ZnNiRFN1N0VySmZ6cjRBaUJxb1lDWmYwK3pJNTVhUWVBSGpJekE5WG02M3JydUF4Qlo5 + |cHM5ejJYTmxRPT0iCgkJCQldLAoJCQkJImljb24iOiAiZGF0YTppbWFnZS9wbmc7YmFzZTY0LGlWQk9S + |dzBLR2dvQUFBQU5TVWhFVWdBQUFFOEFBQUF2Q0FZQUFBQ2l3SmZjQUFBQUFYTlNSMElBcnM0YzZRQUFB + |QVJuUVUxQkFBQ3hqd3Y4WVFVQUFBQUpjRWhaY3dBQURzTUFBQTdEQWNkdnFHUUFBQWFoU1VSQlZHaEQ3 + |WnI1YnhSbEdNZjlLelRCOEFNL1lFaEUyVzdwUVpjV0tLQmNsU3BIQVRsRUxBUkU3a05FQ0NBM0ZrV0sw + |Q0tLU0NGSXNLQmNnVkNEV0dORVNkQVlpZHdnZ2dKQmlSaU1oRmMvNHd5ODg4NHp1OU5kbG5HVGZaSlAy + |bjNuTysrODg5MzNmdmVCQngrUHFDekprVFV2QmJMbXBVRFd2QlRJbXBjQ1NadlhMQ2RYOVIwNVNrMTli + |YjVhdGY1OTlmRysvZXJBNTQxcTQ3YVAxTExWYTlTSXlWTlVpOElpOGQ1a0dUc2kzME5GdjdhaTluN1Fa + |UE13YmR5czJlclUyWE1xVWR5OCtaY2FObUdpbUU4eVhOM1JVZDNhMThuRjBmVWxvdlorMENUeldwZDJW + |aitlT20xYkV5eTZEeDRpNXBVTUdXdmVvNTA2cTIyN2R0dVdCSXVmZnI2b1dwVjBGUE5MaG93MTc1MU5t + |MjFMdlBIM3JWdFdqZno2NkxmcWw4dFg3RlJsOVlGU1hzbVNzZWI5Y2VPR2JZazdNTlVjR1BnOFpzYk1l + |OXJmUVVhYVYvSk1YOXNxZHpEQ1N2cDBrWkhtVFpnOXg3YkxIY01uVGhiMTZlSittVmZRcTh5YVVaUU5H + |NjRpWForMC9rcTZ1T1pGTzBRdGF0ZFdLZlhuUlE5OUJqOTFSNU9JRm5rNTRqTjBta1VpcWxPM1hEVytN + |bCs5OG1LQjZ0VzdyV3BaY1BjKzB6ZzR0THJZbFVjODZFNmVHRGpJTXViVnBjdXNlYXJmZ0lZR1JrNmJy + |aFpWci9KY0h6b29MNzU1MGplZExFeG9wV2NBcGkyWlVxaHU3Skx2clZzUVU4MXprek9QZWVtTVJZdlZ1 + |UXNYN1BiaURRWTVKdlpvbmZ0SysxVlk4SDl1dHg1MzBoMG9iK2ptUllxajZvdWFZdkVlblcvV2xZanA4 + |Y3diTW02ODJ0UHdxVzFSNHRqLzJTSDEzSVJKWWw0bW9adlhwaVNxRHI3ZFh0UUh4YS9QSzMvK0JXc0sx + |ZFRnSHU2Vjh0UUozYndGa3dwRnJVT1E1MHMxcjNsZXZtOHpaY3ExNytCQmF3N0s4bEVLNXF6a1llYXJr + |OUE4cDdQM0d6REsrbmQzRFFvdys2VUM4U1ZOODJpdXYzOGltN050YVh0VjFDVnE2Umd3NHBrc21iZGkz + |YnUyRGU3WWZhQkJ4Y3FmdnFQclVqRlFOVFEyMmxmZFVWVlQ2OHJUSktGNURuU21VamdkcWc0bVNTOXBt + |c2ZESlIzRzZUb0gwaVc5YVY3TFdMSFlYS2xsVER0MExUQXRrWUlhYW1wMVFqVnYrK3V5R1V4VmRKMERO + |VlhTbStiMXFSeHBsODRkZGZYMUxwMU8vZDY5dHNvZDB2czVoR3JlOXh1OG8rZnBMUjFjR2hOVEQ2WjU3 + |QzlLTVdYZWZKZE9aOTRiYjlvcWQxUk9uUzdxSVRUekhpbU1xaXZiTzNnMERkVnlrM1dRQmhCenRLMzVZ + |S05kT25jOE8zYWNTNmZEWkZnS2FYTHNFSnA1cmRybGlCcXA4OWNKY3MvbTdUdnMwcmtqR2ZONGIwa1Bv + |Wm4zVUp1SU9ybloyMnlQMWZtdlV4K081Z1NxZWJWMW0relN1WU5WaHE3VFdiRGlMVnZsanBsTGxvcDZD + |TFhQKzJxdHZHTElMLzF2aW1JU2RNQmd6U29GWnl1NlRxZCtqenhnc1BhVjlCQ3FlZS9OallrNnY2bEs5 + |Y3dpVWMvU1R0ZjFIRHBNM2I1OTJ5N2gzVGh4NW96SzY5SExwWVd1QXdhcVM1Y3YyNnE3Y2ViOGVmVllh + |UmVQM2lGVTh6ajFrblN3WlhITW1uQ2pZME9nYWxvN1VRZlNDTTNxUVFyMkgvWEZQN3NzWHg0NVlsOTFC + |eWVDZXA0bW9ab0grMWZHM3hENHRUN3g4a3d5ajhud2I5ZXYyNlYwQjZkKzdINHpLdnVkQUg1MzdGanF5 + |ek9IZEpuSEV1em1YcS9XanhPYnZOTWJ2N25oeXdzWDJhVnNXdEM4KzQ4YUxlYXBFN3A1d0taaTBBMkFR + |UlY1bnZSNEUrdUpjK2I2MWtBcHFJbnhCZ21kLzRWNVFQL210MThIREM3c1JIZnRtZXU1bG1oVjBybi9B + |TFgyMzJicWQ0QkZuRHg3VmkxY1dTMnVmZjBJYkI0N3FleHhtVWo5UXV0WWp1cGQzdFlENmFiV0JCTXJo + |K2FwTmJPS3JORjErdWdDYTRyaVhHZndNUFB0VmlhdmhVM1lNT0FBbnVVYi9SMDdMMHlPU2VPYWRFODhB + |cHNYRkdmZjMweW5obEpnTTUxQ1U2dk45RXpnbnB2SEJGVXlpVnJhZVBpd0o1M0RGNVpUWm5vbUVOZzg1 + |a05VZDJvSmkyV3ByNE9tbWtmTjR4NHpIZmlWRmM4RHY4Tnp1aE5xT2lkaWxHdkE2REd1ZVp3Tzc4QUFR + |bjZjaUVrNitydzVWY3ZqdnFORFlQT29JVXdhS1NocnhBdVhMbGtINGFZdUdmTVlEYzEwV0Y1VGEzMWhQ + |Sk9mY1VoclUvSmxJTmk2YzZlbFJZZEJwbzYrK1lmang2MWxHTmZSbTRNRDVySjFqM0ZvR0huakRTQk5h + |cllVZ01MeU1zektwYjd0WHBvSGZQczhoM1dwMUx6TmZOazU0WHhDMXdER1VtWXpYWWVmaDZ6L2NLdFZt + |NEVCeGE5VlFHRHpZcjNMclVNUmpIRUtrazd6YUZLWVFBMmhHUVUxeis4NU5GV3BYRHJrejN2eDEwR3F4 + |UTZCemVOYm9CazVuOGs0bmViUmgrazFoV2Z4VEYwRDFFeVdVczVuditkZ1FxS2F4enVDZEUwaXNIbDAy + |TlE4YWgwbVhyMTJMYTNtMGY5d2lrOSt3TE5UTVkvODZNUG84eWkzMU9meG1UNlBXb3FHOStEWnVrWW5h + |NTZtU1p0NVdXU3k1cVZBMXJ3VXlKcVhBbG56a2lhaS9nSFNEN1JrVHlpaG9nQUFBQUJKUlU1RXJrSmdn + |Zz09IgoJCQl9LAoJCQkic3RhdHVzUmVwb3J0cyI6IFt7CgkJCQkic3RhdHVzIjogIkZJRE9fQ0VSVElG + |SUVEIiwKCQkJCSJlZmZlY3RpdmVEYXRlIjogIjIwMTQtMDEtMDQiCgkJCX1dLAoJCQkidGltZU9mTGFz + |dFN0YXR1c0NoYW5nZSI6ICIyMDE0LTAxLTA0IgoJCX0sCgkJewoJCQkiYWFndWlkIjogIjAxMzJkMTEw + |LWJmNGUtNDIwOC1hNDAzLWFiNGY1ZjEyZWZlNSIsCgkJCSJtZXRhZGF0YVN0YXRlbWVudCI6IHsKCQkJ + |CSJsZWdhbEhlYWRlciI6ICJodHRwczovL2ZpZG9hbGxpYW5jZS5vcmcvbWV0YWRhdGEvbWV0YWRhdGEt + |c3RhdGVtZW50LWxlZ2FsLWhlYWRlci8iLAoJCQkJImRlc2NyaXB0aW9uIjogIkZJRE8gQWxsaWFuY2Ug + |U2FtcGxlIEZJRE8yIEF1dGhlbnRpY2F0b3IiLAoJCQkJImFhZ3VpZCI6ICIwMTMyZDExMC1iZjRlLTQy + |MDgtYTQwMy1hYjRmNWYxMmVmZTUiLAoJCQkJImFsdGVybmF0aXZlRGVzY3JpcHRpb25zIjogewoJCQkJ + |CSJydS1SVSI6ICLQn9GA0LjQvNC10YAgRklETzIg0LDRg9GC0LXQvdGC0LjRhNC40LrQsNGC0L7RgNCw + |INC-0YIgRklETyBBbGxpYW5jZSIsCgkJCQkJImZyLUZSIjogIkV4ZW1wbGUgRklETzIgYXV0aGVudGlj + |YXRvciBkZSBGSURPIEFsbGlhbmNlIiwKCQkJCQkiemgtQ04iOiAi5L6G6IeqRklETyBBbGxpYW5jZeea + |hOekuuS-i0ZJRE8y6Lqr5Lu96amX6K2J5ZmoIgoJCQkJfSwKCQkJCSJwcm90b2NvbEZhbWlseSI6ICJm + |aWRvMiIsCgkJCQkic2NoZW1hIjogMywKCQkJCSJhdXRoZW50aWNhdG9yVmVyc2lvbiI6IDUsCgkJCQki + |dXB2IjogW3sKCQkJCQkibWFqb3IiOiAxLAoJCQkJCSJtaW5vciI6IDAKCQkJCX1dLAoJCQkJImF1dGhl + |bnRpY2F0aW9uQWxnb3JpdGhtcyI6IFsic2VjcDI1NnIxX2VjZHNhX3NoYTI1Nl9yYXciLCAicnNhc3Nh + |X3BrY3N2MTVfc2hhMjU2X3JhdyJdLAoJCQkJInB1YmxpY0tleUFsZ0FuZEVuY29kaW5ncyI6IFsiY29z + |ZSJdLAoJCQkJImF0dGVzdGF0aW9uVHlwZXMiOiBbImJhc2ljX2Z1bGwiXSwKCQkJCSJ1c2VyVmVyaWZp + |Y2F0aW9uRGV0YWlscyI6IFsKCQkJCQlbewoJCQkJCQkidXNlclZlcmlmaWNhdGlvbk1ldGhvZCI6ICJu + |b25lIgoJCQkJCX1dLAoJCQkJCVt7CgkJCQkJCSJ1c2VyVmVyaWZpY2F0aW9uTWV0aG9kIjogInByZXNl + |bmNlX2ludGVybmFsIgoJCQkJCX1dLAoJCQkJCVt7CgkJCQkJCSJ1c2VyVmVyaWZpY2F0aW9uTWV0aG9k + |IjogInBhc3Njb2RlX2V4dGVybmFsIiwKCQkJCQkJImNhRGVzYyI6IHsKCQkJCQkJCSJiYXNlIjogMTAs + |CgkJCQkJCQkibWluTGVuZ3RoIjogNAoJCQkJCQl9CgkJCQkJfV0sCgkJCQkJW3sKCQkJCQkJCSJ1c2Vy + |VmVyaWZpY2F0aW9uTWV0aG9kIjogInBhc3Njb2RlX2V4dGVybmFsIiwKCQkJCQkJCSJjYURlc2MiOiB7 + |CgkJCQkJCQkJImJhc2UiOiAxMCwKCQkJCQkJCQkibWluTGVuZ3RoIjogNAoJCQkJCQkJfQoJCQkJCQl9 + |LAoJCQkJCQl7CgkJCQkJCQkidXNlclZlcmlmaWNhdGlvbk1ldGhvZCI6ICJwcmVzZW5jZV9pbnRlcm5h + |bCIKCQkJCQkJfQoJCQkJCV0KCQkJCV0sCgkJCQkia2V5UHJvdGVjdGlvbiI6IFsiaGFyZHdhcmUiLCAi + |c2VjdXJlX2VsZW1lbnQiXSwKCQkJCSJtYXRjaGVyUHJvdGVjdGlvbiI6IFsib25fY2hpcCJdLAoJCQkJ + |ImNyeXB0b1N0cmVuZ3RoIjogMTI4LAoJCQkJImF0dGFjaG1lbnRIaW50IjogWyJleHRlcm5hbCIsICJ3 + |aXJlZCIsICJ3aXJlbGVzcyIsICJuZmMiXSwKCQkJCSJ0Y0Rpc3BsYXkiOiBbXSwKCQkJCSJhdHRlc3Rh + |dGlvblJvb3RDZXJ0aWZpY2F0ZXMiOiBbCgkJCQkJIk1JSUNQVENDQWVPZ0F3SUJBZ0lKQU91ZXh2VTNP + |eTJ3TUFvR0NDcUdTTTQ5QkFNQ01Ic3hJREFlQmdOVkJBTU1GMU5oYlhCc1pTQkJkSFJsYzNSaGRHbHZi + |aUJTYjI5ME1SWXdGQVlEVlFRS0RBMUdTVVJQSUVGc2JHbGhibU5sTVJFd0R3WURWUVFMREFoVlFVWWdW + |RmRITERFU01CQUdBMVVFQnd3SlVHRnNieUJCYkhSdk1Rc3dDUVlEVlFRSURBSkRRVEVMTUFrR0ExVUVC + |aE1DVlZNd0hoY05NVFF3TmpFNE1UTXpNek15V2hjTk5ERXhNVEF6TVRNek16TXlXakI3TVNBd0hnWURW + |UVFEREJkVFlXMXdiR1VnUVhSMFpYTjBZWFJwYjI0Z1VtOXZkREVXTUJRR0ExVUVDZ3dOUmtsRVR5QkJi + |R3hwWVc1alpURVJNQThHQTFVRUN3d0lWVUZHSUZSWFJ5d3hFakFRQmdOVkJBY01DVkJoYkc4Z1FXeDBi + |ekVMTUFrR0ExVUVDQXdDUTBFeEN6QUpCZ05WQkFZVEFsVlRNRmt3RXdZSEtvWkl6ajBDQVFZSUtvWkl6 + |ajBEQVFjRFFnQUVIOGh2MkQwSFhhNTkvQm1wUTdSWmVoTC9GTUd6RmQxUUJnOXZBVXBPWjNham51UTk0 + |UFI3YU16SDMzblVTQnI4ZkhZRHJxT0JiNThweEdxSEpSeVgvNk5RTUU0d0hRWURWUjBPQkJZRUZQb0hB + |M0NMaHhGYkMwSXQ3ekU0dzhoazVFSi9NQjhHQTFVZEl3UVlNQmFBRlBvSEEzQ0xoeEZiQzBJdDd6RTR3 + |OGhrNUVKL01Bd0dBMVVkRXdRRk1BTUJBZjh3Q2dZSUtvWkl6ajBFQXdJRFNBQXdSUUloQUowNlFTWHQ5 + |aWhJYkVLWUtJanNQa3JpVmRMSWd0ZnNiRFN1N0VySmZ6cjRBaUJxb1lDWmYwK3pJNTVhUWVBSGpJekE5 + |WG02M3JydUF4Qlo5cHM5ejJYTmxRPT0iCgkJCQldLAoJCQkJImljb24iOiAiZGF0YTppbWFnZS9wbmc7 + |YmFzZTY0LGlWQk9SdzBLR2dvQUFBQU5TVWhFVWdBQUFFOEFBQUF2Q0FZQUFBQ2l3SmZjQUFBQUFYTlNS + |MElBcnM0YzZRQUFBQVJuUVUxQkFBQ3hqd3Y4WVFVQUFBQUpjRWhaY3dBQURzTUFBQTdEQWNkdnFHUUFB + |QWFoU1VSQlZHaEQ3WnI1YnhSbEdNZjlLelRCOEFNL1lFaEUyVzdwUVpjV0tLQmNsU3BIQVRsRUxBUkU3 + |a05FQ0NBM0ZrV0swQ0tLU0NGSXNLQmNnVkNEV0dORVNkQVlpZHdnZ2dKQmlSaU1oRmMvNHd5ODg4NHp1 + |OU5kbG5HVGZaSlAybjNuTysrODg5MzNmdmVCQngrUHFDekprVFV2QmJMbXBVRFd2QlRJbXBjQ1NadlhM + |Q2RYOVIwNVNrMTliYjVhdGY1OTlmRysvZXJBNTQxcTQ3YVAxTExWYTlTSXlWTlVpOElpOGQ1a0dUc2kz + |ME5GdjdhaTluN1FaUE13YmR5czJlclUyWE1xVWR5OCtaY2FObUdpbUU4eVhOM1JVZDNhMThuRjBmVWxv + |dlorMENUeldwZDJWaitlT20xYkV5eTZEeDRpNXBVTUdXdmVvNTA2cTIyN2R0dVdCSXVmZnI2b1dwVjBG + |UE5MaG93MTc1MU5tMjFMdlBIM3JWdFdqZno2NkxmcWw4dFg3RlJsOVlGU1hzbVNzZWI5Y2VPR2JZazdN + |TlVjR1BnOFpzYk1lOXJmUVVhYVYvSk1YOXNxZHpEQ1N2cDBrWkhtVFpnOXg3YkxIY01uVGhiMTZlSitt + |VmZRcTh5YVVaUU5HNjRpWForMC9rcTZ1T1pGTzBRdGF0ZFdLZlhuUlE5OUJqOTFSNU9JRm5rNTRqTjBt + |a1VpcWxPM1hEVytNbCs5OG1LQjZ0VzdyV3BaY1BjKzB6ZzR0THJZbFVjODZFNmVHRGpJTXViVnBjdXNl + |YXJmZ0lZR1JrNmJyaFpWci9KY0h6b29MNzU1MGplZExFeG9wV2NBcGkyWlVxaHU3Skx2clZzUVU4MXpr + |ek9QZWVtTVJZdlZ1UXNYN1BiaURRWTVKdlpvbmZ0SysxVlk4SDl1dHg1MzBoMG9iK2ptUllxajZvdWFZ + |dkVlblcvV2xZanA4Y3diTW02ODJ0UHdxVzFSNHRqLzJTSDEzSVJKWWw0bW9adlhwaVNxRHI3ZFh0UUh4 + |YS9QSzMvK0JXc0sxZFRnSHU2Vjh0UUozYndGa3dwRnJVT1E1MHMxcjNsZXZtOHpaY3ExNytCQmF3N0s4 + |bEVLNXF6a1llYXJrOUE4cDdQM0d6REsrbmQzRFFvdys2VUM4U1ZOODJpdXYzOGltN050YVh0VjFDVnE2 + |Umd3NHBrc21iZGkzYnUyRGU3WWZhQkJ4Y3FmdnFQclVqRlFOVFEyMmxmZFVWVlQ2OHJUSktGNURuU21V + |amdkcWc0bVNTOXBtc2ZESlIzRzZUb0gwaVc5YVY3TFdMSFlYS2xsVER0MExUQXRrWUlhYW1wMVFqVnYr + |K3V5R1V4VmRKMEROVlhTbStiMXFSeHBsODRkZGZYMUxwMU8vZDY5dHNvZDB2czVoR3JlOXh1OG8rZnBM + |UjFjR2hOVEQ2WjU3QzlLTVdYZWZKZE9aOTRiYjlvcWQxUk9uUzdxSVRUekhpbU1xaXZiTzNnMERkVnlr + |M1dRQmhCenRLMzVZS05kT25jOE8zYWNTNmZEWkZnS2FYTHNFSnA1cmRybGlCcXA4OWNKY3MvbTdUdnMw + |cmtqR2ZONGIwa1BvWm4zVUp1SU9ybloyMnlQMWZtdlV4K081Z1NxZWJWMW0relN1WU5WaHE3VFdiRGlM + |VnZsanBsTGxvcDZDTFhQKzJxdHZHTElMLzF2aW1JU2RNQmd6U29GWnl1NlRxZCtqenhnc1BhVjlCQ3Fl + |ZS9OallrNnY2bEs5Y3dpVWMvU1R0ZjFIRHBNM2I1OTJ5N2gzVGh4NW96SzY5SExwWVd1QXdhcVM1Y3Yy + |NnE3Y2ViOGVmVllhUmVQM2lGVTh6ajFrblN3WlhITW1uQ2pZME9nYWxvN1VRZlNDTTNxUVFyMkgvWEZQ + |N3NzWHg0NVlsOTFCeWVDZXA0bW9ab0grMWZHM3hENHRUN3g4a3d5ajhud2I5ZXYyNlYwQjZkKzdINHpL + |dnVkQUg1MzdGanF5ek9IZEpuSEV1em1YcS9XanhPYnZOTWJ2N25oeXdzWDJhVnNXdEM4KzQ4YUxlYXBF + |N3A1d0taaTBBMkFRUlY1bnZSNEUrdUpjK2I2MWtBcHFJbnhCZ21kLzRWNVFQL210MThIREM3c1JIZnRt + |ZXU1bG1oVjBybi9BTFgyMzJicWQ0QkZuRHg3VmkxY1dTMnVmZjBJYkI0N3FleHhtVWo5UXV0WWp1cGQz + |dFlENmFiV0JCTXJoK2FwTmJPS3JORjErdWdDYTRyaVhHZndNUFB0VmlhdmhVM1lNT0FBbnVVYi9SMDdM + |MHlPU2VPYWRFODhBcHNYRkdmZjMweW5obEpnTTUxQ1U2dk45RXpnbnB2SEJGVXlpVnJhZVBpd0o1M0RG + |NVpUWm5vbUVOZzg1a05VZDJvSmkyV3ByNE9tbWtmTjR4NHpIZmlWRmM4RHY4Tnp1aE5xT2lkaWxHdkE2 + |REd1ZVp3Tzc4QUFRbjZjaUVrNitydzVWY3ZqdnFORFlQT29JVXdhS1NocnhBdVhMbGtINGFZdUdmTVlE + |YzEwV0Y1VGEzMWhQSk9mY1VoclUvSmxJTmk2YzZlbFJZZEJwbzYrK1lmang2MWxHTmZSbTRNRDVySjFq + |M0ZvR0huakRTQk5hcllVZ01MeU1zektwYjd0WHBvSGZQczhoM1dwMUx6TmZOazU0WHhDMXdER1VtWXpY + |WWVmaDZ6L2NLdFZtNEVCeGE5VlFHRHpZcjNMclVNUmpIRUtrazd6YUZLWVFBMmhHUVUxeis4NU5GV3BY + |RHJrejN2eDEwR3F4UTZCemVOYm9CazVuOGs0bmViUmgrazFoV2Z4VEYwRDFFeVdVczVuditkZ1FxS2F4 + |enVDZEUwaXNIbDAyTlE4YWgwbVhyMTJMYTNtMGY5d2lrOSt3TE5UTVkvODZNUG84eWkzMU9meG1UNlBX + |b3FHOStEWnVrWW5hNTZtU1p0NVdXU3k1cVZBMXJ3VXlKcVhBbG56a2lhaS9nSFNEN1JrVHlpaG9nQUFB + |QUJKUlU1RXJrSmdnZz09IiwKCQkJCSJzdXBwb3J0ZWRFeHRlbnNpb25zIjogW3sKCQkJCQkJImlkIjog + |ImhtYWMtc2VjcmV0IiwKCQkJCQkJImZhaWxfaWZfdW5rbm93biI6IGZhbHNlCgkJCQkJfSwKCQkJCQl7 + |CgkJCQkJCSJpZCI6ICJjcmVkUHJvdGVjdCIsCgkJCQkJCSJmYWlsX2lmX3Vua25vd24iOiBmYWxzZQoJ + |CQkJCX0KCQkJCV0sCgkJCQkiYXV0aGVudGljYXRvckdldEluZm8iOiB7CgkJCQkJInZlcnNpb25zIjog + |WyJVMkZfVjIiLCAiRklET18yXzAiXSwKCQkJCQkiZXh0ZW5zaW9ucyI6IFsiY3JlZFByb3RlY3QiLCAi + |aG1hYy1zZWNyZXQiXSwKCQkJCQkiYWFndWlkIjogIjAxMzJkMTEwYmY0ZTQyMDhhNDAzYWI0ZjVmMTJl + |ZmU1IiwKCQkJCQkib3B0aW9ucyI6IHsKCQkJCQkJInBsYXQiOiAiZmFsc2UiLAoJCQkJCQkicmsiOiAi + |dHJ1ZSIsCgkJCQkJCSJjbGllbnRQaW4iOiAidHJ1ZSIsCgkJCQkJCSJ1cCI6ICJ0cnVlIiwKCQkJCQkJ + |InV2IjogInRydWUiLAoJCQkJCQkidXZUb2tlbiI6ICJmYWxzZSIsCgkJCQkJCSJjb25maWciOiAiZmFs + |c2UiCgkJCQkJfSwKCQkJCQkibWF4TXNnU2l6ZSI6IDEyMDAsCgkJCQkJInBpblV2QXV0aFByb3RvY29s + |cyI6IFsxXSwKCQkJCQkibWF4Q3JlZGVudGlhbENvdW50SW5MaXN0IjogMTYsCgkJCQkJIm1heENyZWRl + |bnRpYWxJZExlbmd0aCI6IDEyOCwKCQkJCQkidHJhbnNwb3J0cyI6IFsidXNiIiwgIm5mYyJdLAoJCQkJ + |CSJhbGdvcml0aG1zIjogW3sKCQkJCQkJCSJ0eXBlIjogInB1YmxpYy1rZXkiLAoJCQkJCQkJImFsZyI6 + |IC03CgkJCQkJCX0sCgkJCQkJCXsKCQkJCQkJCSJ0eXBlIjogInB1YmxpYy1rZXkiLAoJCQkJCQkJImFs + |ZyI6IC0yNTcKCQkJCQkJfQoJCQkJCV0sCgkJCQkJIm1heEF1dGhlbnRpY2F0b3JDb25maWdMZW5ndGgi + |OiAxMDI0LAoJCQkJCSJkZWZhdWx0Q3JlZFByb3RlY3QiOiAyLAoJCQkJCSJmaXJtd2FyZVZlcnNpb24i + |OiA1CgkJCQl9CgkJCX0sCgkJCSJzdGF0dXNSZXBvcnRzIjogW3sKCQkJCQkic3RhdHVzIjogIkZJRE9f + |Q0VSVElGSUVEIiwKCQkJCQkiZWZmZWN0aXZlRGF0ZSI6ICIyMDE5LTAxLTA0IgoJCQkJfSwKCQkJCXsK + |CQkJCQkic3RhdHVzIjogIkZJRE9fQ0VSVElGSUVEX0wxIiwKCQkJCQkiZWZmZWN0aXZlRGF0ZSI6ICIy + |MDIwLTExLTE5IiwKCQkJCQkiY2VydGlmaWNhdGlvbkRlc2NyaXB0b3IiOiAiRklETyBBbGxpYW5jZSBT + |YW1wbGUgRklETzIgQXV0aGVudGljYXRvciIsCgkJCQkJImNlcnRpZmljYXRlTnVtYmVyIjogIkZJRE8y + |MTAwMDIwMTUxMjIxMDAxIiwKCQkJCQkiY2VydGlmaWNhdGlvblBvbGljeVZlcnNpb24iOiAiMS4wLjEi + |LAoJCQkJCSJjZXJ0aWZpY2F0aW9uUmVxdWlyZW1lbnRzVmVyc2lvbiI6ICIxLjAuMSIKCQkJCX0KCQkJ + |XSwKCQkJInRpbWVPZkxhc3RTdGF0dXNDaGFuZ2UiOiAiMjAxOS0wMS0wNCIKCQl9CgldCn0 + |""".stripMargin.replaceAll(raw"[ \n]+", "") + + /** Example: JWT + * + * @see + * FIDO + * Metadata Service §3.1.7.1. Metadata BLOB Examples + */ + val BlobJwt: String = + """ + |eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsIng1YyI6WyJNSUlDWlRDQ0FndWdBd0lCQWdJQkFUQUtC + |Z2dxaGtqT1BRUURBakNCb3pFbk1DVUdBMVVFQXd3ZVJWaEJUVkJNUlNCTlJGTXpJRlJGVTFRZ1NVNVVS + |VkpOUlVSSlFWUkZNU0l3SUFZSktvWklodmNOQVFrQkZoTmxlR0Z0Y0d4bFFHVjRZVzF3YkdVdVkyOXRN + |UlF3RWdZRFZRUUtEQXRGZUdGdGNHeGxJRTlTUnpFUU1BNEdBMVVFQ3d3SFJYaGhiWEJzWlRFTE1Ba0dB + |MVVFQmhNQ1ZWTXhDekFKQmdOVkJBZ01BazFaTVJJd0VBWURWUVFIREFsWFlXdGxabWxsYkdRd0hoY05N + |akV3TkRFNU1URXpOVEEzV2hjTk16RXdOREUzTVRFek5UQTNXakNCcFRFcE1DY0dBMVVFQXd3Z1JWaEJU + |VkJNUlNCTlJGTXpJRk5KUjA1SlRrY2dRMFZTVkVsR1NVTkJWRVV4SWpBZ0Jna3Foa2lHOXcwQkNRRVdF + |MlY0WVcxd2JHVkFaWGhoYlhCc1pTNWpiMjB4RkRBU0JnTlZCQW9NQzBWNFlXMXdiR1VnVDFKSE1SQXdE + |Z1lEVlFRTERBZEZlR0Z0Y0d4bE1Rc3dDUVlEVlFRR0V3SlZVekVMTUFrR0ExVUVDQXdDVFZreEVqQVFC + |Z05WQkFjTUNWZGhhMlZtYVdWc1pEQlpNQk1HQnlxR1NNNDlBZ0VHQ0NxR1NNNDlBd0VIQTBJQUJOUUpz + |NndUcWl4YytTK1ZEQWFqRmxQTmF0MTBLRVdKRTVqY1dPdm02cXBPOVNEQUFNWnZiNEhIcnZzK1A1WVJw + |SHJTbFVQZHZLK3VFUWJkV2czMVA5dWpMREFxTUFrR0ExVWRFd1FDTUFBd0hRWURWUjBPQkJZRUZMcXNh + |cGNYVjRab1ZIQW5ScFBad1FlN1l5MjBNQW9HQ0NxR1NNNDlCQU1DQTBnQU1FVUNJUUM2N3phOEVJdXlS + |aUtnTkRYSVAxczFhTHIzanpIOVdWWGZIeDRiSit6Q3NnSWdHL3RWQnV0T0pVVSt2dm9ISW8vb3RBVUFj + |SDViTkhQM3VJemlEUytQVFVjPSIsIk1JSUVIekNDQWdlZ0F3SUJBZ0lCQWpBTkJna3Foa2lHOXcwQkFR + |c0ZBRENCbXpFZk1CMEdBMVVFQXd3V1JWaEJUVkJNUlNCTlJGTXpJRlJGVTFRZ1VrOVBWREVpTUNBR0NT + |cUdTSWIzRFFFSkFSWVRaWGhoYlhCc1pVQmxlR0Z0Y0d4bExtTnZiVEVVTUJJR0ExVUVDZ3dMUlhoaGJY + |QnNaU0JQVWtjeEVEQU9CZ05WQkFzTUIwVjRZVzF3YkdVeEN6QUpCZ05WQkFZVEFsVlRNUXN3Q1FZRFZR + |UUlEQUpOV1RFU01CQUdBMVVFQnd3SlYyRnJaV1pwWld4a01CNFhEVEl4TURReE9URXhNelV3TjFvWERU + |UTRNRGt3TkRFeE16VXdOMW93Z2FNeEp6QWxCZ05WQkFNTUhrVllRVTFRVEVVZ1RVUlRNeUJVUlZOVUlF + |bE9WRVZTVFVWRVNVRlVSVEVpTUNBR0NTcUdTSWIzRFFFSkFSWVRaWGhoYlhCc1pVQmxlR0Z0Y0d4bExt + |TnZiVEVVTUJJR0ExVUVDZ3dMUlhoaGJYQnNaU0JQVWtjeEVEQU9CZ05WQkFzTUIwVjRZVzF3YkdVeEN6 + |QUpCZ05WQkFZVEFsVlRNUXN3Q1FZRFZRUUlEQUpOV1RFU01CQUdBMVVFQnd3SlYyRnJaV1pwWld4a01G + |a3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRU5HdW1CYlluRlFuVGpQMVJTZmM3MGhzaGdi + |aUkxWnRwd1E1bjZ4UkxBL1dxMFBTQ2ZMbDVxUStyN2RsY0sxZDNyM3ZMYSt2bTZHNnZLSEdDUEVlVXpx + |TXZNQzB3REFZRFZSMFRCQVV3QXdFQi96QWRCZ05WSFE0RUZnUVVOazZGNFJKbkdHVkZlKzAvY2Jad2Zy + |WmQ3WlV3RFFZSktvWklodmNOQVFFTEJRQURnZ0lCQUNucDFmbTBGS2xXbVV0VHBsTHVZZzdtcHM0eFAv + |Q091OGRuYjM4dTFuTURWdU9UNCtDWmFpTTlBR3ozMTNHRDIyaGpMR3JtUHVZbjg2d0dPS0kzSE9yRXBz + |R2RNbWZ5N3RUbUtYL2VNL2VTM0ZFRFhabkU4MlBuNW9GSXlCVC9mOHNHdVh5T3NGWnFXQnZWZEJJSURs + |ZENwRDRteE1RWlpPWnRUcmx2M1d2QlFNQy9kc2ljT3hlM1FLWHZXSGk2UWIvUmh1YWlwM3JQbXdNZis0 + |SnBuSk8rSk1QcUFhVTFjQUg4SFZzZnJMQU1vS3MxNDhqMitjdmJwYVdtc1Q1cklvSC9lelZyUGFHL01P + |aUlncTc5dy9lZnV2U2k1QVg4SitrRG9MU0VmM2Q1d09na0pZQXFVcWNSeFhURUV0S0l6RE02aHphQlFG + |aUFXdlRuOUlsVldnbnRRYW1TWHZIK3R4YVRGOWlFbEh4VWY1SU5ZRlZjaUNwenRTcnlkZUh2L09DTlJm + |Ny9MVnJpY01TbG84UmgrTzN5UDlWKzJ1TmYzWDhzUUpOdHVmclFOYXFxMTh3aVhsaVRMdWZTbjAyL2cr + |bWtoSVVpTktmVE9KcHZDaktlQ25DRmN4UVUyL1hUM0toM0c4Z0RKd3NPNkVWUmpNVUp0NEFZS3plL2hF + |VUN3RjU1SUYybTNqSElvQ3U4alZmajI0Q2VFWDVkbmZ2U3IrU1Z2TjVRQjB1WjA1TTRybXlaWHlxQm0w + |ekszZlIraUUwL1pwSW51d0xDN1grVzgyelhsbk1rcGxJM1ErSnhkN2pmUTE1U1lORTJLNnJ2UklUMDF3 + |MFA5WnF5REY3a25HS3BSbHA3T3F4ZDM3YkQvVlViV3BRN2dJQWZzSk5INUtCTG93SEpGRmpXIl19.eyJ + |sZWdhbEhlYWRlciI6IlJldHJpZXZhbCBhbmQgdXNlIG9mIHRoaXMgQkxPQiBpbmRpY2F0ZXMgYWNjZXB + |0YW5jZSBvZiB0aGUgYXBwcm9wcmlhdGUgYWdyZWVtZW50IGxvY2F0ZWQgYXQgaHR0cHM6Ly9maWRvYWx + |saWFuY2Uub3JnL21ldGFkYXRhL21ldGFkYXRhLWxlZ2FsLXRlcm1zLyIsIm5vIjoxNSwibmV4dFVwZGF + |0ZSI6IjIwMjAtMDMtMzAiLCJlbnRyaWVzIjpbeyJhYWlkIjoiMTIzNCM1Njc4IiwibWV0YWRhdGFTdGF + |0ZW1lbnQiOnsibGVnYWxIZWFkZXIiOiJodHRwczovL2ZpZG9hbGxpYW5jZS5vcmcvbWV0YWRhdGEvbWV + |0YWRhdGEtc3RhdGVtZW50LWxlZ2FsLWhlYWRlci8iLCJkZXNjcmlwdGlvbiI6IkZJRE8gQWxsaWFuY2U + |gU2FtcGxlIFVBRiBBdXRoZW50aWNhdG9yIiwiYWFpZCI6IjEyMzQjNTY3OCIsImFsdGVybmF0aXZlRGV + |zY3JpcHRpb25zIjp7InJ1LVJVIjoi0J_RgNC40LzQtdGAIFVBRiDQsNGD0YLQtdC90YLQuNGE0LjQutC + |w0YLQvtGA0LAg0L7RgiBGSURPIEFsbGlhbmNlIiwiZnItRlIiOiJFeGVtcGxlIFVBRiBhdXRoZW50aWN + |hdG9yIGRlIEZJRE8gQWxsaWFuY2UifSwiYXV0aGVudGljYXRvclZlcnNpb24iOjIsInByb3RvY29sRmF + |taWx5IjoidWFmIiwic2NoZW1hIjozLCJ1cHYiOlt7Im1ham9yIjoxLCJtaW5vciI6MH0seyJtYWpvciI + |6MSwibWlub3IiOjF9XSwiYXV0aGVudGljYXRpb25BbGdvcml0aG1zIjpbInNlY3AyNTZyMV9lY2RzYV9 + |zaGEyNTZfcmF3Il0sInB1YmxpY0tleUFsZ0FuZEVuY29kaW5ncyI6WyJlY2NfeDk2Ml9yYXciXSwiYXR + |0ZXN0YXRpb25UeXBlcyI6WyJiYXNpY19mdWxsIl0sInVzZXJWZXJpZmljYXRpb25EZXRhaWxzIjpbW3s + |idXNlclZlcmlmaWNhdGlvbk1ldGhvZCI6ImZpbmdlcnByaW50X2ludGVybmFsIiwiYmFEZXNjIjp7InN + |lbGZBdHRlc3RlZEZBUiI6MC4wMDAwMiwibWF4UmV0cmllcyI6NSwiYmxvY2tTbG93ZG93biI6MzAsIm1 + |heFRlbXBsYXRlcyI6NX19XV0sImtleVByb3RlY3Rpb24iOlsiaGFyZHdhcmUiLCJ0ZWUiXSwiaXNLZXl + |SZXN0cmljdGVkIjp0cnVlLCJtYXRjaGVyUHJvdGVjdGlvbiI6WyJ0ZWUiXSwiY3J5cHRvU3RyZW5ndGg + |iOjEyOCwiYXR0YWNobWVudEhpbnQiOlsiaW50ZXJuYWwiXSwidGNEaXNwbGF5IjpbImFueSIsInRlZSJ + |dLCJ0Y0Rpc3BsYXlDb250ZW50VHlwZSI6ImltYWdlL3BuZyIsInRjRGlzcGxheVBOR0NoYXJhY3Rlcml + |zdGljcyI6W3sid2lkdGgiOjMyMCwiaGVpZ2h0Ijo0ODAsImJpdERlcHRoIjoxNiwiY29sb3JUeXBlIjo + |yLCJjb21wcmVzc2lvbiI6MCwiZmlsdGVyIjowLCJpbnRlcmxhY2UiOjB9XSwiYXR0ZXN0YXRpb25Sb29 + |0Q2VydGlmaWNhdGVzIjpbIk1JSUNQVENDQWVPZ0F3SUJBZ0lKQU91ZXh2VTNPeTJ3TUFvR0NDcUdTTTQ + |5QkFNQ01Ic3hJREFlQmdOVkJBTU1GMU5oYlhCc1pTQkJkSFJsYzNSaGRHbHZiaUJTYjI5ME1SWXdGQVl + |EVlFRS0RBMUdTVVJQSUVGc2JHbGhibU5sTVJFd0R3WURWUVFMREFoVlFVWWdWRmRITERFU01CQUdBMVV + |FQnd3SlVHRnNieUJCYkhSdk1Rc3dDUVlEVlFRSURBSkRRVEVMTUFrR0ExVUVCaE1DVlZNd0hoY05NVFF + |3TmpFNE1UTXpNek15V2hjTk5ERXhNVEF6TVRNek16TXlXakI3TVNBd0hnWURWUVFEREJkVFlXMXdiR1V + |nUVhSMFpYTjBZWFJwYjI0Z1VtOXZkREVXTUJRR0ExVUVDZ3dOUmtsRVR5QkJiR3hwWVc1alpURVJNQTh + |HQTFVRUN3d0lWVUZHSUZSWFJ5d3hFakFRQmdOVkJBY01DVkJoYkc4Z1FXeDBiekVMTUFrR0ExVUVDQXd + |DUTBFeEN6QUpCZ05WQkFZVEFsVlRNRmt3RXdZSEtvWkl6ajBDQVFZSUtvWkl6ajBEQVFjRFFnQUVIOGh + |2MkQwSFhhNTkvQm1wUTdSWmVoTC9GTUd6RmQxUUJnOXZBVXBPWjNham51UTk0UFI3YU16SDMzblVTQnI + |4ZkhZRHJxT0JiNThweEdxSEpSeVgvNk5RTUU0d0hRWURWUjBPQkJZRUZQb0hBM0NMaHhGYkMwSXQ3ekU + |0dzhoazVFSi9NQjhHQTFVZEl3UVlNQmFBRlBvSEEzQ0xoeEZiQzBJdDd6RTR3OGhrNUVKL01Bd0dBMVV + |kRXdRRk1BTUJBZjh3Q2dZSUtvWkl6ajBFQXdJRFNBQXdSUUloQUowNlFTWHQ5aWhJYkVLWUtJanNQa3J + |pVmRMSWd0ZnNiRFN1N0VySmZ6cjRBaUJxb1lDWmYwK3pJNTVhUWVBSGpJekE5WG02M3JydUF4Qlo5cHM + |5ejJYTmxRPT0iXSwiaWNvbiI6ImRhdGE6aW1hZ2UvcG5nO2Jhc2U2NCxpVkJPUncwS0dnb0FBQUFOU1V + |oRVVnQUFBRThBQUFBdkNBWUFBQUNpd0pmY0FBQUFBWE5TUjBJQXJzNGM2UUFBQUFSblFVMUJBQUN4and + |2OFlRVUFBQUFKY0VoWmN3QUFEc01BQUE3REFjZHZxR1FBQUFhaFNVUkJWR2hEN1pyNWJ4UmxHTWY5S3p + |UQjhBTS9ZRWhFMlc3cFFaY1dLS0JjbFNwSEFUbEVMQVJFN2tORUNDQTNGa1dLMENLS1NDRklzS0JjZ1Z + |DRFdHTkVTZEFZaWR3Z2dnSkJpUmlNaEZjLzR3eTg4ODR6dTlOZGxuR1RmWkpQMm4zbk8rKzg4OTMzZnZ + |lQkJ4K1BxQ3pKa1RVdkJiTG1wVURXdkJUSW1wY0NTWnZYTENkWDlSMDVTazE5YmI1YXRmNTk5ZkcrL2V + |yQTU0MXE0N2FQMUxMVmE5U0l5Vk5VaThJaThkNWtHVHNpMzBORnY3YWk5bjdRWlBNd2JkeXMyZXJVMlh + |NcVVkeTgrWmNhTm1HaW1FOHlYTjNSVWQzYTE4bkYwZlVsb3ZaKzBDVHpXcGQyVmorZU9tMWJFeXk2RHg + |0aTVwVU1HV3ZlbzUwNnEyMjdkdHVXQkl1ZmZyNm9XcFYwRlBOTGhvdzE3NTFObTIxTHZQSDNyVnRXamZ + |6NjZMZnFsOHRYN0ZSbDlZRlNYc21Tc2ViOWNlT0diWWs3TU5VY0dQZzhac2JNZTlyZlFVYWFWL0pNWDl + |zcWR6RENTdnAwa1pIbVRaZzl4N2JMSGNNblRoYjE2ZUorbVZmUXE4eWFVWlFORzY0aVhaKzAva3E2dU9 + |aRk8wUXRhdGRXS2ZYblJROTlCajkxUjVPSUZuazU0ak4wbWtVaXFsTzNYRFcrTWwrOThtS0I2dFc3cld + |wWmNQYyswemc0dExyWWxVYzg2RTZlR0RqSU11YlZwY3VzZWFyZmdJWUdSazZicmhaVnIvSmNIem9vTDc + |1NTBqZWRMRXhvcFdjQXBpMlpVcWh1N0pMdnJWc1FVODF6a3pPUGVlbU1SWXZWdVFzWDdQYmlEUVk1SnZ + |ab25mdEsrMVZZOEg5dXR4NTMwaDBvYitqbVJZcWo2b3VhWXZFZW5XL1dsWWpwOGN3Yk1tNjgydFB3cVc + |xUjR0ai8yU0gxM0lSSllsNG1vWnZYcGlTcURyN2RYdFFIeGEvUEszLytCV3NLMWRUZ0h1NlY4dFFKM2J + |3Rmt3cEZyVU9RNTBzMXIzbGV2bTh6WmNxMTcrQkJhdzdLOGxFSzVxemtZZWFyazlBOHA3UDNHekRLK25 + |kM0RRb3crNlVDOFNWTjgyaXV2MzhpbTdOdGFYdFYxQ1ZxNlJndzRwa3NtYmRpM2J1MkRlN1lmYUJCeGN + |xZnZxUHJVakZRTlRRMjJsZmRVVlZUNjhyVEpLRjVEblNtVWpnZHFnNG1TUzlwbXNmREpSM0c2VG9IMGl + |XOWFWN0xXTEhZWEtsbFREdDBMVEF0a1lJYWFtcDFRalZ2Kyt1eUdVeFZkSjBETlZYU20rYjFxUnhwbDg + |0ZGRmWDFMcDFPL2Q2OXRzb2QwdnM1aEdyZTl4dThvK2ZwTFIxY0doTlRENlo1N0M5S01XWGVmSmRPWjk + |0YmI5b3FkMVJPblM3cUlUVHpIaW1NcWl2Yk8zZzBEZFZ5azNXUUJoQnp0SzM1WUtOZE9uYzhPM2FjUzZ + |mRFpGZ0thWExzRUpwNXJkcmxpQnFwODljSmNzL203VHZzMHJrakdmTjRiMGtQb1puM1VKdUlPcm5aMjJ + |5UDFmbXZVeCtPNWdTcWViVjFtK3pTdVlOVmhxN1RXYkRpTFZ2bGpwbExsb3A2Q0xYUCsycXR2R0xJTC8 + |xdmltSVNkTUJnelNvRlp5dTZUcWQranp4Z3NQYVY5QkNxZWUvTmpZazZ2NmxLOWN3aVVjL1NUdGYxSER + |wTTNiNTkyeTdoM1RoeDVveks2OUhMcFlXdUF3YXFTNWN2MjZxN2NlYjhlZlZZYVJlUDNpRlU4emoxa25 + |Td1pYSE1tbkNqWTBPZ2FsbzdVUWZTQ00zcVFRcjJIL1hGUDdzc1h4NDVZbDkxQnllQ2VwNG1vWm9IKzF + |mRzN4RDR0VDd4OGt3eWo4bndiOWV2MjZWMEI2ZCs3SDR6S3Z1ZEFINTM3RmpxeXpPSGRKbkhFdXptWHE + |vV2p4T2J2Tk1idjduaHl3c1gyYVZzV3RDOCs0OGFMZWFwRTdwNXdLWmkwQTJBUVJWNW52UjRFK3VKYyt + |iNjFrQXBxSW54QmdtZC80VjVRUC9tdDE4SERDN3NSSGZ0bWV1NWxtaFYwcm4vQUxYMjMyYnFkNEJGbkR + |4N1ZpMWNXUzJ1ZmYwSWJCNDdxZXh4bVVqOVF1dFlqdXBkM3RZRDZhYldCQk1yaCthcE5iT0tyTkYxK3V + |nQ2E0cmlYR2Z3TVBQdFZpYXZoVTNZTU9BQW51VWIvUjA3TDB5T1NlT2FkRTg4QXBzWEZHZmYzMHluaGx + |KZ001MUNVNnZOOUV6Z25wdkhCRlV5aVZyYWVQaXdKNTNERjVaVFpub21FTmc4NWtOVWQyb0ppMldwcjR + |PbW1rZk40eDR6SGZpVkZjOER2OE56dWhOcU9pZGlsR3ZBNkRHdWVad083OEFBUW42Y2lFazYrcnc1VmN + |2anZxTkRZUE9vSVV3YUtTaHJ4QXVYTGxrSDRhWXVHZk1ZRGMxMFdGNVRhMzFoUEpPZmNVaHJVL0psSU5 + |pNmM2ZWxSWWRCcG82KytZZmp4NjFsR05mUm00TUQ1ckoxajNGb0dIbmpEU0JOYXJZVWdNTHlNc3pLcGI + |3dFhwb0hmUHM4aDNXcDFMek5mTms1NFh4QzF3REdVbVl6WFllZmg2ei9jS3RWbTRFQnhhOVZRR0R6WXI + |zTHJVTVJqSEVLa2s3emFGS1lRQTJoR1FVMXorODVORldwWERya3ozdngxMEdxeFE2QnplTmJvQms1bjh + |rNG5lYlJoK2sxaFdmeFRGMEQxRXlXVXM1bnYrZGdRcUtheHp1Q2RFMGlzSGwwMk5ROGFoMG1YcjEyTGE + |zbTBmOXdpazkrd0xOVE1ZLzg2TVBvOHlpMzFPZnhtVDZQV29xRzkrRFp1a1luYTU2bVNadDVXV1N5NXF + |WQTFyd1V5SnFYQWxuemtpYWkvZ0hTRDdSa1R5aWhvZ0FBQUFCSlJVNUVya0pnZ2c9PSJ9LCJzdGF0dXN + |SZXBvcnRzIjpbeyJzdGF0dXMiOiJGSURPX0NFUlRJRklFRCIsImVmZmVjdGl2ZURhdGUiOiIyMDE0LTA + |xLTA0In1dLCJ0aW1lT2ZMYXN0U3RhdHVzQ2hhbmdlIjoiMjAxNC0wMS0wNCJ9LHsiYWFndWlkIjoiMDE + |zMmQxMTAtYmY0ZS00MjA4LWE0MDMtYWI0ZjVmMTJlZmU1IiwibWV0YWRhdGFTdGF0ZW1lbnQiOnsibGV + |nYWxIZWFkZXIiOiJodHRwczovL2ZpZG9hbGxpYW5jZS5vcmcvbWV0YWRhdGEvbWV0YWRhdGEtc3RhdGV + |tZW50LWxlZ2FsLWhlYWRlci8iLCJkZXNjcmlwdGlvbiI6IkZJRE8gQWxsaWFuY2UgU2FtcGxlIEZJRE8 + |yIEF1dGhlbnRpY2F0b3IiLCJhYWd1aWQiOiIwMTMyZDExMC1iZjRlLTQyMDgtYTQwMy1hYjRmNWYxMmV + |mZTUiLCJhbHRlcm5hdGl2ZURlc2NyaXB0aW9ucyI6eyJydS1SVSI6ItCf0YDQuNC80LXRgCBGSURPMiD + |QsNGD0YLQtdC90YLQuNGE0LjQutCw0YLQvtGA0LAg0L7RgiBGSURPIEFsbGlhbmNlIiwiZnItRlIiOiJ + |FeGVtcGxlIEZJRE8yIGF1dGhlbnRpY2F0b3IgZGUgRklETyBBbGxpYW5jZSIsInpoLUNOIjoi5L6G6Ie + |qRklETyBBbGxpYW5jZeeahOekuuS-i0ZJRE8y6Lqr5Lu96amX6K2J5ZmoIn0sInByb3RvY29sRmFtaWx + |5IjoiZmlkbzIiLCJzY2hlbWEiOjMsImF1dGhlbnRpY2F0b3JWZXJzaW9uIjo1LCJ1cHYiOlt7Im1ham9 + |yIjoxLCJtaW5vciI6MH1dLCJhdXRoZW50aWNhdGlvbkFsZ29yaXRobXMiOlsic2VjcDI1NnIxX2VjZHN + |hX3NoYTI1Nl9yYXciLCJyc2Fzc2FfcGtjc3YxNV9zaGEyNTZfcmF3Il0sInB1YmxpY0tleUFsZ0FuZEV + |uY29kaW5ncyI6WyJjb3NlIl0sImF0dGVzdGF0aW9uVHlwZXMiOlsiYmFzaWNfZnVsbCJdLCJ1c2VyVmV + |yaWZpY2F0aW9uRGV0YWlscyI6W1t7InVzZXJWZXJpZmljYXRpb25NZXRob2QiOiJub25lIn1dLFt7InV + |zZXJWZXJpZmljYXRpb25NZXRob2QiOiJwcmVzZW5jZV9pbnRlcm5hbCJ9XSxbeyJ1c2VyVmVyaWZpY2F + |0aW9uTWV0aG9kIjoicGFzc2NvZGVfZXh0ZXJuYWwiLCJjYURlc2MiOnsiYmFzZSI6MTAsIm1pbkxlbmd + |0aCI6NH19XSxbeyJ1c2VyVmVyaWZpY2F0aW9uTWV0aG9kIjoicGFzc2NvZGVfZXh0ZXJuYWwiLCJjYUR + |lc2MiOnsiYmFzZSI6MTAsIm1pbkxlbmd0aCI6NH19LHsidXNlclZlcmlmaWNhdGlvbk1ldGhvZCI6InB + |yZXNlbmNlX2ludGVybmFsIn1dXSwia2V5UHJvdGVjdGlvbiI6WyJoYXJkd2FyZSIsInNlY3VyZV9lbGV + |tZW50Il0sIm1hdGNoZXJQcm90ZWN0aW9uIjpbIm9uX2NoaXAiXSwiY3J5cHRvU3RyZW5ndGgiOjEyOCw + |iYXR0YWNobWVudEhpbnQiOlsiZXh0ZXJuYWwiLCJ3aXJlZCIsIndpcmVsZXNzIiwibmZjIl0sInRjRGl + |zcGxheSI6W10sImF0dGVzdGF0aW9uUm9vdENlcnRpZmljYXRlcyI6WyJNSUlDUFRDQ0FlT2dBd0lCQWd + |JSkFPdWV4dlUzT3kyd01Bb0dDQ3FHU000OUJBTUNNSHN4SURBZUJnTlZCQU1NRjFOaGJYQnNaU0JCZEh + |SbGMzUmhkR2x2YmlCU2IyOTBNUll3RkFZRFZRUUtEQTFHU1VSUElFRnNiR2xoYm1ObE1SRXdEd1lEVlF + |RTERBaFZRVVlnVkZkSExERVNNQkFHQTFVRUJ3d0pVR0ZzYnlCQmJIUnZNUXN3Q1FZRFZRUUlEQUpEUVR + |FTE1Ba0dBMVVFQmhNQ1ZWTXdIaGNOTVRRd05qRTRNVE16TXpNeVdoY05OREV4TVRBek1UTXpNek15V2p + |CN01TQXdIZ1lEVlFRRERCZFRZVzF3YkdVZ1FYUjBaWE4wWVhScGIyNGdVbTl2ZERFV01CUUdBMVVFQ2d + |3TlJrbEVUeUJCYkd4cFlXNWpaVEVSTUE4R0ExVUVDd3dJVlVGR0lGUlhSeXd4RWpBUUJnTlZCQWNNQ1Z + |CaGJHOGdRV3gwYnpFTE1Ba0dBMVVFQ0F3Q1EwRXhDekFKQmdOVkJBWVRBbFZUTUZrd0V3WUhLb1pJemo + |wQ0FRWUlLb1pJemowREFRY0RRZ0FFSDhodjJEMEhYYTU5L0JtcFE3UlplaEwvRk1HekZkMVFCZzl2QVV + |wT1ozYWpudVE5NFBSN2FNekgzM25VU0JyOGZIWURycU9CYjU4cHhHcUhKUnlYLzZOUU1FNHdIUVlEVlI + |wT0JCWUVGUG9IQTNDTGh4RmJDMEl0N3pFNHc4aGs1RUovTUI4R0ExVWRJd1FZTUJhQUZQb0hBM0NMaHh + |GYkMwSXQ3ekU0dzhoazVFSi9NQXdHQTFVZEV3UUZNQU1CQWY4d0NnWUlLb1pJemowRUF3SURTQUF3UlF + |JaEFKMDZRU1h0OWloSWJFS1lLSWpzUGtyaVZkTElndGZzYkRTdTdFckpmenI0QWlCcW9ZQ1pmMCt6STU + |1YVFlQUhqSXpBOVhtNjNycnVBeEJaOXBzOXoyWE5sUT09Il0sImljb24iOiJkYXRhOmltYWdlL3BuZzt + |iYXNlNjQsaVZCT1J3MEtHZ29BQUFBTlNVaEVVZ0FBQUU4QUFBQXZDQVlBQUFDaXdKZmNBQUFBQVhOU1I + |wSUFyczRjNlFBQUFBUm5RVTFCQUFDeGp3djhZUVVBQUFBSmNFaFpjd0FBRHNNQUFBN0RBY2R2cUdRQUF + |BYWhTVVJCVkdoRDdacjVieFJsR01mOUt6VEI4QU0vWUVoRTJXN3BRWmNXS0tCY2xTcEhBVGxFTEFSRTd + |rTkVDQ0EzRmtXSzBDS0tTQ0ZJc0tCY2dWQ0RXR05FU2RBWWlkd2dnZ0pCaVJpTWhGYy80d3k4ODg0enU + |5TmRsbkdUZlpKUDJuM25PKys4ODkzM2Z2ZUJCeCtQcUN6SmtUVXZCYkxtcFVEV3ZCVEltcGNDU1p2WEx + |DZFg5UjA1U2sxOWJiNWF0ZjU5OWZHKy9lckE1NDFxNDdhUDFMTFZhOVNJeVZOVWk4SWk4ZDVrR1RzaTM + |wTkZ2N2FpOW43UVpQTXdiZHlzMmVyVTJYTXFVZHk4K1pjYU5tR2ltRTh5WE4zUlVkM2ExOG5GMGZVbG9 + |2WiswQ1R6V3BkMlZqK2VPbTFiRXl5NkR4NGk1cFVNR1d2ZW81MDZxMjI3ZHR1V0JJdWZmcjZvV3BWMEZ + |QTkxob3cxNzUxTm0yMUx2UEgzclZ0V2pmejY2TGZxbDh0WDdGUmw5WUZTWHNtU3NlYjljZU9HYllrN01 + |OVWNHUGc4WnNiTWU5cmZRVWFhVi9KTVg5c3FkekRDU3ZwMGtaSG1UWmc5eDdiTEhjTW5UaGIxNmVKK21 + |WZlFxOHlhVVpRTkc2NGlYWiswL2txNnVPWkZPMFF0YXRkV0tmWG5SUTk5Qmo5MVI1T0lGbms1NGpOMG1 + |rVWlxbE8zWERXK01sKzk4bUtCNnRXN3JXcFpjUGMrMHpnNHRMcllsVWM4NkU2ZUdEaklNdWJWcGN1c2V + |hcmZnSVlHUms2YnJoWlZyL0pjSHpvb0w3NTUwamVkTEV4b3BXY0FwaTJaVXFodTdKTHZyVnNRVTgxemt + |6T1BlZW1NUll2VnVRc1g3UGJpRFFZNUp2Wm9uZnRLKzFWWThIOXV0eDUzMGgwb2Iram1SWXFqNm91YVl + |2RWVuVy9XbFlqcDhjd2JNbTY4MnRQd3FXMVI0dGovMlNIMTNJUkpZbDRtb1p2WHBpU3FEcjdkWHRRSHh + |hL1BLMy8rQldzSzFkVGdIdTZWOHRRSjNid0Zrd3BGclVPUTUwczFyM2xldm04elpjcTE3K0JCYXc3Szh + |sRUs1cXprWWVhcms5QThwN1AzR3pESytuZDNEUW93KzZVQzhTVk44Mml1djM4aW03TnRhWHRWMUNWcTZ + |SZ3c0cGtzbWJkaTNidTJEZTdZZmFCQnhjcWZ2cVByVWpGUU5UUTIybGZkVVZWVDY4clRKS0Y1RG5TbVV + |qZ2RxZzRtU1M5cG1zZkRKUjNHNlRvSDBpVzlhVjdMV0xIWVhLbGxURHQwTFRBdGtZSWFhbXAxUWpWdis + |rdXlHVXhWZEowRE5WWFNtK2IxcVJ4cGw4NGRkZlgxTHAxTy9kNjl0c29kMHZzNWhHcmU5eHU4bytmcEx + |SMWNHaE5URDZaNTdDOUtNV1hlZkpkT1o5NGJiOW9xZDFST25TN3FJVFR6SGltTXFpdmJPM2cwRGRWeWs + |zV1FCaEJ6dEszNVlLTmRPbmM4TzNhY1M2ZkRaRmdLYVhMc0VKcDVyZHJsaUJxcDg5Y0pjcy9tN1R2czB + |ya2pHZk40YjBrUG9abjNVSnVJT3JuWjIyeVAxZm12VXgrTzVnU3FlYlYxbSt6U3VZTlZocTdUV2JEaUx + |WdmxqcGxMbG9wNkNMWFArMnF0dkdMSUwvMXZpbUlTZE1CZ3pTb0ZaeXU2VHFkK2p6eGdzUGFWOUJDcWV + |lL05qWWs2djZsSzljd2lVYy9TVHRmMUhEcE0zYjU5Mnk3aDNUaHg1b3pLNjlITHBZV3VBd2FxUzVjdjI + |2cTdjZWI4ZWZWWWFSZVAzaUZVOHpqMWtuU3daWEhNbW5DalkwT2dhbG83VVFmU0NNM3FRUXIySC9YRlA + |3c3NYeDQ1WWw5MUJ5ZUNlcDRtb1pvSCsxZkczeEQ0dFQ3eDhrd3lqOG53YjlldjI2VjBCNmQrN0g0ekt + |2dWRBSDUzN0ZqcXl6T0hkSm5IRXV6bVhxL1dqeE9idk5NYnY3bmh5d3NYMmFWc1d0QzgrNDhhTGVhcEU + |3cDV3S1ppMEEyQVFSVjVudlI0RSt1SmMrYjYxa0FwcUlueEJnbWQvNFY1UVAvbXQxOEhEQzdzUkhmdG1 + |ldTVsbWhWMHJuL0FMWDIzMmJxZDRCRm5EeDdWaTFjV1MydWZmMEliQjQ3cWV4eG1VajlRdXRZanVwZDN + |0WUQ2YWJXQkJNcmgrYXBOYk9Lck5GMSt1Z0NhNHJpWEdmd01QUHRWaWF2aFUzWU1PQUFudVViL1IwN0w + |weU9TZU9hZEU4OEFwc1hGR2ZmMzB5bmhsSmdNNTFDVTZ2TjlFemducHZIQkZVeWlWcmFlUGl3SjUzREY + |1WlRabm9tRU5nODVrTlVkMm9KaTJXcHI0T21ta2ZONHg0ekhmaVZGYzhEdjhOenVoTnFPaWRpbEd2QTZ + |ER3VlWndPNzhBQVFuNmNpRWs2K3J3NVZjdmp2cU5EWVBPb0lVd2FLU2hyeEF1WExsa0g0YVl1R2ZNWUR + |jMTBXRjVUYTMxaFBKT2ZjVWhyVS9KbElOaTZjNmVsUllkQnBvNisrWWZqeDYxbEdOZlJtNE1ENXJKMWo + |zRm9HSG5qRFNCTmFyWVVnTUx5TXN6S3BiN3RYcG9IZlBzOGgzV3AxTHpOZk5rNTRYeEMxd0RHVW1Zelh + |ZZWZoNnovY0t0Vm00RUJ4YTlWUUdEellyM0xyVU1SakhFS2trN3phRktZUUEyaEdRVTF6Kzg1TkZXcFh + |Ecmt6M3Z4MTBHcXhRNkJ6ZU5ib0JrNW44azRuZWJSaCtrMWhXZnhURjBEMUV5V1VzNW52K2RnUXFLYXh + |6dUNkRTBpc0hsMDJOUThhaDBtWHIxMkxhM20wZjl3aWs5K3dMTlRNWS84Nk1Qbzh5aTMxT2Z4bVQ2UFd + |vcUc5K0RadWtZbmE1Nm1TWnQ1V1dTeTVxVkExcndVeUpxWEFsbnpraWFpL2dIU0Q3UmtUeWlob2dBQUF + |BQkpSVTVFcmtKZ2dnPT0iLCJzdXBwb3J0ZWRFeHRlbnNpb25zIjpbeyJpZCI6ImhtYWMtc2VjcmV0Iiw + |iZmFpbF9pZl91bmtub3duIjpmYWxzZX0seyJpZCI6ImNyZWRQcm90ZWN0IiwiZmFpbF9pZl91bmtub3d + |uIjpmYWxzZX1dLCJhdXRoZW50aWNhdG9yR2V0SW5mbyI6eyJ2ZXJzaW9ucyI6WyJVMkZfVjIiLCJGSUR + |PXzJfMCJdLCJleHRlbnNpb25zIjpbImNyZWRQcm90ZWN0IiwiaG1hYy1zZWNyZXQiXSwiYWFndWlkIjo + |iMDEzMmQxMTBiZjRlNDIwOGE0MDNhYjRmNWYxMmVmZTUiLCJvcHRpb25zIjp7InBsYXQiOiJmYWxzZSI + |sInJrIjoidHJ1ZSIsImNsaWVudFBpbiI6InRydWUiLCJ1cCI6InRydWUiLCJ1diI6InRydWUiLCJ1dlR + |va2VuIjoiZmFsc2UiLCJjb25maWciOiJmYWxzZSJ9LCJtYXhNc2dTaXplIjoxMjAwLCJwaW5VdkF1dGh + |Qcm90b2NvbHMiOlsxXSwibWF4Q3JlZGVudGlhbENvdW50SW5MaXN0IjoxNiwibWF4Q3JlZGVudGlhbEl + |kTGVuZ3RoIjoxMjgsInRyYW5zcG9ydHMiOlsidXNiIiwibmZjIl0sImFsZ29yaXRobXMiOlt7InR5cGU + |iOiJwdWJsaWMta2V5IiwiYWxnIjotN30seyJ0eXBlIjoicHVibGljLWtleSIsImFsZyI6LTI1N31dLCJ + |tYXhBdXRoZW50aWNhdG9yQ29uZmlnTGVuZ3RoIjoxMDI0LCJkZWZhdWx0Q3JlZFByb3RlY3QiOjIsImZ + |pcm13YXJlVmVyc2lvbiI6NX19LCJzdGF0dXNSZXBvcnRzIjpbeyJzdGF0dXMiOiJGSURPX0NFUlRJRkl + |FRCIsImVmZmVjdGl2ZURhdGUiOiIyMDE5LTAxLTA0In0seyJzdGF0dXMiOiJGSURPX0NFUlRJRklFRF9 + |MMSIsImVmZmVjdGl2ZURhdGUiOiIyMDIwLTExLTE5IiwiY2VydGlmaWNhdGlvbkRlc2NyaXB0b3IiOiJ + |GSURPIEFsbGlhbmNlIFNhbXBsZSBGSURPMiBBdXRoZW50aWNhdG9yIiwiY2VydGlmaWNhdGVOdW1iZXI + |iOiJGSURPMjEwMDAyMDE1MTIyMTAwMSIsImNlcnRpZmljYXRpb25Qb2xpY3lWZXJzaW9uIjoiMS4wLjE + |iLCJjZXJ0aWZpY2F0aW9uUmVxdWlyZW1lbnRzVmVyc2lvbiI6IjEuMC4xIn1dLCJ0aW1lT2ZMYXN0U3R + |hdHVzQ2hhbmdlIjoiMjAxOS0wMS0wNCJ9XX0.-kc1wrorJA16bxLXXzeDkFEOCsbKAy2WDEzoCY-Aej_ + |N0bWIOAmhpHGxSa3CXgmwFwgAuy230Eq_BHTO_RshsA + |""".stripMargin.replaceAll(raw"[ \n]+", "") + +} 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 new file mode 100644 index 000000000..9b8417814 --- /dev/null +++ b/webauthn-server-attestation/src/test/scala/com/yubico/fido/metadata/FidoMds3Spec.scala @@ -0,0 +1,925 @@ +package com.yubico.fido.metadata + +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.node.ArrayNode +import com.fasterxml.jackson.databind.node.JsonNodeFactory +import com.fasterxml.jackson.databind.node.ObjectNode +import com.fasterxml.jackson.databind.node.TextNode +import com.yubico.internal.util.CertificateParser +import com.yubico.webauthn.FinishRegistrationOptions +import com.yubico.webauthn.RegistrationResult +import com.yubico.webauthn.RelyingParty +import com.yubico.webauthn.TestAuthenticator +import com.yubico.webauthn.TestAuthenticator.AttestationMaker +import com.yubico.webauthn.TestAuthenticator.AttestationSigner +import com.yubico.webauthn.data.ByteArray +import com.yubico.webauthn.data.COSEAlgorithmIdentifier +import com.yubico.webauthn.data.PublicKeyCredentialCreationOptions +import com.yubico.webauthn.data.PublicKeyCredentialParameters +import com.yubico.webauthn.data.RelyingPartyIdentity +import com.yubico.webauthn.data.UserIdentity +import com.yubico.webauthn.test.Helpers +import org.bouncycastle.asn1.x500.X500Name +import org.bouncycastle.cert.jcajce.JcaX500NameUtil +import org.junit.runner.RunWith +import org.scalatest.FunSpec +import org.scalatest.Matchers +import org.scalatest.tags.Network +import org.scalatest.tags.Slow +import org.scalatestplus.junit.JUnitRunner + +import java.nio.charset.StandardCharsets +import java.security.KeyPair +import java.security.cert.CRL +import java.security.cert.CertStore +import java.security.cert.CollectionCertStoreParameters +import java.security.cert.X509Certificate +import java.time.Clock +import java.time.Instant +import java.time.ZoneOffset +import java.util.Collections +import scala.collection.mutable +import scala.jdk.CollectionConverters.SeqHasAsJava +import scala.jdk.CollectionConverters.SetHasAsJava +import scala.jdk.CollectionConverters.SetHasAsScala +import scala.jdk.FunctionConverters.enrichAsJavaPredicate +import scala.jdk.OptionConverters.RichOption +import scala.jdk.OptionConverters.RichOptional + +@Slow +@Network +@RunWith(classOf[JUnitRunner]) +class FidoMds3Spec extends FunSpec with Matchers { + + private val CertValidFrom = Instant.parse("2022-02-15T17:00:00Z") + private val CertValidTo = Instant.parse("2022-03-15T17:00:00Z") + + private def makeTrustRootCert( + distinguishedName: String = + "CN=Yubico java-webauthn-server unit tests, O=Yubico" + ): (X509Certificate, KeyPair, X500Name) = { + val keypair = TestAuthenticator.generateEcKeypair() + val name = new X500Name(distinguishedName) + ( + TestAuthenticator.buildCertificate( + publicKey = keypair.getPublic, + issuerName = name, + subjectName = name, + signingKey = keypair.getPrivate, + signingAlg = COSEAlgorithmIdentifier.ES256, + validFrom = CertValidFrom, + validTo = CertValidTo, + ), + keypair, + name, + ) + } + + private def makeBlob( + body: String + ): (String, X509Certificate, java.util.Set[CRL]) = { + val (cert, keypair, certName) = makeTrustRootCert() + val header = + s"""{"alg":"ES256","x5c": ["${new ByteArray( + cert.getEncoded + ).getBase64}"]}""" + val blobTbs = new ByteArray( + header.getBytes(StandardCharsets.UTF_8) + ).getBase64Url + "." + new ByteArray( + body.getBytes(StandardCharsets.UTF_8) + ).getBase64Url + val blobSignature = TestAuthenticator.sign( + new ByteArray(blobTbs.getBytes(StandardCharsets.UTF_8)), + keypair.getPrivate, + COSEAlgorithmIdentifier.ES256, + ) + ( + blobTbs + "." + blobSignature.getBase64Url, + cert, + Set( + TestAuthenticator.buildCrl( + certName, + keypair.getPrivate, + "SHA256withECDSA", + CertValidFrom, + CertValidTo, + ) + ).asJava, + ) + } + + def makeDownloader( + blobTuple: (String, X509Certificate, java.util.Set[CRL]) + ): FidoMetadataDownloader = + blobTuple match { + case ( + blobJwt: String, + cert: X509Certificate, + blobCrls: java.util.Set[CRL], + ) => + FidoMetadataDownloader + .builder() + .expectLegalHeader( + "Kom ihåg att du aldrig får snyta dig i mattan!" + ) + .useTrustRoot(cert) + .useBlob(blobJwt) + .clock( + Clock + .fixed( + Instant.parse("2022-02-22T18:00:00Z"), + ZoneOffset.UTC, + ) + ) + .useCrls(blobCrls) + .build() + } + + describe("§3.2. Metadata BLOB object processing rules") { + describe("8. Iterate through the individual entries (of type MetadataBLOBPayloadEntry). For each entry:") { + describe("1. Ignore the entry if the AAID, AAGUID or attestationCertificateKeyIdentifiers is not relevant to the relying party (e.g. not acceptable by any policy)") { + val jf: JsonNodeFactory = JsonNodeFactory.instance + + val aaidA = new AAID("aaaa#0000") + val aaidB = new AAID("bbbb#1111") + val aaidC = new AAID("cccc#2222") + + val aaguidA = + new AAGUID(ByteArray.fromHex("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")) + val aaguidB = + new AAGUID(ByteArray.fromHex("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")) + val aaguidC = + new AAGUID(ByteArray.fromHex("cccccccccccccccccccccccccccccccc")) + + val ackiA = Set("aa") + val ackiB = Set("bb") + val ackiC = Set("cc") + + def makeEntry( + aaid: Option[AAID] = None, + aaguid: Option[AAGUID] = None, + acki: Option[Set[String]] = None, + ): String = { + val entry = JacksonCodecs + .jsonWithDefaultEnums() + .readTree(s"""{ + "metadataStatement": { + "authenticatorVersion": 1, + "attachmentHint" : ["internal"], + "attestationRootCertificates": ["MIIB2DCCAX2gAwIBAgICAaswCgYIKoZIzj0EAwIwajEmMCQGA1UEAwwdWXViaWNvIFdlYkF1dGhuIHVuaXQgdGVzdHMgQ0ExDzANBgNVBAoMBll1YmljbzEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjELMAkGA1UEBhMCU0UwHhcNMTgwOTA2MTc0MjAwWhcNMTgwOTEzMTc0MjAwWjBqMSYwJAYDVQQDDB1ZdWJpY28gV2ViQXV0aG4gdW5pdCB0ZXN0cyBDQTEPMA0GA1UECgwGWXViaWNvMSIwIAYDVQQLDBlBdXRoZW50aWNhdG9yIEF0dGVzdGF0aW9uMQswCQYDVQQGEwJTRTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABLtJrr5PYSc4KhmUcwBzgZgNadDnCs/ow2oh2jiKYUqq1A6hFcFf1NPfXLQjP2I4fBI36T6/QR2iY9mbqyP5iVejEzARMA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0EAwIDSQAwRgIhANWaM2Tf2HPKc+ibCr8G4cxpQVr9Gib47a0CpqagCSCwAiEA3oKlX/ID94FKzgHvD2gyCKQU6RltAOMShVwoljj/5+E="], + "attestationTypes" : ["basic_full"], + "authenticationAlgorithms" : ["secp256r1_ecdsa_sha256_raw"], + "description" : "Test authenticator", + "keyProtection" : ["software"], + "matcherProtection" : ["software"], + "protocolFamily" : "u2f", + "publicKeyAlgAndEncodings" : ["ecc_x962_raw"], + "schema" : 3, + "tcDisplay" : [], + "upv" : [{ "major" : 1, "minor" : 1 }], + "userVerificationDetails" : [[{ "userVerificationMethod" : "presence_internal" }]] + }, + "statusReports": [], + "timeOfLastStatusChange": "2022-02-21" + }""") + .asInstanceOf[ObjectNode] + aaid.foreach(aaid => + entry.set[ObjectNode]("aaid", new TextNode(aaid.getValue)) + ) + aaguid.foreach(aaguid => + entry.set[ObjectNode]("aaguid", new TextNode(aaguid.asGuidString)) + ) + acki.foreach(acki => + entry.set[ObjectNode]( + "attestationCertificateKeyIdentifiers", + new ArrayNode( + jf, + acki.toList.map[JsonNode](new TextNode(_)).asJava, + ), + ) + ) + JacksonCodecs.jsonWithDefaultEnums.writeValueAsString(entry) + } + + def makeMds( + blobTuple: (String, X509Certificate, java.util.Set[CRL]), + attestationCrls: Set[CRL] = Set.empty, + )(filter: MetadataBLOBPayloadEntry => Boolean): FidoMetadataService = + FidoMetadataService + .builder() + .useBlob(makeDownloader(blobTuple).loadCachedBlob()) + .prefilter(filter.asJava) + .certStore( + CertStore.getInstance( + "Collection", + new CollectionCertStoreParameters(attestationCrls.asJava), + ) + ) + .build() + + val blobTuple = makeBlob(s"""{ + "legalHeader" : "Kom ihåg att du aldrig får snyta dig i mattan!", + "nextUpdate" : "2022-12-01", + "no" : 0, + "entries": [ + ${makeEntry(aaid = Some(aaidA))}, + ${makeEntry(aaguid = Some(aaguidA))}, + ${makeEntry(acki = Some(ackiA))}, + + ${makeEntry(aaid = Some(aaidB), aaguid = Some(aaguidB))}, + ${makeEntry(aaguid = Some(aaguidB), acki = Some(ackiB))}, + ${makeEntry(aaid = Some(aaidB), acki = Some(ackiB))}, + + ${makeEntry( + aaid = Some(aaidC), + aaguid = Some(aaguidC), + acki = Some(ackiC), + )} + ] + }""") + + it("Filtering in getFilteredEntries works as expected.") { + def count(filter: MetadataBLOBPayloadEntry => Boolean): Long = + makeMds(blobTuple)(filter).findEntries(_ => true).size + + implicit class MetadataBLOBPayloadEntryWithAbbreviatedAttestationCertificateKeyIdentifiers( + entry: MetadataBLOBPayloadEntry + ) { + def getACKI: mutable.Set[String] = + entry.getAttestationCertificateKeyIdentifiers.asScala + } + + count(_ => false) should be(0) + count(_ => true) should be(7) + + count(_.getAaid.toScala.contains(aaidA)) should be(1) + count(_.getAaguid.toScala.contains(aaguidA)) should be(1) + count(_.getACKI == ackiA) should be(1) + + count(_.getAaid.toScala.contains(aaidB)) should be(2) + count(_.getAaguid.toScala.contains(aaguidB)) should be(2) + count(_.getACKI == ackiB) should be(2) + + count(_.getAaid.toScala.contains(aaidC)) should be(1) + count(_.getAaguid.toScala.contains(aaguidC)) should be(1) + count(_.getACKI == ackiC) should be(1) + + count(entry => + entry.getAaid.toScala.contains(aaidA) || entry.getAaguid.toScala + .contains(aaguidA) || entry.getACKI == ackiA + ) should be(3) + count(entry => + entry.getAaid.toScala.contains(aaidB) || entry.getAaguid.toScala + .contains(aaguidB) || entry.getACKI == ackiB + ) should be(3) + count(entry => + entry.getAaid.toScala.contains(aaidC) || entry.getAaguid.toScala + .contains(aaguidC) || entry.getACKI == ackiC + ) should be(1) + + count(!_.getAaid.toScala.contains(aaidA)) should be(6) + count(!_.getAaguid.toScala.contains(aaguidA)) should be(6) + count(_.getACKI != ackiA) should be(6) + + count(!_.getAaid.toScala.contains(aaidB)) should be(5) + count(!_.getAaguid.toScala.contains(aaguidB)) should be(5) + count(_.getACKI != ackiB) should be(5) + + count(!_.getAaid.toScala.contains(aaidC)) should be(6) + count(!_.getAaguid.toScala.contains(aaguidC)) should be(6) + count(_.getACKI != ackiC) should be(6) + + makeMds(blobTuple)( + _.getAaid.toScala.contains(aaidA) + ).findEntries(_ => true).forEach(_.getAaid.get should be(aaidA)) + makeMds(blobTuple)( + _.getAaguid.toScala.contains(aaguidB) + ).findEntries(_ => true).forEach(_.getAaguid.get should be(aaguidB)) + makeMds(blobTuple)( + _.getACKI == ackiC + ).findEntries(_ => true).forEach(_.getAaguid.get should be(aaguidC)) + } + + it("Filtering correctly impacts the trust verdict in RelyingParty.finishRegistration.") { + val rpIdentity = RelyingPartyIdentity + .builder() + .id(TestAuthenticator.Defaults.rpId) + .name("Test RP") + .build() + val (pkc, _, attestationChain) = + TestAuthenticator.createBasicAttestedCredential( + aaguid = aaguidA.asBytes, + attestationMaker = AttestationMaker.packed( + AttestationSigner.ca( + COSEAlgorithmIdentifier.ES256, + aaguid = aaguidA.asBytes, + validFrom = CertValidFrom, + validTo = CertValidTo, + ) + ), + ) + val attestationCrls = attestationChain.tail + .map({ + case (cert, key) => + TestAuthenticator.buildCrl( + JcaX500NameUtil.getSubject(cert), + key, + "SHA256withECDSA", + CertValidFrom, + CertValidTo, + ) + }) + .toSet + val attestationRootBase64 = + new ByteArray(attestationChain.last._1.getEncoded).getBase64 + + val blobTuple = makeBlob(s"""{ + "legalHeader" : "Kom ihåg att du aldrig får snyta dig i mattan!", + "nextUpdate" : "2022-12-01", + "no" : 0, + "entries": [{ + "aaguid": "${aaguidA.asHexString}", + "metadataStatement": { + "authenticatorVersion": 1, + "attachmentHint" : ["internal"], + "attestationRootCertificates": ["${attestationRootBase64}"], + "attestationTypes" : ["basic_full"], + "authenticationAlgorithms" : ["secp256r1_ecdsa_sha256_raw"], + "description" : "Test authenticator", + "keyProtection" : ["software"], + "matcherProtection" : ["software"], + "protocolFamily" : "u2f", + "publicKeyAlgAndEncodings" : ["ecc_x962_raw"], + "schema" : 3, + "tcDisplay" : [], + "upv" : [{ "major" : 1, "minor" : 1 }], + "userVerificationDetails" : [[{ "userVerificationMethod" : "presence_internal" }]] + }, + "statusReports": [], + "timeOfLastStatusChange": "2022-02-21" + }] + }""") + + val finishRegistrationOptions = FinishRegistrationOptions + .builder() + .request( + PublicKeyCredentialCreationOptions + .builder() + .rp(rpIdentity) + .user( + UserIdentity + .builder() + .name("test") + .displayName("Test user") + .id(ByteArray.fromHex("01020304")) + .build() + ) + .challenge(TestAuthenticator.Defaults.challenge) + .pubKeyCredParams( + Collections.singletonList(PublicKeyCredentialParameters.ES256) + ) + .build() + ) + .response(pkc) + .build() + + def finishRegistration( + filter: MetadataBLOBPayloadEntry => Boolean + ): RegistrationResult = { + val mds = + makeMds(blobTuple, attestationCrls = attestationCrls)(filter) + RelyingParty + .builder() + .identity(rpIdentity) + .credentialRepository(Helpers.CredentialRepository.empty) + .attestationTrustSource(mds) + .clock(Clock.fixed(CertValidFrom, ZoneOffset.UTC)) + .build() + .finishRegistration(finishRegistrationOptions) + } + + finishRegistration( + _.getAaguid.toScala.contains(aaguidA) + ).isAttestationTrusted should be(true) + finishRegistration( + _.getAaguid.toScala.contains(aaguidB) + ).isAttestationTrusted should be(false) + } + } + + describe("2.1. Check whether the status report of the authenticator model has changed compared to the cached entry by looking at the fields timeOfLastStatusChange and statusReport.") { + it("Nothing to test - cache is implemented on the metadata BLOB as a whole.") {} + } + + describe("2.2. Update the status of the cached entry. It is up to the relying party to specify behavior for authenticators with status reports that indicate a lack of certification, or known security issues. However, the status REVOKED indicates significant security issues related to such authenticators.") { + it("Nothing to test for caching - cache is implemented on the metadata BLOB as a whole.") {} + + it("REVOKED authenticators are untrusted by default") { + val aaguidA = + new AAGUID(ByteArray.fromHex("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")) + val aaguidB = + new AAGUID(ByteArray.fromHex("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")) + + def makeMds( + blobTuple: (String, X509Certificate, java.util.Set[CRL]) + ): FidoMetadataService = + FidoMetadataService + .builder() + .useBlob(makeDownloader(blobTuple).loadCachedBlob()) + .build() + + val mds = makeMds(makeBlob(s"""{ + "legalHeader" : "Kom ihåg att du aldrig får snyta dig i mattan!", + "nextUpdate" : "2022-12-01", + "no" : 0, + "entries": [ + { + "aaguid": "${aaguidA.asGuidString()}", + "metadataStatement": { + "authenticatorVersion": 1, + "attachmentHint" : ["internal"], + "attestationRootCertificates": ["MIIB2DCCAX2gAwIBAgICAaswCgYIKoZIzj0EAwIwajEmMCQGA1UEAwwdWXViaWNvIFdlYkF1dGhuIHVuaXQgdGVzdHMgQ0ExDzANBgNVBAoMBll1YmljbzEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjELMAkGA1UEBhMCU0UwHhcNMTgwOTA2MTc0MjAwWhcNMTgwOTEzMTc0MjAwWjBqMSYwJAYDVQQDDB1ZdWJpY28gV2ViQXV0aG4gdW5pdCB0ZXN0cyBDQTEPMA0GA1UECgwGWXViaWNvMSIwIAYDVQQLDBlBdXRoZW50aWNhdG9yIEF0dGVzdGF0aW9uMQswCQYDVQQGEwJTRTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABLtJrr5PYSc4KhmUcwBzgZgNadDnCs/ow2oh2jiKYUqq1A6hFcFf1NPfXLQjP2I4fBI36T6/QR2iY9mbqyP5iVejEzARMA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0EAwIDSQAwRgIhANWaM2Tf2HPKc+ibCr8G4cxpQVr9Gib47a0CpqagCSCwAiEA3oKlX/ID94FKzgHvD2gyCKQU6RltAOMShVwoljj/5+E="], + "attestationTypes" : ["basic_full"], + "authenticationAlgorithms" : ["secp256r1_ecdsa_sha256_raw"], + "description" : "Test authenticator", + "keyProtection" : ["software"], + "matcherProtection" : ["software"], + "protocolFamily" : "u2f", + "publicKeyAlgAndEncodings" : ["ecc_x962_raw"], + "schema" : 3, + "tcDisplay" : [], + "upv" : [{ "major" : 1, "minor" : 1 }], + "userVerificationDetails" : [[{ "userVerificationMethod" : "presence_internal" }]] + }, + "statusReports": [], + "timeOfLastStatusChange": "2022-02-21" + }, + { + "aaguid": "${aaguidB.asGuidString()}", + "metadataStatement": { + "authenticatorVersion": 1, + "attachmentHint" : ["internal"], + "attestationRootCertificates": ["MIIB2DCCAX2gAwIBAgICAaswCgYIKoZIzj0EAwIwajEmMCQGA1UEAwwdWXViaWNvIFdlYkF1dGhuIHVuaXQgdGVzdHMgQ0ExDzANBgNVBAoMBll1YmljbzEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjELMAkGA1UEBhMCU0UwHhcNMTgwOTA2MTc0MjAwWhcNMTgwOTEzMTc0MjAwWjBqMSYwJAYDVQQDDB1ZdWJpY28gV2ViQXV0aG4gdW5pdCB0ZXN0cyBDQTEPMA0GA1UECgwGWXViaWNvMSIwIAYDVQQLDBlBdXRoZW50aWNhdG9yIEF0dGVzdGF0aW9uMQswCQYDVQQGEwJTRTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABLtJrr5PYSc4KhmUcwBzgZgNadDnCs/ow2oh2jiKYUqq1A6hFcFf1NPfXLQjP2I4fBI36T6/QR2iY9mbqyP5iVejEzARMA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0EAwIDSQAwRgIhANWaM2Tf2HPKc+ibCr8G4cxpQVr9Gib47a0CpqagCSCwAiEA3oKlX/ID94FKzgHvD2gyCKQU6RltAOMShVwoljj/5+E="], + "attestationTypes" : ["basic_full"], + "authenticationAlgorithms" : ["secp256r1_ecdsa_sha256_raw"], + "description" : "Test authenticator", + "keyProtection" : ["software"], + "matcherProtection" : ["software"], + "protocolFamily" : "u2f", + "publicKeyAlgAndEncodings" : ["ecc_x962_raw"], + "schema" : 3, + "tcDisplay" : [], + "upv" : [{ "major" : 1, "minor" : 1 }], + "userVerificationDetails" : [[{ "userVerificationMethod" : "presence_internal" }]] + }, + "statusReports": [{ "status": "REVOKED" }], + "timeOfLastStatusChange": "2022-02-21" + } + ] + }""")) + + mds + .findEntries(_ => true) + .asScala + .map(_.getAaguid.toScala) should equal(Set(Some(aaguidA))) + } + } + + describe("2.3. Note: Authenticators with an unacceptable status should be marked accordingly. This information is required for building registration and authentication policies included in the registration request and the authentication request [UAFProtocol].") { + it("Nothing to test - status processing is left for library users to implement.") {} + } + + describe("3. Update the cached metadata statement.") { + it("Nothing to test - cache is implemented on the metadata BLOB as a whole.") {} + } + } + } + + it("More [AuthenticatorTransport] values might be added in the future. FIDO Servers MUST silently ignore all unknown AuthenticatorStatus values.") { + val (blobJwt, cert, crls) = makeBlob("""{ + "legalHeader" : "Kom ihåg att du aldrig får snyta dig i mattan!", + "nextUpdate" : "2022-12-01", + "no" : 0, + "entries": [ + { + "aaguid": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", + "statusReports": [ + { + "status": "ARGHABLARGHLER", + "effectiveDate": "2022-02-15" + }, + { + "status": "NOT_FIDO_CERTIFIED", + "effectiveDate": "2022-02-16" + } + ], + "timeOfLastStatusChange": "2022-02-15" + } + ] + }""") + val downloader: FidoMetadataDownloader = FidoMetadataDownloader + .builder() + .expectLegalHeader("Kom ihåg att du aldrig får snyta dig i mattan!") + .useTrustRoot(cert) + .useBlob(blobJwt) + .clock( + Clock.fixed(Instant.parse("2022-02-15T18:00:00Z"), ZoneOffset.UTC) + ) + .useCrls(crls) + .build() + val mds = + FidoMetadataService.builder().useBlob(downloader.loadCachedBlob()).build() + mds should not be null + + val entries = mds + .findEntries( + Collections.emptyList(), + Some( + new AAGUID(ByteArray.fromHex("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")) + ).toJava, + ) + .asScala + entries should not be empty + entries should have size 1 + entries.head.getStatusReports should have size 1 + entries.head.getStatusReports.get(0).getStatus should be( + AuthenticatorStatus.NOT_FIDO_CERTIFIED + ) + } + + describe("The Relying party MUST reject the Metadata Statement if the authenticatorVersion has not increased [with an UPDATE_AVAILABLE AuthenticatorStatus].") { + + val aaguid = + new AAGUID(ByteArray.fromHex("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")) + + def makeStatusReportsBlob( + statusReports: String, + timeOfLastStatusChange: String, + authenticatorVersion: Int = 1, + ): (String, X509Certificate, java.util.Set[CRL]) = + makeBlob(s"""{ + "legalHeader" : "Kom ihåg att du aldrig får snyta dig i mattan!", + "nextUpdate" : "2022-12-01", + "no" : 0, + "entries": [ + { + "aaguid": "${aaguid.asGuidString}", + "metadataStatement": { + "authenticatorVersion": ${authenticatorVersion}, + "attachmentHint" : ["internal"], + "attestationRootCertificates" : ["MIIB2DCCAX2gAwIBAgICAaswCgYIKoZIzj0EAwIwajEmMCQGA1UEAwwdWXViaWNvIFdlYkF1dGhuIHVuaXQgdGVzdHMgQ0ExDzANBgNVBAoMBll1YmljbzEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjELMAkGA1UEBhMCU0UwHhcNMTgwOTA2MTc0MjAwWhcNMTgwOTEzMTc0MjAwWjBqMSYwJAYDVQQDDB1ZdWJpY28gV2ViQXV0aG4gdW5pdCB0ZXN0cyBDQTEPMA0GA1UECgwGWXViaWNvMSIwIAYDVQQLDBlBdXRoZW50aWNhdG9yIEF0dGVzdGF0aW9uMQswCQYDVQQGEwJTRTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABLtJrr5PYSc4KhmUcwBzgZgNadDnCs/ow2oh2jiKYUqq1A6hFcFf1NPfXLQjP2I4fBI36T6/QR2iY9mbqyP5iVejEzARMA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0EAwIDSQAwRgIhANWaM2Tf2HPKc+ibCr8G4cxpQVr9Gib47a0CpqagCSCwAiEA3oKlX/ID94FKzgHvD2gyCKQU6RltAOMShVwoljj/5+E="], + "attestationTypes" : ["basic_full"], + "authenticationAlgorithms" : ["secp256r1_ecdsa_sha256_raw"], + "description" : "Test authenticator", + "keyProtection" : ["software"], + "matcherProtection" : ["software"], + "protocolFamily" : "u2f", + "publicKeyAlgAndEncodings" : ["ecc_x962_raw"], + "schema" : 3, + "tcDisplay" : [], + "upv" : [{ "major" : 1, "minor" : 1 }], + "userVerificationDetails" : [[{ "userVerificationMethod" : "presence_internal" }]] + }, + "statusReports": ${statusReports}, + "timeOfLastStatusChange": "${timeOfLastStatusChange}" + } + ] + }""") + + def makeMds( + blobTuple: (String, X509Certificate, java.util.Set[CRL]) + ): FidoMetadataService = + FidoMetadataService + .builder() + .useBlob(makeDownloader(blobTuple).loadCachedBlob()) + .build() + + it("A metadata statement with UPDATE_AVAILABLE with authenticatorVersion greater than top-level authenticatorVersion is ignored.") { + val mds = makeMds( + makeStatusReportsBlob( + """[ + { + "status": "UPDATE_AVAILABLE", + "effectiveDate": "2022-02-15", + "authenticatorVersion": 2 + } + ]""", + "2022-02-16", + authenticatorVersion = 1, + ) + ) + + mds + .findEntries(Collections.emptyList(), Some(aaguid).toJava) + .asScala shouldBe empty + } + + it("A metadata statement with UPDATE_AVAILABLE with authenticatorVersion equal to top-level authenticatorVersion is accepted.") { + val mds = makeMds( + makeStatusReportsBlob( + """[ + { + "status": "UPDATE_AVAILABLE", + "effectiveDate": "2022-02-15", + "authenticatorVersion": 2 + } + ]""", + "2022-02-16", + authenticatorVersion = 2, + ) + ) + + mds + .findEntries(Collections.emptyList(), Some(aaguid).toJava) + .asScala should not be empty + } + + it("A metadata statement with UPDATE_AVAILABLE with authenticatorVersion less than top-level authenticatorVersion is accepted.") { + val mds = makeMds( + makeStatusReportsBlob( + """[ + { + "status": "UPDATE_AVAILABLE", + "effectiveDate": "2022-02-15", + "authenticatorVersion": 2 + } + ]""", + "2022-02-16", + authenticatorVersion = 3, + ) + ) + + mds + .findEntries(Collections.emptyList(), Some(aaguid).toJava) + .asScala should not be empty + } + } + + describe("The noAttestationKeyCompromise filter") { + + val attestationRoot = TestAuthenticator.generateAttestationCaCertificate() + val rootCertBase64 = new ByteArray(attestationRoot._1.getEncoded).getBase64 + + val (compromisedCert, _) = + TestAuthenticator.generateAttestationCertificate( + name = new X500Name("CN=Compromised cert 1"), + caCertAndKey = Some(attestationRoot), + ) + val (goodCert, _) = TestAuthenticator.generateAttestationCertificate( + name = new X500Name("CN=Good cert"), + caCertAndKey = Some(attestationRoot), + ) + + val (compromisedCert2a, _) = + TestAuthenticator.generateAttestationCertificate( + name = new X500Name("CN=Compromised cert 2a"), + caCertAndKey = Some(attestationRoot), + ) + val (compromisedCert2b, _) = + TestAuthenticator.generateAttestationCertificate( + name = new X500Name("CN=Compromised cert 2b"), + caCertAndKey = Some(attestationRoot), + ) + + val (unrelatedCert, _) = + TestAuthenticator.generateAttestationCertificate(name = + new X500Name("CN=Unrelated cert") + ) + + val compromisedCertKeyIdentifier = new ByteArray( + CertificateParser.computeSubjectKeyIdentifier(compromisedCert) + ).getHex + val compromisedCert2aKeyIdentifier = new ByteArray( + CertificateParser.computeSubjectKeyIdentifier(compromisedCert2a) + ).getHex + val compromisedCert2bKeyIdentifier = new ByteArray( + CertificateParser.computeSubjectKeyIdentifier(compromisedCert2b) + ).getHex + val goodCertKeyIdentifier = new ByteArray( + CertificateParser.computeSubjectKeyIdentifier(goodCert) + ).getHex + + val aaguidA = + new AAGUID(ByteArray.fromHex("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")) + val aaguidB = + new AAGUID(ByteArray.fromHex("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")) + val aaguidC = + new AAGUID(ByteArray.fromHex("cccccccccccccccccccccccccccccccc")) + + val blob: MetadataBLOBPayload = + JacksonCodecs.jsonWithDefaultEnums.readValue( + s"""{ + "legalHeader" : "Kom ihåg att du aldrig får snyta dig i mattan!", + "nextUpdate" : "2022-12-01", + "no" : 0, + "entries": [ + { + "aaguid": "${aaguidA.asGuidString()}", + "attestationCertificateKeyIdentifiers": ["${goodCertKeyIdentifier}"], + "metadataStatement": { + "aaguid": "${aaguidA.asGuidString()}", + "attestationCertificateKeyIdentifiers": ["${goodCertKeyIdentifier}"], + "authenticatorVersion": 1, + "attachmentHint" : ["internal"], + "attestationRootCertificates": ["${rootCertBase64}"], + "attestationTypes" : ["basic_full"], + "authenticationAlgorithms" : ["secp256r1_ecdsa_sha256_raw"], + "description" : "Test authenticator", + "keyProtection" : ["software"], + "matcherProtection" : ["software"], + "protocolFamily" : "u2f", + "publicKeyAlgAndEncodings" : ["ecc_x962_raw"], + "schema" : 3, + "tcDisplay" : [], + "upv" : [{ "major" : 1, "minor" : 1 }], + "userVerificationDetails" : [[{ "userVerificationMethod" : "presence_internal" }]] + }, + "statusReports": [], + "timeOfLastStatusChange": "2022-02-15" + }, + + { + "aaguid": "${aaguidB.asGuidString()}", + "attestationCertificateKeyIdentifiers": ["${compromisedCertKeyIdentifier}"], + "metadataStatement": { + "aaguid": "${aaguidB.asGuidString()}", + "attestationCertificateKeyIdentifiers": ["${compromisedCertKeyIdentifier}"], + "authenticatorVersion": 1, + "attachmentHint" : ["internal"], + "attestationRootCertificates": ["${rootCertBase64}"], + "attestationTypes" : ["basic_full"], + "authenticationAlgorithms" : ["secp256r1_ecdsa_sha256_raw"], + "description" : "Test authenticator", + "keyProtection" : ["software"], + "matcherProtection" : ["software"], + "protocolFamily" : "u2f", + "publicKeyAlgAndEncodings" : ["ecc_x962_raw"], + "schema" : 3, + "tcDisplay" : [], + "upv" : [{ "major" : 1, "minor" : 1 }], + "userVerificationDetails" : [[{ "userVerificationMethod" : "presence_internal" }]] + }, + "statusReports": [ + { + "status": "ATTESTATION_KEY_COMPROMISE", + "certificate": "${new ByteArray(compromisedCert.getEncoded).getBase64}" + } + ], + "timeOfLastStatusChange": "2022-02-15" + }, + + { + "aaguid": "${aaguidC.asGuidString()}", + "attestationCertificateKeyIdentifiers": ["${compromisedCert2aKeyIdentifier}"], + "metadataStatement": { + "aaguid": "${aaguidC.asGuidString()}", + "attestationCertificateKeyIdentifiers": ["${compromisedCert2bKeyIdentifier}"], + "authenticatorVersion": 1, + "attachmentHint" : ["internal"], + "attestationRootCertificates": ["${rootCertBase64}"], + "attestationTypes" : ["basic_full"], + "authenticationAlgorithms" : ["secp256r1_ecdsa_sha256_raw"], + "description" : "Test authenticator", + "keyProtection" : ["software"], + "matcherProtection" : ["software"], + "protocolFamily" : "u2f", + "publicKeyAlgAndEncodings" : ["ecc_x962_raw"], + "schema" : 3, + "tcDisplay" : [], + "upv" : [{ "major" : 1, "minor" : 1 }], + "userVerificationDetails" : [[{ "userVerificationMethod" : "presence_internal" }]] + }, + "statusReports": [ + { "status": "ATTESTATION_KEY_COMPROMISE" } + ], + "timeOfLastStatusChange": "2022-02-15" + } + ] + }""".stripMargin, + classOf[MetadataBLOBPayload], + ) + + it("is enabled by default.") { + val mds = FidoMetadataService.builder().useBlob(blob).build() + + mds + .findTrustRoots( + List(unrelatedCert).asJava, + Some(aaguidA.asBytes).toJava, + ) + .getTrustRoots + .asScala should not be empty + + mds + .findTrustRoots( + List(goodCert).asJava, + None.toJava, + ) + .getTrustRoots + .asScala should not be empty + + mds + .findTrustRoots( + List(compromisedCert).asJava, + Some(aaguidB.asBytes).toJava, + ) + .getTrustRoots + .asScala shouldBe empty + + mds + .findTrustRoots( + List(unrelatedCert).asJava, + Some(aaguidC.asBytes).toJava, + ) + .getTrustRoots + .asScala shouldBe empty + + mds + .findTrustRoots( + List(compromisedCert).asJava, + None.toJava, + ) + .getTrustRoots + .asScala shouldBe empty + + mds + .findTrustRoots( + List(compromisedCert2a).asJava, + None.toJava, + ) + .getTrustRoots + .asScala shouldBe empty + + mds + .findTrustRoots( + List(compromisedCert2b).asJava, + None.toJava, + ) + .getTrustRoots + .asScala shouldBe empty + } + + it("can be enabled explicitly.") { + val mds = FidoMetadataService + .builder() + .useBlob(blob) + .filter(FidoMetadataService.Filters.noAttestationKeyCompromise()) + .build() + + mds + .findTrustRoots( + List(goodCert).asJava, + Some(aaguidA.asBytes).toJava, + ) + .getTrustRoots + .asScala should not be empty + + mds + .findTrustRoots( + List(compromisedCert).asJava, + Some(aaguidB.asBytes).toJava, + ) + .getTrustRoots + .asScala shouldBe empty + + mds + .findTrustRoots( + List(unrelatedCert).asJava, + Some(aaguidC.asBytes).toJava, + ) + .getTrustRoots + .asScala shouldBe empty + } + + it("can be overridden with a different filter.") { + val mds = + FidoMetadataService.builder().useBlob(blob).filter(_ => true).build() + + mds + .findTrustRoots( + List(compromisedCert).asJava, + Some(aaguidB.asBytes).toJava, + ) + .getTrustRoots + .asScala should not be empty + + mds + .findTrustRoots( + List(compromisedCert).asJava, + Some(aaguidB.asBytes).toJava, + ) + .getTrustRoots + .asScala should not be empty + + mds + .findTrustRoots( + List(unrelatedCert).asJava, + Some(aaguidC.asBytes).toJava, + ) + .getTrustRoots + .asScala should not be empty + } + + } + +} 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 new file mode 100644 index 000000000..e23c2b126 --- /dev/null +++ b/webauthn-server-attestation/src/test/scala/com/yubico/fido/metadata/FidoMetadataDownloaderSpec.scala @@ -0,0 +1,1850 @@ +package com.yubico.fido.metadata + +import com.fasterxml.jackson.databind.node.IntNode +import com.fasterxml.jackson.databind.node.ObjectNode +import com.yubico.fido.metadata.FidoMetadataDownloaderException.Reason +import com.yubico.internal.util.BinaryUtil +import com.yubico.internal.util.JacksonCodecs +import com.yubico.webauthn.TestAuthenticator +import com.yubico.webauthn.data.ByteArray +import com.yubico.webauthn.data.COSEAlgorithmIdentifier +import org.bouncycastle.asn1.x500.X500Name +import org.eclipse.jetty.server.HttpConfiguration +import org.eclipse.jetty.server.HttpConnectionFactory +import org.eclipse.jetty.server.Request +import org.eclipse.jetty.server.SecureRequestCustomizer +import org.eclipse.jetty.server.Server +import org.eclipse.jetty.server.ServerConnector +import org.eclipse.jetty.server.SslConnectionFactory +import org.eclipse.jetty.server.handler.AbstractHandler +import org.eclipse.jetty.util.ssl.SslContextFactory +import org.eclipse.jetty.util.thread.QueuedThreadPool +import org.junit.runner.RunWith +import org.scalatest.BeforeAndAfter +import org.scalatest.FunSpec +import org.scalatest.Matchers +import org.scalatest.tags.Network +import org.scalatestplus.junit.JUnitRunner + +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.net.URL +import java.nio.charset.StandardCharsets +import java.security.DigestException +import java.security.KeyPair +import java.security.KeyStore +import java.security.SecureRandom +import java.security.cert.CRL +import java.security.cert.CertPathValidatorException +import java.security.cert.CertPathValidatorException.BasicReason +import java.security.cert.X509Certificate +import java.time.Clock +import java.time.Instant +import java.time.LocalDate +import java.time.ZoneOffset +import java.util.Optional +import javax.servlet.http.HttpServletRequest +import javax.servlet.http.HttpServletResponse +import scala.jdk.CollectionConverters.ListHasAsScala +import scala.jdk.CollectionConverters.SeqHasAsJava +import scala.jdk.CollectionConverters.SetHasAsJava +import scala.util.Success +import scala.util.Try + +@Network +@RunWith(classOf[JUnitRunner]) +class FidoMetadataDownloaderSpec + extends FunSpec + with Matchers + with BeforeAndAfter { + + var httpServer: Option[Server] = None + after { + for { server <- httpServer } { + server.stop() + } + httpServer = None + } + private def startServer(server: Server): Unit = { + httpServer = Some(server) + server.start() + } + + val CertValidFrom: Instant = Instant.parse("2022-02-18T12:00:00Z") + val CertValidTo: Instant = Instant.parse("2022-03-20T12:00:00Z") + + private def makeTrustRootCert( + distinguishedName: String = + "CN=Yubico java-webauthn-server unit tests CA, O=Yubico", + validFrom: Instant = CertValidFrom, + validTo: Instant = CertValidTo, + ): (X509Certificate, KeyPair, X500Name) = { + val keypair = TestAuthenticator.generateEcKeypair() + val name = new X500Name(distinguishedName) + ( + TestAuthenticator.buildCertificate( + publicKey = keypair.getPublic, + issuerName = name, + subjectName = name, + signingKey = keypair.getPrivate, + signingAlg = COSEAlgorithmIdentifier.ES256, + isCa = true, + validFrom = validFrom, + validTo = validTo, + ), + keypair, + name, + ) + } + + private def makeCert( + caKeypair: KeyPair, + caName: X500Name, + validFrom: Instant = CertValidFrom, + validTo: Instant = CertValidTo, + isCa: Boolean = false, + name: String = + "CN=Yubico java-webauthn-server unit tests blob cert, O=Yubico", + ): (X509Certificate, KeyPair, X500Name) = { + val keypair = TestAuthenticator.generateEcKeypair() + val x500Name = new X500Name(name) + ( + TestAuthenticator.buildCertificate( + publicKey = keypair.getPublic, + issuerName = caName, + subjectName = x500Name, + signingKey = caKeypair.getPrivate, + signingAlg = COSEAlgorithmIdentifier.ES256, + isCa = isCa, + validFrom = validFrom, + validTo = validTo, + ), + keypair, + x500Name, + ) + } + + private def makeCertChain( + caKeypair: KeyPair, + caName: X500Name, + chainLength: Int, + validFrom: Instant = CertValidFrom, + validTo: Instant = CertValidTo, + leafName: String = + "CN=Yubico java-webauthn-server unit tests blob cert, O=Yubico", + ): List[(X509Certificate, KeyPair, X500Name)] = { + var certs: List[(X509Certificate, KeyPair, X500Name)] = Nil + var currentKeypair = caKeypair + var currentName = caName + + for { i <- 1 to chainLength } { + val (cert, keypair, name) = makeCert( + currentKeypair, + currentName, + validFrom = validFrom, + validTo = validTo, + name = + if (i == chainLength) leafName else s"CN=Test intermediate CA ${i}", + isCa = i != chainLength, + ) + certs = (cert, keypair, name) +: certs + currentKeypair = keypair + currentName = name + } + + certs + } + + private def makeBlob( + blobKeypair: KeyPair, + header: String, + body: String, + ): String = { + val blobTbs = new ByteArray( + header.getBytes(StandardCharsets.UTF_8) + ).getBase64Url + "." + new ByteArray( + body.getBytes(StandardCharsets.UTF_8) + ).getBase64Url + val blobSignature = TestAuthenticator.sign( + new ByteArray(blobTbs.getBytes(StandardCharsets.UTF_8)), + blobKeypair.getPrivate, + COSEAlgorithmIdentifier.ES256, + ) + blobTbs + "." + blobSignature.getBase64Url + } + + private def makeBlob( + certChain: List[X509Certificate], + blobKeypair: KeyPair, + nextUpdate: LocalDate, + legalHeader: String = "Kom ihåg att du aldrig får snyta dig i mattan!", + no: Int = 1, + ): String = { + val blobHeader = + s"""{"alg":"ES256","x5c": [${certChain + .map(cert => new ByteArray(cert.getEncoded).getBase64) + .mkString("\"", "\",\"", "\"")}]}""" + val blobBody = s"""{ + "legalHeader": "${legalHeader}", + "no": ${no}, + "nextUpdate": "${nextUpdate}", + "entries": [] + }""" + makeBlob(blobKeypair, blobHeader, blobBody) + } + + private def makeHttpServer( + path: String, + response: String, + ): (Server, String, X509Certificate) = + makeHttpServer(Map(path -> response.getBytes(StandardCharsets.UTF_8))) + private def makeHttpServer( + path: String, + response: Array[Byte], + ): (Server, String, X509Certificate) = + makeHttpServer(Map(path -> response)) + private def makeHttpServer( + responses: Map[String, Array[Byte]] + ): (Server, String, X509Certificate) = { + val tlsKey = TestAuthenticator.generateEcKeypair() + val tlsCert = TestAuthenticator.buildCertificate( + tlsKey.getPublic, + new X500Name("CN=localhost"), + new X500Name("CN=localhost"), + tlsKey.getPrivate, + signingAlg = COSEAlgorithmIdentifier.ES256, + ) + val keystorePassword = "foo" + val keyStore = KeyStore.getInstance(KeyStore.getDefaultType) + keyStore.load(null) + keyStore.setKeyEntry( + "default", + tlsKey.getPrivate, + keystorePassword.toCharArray, + Array(tlsCert), + ) + + val httpConfig = new HttpConfiguration() + httpConfig.addCustomizer(new SecureRequestCustomizer()) + val http11 = new HttpConnectionFactory(httpConfig) + val sslContextFactory = new SslContextFactory.Server() + sslContextFactory.setKeyStore(keyStore) + sslContextFactory.setKeyStorePassword(keystorePassword) + val tls = new SslConnectionFactory(sslContextFactory, http11.getProtocol) + + val threadPool = new QueuedThreadPool() + threadPool.setName("server") + val server = new Server(threadPool) + val connector = new ServerConnector(server, tls, http11) + val port = 8443 + connector.setPort(port) + server.addConnector(connector) + server.setHandler(new AbstractHandler { + override def handle( + target: String, + jettyRequest: Request, + request: HttpServletRequest, + response: HttpServletResponse, + ): Unit = { + responses.get(target) match { + case Some(responseBody) => { + response.getOutputStream.write(responseBody) + response.setStatus(200) + } + case None => response.setStatus(404) + } + + jettyRequest.setHandled(true) + } + }) + + (server, s"https://localhost:${port}", tlsCert) + } + + describe("§3.2. Metadata BLOB object processing rules") { + describe("1. Download and cache the root signing trust anchor from the respective MDS root location e.g. More information can be found at https://fidoalliance.org/metadata/") { + it( + "The trust root is downloaded and cached if there isn't a supplier-cached one." + ) { + val random = new SecureRandom() + val trustRootDistinguishedName = + s"CN=Test trust root ${random.nextInt(10000)}" + val (trustRootCert, caKeypair, caName) = + makeTrustRootCert(distinguishedName = trustRootDistinguishedName) + val (blobCert, blobKeypair, _) = makeCert(caKeypair, caName) + val blobJwt = + makeBlob(List(blobCert), blobKeypair, LocalDate.now()) + val crls = List[CRL]( + TestAuthenticator.buildCrl( + caName, + caKeypair.getPrivate, + "SHA256withECDSA", + CertValidFrom, + CertValidTo, + ) + ) + + var writtenCache: Option[ByteArray] = None + + val (server, serverUrl, httpsCert) = + makeHttpServer("/trust-root.der", trustRootCert.getEncoded) + startServer(server) + + val blob = FidoMetadataDownloader + .builder() + .expectLegalHeader("Kom ihåg att du aldrig får snyta dig i mattan!") + .downloadTrustRoot( + new URL(s"${serverUrl}/trust-root.der"), + Set( + TestAuthenticator.sha256(new ByteArray(trustRootCert.getEncoded)) + ).asJava, + ) + .useTrustRootCache( + () => Optional.empty(), + newCache => { writtenCache = Some(newCache) }, + ) + .useBlob(blobJwt) + .clock(Clock.fixed(CertValidFrom, ZoneOffset.UTC)) + .useCrls(crls.asJava) + .trustHttpsCerts(httpsCert) + .build() + .loadCachedBlob + blob should not be null + blob.getHeader.getX5c.get.asScala.last.getIssuerDN.getName should equal( + trustRootDistinguishedName + ) + writtenCache should equal(Some(new ByteArray(trustRootCert.getEncoded))) + } + + it("The trust root is downloaded and cached if there's an expired one in supplier-cache.") { + val random = new SecureRandom() + + val oldTrustRootDistinguishedName = + s"CN=Test trust root ${random.nextInt(10000)}" + val newTrustRootDistinguishedName = + s"CN=Test trust root ${random.nextInt(10000) + 10000}" + val (oldTrustRootCert, _, _) = + makeTrustRootCert( + distinguishedName = oldTrustRootDistinguishedName, + validFrom = CertValidFrom.minusSeconds(600), + validTo = CertValidFrom.minusSeconds(1), + ) + val (newTrustRootCert, caKeypair, caName) = + makeTrustRootCert(distinguishedName = newTrustRootDistinguishedName) + + val (blobCert, blobKeypair, _) = makeCert(caKeypair, caName) + val blobJwt = + makeBlob(List(blobCert), blobKeypair, LocalDate.now()) + val crls = List[CRL]( + TestAuthenticator.buildCrl( + caName, + caKeypair.getPrivate, + "SHA256withECDSA", + CertValidFrom, + CertValidTo, + ) + ) + + var writtenCache: Option[ByteArray] = None + + val (server, serverUrl, httpsCert) = + makeHttpServer("/trust-root.der", newTrustRootCert.getEncoded) + startServer(server) + + val blob = FidoMetadataDownloader + .builder() + .expectLegalHeader("Kom ihåg att du aldrig får snyta dig i mattan!") + .downloadTrustRoot( + new URL(s"${serverUrl}/trust-root.der"), + Set( + TestAuthenticator.sha256( + new ByteArray(newTrustRootCert.getEncoded) + ) + ).asJava, + ) + .useTrustRootCache( + () => Optional.of(new ByteArray(oldTrustRootCert.getEncoded)), + newCache => { writtenCache = Some(newCache) }, + ) + .useBlob(blobJwt) + .clock(Clock.fixed(CertValidFrom, ZoneOffset.UTC)) + .useCrls(crls.asJava) + .trustHttpsCerts(httpsCert) + .build() + .loadCachedBlob + blob should not be null + blob.getHeader.getX5c.get.asScala.last.getIssuerDN.getName should equal( + newTrustRootDistinguishedName + ) + writtenCache should equal( + Some(new ByteArray(newTrustRootCert.getEncoded)) + ) + } + + it( + "The trust root is not downloaded if there's a valid one in file cache." + ) { + val random = new SecureRandom() + val trustRootDistinguishedName = + s"CN=Test trust root ${random.nextInt(10000)}" + val (trustRootCert, caKeypair, caName) = + makeTrustRootCert(distinguishedName = trustRootDistinguishedName) + val (blobCert, blobKeypair, _) = makeCert(caKeypair, caName) + val blobJwt = + makeBlob(List(blobCert), blobKeypair, LocalDate.now()) + val crls = List[CRL]( + TestAuthenticator.buildCrl( + caName, + caKeypair.getPrivate, + "SHA256withECDSA", + CertValidFrom, + CertValidTo, + ) + ) + + val cacheFile = File.createTempFile( + s"${getClass.getCanonicalName}_test_cache_", + ".tmp", + ) + val f = new FileOutputStream(cacheFile) + f.write(trustRootCert.getEncoded) + f.close() + cacheFile.deleteOnExit() + + val blob = FidoMetadataDownloader + .builder() + .expectLegalHeader("Kom ihåg att du aldrig får snyta dig i mattan!") + .useDefaultTrustRoot() + .useTrustRootCacheFile(cacheFile) + .useBlob(blobJwt) + .clock(Clock.fixed(CertValidFrom, ZoneOffset.UTC)) + .useCrls(crls.asJava) + .build() + .loadCachedBlob + blob should not be null + blob.getHeader.getX5c.get.asScala.last.getIssuerDN.getName should equal( + trustRootDistinguishedName + ) + } + + it( + "The trust root is downloaded and cached if there isn't a file-cached one." + ) { + val random = new SecureRandom() + val trustRootDistinguishedName = + s"CN=Test trust root ${random.nextInt(10000)}" + val (trustRootCert, caKeypair, caName) = + makeTrustRootCert(distinguishedName = trustRootDistinguishedName) + val (blobCert, blobKeypair, _) = makeCert(caKeypair, caName) + val blobJwt = + makeBlob(List(blobCert), blobKeypair, LocalDate.now()) + val crls = List[CRL]( + TestAuthenticator.buildCrl( + caName, + caKeypair.getPrivate, + "SHA256withECDSA", + CertValidFrom, + CertValidTo, + ) + ) + + val (server, serverUrl, httpsCert) = + makeHttpServer("/trust-root.der", trustRootCert.getEncoded) + startServer(server) + + val cacheFile = File.createTempFile( + s"${getClass.getCanonicalName}_test_cache_", + ".tmp", + ) + cacheFile.delete() + cacheFile.deleteOnExit() + + val blob = FidoMetadataDownloader + .builder() + .expectLegalHeader("Kom ihåg att du aldrig får snyta dig i mattan!") + .downloadTrustRoot( + new URL(s"${serverUrl}/trust-root.der"), + Set( + TestAuthenticator.sha256(new ByteArray(trustRootCert.getEncoded)) + ).asJava, + ) + .useTrustRootCacheFile(cacheFile) + .useBlob(blobJwt) + .clock(Clock.fixed(CertValidFrom, ZoneOffset.UTC)) + .useCrls(crls.asJava) + .trustHttpsCerts(httpsCert) + .build() + .loadCachedBlob + blob should not be null + blob.getHeader.getX5c.get.asScala.last.getIssuerDN.getName should equal( + trustRootDistinguishedName + ) + cacheFile.exists() should be(true) + BinaryUtil.readAll(new FileInputStream(cacheFile)) should equal( + trustRootCert.getEncoded + ) + } + + it("The trust root is downloaded and cached if there's an expired one in file cache.") { + val random = new SecureRandom() + + val oldTrustRootDistinguishedName = + s"CN=Test trust root ${random.nextInt(10000)}" + val newTrustRootDistinguishedName = + s"CN=Test trust root ${random.nextInt(10000) + 10000}" + val (oldTrustRootCert, _, _) = + makeTrustRootCert( + distinguishedName = oldTrustRootDistinguishedName, + validFrom = CertValidFrom.minusSeconds(600), + validTo = CertValidFrom.minusSeconds(1), + ) + val (newTrustRootCert, caKeypair, caName) = + makeTrustRootCert(distinguishedName = newTrustRootDistinguishedName) + + val (blobCert, blobKeypair, _) = makeCert(caKeypair, caName) + val blobJwt = + makeBlob(List(blobCert), blobKeypair, LocalDate.now()) + val crls = List[CRL]( + TestAuthenticator.buildCrl( + caName, + caKeypair.getPrivate, + "SHA256withECDSA", + CertValidFrom, + CertValidTo, + ) + ) + + val (server, serverUrl, httpsCert) = + makeHttpServer("/trust-root.der", newTrustRootCert.getEncoded) + startServer(server) + + val cacheFile = File.createTempFile( + s"${getClass.getCanonicalName}_test_cache_", + ".tmp", + ) + val f = new FileOutputStream(cacheFile) + f.write(oldTrustRootCert.getEncoded) + f.close() + cacheFile.deleteOnExit() + + val blob = FidoMetadataDownloader + .builder() + .expectLegalHeader("Kom ihåg att du aldrig får snyta dig i mattan!") + .downloadTrustRoot( + new URL(s"${serverUrl}/trust-root.der"), + Set( + TestAuthenticator.sha256( + new ByteArray(newTrustRootCert.getEncoded) + ) + ).asJava, + ) + .useTrustRootCacheFile(cacheFile) + .useBlob(blobJwt) + .clock(Clock.fixed(CertValidFrom, ZoneOffset.UTC)) + .useCrls(crls.asJava) + .trustHttpsCerts(httpsCert) + .build() + .loadCachedBlob + blob should not be null + blob.getHeader.getX5c.get.asScala.last.getIssuerDN.getName should equal( + newTrustRootDistinguishedName + ) + cacheFile.exists() should be(true) + BinaryUtil.readAll(new FileInputStream(cacheFile)) should equal( + newTrustRootCert.getEncoded + ) + } + + it("The trust root is not downloaded if there's a valid one in supplier-cache.") { + val random = new SecureRandom() + val trustRootDistinguishedName = + s"CN=Test trust root ${random.nextInt(10000)}" + val (trustRootCert, caKeypair, caName) = + makeTrustRootCert(distinguishedName = trustRootDistinguishedName) + val (blobCert, blobKeypair, _) = makeCert(caKeypair, caName) + val blobJwt = + makeBlob(List(blobCert), blobKeypair, LocalDate.now()) + val crls = List[CRL]( + TestAuthenticator.buildCrl( + caName, + caKeypair.getPrivate, + "SHA256withECDSA", + CertValidFrom, + CertValidTo, + ) + ) + + var writtenCache: Option[ByteArray] = None + + val blob = FidoMetadataDownloader + .builder() + .expectLegalHeader("Kom ihåg att du aldrig får snyta dig i mattan!") + .useDefaultTrustRoot() + .useTrustRootCache( + () => Optional.of(new ByteArray(trustRootCert.getEncoded)), + newCache => { writtenCache = Some(newCache) }, + ) + .useBlob(blobJwt) + .clock(Clock.fixed(CertValidFrom, ZoneOffset.UTC)) + .useCrls(crls.asJava) + .build() + .loadCachedBlob + blob should not be null + blob.getHeader.getX5c.get.asScala.last.getIssuerDN.getName should equal( + trustRootDistinguishedName + ) + writtenCache should equal(None) + } + + it("The downloaded trust root cert must match one of the expected SHA256 hashes.") { + val (trustRootCert, caKeypair, caName) = makeTrustRootCert() + val (blobCert, blobKeypair, _) = makeCert(caKeypair, caName) + val blobJwt = makeBlob(List(blobCert), blobKeypair, LocalDate.now()) + val crls = List[CRL]( + TestAuthenticator.buildCrl( + caName, + caKeypair.getPrivate, + "SHA256withECDSA", + CertValidFrom, + CertValidTo, + ) + ) + + val (server, serverUrl, httpsCert) = + makeHttpServer("/trust-root.der", trustRootCert.getEncoded) + startServer(server) + + def testWithHashes(hashes: Set[ByteArray]): MetadataBLOB = { + FidoMetadataDownloader + .builder() + .expectLegalHeader("Kom ihåg att du aldrig får snyta dig i mattan!") + .downloadTrustRoot( + new URL(s"${serverUrl}/trust-root.der"), + hashes.asJava, + ) + .useTrustRootCache(() => Optional.empty(), _ => {}) + .useBlob(blobJwt) + .useCrls(crls.asJava) + .trustHttpsCerts(httpsCert) + .clock(Clock.fixed(CertValidFrom, ZoneOffset.UTC)) + .build() + .loadCachedBlob + } + + val goodHash = + TestAuthenticator.sha256(new ByteArray(trustRootCert.getEncoded)) + val badHash = TestAuthenticator.sha256(goodHash) + + a[DigestException] should be thrownBy { testWithHashes(Set(badHash)) } + testWithHashes(Set(goodHash)) should not be null + testWithHashes(Set(badHash, goodHash)) should not be null + } + } + + describe("2. To validate the digital certificates used in the digital signature, the certificate revocation information MUST be available in the form of CRLs at the respective MDS CRL location e.g. More information can be found at https://fidoalliance.org/metadata/") { + val (trustRootCert, caKeypair, caName) = makeTrustRootCert() + val (blobCert, blobKeypair, _) = makeCert(caKeypair, caName) + val blobJwt = + makeBlob(List(blobCert), blobKeypair, LocalDate.parse("2022-01-19")) + + it( + "Verification fails if the certs don't declare CRL distribution points." + ) { + val thrown = the[CertPathValidatorException] thrownBy { + FidoMetadataDownloader + .builder() + .expectLegalHeader("Kom ihåg att du aldrig får snyta dig i mattan!") + .useTrustRoot(trustRootCert) + .useBlob(blobJwt) + .clock(Clock.fixed(CertValidFrom, ZoneOffset.UTC)) + .build() + .loadCachedBlob() + } + thrown.getReason should equal( + BasicReason.UNDETERMINED_REVOCATION_STATUS + ) + } + + it("Verification succeeds if explicitly given appropriate CRLs.") { + val crls = List[CRL]( + TestAuthenticator.buildCrl( + caName, + caKeypair.getPrivate, + "SHA256withECDSA", + CertValidFrom, + CertValidTo, + ) + ) + + val blob = FidoMetadataDownloader + .builder() + .expectLegalHeader("Kom ihåg att du aldrig får snyta dig i mattan!") + .useTrustRoot(trustRootCert) + .useBlob(blobJwt) + .useCrls(crls.asJava) + .clock(Clock.fixed(CertValidFrom, ZoneOffset.UTC)) + .build() + .loadCachedBlob() + blob should not be null + } + + describe("Intermediate certificates") { + + val (intermediateCert, intermediateKeypair, intermediateName) = + makeCert( + caKeypair, + caName, + isCa = true, + name = "CN=Yubico java-webauthn-server unit tests intermediate CA, O=Yubico", + ) + val (blobCert, blobKeypair, _) = + makeCert(intermediateKeypair, intermediateName) + val blobJwt = makeBlob( + List(blobCert, intermediateCert), + blobKeypair, + LocalDate.parse("2022-01-19"), + ) + + it("each require their own CRL.") { + val thrown = the[CertPathValidatorException] thrownBy { + FidoMetadataDownloader + .builder() + .expectLegalHeader( + "Kom ihåg att du aldrig får snyta dig i mattan!" + ) + .useTrustRoot(trustRootCert) + .useBlob(blobJwt) + .clock(Clock.fixed(CertValidFrom, ZoneOffset.UTC)) + .build() + .loadCachedBlob() + } + thrown.getReason should equal( + BasicReason.UNDETERMINED_REVOCATION_STATUS + ) + + val rootCrl = TestAuthenticator.buildCrl( + caName, + caKeypair.getPrivate, + "SHA256withECDSA", + CertValidFrom, + CertValidTo, + ) + + val thrown2 = the[CertPathValidatorException] thrownBy { + FidoMetadataDownloader + .builder() + .expectLegalHeader( + "Kom ihåg att du aldrig får snyta dig i mattan!" + ) + .useTrustRoot(trustRootCert) + .useBlob(blobJwt) + .clock(Clock.fixed(CertValidFrom, ZoneOffset.UTC)) + .useCrls(List[CRL](rootCrl).asJava) + .build() + .loadCachedBlob() + } + thrown2.getReason should equal( + BasicReason.UNDETERMINED_REVOCATION_STATUS + ) + + val intermediateCrl = TestAuthenticator.buildCrl( + intermediateName, + intermediateKeypair.getPrivate, + "SHA256withECDSA", + CertValidFrom, + CertValidTo, + ) + + val thrown3 = the[CertPathValidatorException] thrownBy { + FidoMetadataDownloader + .builder() + .expectLegalHeader( + "Kom ihåg att du aldrig får snyta dig i mattan!" + ) + .useTrustRoot(trustRootCert) + .useBlob(blobJwt) + .clock(Clock.fixed(CertValidFrom, ZoneOffset.UTC)) + .useCrls(List[CRL](intermediateCrl).asJava) + .build() + .loadCachedBlob() + } + thrown3.getReason should equal( + BasicReason.UNDETERMINED_REVOCATION_STATUS + ) + + val blob = FidoMetadataDownloader + .builder() + .expectLegalHeader("Kom ihåg att du aldrig får snyta dig i mattan!") + .useTrustRoot(trustRootCert) + .useBlob(blobJwt) + .useCrls(List[CRL](rootCrl, intermediateCrl).asJava) + .clock(Clock.fixed(CertValidFrom, ZoneOffset.UTC)) + .build() + .loadCachedBlob() + blob should not be null + } + + it("can revoke downstream certificates too.") { + val rootCrl = TestAuthenticator.buildCrl( + caName, + caKeypair.getPrivate, + "SHA256withECDSA", + CertValidFrom, + CertValidTo, + ) + val intermediateCrl = TestAuthenticator.buildCrl( + intermediateName, + intermediateKeypair.getPrivate, + "SHA256withECDSA", + CertValidFrom, + CertValidTo, + revoked = Set(blobCert), + ) + val crls = List(rootCrl, intermediateCrl) + + val thrown = the[CertPathValidatorException] thrownBy { + FidoMetadataDownloader + .builder() + .expectLegalHeader( + "Kom ihåg att du aldrig får snyta dig i mattan!" + ) + .useTrustRoot(trustRootCert) + .useBlob(blobJwt) + .useCrls(crls.asJava) + .clock(Clock.fixed(CertValidFrom.plusSeconds(1), ZoneOffset.UTC)) + .build() + .loadCachedBlob() + } + thrown.getReason should equal( + BasicReason.REVOKED + ) + } + } + } + + describe("3. The FIDO Server MUST be able to download the latest metadata BLOB object from the well-known URL when appropriate, e.g. https://mds.fidoalliance.org/. The nextUpdate field of the Metadata BLOB specifies a date when the download SHOULD occur at latest.") { + it("The BLOB is downloaded if there isn't a cached one.") { + val random = new SecureRandom() + val blobLegalHeader = + s"Kom ihåg att du aldrig får snyta dig i mattan! ${random.nextInt(10000)}" + val blobNo = random.nextInt(10000); + + val (trustRootCert, caKeypair, caName) = makeTrustRootCert() + val (blobCert, blobKeypair, _) = makeCert(caKeypair, caName) + val blobJwt = + makeBlob( + List(blobCert), + blobKeypair, + LocalDate.parse("2022-01-19"), + no = blobNo, + legalHeader = blobLegalHeader, + ) + val crls = List[CRL]( + TestAuthenticator.buildCrl( + caName, + caKeypair.getPrivate, + "SHA256withECDSA", + CertValidFrom, + CertValidTo, + ) + ) + + val (server, serverUrl, httpsCert) = + makeHttpServer("/blob.jwt", blobJwt) + startServer(server) + + val blob = FidoMetadataDownloader + .builder() + .expectLegalHeader(blobLegalHeader) + .useTrustRoot(trustRootCert) + .downloadBlob(new URL(s"${serverUrl}/blob.jwt")) + .useBlobCache(() => Optional.empty(), _ => {}) + .clock(Clock.fixed(CertValidFrom, ZoneOffset.UTC)) + .useCrls(crls.asJava) + .trustHttpsCerts(httpsCert) + .build() + .loadCachedBlob() + .getPayload + blob should not be null + blob.getLegalHeader should equal(blobLegalHeader) + blob.getNo should equal(blobNo) + } + + it("The BLOB is downloaded if the cached one is out of date.") { + val oldBlobNo = 1 + val newBlobNo = 2 + + val (trustRootCert, caKeypair, caName) = makeTrustRootCert() + val (blobCert, blobKeypair, _) = makeCert(caKeypair, caName) + val oldBlobJwt = + makeBlob( + List(blobCert), + blobKeypair, + CertValidFrom.atOffset(ZoneOffset.UTC).toLocalDate, + no = oldBlobNo, + ) + val newBlobJwt = + makeBlob( + List(blobCert), + blobKeypair, + CertValidTo.atOffset(ZoneOffset.UTC).toLocalDate, + no = newBlobNo, + ) + val crls = List[CRL]( + TestAuthenticator.buildCrl( + caName, + caKeypair.getPrivate, + "SHA256withECDSA", + CertValidFrom, + CertValidTo, + ) + ) + + val (server, serverUrl, httpsCert) = + makeHttpServer("/blob.jwt", newBlobJwt) + startServer(server) + + val blob = FidoMetadataDownloader + .builder() + .expectLegalHeader("Kom ihåg att du aldrig får snyta dig i mattan!") + .useTrustRoot(trustRootCert) + .downloadBlob(new URL(s"${serverUrl}/blob.jwt")) + .useBlobCache( + () => + Optional.of( + new ByteArray(oldBlobJwt.getBytes(StandardCharsets.UTF_8)) + ), + _ => {}, + ) + .clock(Clock.fixed(CertValidFrom, ZoneOffset.UTC)) + .useCrls(crls.asJava) + .trustHttpsCerts(httpsCert) + .build() + .loadCachedBlob() + .getPayload + blob should not be null + blob.getNo should equal(newBlobNo) + } + + it( + "The BLOB is not downloaded if the cached one is not yet out of date." + ) { + val oldBlobNo = 1 + val newBlobNo = 2 + + val (trustRootCert, caKeypair, caName) = makeTrustRootCert() + val (blobCert, blobKeypair, _) = makeCert(caKeypair, caName) + val oldBlobJwt = + makeBlob( + List(blobCert), + blobKeypair, + CertValidTo.atOffset(ZoneOffset.UTC).toLocalDate, + no = oldBlobNo, + ) + val newBlobJwt = + makeBlob( + List(blobCert), + blobKeypair, + CertValidTo.atOffset(ZoneOffset.UTC).toLocalDate, + no = newBlobNo, + ) + val crls = List[CRL]( + TestAuthenticator.buildCrl( + caName, + caKeypair.getPrivate, + "SHA256withECDSA", + CertValidFrom, + CertValidTo, + ) + ) + + val (server, serverUrl, httpsCert) = + makeHttpServer("/blob.jwt", newBlobJwt) + startServer(server) + + val blob = FidoMetadataDownloader + .builder() + .expectLegalHeader("Kom ihåg att du aldrig får snyta dig i mattan!") + .useTrustRoot(trustRootCert) + .downloadBlob(new URL(s"${serverUrl}/blob.jwt")) + .useBlobCache( + () => + Optional.of( + new ByteArray(oldBlobJwt.getBytes(StandardCharsets.UTF_8)) + ), + _ => {}, + ) + .clock(Clock.fixed(CertValidFrom, ZoneOffset.UTC)) + .useCrls(crls.asJava) + .trustHttpsCerts(httpsCert) + .build() + .loadCachedBlob() + .getPayload + blob should not be null + blob.getNo should equal(oldBlobNo) + } + + } + + describe("4. If the x5u attribute is present in the JWT Header, then:") { + + describe("1. The FIDO Server MUST verify that the URL specified by the x5u attribute has the same web-origin as the URL used to download the metadata BLOB from. The FIDO Server SHOULD ignore the file if the web-origin differs (in order to prevent loading objects from arbitrary sites).") { + it("x5u on a different host is rejected.") { + val (trustRootCert, caKeypair, caName) = makeTrustRootCert() + val (blobCert, blobKeypair, _) = makeCert(caKeypair, caName) + + val certChain = List(blobCert) + val certChainPem = certChain + .map(cert => new ByteArray(cert.getEncoded).getBase64) + .mkString( + "-----BEGIN CERTIFICATE-----\n", + "\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\n", + "\n-----END CERTIFICATE-----", + ) + + val blobJwt = + makeBlob( + blobKeypair, + s"""{"alg":"ES256","x5u": "https://localhost:8444/chain.pem"}""", + s"""{ + "legalHeader": "Kom ihåg att du aldrig får snyta dig i mattan!", + "no": 1, + "nextUpdate": "2022-01-19", + "entries": [] + }""", + ) + + val (server, _, httpsCert) = + makeHttpServer( + Map( + "/chain.pem" -> certChainPem.getBytes(StandardCharsets.UTF_8), + "/blob.jwt" -> blobJwt.getBytes(StandardCharsets.UTF_8), + ) + ) + startServer(server) + + val crls = List[CRL]( + TestAuthenticator.buildCrl( + caName, + caKeypair.getPrivate, + "SHA256withECDSA", + CertValidFrom, + CertValidTo, + ) + ) + + val thrown = the[IllegalArgumentException] thrownBy { + FidoMetadataDownloader + .builder() + .expectLegalHeader( + "Kom ihåg att du aldrig får snyta dig i mattan!" + ) + .useTrustRoot(trustRootCert) + .downloadBlob(new URL("https://localhost:8443/blob.jwt")) + .useBlobCache(() => Optional.empty(), _ => {}) + .useCrls(crls.asJava) + .trustHttpsCerts(httpsCert) + .build() + .loadCachedBlob + } + thrown should not be null + } + } + + describe("2. The FIDO Server MUST download the certificate (chain) from the URL specified by the x5u attribute [JWS]. The certificate chain MUST be verified to properly chain to the metadata BLOB signing trust anchor according to [RFC5280]. All certificates in the chain MUST be checked for revocation according to [RFC5280].") { + it("x5u with one cert is accepted.") { + val (trustRootCert, caKeypair, caName) = makeTrustRootCert() + val (blobCert, blobKeypair, _) = makeCert(caKeypair, caName) + + val certChain = List(blobCert) + val certChainPem = certChain + .map(cert => new ByteArray(cert.getEncoded).getBase64) + .mkString( + "-----BEGIN CERTIFICATE-----\n", + "\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\n", + "\n-----END CERTIFICATE-----", + ) + + val (server, serverUrl, httpsCert) = + makeHttpServer("/chain.pem", certChainPem) + startServer(server) + + val blobJwt = + makeBlob( + blobKeypair, + s"""{"alg":"ES256","x5u": "${serverUrl}/chain.pem"}""", + s"""{ + "legalHeader": "Kom ihåg att du aldrig får snyta dig i mattan!", + "no": 1, + "nextUpdate": "2022-01-19", + "entries": [] + }""", + ) + val crls = List[CRL]( + TestAuthenticator.buildCrl( + caName, + caKeypair.getPrivate, + "SHA256withECDSA", + CertValidFrom, + CertValidTo, + ) + ) + + val blob = FidoMetadataDownloader + .builder() + .expectLegalHeader("Kom ihåg att du aldrig får snyta dig i mattan!") + .useTrustRoot(trustRootCert) + .useBlob(blobJwt) + .useCrls(crls.asJava) + .trustHttpsCerts(httpsCert) + .clock(Clock.fixed(CertValidFrom, ZoneOffset.UTC)) + .build() + .loadCachedBlob + blob should not be null + } + + it("x5u with an unknown trust anchor is rejected.") { + val (trustRootCert, caKeypair, caName) = makeTrustRootCert() + val (_, untrustedCaKeypair, untrustedCaName) = makeTrustRootCert() + val (blobCert, blobKeypair, _) = + makeCert(untrustedCaKeypair, untrustedCaName) + + val certChain = List(blobCert) + val certChainPem = certChain + .map(cert => new ByteArray(cert.getEncoded).getBase64) + .mkString( + "-----BEGIN CERTIFICATE-----\n", + "\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\n", + "\n-----END CERTIFICATE-----", + ) + + val (server, serverUrl, httpsCert) = + makeHttpServer("/chain.pem", certChainPem) + startServer(server) + + val blobJwt = + makeBlob( + blobKeypair, + s"""{"alg":"ES256","x5u": "${serverUrl}/chain.pem"}""", + s"""{ + "legalHeader": "Kom ihåg att du aldrig får snyta dig i mattan!", + "no": 1, + "nextUpdate": "2022-01-19", + "entries": [] + }""", + ) + val crls = List[CRL]( + TestAuthenticator.buildCrl( + caName, + caKeypair.getPrivate, + "SHA256withECDSA", + CertValidFrom, + CertValidTo, + ) + ) + + val thrown = the[CertPathValidatorException] thrownBy { + FidoMetadataDownloader + .builder() + .expectLegalHeader( + "Kom ihåg att du aldrig får snyta dig i mattan!" + ) + .useTrustRoot(trustRootCert) + .useBlob(blobJwt) + .useCrls(crls.asJava) + .trustHttpsCerts(httpsCert) + .clock(Clock.fixed(CertValidFrom, ZoneOffset.UTC)) + .build() + .loadCachedBlob + } + thrown should not be null + thrown.getReason should be( + CertPathValidatorException.BasicReason.INVALID_SIGNATURE + ) + } + + it("x5u with three certs requires a CRL for each CA certificate.") { + val (trustRootCert, caKeypair, caName) = makeTrustRootCert() + val certChain = makeCertChain(caKeypair, caName, 3) + certChain.length should be(3) + val certChainPem = certChain + .map({ + case (cert, _, _) => new ByteArray(cert.getEncoded).getBase64 + }) + .mkString( + "-----BEGIN CERTIFICATE-----\n", + "\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\n", + "\n-----END CERTIFICATE-----", + ) + + val crls = + (certChain.tail :+ (trustRootCert, caKeypair, caName)).map({ + case (_, keypair, name) => + TestAuthenticator.buildCrl( + name, + keypair.getPrivate, + "SHA256withECDSA", + CertValidFrom, + CertValidTo, + ) + }) + + val (server, serverUrl, httpsCert) = + makeHttpServer("/chain.pem", certChainPem) + startServer(server) + + val blobJwt = + makeBlob( + certChain.head._2, + s"""{"alg":"ES256","x5u": "${serverUrl}/chain.pem"}""", + s"""{ + "legalHeader": "Kom ihåg att du aldrig får snyta dig i mattan!", + "no": 1, + "nextUpdate": "2022-01-19", + "entries": [] + }""", + ) + + val clock = Clock.fixed(CertValidFrom, ZoneOffset.UTC) + + val blob = FidoMetadataDownloader + .builder() + .expectLegalHeader("Kom ihåg att du aldrig får snyta dig i mattan!") + .useTrustRoot(trustRootCert) + .useBlob(blobJwt) + .useCrls(crls.asJava) + .trustHttpsCerts(httpsCert) + .clock(clock) + .build() + .loadCachedBlob + blob should not be null + + for { i <- certChain.indices } { + val splicedCrls = crls.take(i) ++ crls.drop(i + 1) + splicedCrls.length should be(crls.length - 1) + val thrown = the[CertPathValidatorException] thrownBy { + FidoMetadataDownloader + .builder() + .expectLegalHeader( + "Kom ihåg att du aldrig får snyta dig i mattan!" + ) + .useTrustRoot(trustRootCert) + .useBlob(blobJwt) + .useCrls(splicedCrls.asJava) + .trustHttpsCerts(httpsCert) + .clock(clock) + .build() + .loadCachedBlob + } + thrown should not be null + thrown.getReason should be( + CertPathValidatorException.BasicReason.UNDETERMINED_REVOCATION_STATUS + ) + } + } + } + + describe("3. The FIDO Server SHOULD ignore the file if the chain cannot be verified or if one of the chain certificates is revoked.") { + it("Verification fails if explicitly given CRLs where a cert in the chain is revoked.") { + val (trustRootCert, caKeypair, caName) = makeTrustRootCert() + val certChain = makeCertChain(caKeypair, caName, 3) + certChain.length should be(3) + val certChainPem = certChain + .map({ + case (cert, _, _) => new ByteArray(cert.getEncoded).getBase64 + }) + .mkString( + "-----BEGIN CERTIFICATE-----\n", + "\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\n", + "\n-----END CERTIFICATE-----", + ) + + val crls = + (certChain.tail :+ (trustRootCert, caKeypair, caName)).map({ + case (_, keypair, name) => + TestAuthenticator.buildCrl( + name, + keypair.getPrivate, + "SHA256withECDSA", + CertValidFrom, + CertValidTo, + ) + }) + + val (server, serverUrl, httpsCert) = + makeHttpServer("/chain.pem", certChainPem) + startServer(server) + + val blobJwt = + makeBlob( + certChain.head._2, + s"""{"alg":"ES256","x5u": "${serverUrl}/chain.pem"}""", + s"""{ + "legalHeader": "Kom ihåg att du aldrig får snyta dig i mattan!", + "no": 1, + "nextUpdate": "2022-01-19", + "entries": [] + }""", + ) + + val clock = Clock.fixed(CertValidFrom.plusSeconds(1), ZoneOffset.UTC) + + val blob = FidoMetadataDownloader + .builder() + .expectLegalHeader("Kom ihåg att du aldrig får snyta dig i mattan!") + .useTrustRoot(trustRootCert) + .useBlob(blobJwt) + .useCrls(crls.asJava) + .trustHttpsCerts(httpsCert) + .clock(clock) + .build() + .loadCachedBlob + blob should not be null + + for { i <- certChain.indices } { + val crlsWithRevocation = + crls.take(i) ++ crls.drop(i + 1) :+ TestAuthenticator.buildCrl( + certChain.lift(i + 1).map(_._3).getOrElse(caName), + certChain.lift(i + 1).map(_._2).getOrElse(caKeypair).getPrivate, + "SHA256withECDSA", + CertValidFrom, + CertValidTo, + revoked = Set(certChain(i)._1), + ) + crlsWithRevocation.length should equal(crls.length) + val thrown = the[CertPathValidatorException] thrownBy { + FidoMetadataDownloader + .builder() + .expectLegalHeader( + "Kom ihåg att du aldrig får snyta dig i mattan!" + ) + .useTrustRoot(trustRootCert) + .useBlob(blobJwt) + .useCrls(crlsWithRevocation.asJava) + .trustHttpsCerts(httpsCert) + .clock(clock) + .build() + .loadCachedBlob + } + thrown should not be null + thrown.getReason should be(BasicReason.REVOKED) + thrown.getIndex should equal(i) + } + } + } + } + + describe("5. If the x5u attribute is missing, the chain should be retrieved from the x5c attribute. If that attribute is missing as well, Metadata BLOB signing trust anchor is considered the BLOB signing certificate chain.") { + it("x5c with one cert is accepted.") { + val (trustRootCert, caKeypair, caName) = makeTrustRootCert() + val (blobCert, blobKeypair, _) = makeCert(caKeypair, caName) + val certChain = List(blobCert) + val certChainJson = certChain + .map(cert => new ByteArray(cert.getEncoded).getBase64) + .mkString("[\"", "\",\"", "\"]") + val blobJwt = + makeBlob( + blobKeypair, + s"""{"alg":"ES256","x5c": ${certChainJson}}""", + s"""{ + "legalHeader": "Kom ihåg att du aldrig får snyta dig i mattan!", + "no": 1, + "nextUpdate": "2022-01-19", + "entries": [] + }""", + ) + val crls = List[CRL]( + TestAuthenticator.buildCrl( + caName, + caKeypair.getPrivate, + "SHA256withECDSA", + CertValidFrom, + CertValidTo, + ) + ) + + val blob = FidoMetadataDownloader + .builder() + .expectLegalHeader("Kom ihåg att du aldrig får snyta dig i mattan!") + .useTrustRoot(trustRootCert) + .useBlob(blobJwt) + .useCrls(crls.asJava) + .clock(Clock.fixed(CertValidFrom, ZoneOffset.UTC)) + .build() + .loadCachedBlob + blob should not be null + } + + it("x5c with three certs requires a CRL for each CA certificate.") { + val (trustRootCert, caKeypair, caName) = makeTrustRootCert() + val certChain = makeCertChain(caKeypair, caName, 3) + certChain.length should be(3) + val certChainJson = certChain + .map({ + case (cert, _, _) => new ByteArray(cert.getEncoded).getBase64 + }) + .mkString("[\"", "\",\"", "\"]") + + val blobJwt = + makeBlob( + certChain.head._2, + s"""{"alg":"ES256","x5c": ${certChainJson}}""", + s"""{ + "legalHeader": "Kom ihåg att du aldrig får snyta dig i mattan!", + "no": 1, + "nextUpdate": "2022-01-19", + "entries": [] + }""", + ) + val crls = (certChain.tail :+ (trustRootCert, caKeypair, caName)).map({ + case (_, keypair, name) => + TestAuthenticator.buildCrl( + name, + keypair.getPrivate, + "SHA256withECDSA", + CertValidFrom, + CertValidTo, + ) + }) + + val clock = Clock.fixed(CertValidFrom, ZoneOffset.UTC) + + val blob = Try( + FidoMetadataDownloader + .builder() + .expectLegalHeader("Kom ihåg att du aldrig får snyta dig i mattan!") + .useTrustRoot(trustRootCert) + .useBlob(blobJwt) + .useCrls(crls.asJava) + .clock(clock) + .build() + .loadCachedBlob + ) + blob should not be null + blob shouldBe a[Success[_]] + + for { i <- certChain.indices } { + val splicedCrls = crls.take(i) ++ crls.drop(i + 1) + splicedCrls.length should be(crls.length - 1) + val thrown = the[CertPathValidatorException] thrownBy { + FidoMetadataDownloader + .builder() + .expectLegalHeader( + "Kom ihåg att du aldrig får snyta dig i mattan!" + ) + .useTrustRoot(trustRootCert) + .useBlob(blobJwt) + .useCrls(splicedCrls.asJava) + .clock(clock) + .build() + .loadCachedBlob + } + thrown should not be null + thrown.getReason should be( + CertPathValidatorException.BasicReason.UNDETERMINED_REVOCATION_STATUS + ) + } + } + + it("Missing x5c means the trust root cert is used as the signer.") { + val (trustRootCert, caKeypair, caName) = makeTrustRootCert() + val blobJwt = + makeBlob( + caKeypair, + s"""{"alg":"ES256"}""", + s"""{ + "legalHeader": "Kom ihåg att du aldrig får snyta dig i mattan!", + "no": 1, + "nextUpdate": "2022-01-19", + "entries": [] + }""", + ) + + val crls = List( + TestAuthenticator.buildCrl( + caName, + caKeypair.getPrivate, + "SHA256withECDSA", + CertValidFrom, + CertValidTo, + ) + ) + + val blob = FidoMetadataDownloader + .builder() + .expectLegalHeader("Kom ihåg att du aldrig får snyta dig i mattan!") + .useTrustRoot(trustRootCert) + .useBlob(blobJwt) + .useCrls(crls.asJava) + .clock(Clock.fixed(CertValidFrom, ZoneOffset.UTC)) + .build() + .loadCachedBlob + blob should not be null + } + } + + describe("6. Verify the signature of the Metadata BLOB object using the BLOB signing certificate chain (as determined by the steps above). The FIDO Server SHOULD ignore the file if the signature is invalid. It SHOULD also ignore the file if its number (no) is less or equal to the number of the last Metadata BLOB object cached locally.") { + it("Invalid signatures are detected.") { + val (trustRootCert, caKeypair, caName) = makeTrustRootCert() + val (blobCert, blobKeypair, _) = makeCert(caKeypair, caName) + + val validBlobJwt = + makeBlob(List(blobCert), blobKeypair, LocalDate.parse("2022-01-19")) + val crls = List( + TestAuthenticator.buildCrl( + caName, + caKeypair.getPrivate, + "SHA256withECDSA", + CertValidFrom, + CertValidTo, + ) + ) + val badBlobJwt = validBlobJwt + .split(raw"\.") + .updated( + 1, { + val json = JacksonCodecs.json() + val badBlobBody = json + .readTree( + ByteArray + .fromBase64Url(validBlobJwt.split(raw"\.")(1)) + .getBytes + ) + .asInstanceOf[ObjectNode] + badBlobBody.set("no", new IntNode(7)) + new ByteArray( + json + .writeValueAsString(badBlobBody) + .getBytes(StandardCharsets.UTF_8) + ).getBase64 + }, + ) + .mkString(".") + + val thrown = the[FidoMetadataDownloaderException] thrownBy { + FidoMetadataDownloader + .builder() + .expectLegalHeader("Kom ihåg att du aldrig får snyta dig i mattan!") + .useTrustRoot(trustRootCert) + .useBlob(badBlobJwt) + .useCrls(crls.asJava) + .build() + .loadCachedBlob + } + thrown.getReason should be(Reason.BAD_SIGNATURE) + } + + it("""A newly downloaded BLOB is disregarded if the cached one has a greater "no".""") { + val oldBlobNo = 2 + val newBlobNo = 1 + + val (trustRootCert, caKeypair, caName) = makeTrustRootCert() + val (blobCert, blobKeypair, _) = makeCert(caKeypair, caName) + val oldBlobJwt = + makeBlob( + List(blobCert), + blobKeypair, + CertValidFrom.atOffset(ZoneOffset.UTC).toLocalDate, + no = oldBlobNo, + ) + val newBlobJwt = + makeBlob( + List(blobCert), + blobKeypair, + CertValidTo.atOffset(ZoneOffset.UTC).toLocalDate, + no = newBlobNo, + ) + val crls = List[CRL]( + TestAuthenticator.buildCrl( + caName, + caKeypair.getPrivate, + "SHA256withECDSA", + CertValidFrom, + CertValidTo, + ) + ) + + val (server, serverUrl, httpsCert) = + makeHttpServer("/blob.jwt", newBlobJwt) + startServer(server) + + val blob = FidoMetadataDownloader + .builder() + .expectLegalHeader("Kom ihåg att du aldrig får snyta dig i mattan!") + .useTrustRoot(trustRootCert) + .downloadBlob(new URL(s"${serverUrl}/blob.jwt")) + .useBlobCache( + () => + Optional.of( + new ByteArray(oldBlobJwt.getBytes(StandardCharsets.UTF_8)) + ), + _ => {}, + ) + .clock(Clock.fixed(CertValidFrom, ZoneOffset.UTC)) + .useCrls(crls.asJava) + .trustHttpsCerts(httpsCert) + .build() + .loadCachedBlob + .getPayload + blob should not be null + blob.getNo should equal(oldBlobNo) + } + + it("A newly downloaded BLOB is disregarded if it has an invalid signature but the cached one has a valid signature.") { + val oldBlobNo = 1 + val newBlobNo = 2 + + val (trustRootCert, caKeypair, caName) = makeTrustRootCert() + val (blobCert, blobKeypair, _) = makeCert(caKeypair, caName) + val oldBlobJwt = + makeBlob( + List(blobCert), + blobKeypair, + CertValidFrom.atOffset(ZoneOffset.UTC).toLocalDate, + no = oldBlobNo, + ) + val newBlobJwt = + makeBlob( + List(blobCert), + blobKeypair, + CertValidTo.atOffset(ZoneOffset.UTC).toLocalDate, + no = newBlobNo, + ) + val crls = List[CRL]( + TestAuthenticator.buildCrl( + caName, + caKeypair.getPrivate, + "SHA256withECDSA", + CertValidFrom, + CertValidTo, + ) + ) + + val badNewBlobJwt = newBlobJwt + .split(raw"\.") + .updated( + 1, { + val json = JacksonCodecs.json() + val badBlobBody = json + .readTree( + ByteArray.fromBase64Url(newBlobJwt.split(raw"\.")(1)).getBytes + ) + .asInstanceOf[ObjectNode] + badBlobBody.set("no", new IntNode(7)) + new ByteArray( + json + .writeValueAsString(badBlobBody) + .getBytes(StandardCharsets.UTF_8) + ).getBase64 + }, + ) + .mkString(".") + + val (server, serverUrl, httpsCert) = + makeHttpServer("/blob.jwt", badNewBlobJwt) + startServer(server) + + val blob = FidoMetadataDownloader + .builder() + .expectLegalHeader("Kom ihåg att du aldrig får snyta dig i mattan!") + .useTrustRoot(trustRootCert) + .downloadBlob(new URL(s"${serverUrl}/blob.jwt")) + .useBlobCache( + () => + Optional.of( + new ByteArray(oldBlobJwt.getBytes(StandardCharsets.UTF_8)) + ), + _ => {}, + ) + .clock(Clock.fixed(CertValidFrom, ZoneOffset.UTC)) + .useCrls(crls.asJava) + .trustHttpsCerts(httpsCert) + .build() + .loadCachedBlob + .getPayload + blob should not be null + blob.getNo should equal(oldBlobNo) + } + } + + describe("7. Write the verified object to a local cache as required.") { + it("Cache consumer works.") { + val (trustRootCert, caKeypair, caName) = makeTrustRootCert() + val (blobCert, blobKeypair, _) = makeCert(caKeypair, caName) + val blobJwt = + makeBlob(List(blobCert), blobKeypair, LocalDate.parse("2022-01-19")) + val crls = List( + TestAuthenticator.buildCrl( + caName, + caKeypair.getPrivate, + "SHA256withECDSA", + CertValidFrom, + CertValidTo, + ) + ) + + val (server, serverUrl, httpsCert) = + makeHttpServer("/blob.jwt", blobJwt) + startServer(server) + + var writtenCache: Option[ByteArray] = None + + val blob = FidoMetadataDownloader + .builder() + .expectLegalHeader("Kom ihåg att du aldrig får snyta dig i mattan!") + .useTrustRoot(trustRootCert) + .downloadBlob(new URL(s"${serverUrl}/blob.jwt")) + .useBlobCache( + () => Optional.empty(), + cacheme => { writtenCache = Some(cacheme) }, + ) + .clock(Clock.fixed(CertValidFrom, ZoneOffset.UTC)) + .useCrls(crls.asJava) + .trustHttpsCerts(httpsCert) + .build() + .loadCachedBlob + .getPayload + blob should not be null + writtenCache should equal( + Some(new ByteArray(blobJwt.getBytes(StandardCharsets.UTF_8))) + ) + } + + describe("File cache") { + val (trustRootCert, caKeypair, caName) = makeTrustRootCert() + val (blobCert, blobKeypair, _) = makeCert(caKeypair, caName) + val blobJwt = + makeBlob( + List(blobCert), + blobKeypair, + LocalDate.parse("2022-01-19"), + no = 2, + ) + val oldBlobJwt = + makeBlob( + List(blobCert), + blobKeypair, + LocalDate.parse("2022-01-19"), + no = 1, + ) + val crls = List( + TestAuthenticator.buildCrl( + caName, + caKeypair.getPrivate, + "SHA256withECDSA", + CertValidFrom, + CertValidTo, + ) + ) + + it("is overwritten if it exists.") { + val (server, serverUrl, httpsCert) = + makeHttpServer("/blob.jwt", blobJwt) + startServer(server) + + val cacheFile = File.createTempFile( + s"${getClass.getCanonicalName}_test_cache_", + ".tmp", + ) + val f = new FileOutputStream(cacheFile) + f.write(oldBlobJwt.getBytes(StandardCharsets.UTF_8)) + f.close() + cacheFile.deleteOnExit() + + val blob = FidoMetadataDownloader + .builder() + .expectLegalHeader("Kom ihåg att du aldrig får snyta dig i mattan!") + .useTrustRoot(trustRootCert) + .downloadBlob(new URL(s"${serverUrl}/blob.jwt")) + .useBlobCacheFile(cacheFile) + .clock( + Clock.fixed(Instant.parse("2022-01-19T00:00:00Z"), ZoneOffset.UTC) + ) + .useCrls(crls.asJava) + .trustHttpsCerts(httpsCert) + .clock(Clock.fixed(CertValidFrom, ZoneOffset.UTC)) + .build() + .loadCachedBlob + .getPayload + blob should not be null + blob.getNo should be(2) + cacheFile.exists() should be(true) + BinaryUtil.readAll(new FileInputStream(cacheFile)) should equal( + blobJwt.getBytes(StandardCharsets.UTF_8) + ) + } + + it("is created if it does not exist.") { + val (server, serverUrl, httpsCert) = + makeHttpServer("/blob.jwt", blobJwt) + startServer(server) + + val cacheFile = File.createTempFile( + s"${getClass.getCanonicalName}_test_cache_", + ".tmp", + ) + cacheFile.delete() + cacheFile.deleteOnExit() + + val blob = FidoMetadataDownloader + .builder() + .expectLegalHeader("Kom ihåg att du aldrig får snyta dig i mattan!") + .useTrustRoot(trustRootCert) + .downloadBlob(new URL(s"${serverUrl}/blob.jwt")) + .useBlobCacheFile(cacheFile) + .clock( + Clock.fixed(Instant.parse("2022-01-19T00:00:00Z"), ZoneOffset.UTC) + ) + .useCrls(crls.asJava) + .trustHttpsCerts(httpsCert) + .clock(Clock.fixed(CertValidFrom, ZoneOffset.UTC)) + .build() + .loadCachedBlob + .getPayload + blob should not be null + blob.getNo should be(2) + cacheFile.exists() should be(true) + BinaryUtil.readAll(new FileInputStream(cacheFile)) should equal( + blobJwt.getBytes(StandardCharsets.UTF_8) + ) + } + + it("is read from.") { + val (server, serverUrl, httpsCert) = + makeHttpServer("/blob.jwt", oldBlobJwt) + startServer(server) + + val cacheFile = File.createTempFile( + s"${getClass.getCanonicalName}_test_cache_", + ".tmp", + ) + cacheFile.deleteOnExit() + val f = new FileOutputStream(cacheFile) + f.write(blobJwt.getBytes(StandardCharsets.UTF_8)) + f.close() + + val blob = FidoMetadataDownloader + .builder() + .expectLegalHeader("Kom ihåg att du aldrig får snyta dig i mattan!") + .useTrustRoot(trustRootCert) + .downloadBlob(new URL(s"${serverUrl}/blob.jwt")) + .useBlobCacheFile(cacheFile) + .clock(Clock.fixed(CertValidFrom, ZoneOffset.UTC)) + .useCrls(crls.asJava) + .trustHttpsCerts(httpsCert) + .build() + .loadCachedBlob + .getPayload + blob should not be null + blob.getNo should be(2) + } + } + } + + describe("8. Iterate through the individual entries (of type MetadataBLOBPayloadEntry). For each entry:") { + it("Nothing to test - see instead FidoMetadataService.") {} + } + } + +} 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 new file mode 100644 index 000000000..2329a8c35 --- /dev/null +++ b/webauthn-server-attestation/src/test/scala/com/yubico/fido/metadata/Generators.scala @@ -0,0 +1,489 @@ +package com.yubico.fido.metadata + +import com.yubico.scalacheck.gen.JavaGenerators.arbitraryUrl +import com.yubico.webauthn.TestAuthenticator +import com.yubico.webauthn.data.AuthenticatorTransport +import com.yubico.webauthn.data.Generators.arbitraryAuthenticatorTransport +import com.yubico.webauthn.data.Generators.arbitraryPublicKeyCredentialParameters +import com.yubico.webauthn.data.Generators.byteArray +import com.yubico.webauthn.data.PublicKeyCredentialParameters +import com.yubico.webauthn.extension.uvm.KeyProtectionType +import com.yubico.webauthn.extension.uvm.MatcherProtectionType +import com.yubico.webauthn.extension.uvm.UserVerificationMethod +import org.scalacheck.Arbitrary +import org.scalacheck.Arbitrary.arbitrary +import org.scalacheck.Gen + +import java.net.URL +import java.security.cert.X509Certificate +import java.time.LocalDate +import scala.jdk.CollectionConverters.MapHasAsJava +import scala.jdk.CollectionConverters.SeqHasAsJava +import scala.jdk.CollectionConverters.SetHasAsJava + +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() + ) + + 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, + ) + ) + + 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() + ) + + implicit val arbitraryAaid: Arbitrary[AAID] = Arbitrary(for { + prefix <- byteArray(2, 2) + suffix <- byteArray(2, 2) + } yield new AAID(s"${prefix.getHex}#${suffix.getHex}")) + + implicit val arbitraryAaguid: Arbitrary[AAGUID] = Arbitrary( + byteArray(16, 16).map(new AAGUID(_)) + ) + + 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( + 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 + .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 { + 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)) + + implicit val arbitraryVersion: Arbitrary[Version] = Arbitrary(for { + major <- arbitrary[Int] + minor <- arbitrary[Int] + } yield new Version(major, minor)) + + implicit val arbitraryVerificationMethodDescriptor + : Arbitrary[VerificationMethodDescriptor] = Arbitrary( + for { + userVerificationMethod <- arbitrary[UserVerificationMethod] + caDesc <- arbitrary[CodeAccuracyDescriptor] + baDesc <- arbitrary[BiometricAccuracyDescriptor] + paDesc <- arbitrary[PatternAccuracyDescriptor] + } yield new VerificationMethodDescriptor( + userVerificationMethod, + caDesc, + baDesc, + paDesc, + ) + ) + + implicit val arbitraryCodeAccuracyDescriptor + : Arbitrary[CodeAccuracyDescriptor] = Arbitrary( + for { + base <- arbitrary[Int] + minLength <- arbitrary[Int] + maxRetries <- arbitrary[Option[Int]] + blockSlowdown <- arbitrary[Option[Int]] + } yield CodeAccuracyDescriptor + .builder() + .base(base) + .minLength(minLength) + .maxRetries(maxRetries.map(Integer.valueOf).orNull) + .blockSlowdown(blockSlowdown.map(Integer.valueOf).orNull) + .build() + ) + + implicit val arbitraryBiometricAccuracyDescriptor + : Arbitrary[BiometricAccuracyDescriptor] = Arbitrary( + for { + selfAttestedFRR <- arbitrary[Option[Double]] + selfAttestedFAR <- arbitrary[Option[Double]] + maxTemplates <- arbitrary[Option[Int]] + maxRetries <- arbitrary[Option[Int]] + blockSlowdown <- arbitrary[Option[Int]] + } yield new BiometricAccuracyDescriptor( + selfAttestedFRR.map(Double.box).orNull, + selfAttestedFAR.map(Double.box).orNull, + maxTemplates.map(Integer.valueOf).orNull, + maxRetries.map(Integer.valueOf).orNull, + blockSlowdown.map(Integer.valueOf).orNull, + ) + ) + + implicit val arbitraryPatternAccuracyDescriptor + : Arbitrary[PatternAccuracyDescriptor] = Arbitrary( + for { + minComplexity <- arbitrary[Long] + maxRetries <- arbitrary[Option[Int]] + blockSlowdown <- arbitrary[Option[Int]] + } yield PatternAccuracyDescriptor + .builder() + .minComplexity(minComplexity) + .maxRetries(maxRetries.map(Integer.valueOf).orNull) + .blockSlowdown(blockSlowdown.map(Integer.valueOf).orNull) + .build() + ) + + implicit val arbitraryDisplayPNGCharacteristicsDescriptor + : Arbitrary[DisplayPNGCharacteristicsDescriptor] = Arbitrary( + for { + width <- arbitrary[Long] + height <- arbitrary[Long] + bitDepth <- arbitrary[Short] + colorType <- arbitrary[Short] + compression <- arbitrary[Short] + filter <- arbitrary[Short] + interlace <- arbitrary[Short] + plte <- arbitrary[Option[List[RgbPaletteEntry]]] + } yield DisplayPNGCharacteristicsDescriptor + .builder() + .width(width) + .height(height) + .bitDepth(bitDepth) + .colorType(colorType) + .compression(compression) + .filter(filter) + .interlace(interlace) + .plte(plte.map(_.asJava).orNull) + .build() + ) + + implicit val arbitraryRgbPaletteEntry: Arbitrary[RgbPaletteEntry] = Arbitrary( + for { + r <- arbitrary[Int] + g <- arbitrary[Int] + b <- arbitrary[Int] + } yield new RgbPaletteEntry(r, g, b) + ) + + 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() + ) + + 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() + ) + + implicit val arbitrarySupportedCtapOptions: Arbitrary[SupportedCtapOptions] = + Arbitrary( + for { + plat <- arbitrary[Boolean] + rk <- arbitrary[Boolean] + clientPin <- arbitrary[Boolean] + up <- arbitrary[Boolean] + uv <- arbitrary[Boolean] + pinUvAuthToken <- arbitrary[Boolean] + noMcGaPermissionsWithClientPin <- arbitrary[Boolean] + largeBlobs <- arbitrary[Boolean] + ep <- arbitrary[Boolean] + bioEnroll <- arbitrary[Boolean] + userVerificationMgmtPreview <- arbitrary[Boolean] + uvBioEnroll <- arbitrary[Boolean] + authnrCfg <- arbitrary[Boolean] + uvAcfg <- arbitrary[Boolean] + credMgmt <- arbitrary[Boolean] + credentialMgmtPreview <- arbitrary[Boolean] + setMinPINLength <- arbitrary[Boolean] + makeCredUvNotRqd <- arbitrary[Boolean] + alwaysUv <- arbitrary[Boolean] + } yield SupportedCtapOptions + .builder() + .plat(plat) + .rk(rk) + .clientPin(clientPin) + .up(up) + .uv(uv) + .pinUvAuthToken(pinUvAuthToken) + .noMcGaPermissionsWithClientPin(noMcGaPermissionsWithClientPin) + .largeBlobs(largeBlobs) + .ep(ep) + .bioEnroll(bioEnroll) + .userVerificationMgmtPreview(userVerificationMgmtPreview) + .uvBioEnroll(uvBioEnroll) + .authnrCfg(authnrCfg) + .uvAcfg(uvAcfg) + .credMgmt(credMgmt) + .credentialMgmtPreview(credentialMgmtPreview) + .setMinPINLength(setMinPINLength) + .makeCredUvNotRqd(makeCredUvNotRqd) + .alwaysUv(alwaysUv) + .build() + ) + + 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) + ) + ) + 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 new file mode 100644 index 000000000..419af153e --- /dev/null +++ b/webauthn-server-attestation/src/test/scala/com/yubico/fido/metadata/JsonIoSpec.scala @@ -0,0 +1,112 @@ +// Copyright (c) 2018, Yubico AB +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package com.yubico.fido.metadata + +import com.fasterxml.jackson.core.`type`.TypeReference +import com.fasterxml.jackson.databind.ObjectMapper +import com.yubico.fido.metadata.Generators._ +import org.junit.runner.RunWith +import org.scalacheck.Arbitrary +import org.scalatest.FunSpec +import org.scalatest.Matchers +import org.scalatestplus.junit.JUnitRunner +import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks + +@RunWith(classOf[JUnitRunner]) +class JsonIoSpec + extends FunSpec + with Matchers + with ScalaCheckDrivenPropertyChecks { + + def json: ObjectMapper = JacksonCodecs.jsonWithDefaultEnums() + + describe("The class") { + + 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 => + val encoded: String = json.writeValueAsString(value) + val decoded: A = json.readValue(encoded, tpe) + decoded should equal(value) + + val recoded: String = json.writeValueAsString(decoded) + val redecoded: A = json.readValue(recoded, tpe) + redecoded should equal(value) + } + } + } + } + + test(new TypeReference[AAGUID]() {}) + test(new TypeReference[AAID]() {}) + test(new TypeReference[AlternativeDescriptions]() {}) + test(new TypeReference[AttachmentHint]() {}) + test(new TypeReference[AuthenticationAlgorithm]() {}) + test(new TypeReference[AuthenticatorAttestationType]() {}) + test(new TypeReference[AuthenticatorGetInfo]() {}) + test(new TypeReference[AuthenticatorStatus]() {}) + test(new TypeReference[BiometricAccuracyDescriptor]() {}) + test(new TypeReference[BiometricStatusReport]() {}) + test(new TypeReference[CodeAccuracyDescriptor]() {}) + test(new TypeReference[CtapCertificationId]() {}) + test(new TypeReference[CtapPinUvAuthProtocolVersion]() {}) + test(new TypeReference[CtapVersion]() {}) + test(new TypeReference[DisplayPNGCharacteristicsDescriptor]() {}) + test(new TypeReference[ExtensionDescriptor]() {}) + test(new TypeReference[MetadataBLOBHeader]() {}) + test(new TypeReference[MetadataBLOBPayload]() {}) + test(new TypeReference[MetadataBLOBPayloadEntry]() {}) + test(new TypeReference[MetadataStatement]() {}) + test(new TypeReference[PatternAccuracyDescriptor]() {}) + test(new TypeReference[ProtocolFamily]() {}) + test(new TypeReference[PublicKeyRepresentationFormat]() {}) + test(new TypeReference[RgbPaletteEntry]() {}) + test(new TypeReference[StatusReport]() {}) + test(new TypeReference[SupportedCtapOptions]() {}) + test(new TypeReference[TransactionConfirmationDisplayType]() {}) + test(new TypeReference[VerificationMethodDescriptor]() {}) + test(new TypeReference[Version]() {}) + } + +} diff --git a/webauthn-server-attestation/src/test/scala/com/yubico/fido/metadata/MetadataBlobSpec.scala b/webauthn-server-attestation/src/test/scala/com/yubico/fido/metadata/MetadataBlobSpec.scala new file mode 100644 index 000000000..7d07a1e82 --- /dev/null +++ b/webauthn-server-attestation/src/test/scala/com/yubico/fido/metadata/MetadataBlobSpec.scala @@ -0,0 +1,53 @@ +package com.yubico.fido.metadata + +import com.yubico.internal.util.JacksonCodecs +import com.yubico.webauthn.data.ByteArray +import org.scalatest.FunSpec +import org.scalatest.Matchers +import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks + +class MetadataBlobSpec + extends FunSpec + with Matchers + with ScalaCheckDrivenPropertyChecks { + + describe("FIDO Metadata Service 3 blob payloads") { + it("can be parsed as MetadataBLOBPayload.") { + val blob = JacksonCodecs + .json() + .readValue( + ByteArray + .fromBase64Url(FidoMds3Examples.BlobPayloadBase64url) + .getBytes, + classOf[MetadataBLOBPayload], + ) + blob should not be null + blob.getLegalHeader should equal( + "Retrieval and use of this BLOB indicates acceptance of the appropriate agreement located at https://fidoalliance.org/metadata/metadata-legal-terms/" + ) + } + + it( + "are structurally identical after multiple (de)serialization round-trips." + ) { + val json = JacksonCodecs.json() + val blob1 = json + .readValue( + ByteArray + .fromBase64Url(FidoMds3Examples.BlobPayloadBase64url) + .getBytes, + classOf[MetadataBLOBPayload], + ) + val encodedBlob1 = json.writeValueAsBytes(blob1) + val blob2 = json.readValue(encodedBlob1, classOf[MetadataBLOBPayload]) + val encodedBlob2 = json.writeValueAsBytes(blob2) + val blob3 = json.readValue(encodedBlob2, classOf[MetadataBLOBPayload]) + + blob2 should not be null + blob2 should equal(blob1) + blob3 should not be null + blob3 should equal(blob1) + } + } + +} diff --git a/webauthn-server-attestation/src/test/scala/com/yubico/webauthn/attestation/DeviceIdentificationSpec.scala b/webauthn-server-attestation/src/test/scala/com/yubico/webauthn/attestation/DeviceIdentificationSpec.scala deleted file mode 100644 index 082ac3448..000000000 --- a/webauthn-server-attestation/src/test/scala/com/yubico/webauthn/attestation/DeviceIdentificationSpec.scala +++ /dev/null @@ -1,455 +0,0 @@ -// Copyright (c) 2018, Yubico AB -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, this -// list of conditions and the following disclaimer. -// -// 2. Redistributions in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -package com.yubico.webauthn.attestation - -import com.yubico.internal.util.CertificateParser -import com.yubico.internal.util.JacksonCodecs -import com.yubico.webauthn.FinishRegistrationOptions -import com.yubico.webauthn.RelyingParty -import com.yubico.webauthn.attestation.Transport.LIGHTNING -import com.yubico.webauthn.attestation.Transport.NFC -import com.yubico.webauthn.attestation.Transport.USB -import com.yubico.webauthn.attestation.resolver.SimpleAttestationResolver -import com.yubico.webauthn.attestation.resolver.SimpleTrustResolver -import com.yubico.webauthn.data.PublicKeyCredentialCreationOptions -import com.yubico.webauthn.data.PublicKeyCredentialParameters -import com.yubico.webauthn.test.Helpers -import com.yubico.webauthn.test.RealExamples -import org.junit.runner.RunWith -import org.scalatest.FunSpec -import org.scalatest.Matchers -import org.scalatestplus.junit.JUnitRunner - -import java.util.Collections -import scala.jdk.CollectionConverters._ - -@RunWith(classOf[JUnitRunner]) -class DeviceIdentificationSpec extends FunSpec with Matchers { - - def metadataService(metadataJson: String): StandardMetadataService = { - val metadata = Collections.singleton( - JacksonCodecs.json().readValue(metadataJson, classOf[MetadataObject]) - ) - new StandardMetadataService( - new SimpleAttestationResolver( - metadata, - SimpleTrustResolver.fromMetadata(metadata), - ) - ) - } - - describe("A RelyingParty with the default StandardMetadataService") { - - describe("correctly identifies") { - def check( - expectedName: String, - testData: RealExamples.Example, - transports: Set[Transport], - ): Unit = { - val rp = RelyingParty - .builder() - .identity(testData.rp) - .credentialRepository(Helpers.CredentialRepository.empty) - .metadataService(new StandardMetadataService()) - .allowUnrequestedExtensions(true) - .build() - - val result = rp.finishRegistration( - FinishRegistrationOptions - .builder() - .request( - PublicKeyCredentialCreationOptions - .builder() - .rp(testData.rp) - .user(testData.user) - .challenge(testData.attestation.challenge) - .pubKeyCredParams( - List( - PublicKeyCredentialParameters.ES256, - PublicKeyCredentialParameters.EdDSA, - ).asJava - ) - .build() - ) - .response(testData.attestation.credential) - .build() - ); - - result.isAttestationTrusted should be(true) - result.getAttestationMetadata.isPresent should be(true) - result.getAttestationMetadata.get.getDeviceProperties.isPresent should be( - true - ) - result.getAttestationMetadata.get.getDeviceProperties - .get() - .get("displayName") should equal(expectedName) - result.getAttestationMetadata.get.getTransports.isPresent should be( - true - ) - result.getAttestationMetadata.get.getTransports.get.asScala should equal( - transports - ) - } - - it("a YubiKey NEO.") { - check("YubiKey NEO/NEO-n", RealExamples.YubiKeyNeo, Set(USB, NFC)) - } - it("a YubiKey 4.") { - check("YubiKey 4/YubiKey 4 Nano", RealExamples.YubiKey4, Set(USB)) - } - it("a YubiKey 5 NFC.") { - check("YubiKey 5 NFC", RealExamples.YubiKey5, Set(USB, NFC)) - } - it("an early YubiKey 5 NFC.") { - check("YubiKey 5 NFC", RealExamples.YubiKey5Nfc, Set(USB, NFC)) - } - it("a newer YubiKey 5 NFC.") { - check( - "YubiKey 5/5C NFC", - RealExamples.YubiKey5NfcPost5cNfc, - Set(USB, NFC), - ) - } - it("a YubiKey 5C NFC.") { - check("YubiKey 5/5C NFC", RealExamples.YubiKey5cNfc, Set(USB, NFC)) - } - it("a YubiKey 5 Nano.") { - check("YubiKey 5 Series", RealExamples.YubiKey5Nano, Set(USB)) - } - it("a YubiKey 5Ci.") { - check("YubiKey 5Ci", RealExamples.YubiKey5Ci, Set(USB, LIGHTNING)) - } - it("a Security Key by Yubico.") { - check("Security Key by Yubico", RealExamples.SecurityKey, Set(USB)) - } - it("a Security Key 2 by Yubico.") { - check("Security Key by Yubico", RealExamples.SecurityKey2, Set(USB)) - } - it("a Security Key NFC by Yubico.") { - check( - "Security Key NFC by Yubico", - RealExamples.SecurityKeyNfc, - Set(USB, NFC), - ) - } - - it("a YubiKey 5.4 NFC FIPS.") { - check( - "YubiKey 5/5C NFC FIPS", - RealExamples.YubikeyFips5Nfc, - Set(USB, NFC), - ) - } - it("a YubiKey 5.4 Ci FIPS.") { - check( - "YubiKey 5Ci FIPS", - RealExamples.Yubikey5ciFips, - Set(USB, LIGHTNING), - ) - } - - it("a YubiKey Bio.") { - check( - "YubiKey Bio - FIDO Edition", - RealExamples.YubikeyBio_5_5_4, - Set(USB), - ) - check( - "YubiKey Bio - FIDO Edition", - RealExamples.YubikeyBio_5_5_5, - Set(USB), - ) - } - } - - describe("fails to identify") { - def check(testData: RealExamples.Example): Unit = { - val rp = RelyingParty - .builder() - .identity(testData.rp) - .credentialRepository(Helpers.CredentialRepository.empty) - .metadataService(new StandardMetadataService()) - .build() - - val result = rp.finishRegistration( - FinishRegistrationOptions - .builder() - .request( - PublicKeyCredentialCreationOptions - .builder() - .rp(testData.rp) - .user(testData.user) - .challenge(testData.attestation.challenge) - .pubKeyCredParams( - List(PublicKeyCredentialParameters.ES256).asJava - ) - .build() - ) - .response(testData.attestation.credential) - .build() - ); - - result.isAttestationTrusted should be(false) - result.getAttestationMetadata.isPresent should be(true) - result.getAttestationMetadata.get.getDeviceProperties.isPresent should be( - false - ) - result.getAttestationMetadata.get.getVendorProperties.isPresent should be( - false - ) - result.getAttestationMetadata.get.getTransports.isPresent should be( - false - ) - } - - it("an Apple iOS device.") { - check(RealExamples.AppleAttestationIos) - } - } - } - - describe("The default AttestationResolver") { - describe("successfully identifies") { - def check( - expectedName: String, - testData: RealExamples.Example, - transports: Set[Transport], - ): Unit = { - val cert = CertificateParser.parseDer(testData.attestationCert.getBytes) - val resolved = StandardMetadataService - .createDefaultAttestationResolver() - .resolve(cert) - resolved.isPresent should be(true) - resolved.get.getDeviceProperties.isPresent should be(true) - resolved.get.getDeviceProperties.get.get("displayName") should equal( - expectedName - ) - resolved.get.getTransports.isPresent should be(true) - resolved.get.getTransports.get.asScala should equal(transports) - } - - it("a YubiKey NEO.") { - check("YubiKey NEO/NEO-n", RealExamples.YubiKeyNeo, Set(USB, NFC)) - } - it("a YubiKey 4.") { - check("YubiKey 4/YubiKey 4 Nano", RealExamples.YubiKey4, Set(USB)) - } - it("a YubiKey 5 NFC.") { - check("YubiKey 5 NFC", RealExamples.YubiKey5, Set(USB, NFC)) - } - it("an early YubiKey 5 NFC.") { - check("YubiKey 5 NFC", RealExamples.YubiKey5Nfc, Set(USB, NFC)) - } - it("a newer YubiKey 5 NFC.") { - check( - "YubiKey 5/5C NFC", - RealExamples.YubiKey5NfcPost5cNfc, - Set(USB, NFC), - ) - } - it("a YubiKey 5C NFC.") { - check("YubiKey 5/5C NFC", RealExamples.YubiKey5cNfc, Set(USB, NFC)) - } - it("a YubiKey 5 Nano.") { - check("YubiKey 5 Series", RealExamples.YubiKey5Nano, Set(USB)) - } - it("a YubiKey 5Ci.") { - check("YubiKey 5Ci", RealExamples.YubiKey5Ci, Set(USB, LIGHTNING)) - } - it("a Security Key by Yubico.") { - check("Security Key by Yubico", RealExamples.SecurityKey, Set(USB)) - } - it("a Security Key 2 by Yubico.") { - check("Security Key by Yubico", RealExamples.SecurityKey2, Set(USB)) - } - it("a Security Key NFC by Yubico.") { - check( - "Security Key NFC by Yubico", - RealExamples.SecurityKeyNfc, - Set(USB, NFC), - ) - } - - it("a YubiKey 5.4 NFC FIPS.") { - check( - "YubiKey 5/5C NFC FIPS", - RealExamples.YubikeyFips5Nfc, - Set(USB, NFC), - ) - } - it("a YubiKey 5.4 Ci FIPS.") { - check( - "YubiKey 5Ci FIPS", - RealExamples.Yubikey5ciFips, - Set(USB, LIGHTNING), - ) - } - - it("a YubiKey Bio.") { - check( - "YubiKey Bio - FIDO Edition", - RealExamples.YubikeyBio_5_5_4, - Set(USB), - ) - check( - "YubiKey Bio - FIDO Edition", - RealExamples.YubikeyBio_5_5_5, - Set(USB), - ) - } - } - } - - describe( - "A StandardMetadataService configured with an Apple root certificate" - ) { - // Apple WebAuthn Root CA cert downloaded from https://www.apple.com/certificateauthority/private/ on 2021-04-12 - // https://www.apple.com/certificateauthority/Apple_WebAuthn_Root_CA.pem - val mds = metadataService("""{ - | "identifier": "98cf2729-e2b9-4633-8b6a-b295cda99ccf", - | "version": 1, - | "vendorInfo": { - | "name": "Apple Inc. (Metadata file by Yubico)" - | }, - | "trustedCertificates": [ - | "-----BEGIN CERTIFICATE-----\nMIICEjCCAZmgAwIBAgIQaB0BbHo84wIlpQGUKEdXcTAKBggqhkjOPQQDAzBLMR8w\nHQYDVQQDDBZBcHBsZSBXZWJBdXRobiBSb290IENBMRMwEQYDVQQKDApBcHBsZSBJ\nbmMuMRMwEQYDVQQIDApDYWxpZm9ybmlhMB4XDTIwMDMxODE4MjEzMloXDTQ1MDMx\nNTAwMDAwMFowSzEfMB0GA1UEAwwWQXBwbGUgV2ViQXV0aG4gUm9vdCBDQTETMBEG\nA1UECgwKQXBwbGUgSW5jLjETMBEGA1UECAwKQ2FsaWZvcm5pYTB2MBAGByqGSM49\nAgEGBSuBBAAiA2IABCJCQ2pTVhzjl4Wo6IhHtMSAzO2cv+H9DQKev3//fG59G11k\nxu9eI0/7o6V5uShBpe1u6l6mS19S1FEh6yGljnZAJ+2GNP1mi/YK2kSXIuTHjxA/\npcoRf7XkOtO4o1qlcaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUJtdk\n2cV4wlpn0afeaxLQG2PxxtcwDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2cA\nMGQCMFrZ+9DsJ1PW9hfNdBywZDsWDbWFp28it1d/5w2RPkRX3Bbn/UbDTNLx7Jr3\njAGGiQIwHFj+dJZYUJR786osByBelJYsVZd2GbHQu209b5RCmGQ21gpSAk9QZW4B\n1bWeT0vT\n-----END CERTIFICATE-----" - | ], - | "devices": [ - | { - | "displayName": "Apple device", - | "selectors": [ - | { - | "type": "x509Extension", - | "parameters": { - | "key": "1.2.840.113635.100.8.2" - | } - | } - | ] - | } - | ] - |}""".stripMargin) - - describe("successfully identifies") { - def check( - expectedName: String, - testData: RealExamples.Example, - ): Unit = { - val rp = RelyingParty - .builder() - .identity(testData.rp) - .credentialRepository(Helpers.CredentialRepository.empty) - .metadataService(mds) - .build() - - val result = rp.finishRegistration( - FinishRegistrationOptions - .builder() - .request( - PublicKeyCredentialCreationOptions - .builder() - .rp(testData.rp) - .user(testData.user) - .challenge(testData.attestation.challenge) - .pubKeyCredParams( - List(PublicKeyCredentialParameters.ES256).asJava - ) - .build() - ) - .response(testData.attestation.credential) - .build() - ) - - result.isAttestationTrusted should be(true) - result.getAttestationMetadata.isPresent should be(true) - result.getAttestationMetadata.get.getDeviceProperties.isPresent should be( - true - ) - result.getAttestationMetadata.get.getDeviceProperties - .get() - .get("displayName") should equal(expectedName) - result.getAttestationMetadata.get.getTransports.isPresent should be( - false - ) - } - - it("an Apple iOS device.") { - check( - "Apple device", - RealExamples.AppleAttestationIos, - ) - } - - it("an Apple MacOS device.") { - check( - "Apple device", - RealExamples.AppleAttestationMacos, - ) - } - } - - describe("fails to identify") { - def check(testData: RealExamples.Example): Unit = { - val rp = RelyingParty - .builder() - .identity(testData.rp) - .credentialRepository(Helpers.CredentialRepository.empty) - .metadataService(mds) - .build() - - val result = rp.finishRegistration( - FinishRegistrationOptions - .builder() - .request( - PublicKeyCredentialCreationOptions - .builder() - .rp(testData.rp) - .user(testData.user) - .challenge(testData.attestation.challenge) - .pubKeyCredParams( - List(PublicKeyCredentialParameters.ES256).asJava - ) - .build() - ) - .response(testData.attestation.credential) - .build() - ) - - result.isAttestationTrusted should be(false) - result.getAttestationMetadata.isPresent should be(true) - result.getAttestationMetadata.get.getVendorProperties.isPresent should be( - false - ) - result.getAttestationMetadata.get.getDeviceProperties.isPresent should be( - false - ) - } - - it("a YubiKey 5 NFC.") { - check(RealExamples.YubiKey5) - } - } - } - -} diff --git a/webauthn-server-attestation/src/test/scala/com/yubico/webauthn/attestation/StandardMetadataServiceSpec.scala b/webauthn-server-attestation/src/test/scala/com/yubico/webauthn/attestation/StandardMetadataServiceSpec.scala deleted file mode 100644 index 595b71fe2..000000000 --- a/webauthn-server-attestation/src/test/scala/com/yubico/webauthn/attestation/StandardMetadataServiceSpec.scala +++ /dev/null @@ -1,279 +0,0 @@ -// Copyright (c) 2018, Yubico AB -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, this -// list of conditions and the following disclaimer. -// -// 2. Redistributions in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -package com.yubico.webauthn.attestation - -import com.yubico.internal.util.JacksonCodecs -import com.yubico.internal.util.scala.JavaConverters._ -import com.yubico.webauthn.TestAuthenticator -import com.yubico.webauthn.attestation.resolver.SimpleAttestationResolver -import com.yubico.webauthn.attestation.resolver.SimpleTrustResolver -import org.bouncycastle.asn1.DERBitString -import org.bouncycastle.asn1.DEROctetString -import org.bouncycastle.asn1.x500.X500Name -import org.junit.runner.RunWith -import org.scalatest.FunSpec -import org.scalatest.Matchers -import org.scalatestplus.junit.JUnitRunner - -import java.security.cert.X509Certificate -import java.util.Base64 -import java.util.Collections -import scala.jdk.CollectionConverters._ - -@RunWith(classOf[JUnitRunner]) -class StandardMetadataServiceSpec extends FunSpec with Matchers { - - private val TRANSPORTS_EXT_OID = "1.3.6.1.4.1.45724.2.1.1" - - private val ooidA = "1.3.6.1.4.1.41482.1.1" - private val ooidB = "1.3.6.1.4.1.41482.1.2" - - def metadataService(metadataJson: String): StandardMetadataService = { - val metadata = Collections.singleton( - JacksonCodecs.json().readValue(metadataJson, classOf[MetadataObject]) - ) - new StandardMetadataService( - new SimpleAttestationResolver( - metadata, - SimpleTrustResolver.fromMetadata(metadata), - ) - ) - } - - def toPem(cert: X509Certificate): String = - ( - "-----BEGIN CERTIFICATE-----\n" - + Base64 - .getMimeEncoder( - 64, - System.getProperty("line.separator").getBytes("UTF-8"), - ) - .encodeToString(cert.getEncoded) - + "\n-----END CERTIFICATE-----\n" - ) - - describe("StandardMetadataService") { - - describe("has a getAttestation method which") { - - val cacaca = TestAuthenticator.generateAttestationCaCertificate( - name = new X500Name("CN=CA CA CA"), - extensions = List((ooidB, false, new DEROctetString(Array[Byte]()))), - ) - val caca = TestAuthenticator.generateAttestationCaCertificate( - name = new X500Name("CN=CA CA"), - superCa = Some(cacaca), - extensions = List((ooidB, false, new DEROctetString(Array[Byte]()))), - ) - val (caCert, caKey) = TestAuthenticator.generateAttestationCaCertificate( - name = new X500Name("CN=CA"), - superCa = Some(caca), - extensions = List((ooidB, false, new DEROctetString(Array[Byte]()))), - ) - - val (certA, _) = TestAuthenticator.generateAttestationCertificate( - name = new X500Name("CN=Cert A"), - caCertAndKey = Some((caCert, caKey)), - extensions = List( - (ooidA, false, new DEROctetString(Array[Byte]())), - (TRANSPORTS_EXT_OID, false, new DERBitString(Array[Byte](0x60))), - ), - ) - val (certB, _) = TestAuthenticator.generateAttestationCertificate( - name = new X500Name("CN=Cert B"), - caCertAndKey = Some((caCert, caKey)), - extensions = List((ooidB, false, new DEROctetString(Array[Byte]()))), - ) - - val metadataJson = - s"""{ - "identifier": "44c87ead-4455-423e-88eb-9248e0ebe847", - "version": 1, - "trustedCertificates": ["${toPem(caCert).linesIterator.mkString( - raw"\n" - )}"], - "vendorInfo": {}, - "devices": [ - { - "deviceId": "DevA", - "displayName": "Device A", - "selectors": [ - { - "type": "x509Extension", - "parameters": { - "key": "${ooidA}" - } - } - ] - }, - { - "deviceId": "DevB", - "displayName": "Device B", - "selectors": [ - { - "type": "x509Extension", - "parameters": { - "key": "${ooidB}" - } - } - ] - } - ] - }""" - val service = metadataService(metadataJson) - - it("returns the trusted attestation matching the single cert passed, if it is signed by a trusted certificate.") { - val attestationA: Attestation = - service.getAttestation(List(certA).asJava) - val attestationB: Attestation = - service.getAttestation(List(certB).asJava) - - attestationA.isTrusted should be(true) - attestationA.getDeviceProperties.get.get("deviceId") should be("DevA") - - attestationB.isTrusted should be(true) - attestationB.getDeviceProperties.get.get("deviceId") should be("DevB") - } - - it("returns the trusted attestation matching the first cert in the chain if it is signed by a trusted certificate.") { - val attestationA: Attestation = - service.getAttestation(List(certA, certB).asJava) - val attestationB: Attestation = - service.getAttestation(List(certB, certA).asJava) - - attestationA.isTrusted should be(true) - attestationA.getDeviceProperties.get.get("deviceId") should be("DevA") - - attestationB.isTrusted should be(true) - attestationB.getDeviceProperties.get.get("deviceId") should be("DevB") - } - - it("returns a trusted best-effort attestation if the certificate is trusted but matches no known metadata.") { - val metadataJson = - s"""{ - "identifier": "44c87ead-4455-423e-88eb-9248e0ebe847", - "version": 1, - "trustedCertificates": ["${toPem(caCert).linesIterator.mkString( - raw"\n" - )}"], - "vendorInfo": {}, - "devices": [] - }""" - val service = metadataService(metadataJson) - - val attestation: Attestation = - service.getAttestation(List(certA).asJava) - - attestation.isTrusted should be(true) - attestation.getDeviceProperties.asScala shouldBe empty - attestation.getTransports.get.asScala should equal( - Set(Transport.BLE, Transport.USB) - ) - } - - it("returns an untrusted attestation with transports if the certificate is not trusted.") { - val metadataJson = - s"""{ - "identifier": "44c87ead-4455-423e-88eb-9248e0ebe847", - "version": 1, - "trustedCertificates": [], - "vendorInfo": {}, - "devices": [] - }""" - val service = metadataService(metadataJson) - - val attestation: Attestation = - service.getAttestation(List(certA).asJava) - - attestation.isTrusted should be(false) - attestation.getMetadataIdentifier.asScala shouldBe empty - attestation.getVendorProperties.asScala shouldBe empty - attestation.getDeviceProperties.asScala shouldBe empty - attestation.getTransports.get.asScala should equal( - Set(Transport.BLE, Transport.USB) - ) - } - - it("returns the trusted attestation matching the first cert in the chain if the chain resolves to a trusted certificate.") { - val metadataJson = - s"""{ - "identifier": "44c87ead-4455-423e-88eb-9248e0ebe847", - "version": 1, - "trustedCertificates": ["${toPem(cacaca._1).linesIterator - .mkString(raw"\n")}"], - "vendorInfo": {}, - "devices": [ - { - "deviceId": "DevA", - "displayName": "Device A", - "selectors": [ - { - "type": "x509Extension", - "parameters": { - "key": "${ooidA}" - } - } - ] - } - ] - }""" - val service = metadataService(metadataJson) - - val attestation: Attestation = - service.getAttestation(List(certA, caCert, caca._1).asJava) - - attestation.isTrusted should be(true) - attestation.getDeviceProperties.get.get("deviceId") should be("DevA") - } - - it("matches any certificate to a device with no selectors.") { - val metadataJson = - s"""{ - "identifier": "44c87ead-4455-423e-88eb-9248e0ebe847", - "version": 1, - "trustedCertificates": ["${toPem(caCert).linesIterator.mkString( - raw"\n" - )}"], - "vendorInfo": {}, - "devices": [ - { - "deviceId": "DevA", - "displayName": "Device A" - } - ] - }""" - val service = metadataService(metadataJson) - - val resultA = service.getAttestation(List(certA).asJava) - val resultB = service.getAttestation(List(certB).asJava) - resultA.getDeviceProperties.get.get("deviceId") should be("DevA") - resultB.getDeviceProperties.get.get("deviceId") should be("DevA") - } - - } - - } - -} diff --git a/webauthn-server-core-bundle/build.gradle b/webauthn-server-core-bundle/build.gradle deleted file mode 100644 index aec8218dc..000000000 --- a/webauthn-server-core-bundle/build.gradle +++ /dev/null @@ -1,37 +0,0 @@ -plugins { - id 'java-library' - id 'maven-publish' - id 'signing' -} - -description = 'Yubico WebAuthn server core API' - -project.ext.publishMe = true - -sourceCompatibility = 1.8 -targetCompatibility = 1.8 - -dependencies { - api(platform(rootProject)) - - api( - project(':webauthn-server-core-minimal'), - ) - - implementation( - 'org.bouncycastle:bcprov-jdk15on', - ) -} - - -jar { - manifest { - attributes([ - 'Implementation-Title': 'Yubico Web Authentication server library meta-package', - 'Implementation-Version': project.version, - 'Implementation-Vendor': 'Yubico', - 'Implementation-Source-Url': 'https://github.com/Yubico/java-webauthn-server', - 'Git-Commit': getGitCommitOrUnknown(), - ]) - } -} diff --git a/webauthn-server-core/build.gradle b/webauthn-server-core/build.gradle index bc9dec636..f593e94e7 100644 --- a/webauthn-server-core/build.gradle +++ b/webauthn-server-core/build.gradle @@ -7,7 +7,7 @@ plugins { id 'io.github.cosmicsilence.scalafix' } -description = 'Yubico WebAuthn server core API (fewer dependencies)' +description = 'Yubico WebAuthn server core API' project.ext.publishMe = true @@ -21,15 +21,10 @@ dependencies { project(':yubico-util'), ) - compileOnly( - platform(rootProject), - 'org.bouncycastle:bcprov-jdk15on', - ) - implementation( 'com.augustcellars.cose:cose-java', - 'com.google.guava:guava', 'com.fasterxml.jackson.core:jackson-databind', + 'com.google.guava:guava', 'com.upokecenter:cbor', 'org.apache.httpcomponents:httpclient', 'org.slf4j:slf4j-api', @@ -45,7 +40,14 @@ dependencies { 'org.scala-lang:scala-library', 'org.scalacheck:scalacheck_2.13', 'org.scalatest:scalatest_2.13', + 'uk.org.lidalia:slf4j-test', ) + + testImplementation('org.slf4j:slf4j-api') { + version { + strictly '[1.7.25,1.8-a)' // Pre-1.8 version required by slf4j-test + } + } } jar { 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 d728bb24f..c135374a0 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 @@ -114,7 +114,7 @@ private boolean verifySignature(JsonWebSignatureCustom jws) { Signature signatureVerifier; try { - signatureVerifier = Crypto.getSignature(signatureAlgorithmName); + signatureVerifier = Signature.getInstance(signatureAlgorithmName); } catch (NoSuchAlgorithmException e) { throw ExceptionUtil.wrapAndLog( log, "Failed to get a Signature instance for " + signatureAlgorithmName, e); diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/AssertionResult.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/AssertionResult.java index c41a66adb..2b8c3c0f1 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/AssertionResult.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/AssertionResult.java @@ -26,14 +26,12 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; -import com.yubico.internal.util.CollectionUtil; import com.yubico.webauthn.data.AuthenticatorAssertionExtensionOutputs; import com.yubico.webauthn.data.AuthenticatorData; import com.yubico.webauthn.data.ByteArray; import com.yubico.webauthn.data.ClientAssertionExtensionOutputs; import com.yubico.webauthn.data.PublicKeyCredentialRequestOptions; import com.yubico.webauthn.data.UserIdentity; -import java.util.List; import java.util.Optional; import lombok.Builder; import lombok.NonNull; @@ -109,9 +107,6 @@ public class AssertionResult { private final AuthenticatorAssertionExtensionOutputs authenticatorExtensionOutputs; - /** Zero or more human-readable messages about non-critical issues. */ - @NonNull private final List warnings; - @JsonCreator private AssertionResult( @JsonProperty("success") boolean success, @@ -123,8 +118,7 @@ private AssertionResult( @JsonProperty("clientExtensionOutputs") ClientAssertionExtensionOutputs clientExtensionOutputs, @JsonProperty("authenticatorExtensionOutputs") - AuthenticatorAssertionExtensionOutputs authenticatorExtensionOutputs, - @NonNull @JsonProperty("warnings") List warnings) { + AuthenticatorAssertionExtensionOutputs authenticatorExtensionOutputs) { this.success = success; this.credentialId = credentialId; this.userHandle = userHandle; @@ -136,7 +130,6 @@ private AssertionResult( ? null : clientExtensionOutputs; this.authenticatorExtensionOutputs = authenticatorExtensionOutputs; - this.warnings = CollectionUtil.immutableList(warnings); } /** @@ -230,16 +223,9 @@ public Step8 clientExtensionOutputs( } public class Step8 { - public Step9 assertionExtensionOutputs( + public AssertionResultBuilder assertionExtensionOutputs( AuthenticatorAssertionExtensionOutputs authenticatorExtensionOutputs) { - builder.authenticatorExtensionOutputs(authenticatorExtensionOutputs); - return new Step9(); - } - } - - public class Step9 { - public AssertionResultBuilder warnings(List warnings) { - return builder.warnings(warnings); + return builder.authenticatorExtensionOutputs(authenticatorExtensionOutputs); } } } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/AttestationTrustResolver.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/AttestationTrustResolver.java deleted file mode 100644 index ac87a2af2..000000000 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/AttestationTrustResolver.java +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) 2018, Yubico AB -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, this -// list of conditions and the following disclaimer. -// -// 2. Redistributions in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -package com.yubico.webauthn; - -import com.yubico.webauthn.attestation.Attestation; -import java.security.cert.CertificateEncodingException; -import java.security.cert.X509Certificate; -import java.util.List; - -interface AttestationTrustResolver { - - Attestation resolveTrustAnchor(List certificateChain) - throws CertificateEncodingException; -} 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 9075f95f1..5893f0dd6 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 @@ -35,9 +35,8 @@ import java.math.BigInteger; import java.nio.charset.StandardCharsets; import java.security.GeneralSecurityException; -import java.security.KeyFactory; +import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; -import java.security.Provider; import java.security.PublicKey; import java.security.Signature; import java.security.cert.X509Certificate; @@ -46,7 +45,6 @@ import java.security.spec.EllipticCurve; import lombok.experimental.UtilityClass; import lombok.extern.slf4j.Slf4j; -import org.bouncycastle.jce.provider.BouncyCastleProvider; @UtilityClass @Slf4j @@ -65,47 +63,6 @@ final class Crypto { new BigInteger( "41058363725152142129326129780047268409114441015993725554835256314039467401291", 10)); - /* - * TODO: Delete this in the next major version release - */ - private static class BouncyCastleLoader { - private static Provider getProvider() { - return new BouncyCastleProvider(); - } - } - - /* - * TODO: Delete this in the next major version release - */ - public static KeyFactory getKeyFactory(String algorithm) throws NoSuchAlgorithmException { - try { - return KeyFactory.getInstance(algorithm); - } catch (NoSuchAlgorithmException e) { - log.debug("Caught {}. Attempting fallback to BouncyCastle...", e.toString()); - try { - return KeyFactory.getInstance(algorithm, BouncyCastleLoader.getProvider()); - } catch (NoSuchAlgorithmException | NoClassDefFoundError e2) { - throw e; - } - } - } - - /* - * TODO: Delete this in the next major version release - */ - public static Signature getSignature(String algorithm) throws NoSuchAlgorithmException { - try { - return Signature.getInstance(algorithm); - } catch (NoSuchAlgorithmException e) { - log.debug("Caught {}. Attempting fallback to BouncyCastle...", e.toString()); - try { - return Signature.getInstance(algorithm, BouncyCastleLoader.getProvider()); - } catch (NoSuchAlgorithmException | NoClassDefFoundError e2) { - throw e; - } - } - } - static boolean isP256(ECParameterSpec params) { return P256.equals(params.getCurve()); } @@ -145,4 +102,8 @@ public static ByteArray sha256(ByteArray bytes) { public static ByteArray sha256(String str) { return sha256(new ByteArray(str.getBytes(StandardCharsets.UTF_8))); } + + public static ByteArray sha1(ByteArray bytes) throws NoSuchAlgorithmException { + return new ByteArray(MessageDigest.getInstance("SHA-1").digest(bytes.getBytes())); + } } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/ExtensionsValidation.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/ExtensionsValidation.java deleted file mode 100644 index 6c82e5357..000000000 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/ExtensionsValidation.java +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright (c) 2018, Yubico AB -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, this -// list of conditions and the following disclaimer. -// -// 2. Redistributions in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -package com.yubico.webauthn; - -import com.upokecenter.cbor.CBORObject; -import com.yubico.webauthn.data.AuthenticatorResponse; -import com.yubico.webauthn.data.ClientExtensionOutputs; -import com.yubico.webauthn.data.ExtensionInputs; -import com.yubico.webauthn.data.PublicKeyCredential; -import java.util.HashSet; -import java.util.Set; -import java.util.stream.Collectors; -import lombok.experimental.UtilityClass; - -@UtilityClass -class ExtensionsValidation { - - static boolean validate( - ExtensionInputs requested, - PublicKeyCredential - response) { - Set requestedExtensionIds = requested.getExtensionIds(); - Set clientExtensionIds = response.getClientExtensionResults().getExtensionIds(); - - if (!requestedExtensionIds.containsAll(clientExtensionIds)) { - throw new IllegalArgumentException( - String.format( - "Client extensions {%s} are not a subset of requested extensions {%s}.", - String.join(", ", clientExtensionIds), String.join(", ", requestedExtensionIds))); - } - - Set authenticatorExtensionIds = - response - .getResponse() - .getParsedAuthenticatorData() - .getExtensions() - .map( - extensions -> - extensions.getKeys().stream() - .map(CBORObject::AsString) - .collect(Collectors.toSet())) - .orElseGet(HashSet::new); - - if (!requestedExtensionIds.containsAll(authenticatorExtensionIds)) { - throw new IllegalArgumentException( - String.format( - "Authenticator extensions {%s} are not a subset of requested extensions {%s}.", - String.join(", ", authenticatorExtensionIds), - String.join(", ", requestedExtensionIds))); - } - - return true; - } -} 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 04024cad7..91496ec98 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 @@ -27,7 +27,10 @@ import static com.yubico.internal.util.ExceptionUtil.assure; import COSE.CoseException; -import com.yubico.internal.util.CollectionUtil; +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.webauthn.data.AuthenticatorAssertionExtensionOutputs; import com.yubico.webauthn.data.AuthenticatorAssertionResponse; import com.yubico.webauthn.data.ByteArray; @@ -42,10 +45,6 @@ import java.security.NoSuchAlgorithmException; import java.security.PublicKey; import java.security.spec.InvalidKeySpecException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; import java.util.Optional; import java.util.Set; import lombok.Builder; @@ -71,8 +70,8 @@ final class FinishAssertionSteps { @Builder.Default private final boolean allowUnrequestedExtensions = false; @Builder.Default private final boolean validateSignatureCounter = true; - public Step0 begin() { - return new Step0(); + public Step5 begin() { + return new Step5(); } public AssertionResult run() throws InvalidSignatureCountException { @@ -84,23 +83,10 @@ interface Step> { void validate() throws InvalidSignatureCountException; - List getPrevWarnings(); - default Optional result() { return Optional.empty(); } - default List getWarnings() { - return Collections.emptyList(); - } - - default List allWarnings() { - List result = new ArrayList<>(getPrevWarnings().size() + getWarnings().size()); - result.addAll(getPrevWarnings()); - result.addAll(getWarnings()); - return CollectionUtil.immutableList(result); - } - default Next next() throws InvalidSignatureCountException { validate(); return nextStep(); @@ -115,8 +101,32 @@ default AssertionResult run() throws InvalidSignatureCountException { } } + // Steps 1 through 4 are to create the request and run the client-side part + @Value - class Step0 implements Step { + class Step5 implements Step { + @Override + public Step6 nextStep() { + return new Step6(); + } + + @Override + public void validate() { + request + .getPublicKeyCredentialRequestOptions() + .getAllowCredentials() + .ifPresent( + allowed -> { + assure( + allowed.stream().anyMatch(allow -> allow.getId().equals(response.getId())), + "Unrequested credential ID: %s", + response.getId()); + }); + } + } + + @Value + class Step6 implements Step { private final Optional userHandle = response @@ -138,9 +148,12 @@ class Step0 implements Step { .getUserHandle() .flatMap(credentialRepository::getUsernameForUserHandle)); + private final Optional registration = + userHandle.flatMap(uh -> credentialRepository.lookup(response.getId(), uh)); + @Override - public Step1 nextStep() { - return new Step1(username.get(), userHandle.get(), allWarnings()); + public Step7 nextStep() { + return new Step7(username.get(), userHandle.get(), registration); } @Override @@ -148,92 +161,49 @@ public void validate() { assure( request.getUsername().isPresent() || response.getResponse().getUserHandle().isPresent(), "At least one of username and user handle must be given; none was."); + assure( userHandle.isPresent(), "User handle not found for username: %s", request.getUsername(), response.getResponse().getUserHandle()); + assure( username.isPresent(), "Username not found for userHandle: %s", request.getUsername(), response.getResponse().getUserHandle()); - } - - @Override - public List getPrevWarnings() { - return Collections.emptyList(); - } - } - - @Value - class Step1 implements Step { - private final String username; - private final ByteArray userHandle; - private final List prevWarnings; - - @Override - public Step2 nextStep() { - return new Step2(username, userHandle, allWarnings()); - } - - @Override - public void validate() { - request - .getPublicKeyCredentialRequestOptions() - .getAllowCredentials() - .ifPresent( - allowed -> { - assure( - allowed.stream().anyMatch(allow -> allow.getId().equals(response.getId())), - "Unrequested credential ID: %s", - response.getId()); - }); - } - } - - @Value - class Step2 implements Step { - private final String username; - private final ByteArray userHandle; - private final List prevWarnings; - - private final Optional registration; - - public Step2(String username, ByteArray userHandle, List prevWarnings) { - this.username = username; - this.userHandle = userHandle; - this.prevWarnings = prevWarnings; - this.registration = credentialRepository.lookup(response.getId(), userHandle); - } - - @Override - public Step3 nextStep() { - return new Step3(username, userHandle, registration, allWarnings()); - } - @Override - public void validate() { assure(registration.isPresent(), "Unknown credential: %s", response.getId()); assure( - userHandle.equals(registration.get().getUserHandle()), + userHandle.get().equals(registration.get().getUserHandle()), "User handle %s does not own credential %s", - userHandle, + userHandle.get(), response.getId()); + + final Optional usernameFromRequest = request.getUsername(); + final Optional userHandleFromResponse = response.getResponse().getUserHandle(); + if (usernameFromRequest.isPresent() && userHandleFromResponse.isPresent()) { + assure( + userHandleFromResponse.equals( + credentialRepository.getUserHandleForUsername(usernameFromRequest.get())), + "User handle %s in response does not match username %s in request", + userHandleFromResponse, + usernameFromRequest); + } } } @Value - class Step3 implements Step { + class Step7 implements Step { private final String username; private final ByteArray userHandle; private final Optional credential; - private final List prevWarnings; @Override - public Step4 nextStep() { - return new Step4(username, userHandle, credential.get(), allWarnings()); + public Step8 nextStep() { + return new Step8(username, userHandle, credential.get()); } @Override @@ -247,12 +217,11 @@ public void validate() { } @Value - class Step4 implements Step { + class Step8 implements Step { private final String username; private final ByteArray userHandle; private final RegisteredCredential credential; - private final List prevWarnings; @Override public void validate() { @@ -262,8 +231,8 @@ public void validate() { } @Override - public Step5 nextStep() { - return new Step5(username, userHandle, credential, allWarnings()); + public Step10 nextStep() { + return new Step10(username, userHandle, credential); } public ByteArray authenticatorData() { @@ -279,29 +248,13 @@ public ByteArray signature() { } } - @Value - class Step5 implements Step { - private final String username; - private final ByteArray userHandle; - private final RegisteredCredential credential; - private final List prevWarnings; - - // Nothing to do - @Override - public void validate() {} - - @Override - public Step6 nextStep() { - return new Step6(username, userHandle, credential, allWarnings()); - } - } + // Nothing to do for step 9 @Value - class Step6 implements Step { + class Step10 implements Step { private final String username; private final ByteArray userHandle; private final RegisteredCredential credential; - private final List prevWarnings; @Override public void validate() { @@ -309,8 +262,8 @@ public void validate() { } @Override - public Step7 nextStep() { - return new Step7(username, userHandle, credential, clientData(), allWarnings()); + public Step11 nextStep() { + return new Step11(username, userHandle, credential, clientData()); } public CollectedClientData clientData() { @@ -319,20 +272,11 @@ public CollectedClientData clientData() { } @Value - class Step7 implements Step { - + class Step11 implements Step { private final String username; private final ByteArray userHandle; private final RegisteredCredential credential; private final CollectedClientData clientData; - private final List prevWarnings; - - private List warnings = new LinkedList<>(); - - @Override - public List getWarnings() { - return CollectionUtil.immutableList(warnings); - } @Override public void validate() { @@ -344,17 +288,16 @@ public void validate() { } @Override - public Step8 nextStep() { - return new Step8(username, userHandle, credential, allWarnings()); + public Step12 nextStep() { + return new Step12(username, userHandle, credential); } } @Value - class Step8 implements Step { + class Step12 implements Step { private final String username; private final ByteArray userHandle; private final RegisteredCredential credential; - private final List prevWarnings; @Override public void validate() { @@ -367,17 +310,16 @@ public void validate() { } @Override - public Step9 nextStep() { - return new Step9(username, userHandle, credential, allWarnings()); + public Step13 nextStep() { + return new Step13(username, userHandle, credential); } } @Value - class Step9 implements Step { + class Step13 implements Step { private final String username; private final ByteArray userHandle; private final RegisteredCredential credential; - private final List prevWarnings; @Override public void validate() { @@ -388,17 +330,16 @@ public void validate() { } @Override - public Step10 nextStep() { - return new Step10(username, userHandle, credential, allWarnings()); + public Step14 nextStep() { + return new Step14(username, userHandle, credential); } } @Value - class Step10 implements Step { + class Step14 implements Step { private final String username; private final ByteArray userHandle; private final RegisteredCredential credential; - private final List prevWarnings; @Override public void validate() { @@ -407,17 +348,16 @@ public void validate() { } @Override - public Step11 nextStep() { - return new Step11(username, userHandle, credential, allWarnings()); + public Step15 nextStep() { + return new Step15(username, userHandle, credential); } } @Value - class Step11 implements Step { + class Step15 implements Step { private final String username; private final ByteArray userHandle; private final RegisteredCredential credential; - private final List prevWarnings; @Override public void validate() { @@ -441,17 +381,16 @@ public void validate() { } @Override - public Step12 nextStep() { - return new Step12(username, userHandle, credential, allWarnings()); + public Step16 nextStep() { + return new Step16(username, userHandle, credential); } } @Value - class Step12 implements Step { + class Step16 implements Step { private final String username; private final ByteArray userHandle; private final RegisteredCredential credential; - private final List prevWarnings; @Override public void validate() { @@ -461,22 +400,23 @@ public void validate() { } @Override - public Step13 nextStep() { - return new Step13(username, userHandle, credential, allWarnings()); + public Step17 nextStep() { + return new Step17(username, userHandle, credential); } } @Value - class Step13 implements Step { + class Step17 implements Step { private final String username; private final ByteArray userHandle; private final RegisteredCredential credential; - private final List prevWarnings; @Override public void validate() { - if (request.getPublicKeyCredentialRequestOptions().getUserVerification() - == UserVerificationRequirement.REQUIRED) { + if (request + .getPublicKeyCredentialRequestOptions() + .getUserVerification() + .equals(Optional.of(UserVerificationRequirement.REQUIRED))) { assure( response.getResponse().getParsedAuthenticatorData().getFlags().UV, "User Verification is required."); @@ -484,49 +424,31 @@ public void validate() { } @Override - public Step14 nextStep() { - return new Step14(username, userHandle, credential, allWarnings()); + public Step18 nextStep() { + return new Step18(username, userHandle, credential); } } @Value - class Step14 implements Step { + class Step18 implements Step { private final String username; private final ByteArray userHandle; private final RegisteredCredential credential; - private final List prevWarnings; @Override - public void validate() { - if (!allowUnrequestedExtensions) { - ExtensionsValidation.validate( - request.getPublicKeyCredentialRequestOptions().getExtensions(), response); - } - } - - @Override - public List getWarnings() { - try { - ExtensionsValidation.validate( - request.getPublicKeyCredentialRequestOptions().getExtensions(), response); - return Collections.emptyList(); - } catch (Exception e) { - return CollectionUtil.immutableList(Collections.singletonList(e.getMessage())); - } - } + public void validate() {} @Override - public Step15 nextStep() { - return new Step15(username, userHandle, credential, allWarnings()); + public Step19 nextStep() { + return new Step19(username, userHandle, credential); } } @Value - class Step15 implements Step { + class Step19 implements Step { private final String username; private final ByteArray userHandle; private final RegisteredCredential credential; - private final List prevWarnings; @Override public void validate() { @@ -534,8 +456,8 @@ public void validate() { } @Override - public Step16 nextStep() { - return new Step16(username, userHandle, credential, clientDataJsonHash(), allWarnings()); + public Step20 nextStep() { + return new Step20(username, userHandle, credential, clientDataJsonHash()); } public ByteArray clientDataJsonHash() { @@ -544,12 +466,11 @@ public ByteArray clientDataJsonHash() { } @Value - class Step16 implements Step { + class Step20 implements Step { private final String username; private final ByteArray userHandle; private final RegisteredCredential credential; private final ByteArray clientDataJsonHash; - private final List prevWarnings; @Override public void validate() { @@ -581,8 +502,8 @@ public void validate() { } @Override - public Step17 nextStep() { - return new Step17(username, userHandle, credential, allWarnings()); + public Step21 nextStep() { + return new Step21(username, userHandle, credential); } public ByteArray signedBytes() { @@ -591,22 +512,16 @@ public ByteArray signedBytes() { } @Value - class Step17 implements Step { + class Step21 implements Step { private final String username; private final ByteArray userHandle; private final RegisteredCredential credential; - private final List prevWarnings; private final long storedSignatureCountBefore; - public Step17( - String username, - ByteArray userHandle, - RegisteredCredential credential, - List prevWarnings) { + public Step21(String username, ByteArray userHandle, RegisteredCredential credential) { this.username = username; this.userHandle = userHandle; this.credential = credential; - this.prevWarnings = prevWarnings; this.storedSignatureCountBefore = credential.getSignatureCount(); } @@ -625,8 +540,7 @@ private boolean signatureCounterValid() { @Override public Finished nextStep() { - return new Finished( - username, userHandle, assertionSignatureCount(), signatureCounterValid(), allWarnings()); + return new Finished(username, userHandle, assertionSignatureCount(), signatureCounterValid()); } private long assertionSignatureCount() { @@ -640,7 +554,6 @@ class Finished implements Step { private final ByteArray userHandle; private final long assertionSignatureCount; private final boolean signatureCounterValid; - private final List prevWarnings; @Override public void validate() { @@ -667,7 +580,6 @@ public Optional result() { AuthenticatorAssertionExtensionOutputs.fromAuthenticatorData( response.getResponse().getParsedAuthenticatorData()) .orElse(null)) - .warnings(allWarnings()) .build()); } } 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 d51b7c636..66bf3bd77 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 @@ -29,9 +29,7 @@ import COSE.CoseException; import com.upokecenter.cbor.CBORObject; -import com.yubico.internal.util.CollectionUtil; -import com.yubico.webauthn.attestation.Attestation; -import com.yubico.webauthn.attestation.MetadataService; +import com.yubico.webauthn.attestation.AttestationTrustSource; import com.yubico.webauthn.data.AttestationObject; import com.yubico.webauthn.data.AttestationType; import com.yubico.webauthn.data.AuthenticatorAttestationResponse; @@ -45,13 +43,19 @@ import com.yubico.webauthn.data.PublicKeyCredentialDescriptor; import com.yubico.webauthn.data.UserVerificationRequirement; import java.io.IOException; +import java.security.InvalidAlgorithmParameterException; import java.security.NoSuchAlgorithmException; -import java.security.cert.CertificateEncodingException; +import java.security.cert.CertPath; +import java.security.cert.CertPathValidator; +import java.security.cert.CertPathValidatorException; import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.PKIXParameters; +import java.security.cert.TrustAnchor; import java.security.cert.X509Certificate; import java.security.spec.InvalidKeySpecException; -import java.util.ArrayList; -import java.util.Collections; +import java.sql.Date; +import java.time.Clock; import java.util.List; import java.util.Optional; import java.util.Set; @@ -65,6 +69,8 @@ final class FinishRegistrationSteps { private static final String CLIENT_DATA_TYPE = "webauthn.create"; + private static final ByteArray ZERO_AAGUID = + new ByteArray(new byte[] {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}); private final PublicKeyCredentialCreationOptions request; private final PublicKeyCredential< @@ -74,15 +80,16 @@ final class FinishRegistrationSteps { private final Set origins; private final String rpId; private final boolean allowUntrustedAttestation; - private final Optional metadataService; + private final Optional attestationTrustSource; private final CredentialRepository credentialRepository; + private final Clock clock; @Builder.Default private final boolean allowOriginPort = false; @Builder.Default private final boolean allowOriginSubdomain = false; @Builder.Default private final boolean allowUnrequestedExtensions = false; - public Step1 begin() { - return new Step1(); + public Step6 begin() { + return new Step6(); } public RegistrationResult run() { @@ -94,23 +101,10 @@ interface Step> { void validate(); - List getPrevWarnings(); - default Optional result() { return Optional.empty(); } - default List getWarnings() { - return Collections.emptyList(); - } - - default List allWarnings() { - List result = new ArrayList<>(getPrevWarnings().size() + getWarnings().size()); - result.addAll(getPrevWarnings()); - result.addAll(getWarnings()); - return CollectionUtil.immutableList(result); - } - default Next next() { validate(); return nextStep(); @@ -125,37 +119,20 @@ default RegistrationResult run() { } } - @Value - class Step1 implements Step { - @Override - public void validate() {} - - @Override - public Step2 nextStep() { - return new Step2(); - } + // Steps 1 through 4 are to create the request and run the client-side part - @Override - public List getPrevWarnings() { - return Collections.emptyList(); - } - } + // Step 5 is integrated into step 6 here @Value - class Step2 implements Step { + class Step6 implements Step { @Override public void validate() { assure(clientData() != null, "Client data must not be null."); } @Override - public Step3 nextStep() { - return new Step3(clientData()); - } - - @Override - public List getPrevWarnings() { - return Collections.emptyList(); + public Step7 nextStep() { + return new Step7(clientData()); } public CollectedClientData clientData() { @@ -164,11 +141,9 @@ public CollectedClientData clientData() { } @Value - class Step3 implements Step { + class Step7 implements Step { private final CollectedClientData clientData; - private List warnings = new ArrayList<>(0); - @Override public void validate() { assure( @@ -179,25 +154,14 @@ public void validate() { } @Override - public Step4 nextStep() { - return new Step4(clientData, allWarnings()); - } - - @Override - public List getPrevWarnings() { - return Collections.emptyList(); - } - - @Override - public List getWarnings() { - return CollectionUtil.immutableList(warnings); + public Step8 nextStep() { + return new Step8(clientData); } } @Value - class Step4 implements Step { + class Step8 implements Step { private final CollectedClientData clientData; - private final List prevWarnings; @Override public void validate() { @@ -205,15 +169,14 @@ public void validate() { } @Override - public Step5 nextStep() { - return new Step5(clientData, allWarnings()); + public Step9 nextStep() { + return new Step9(clientData); } } @Value - class Step5 implements Step { + class Step9 implements Step { private final CollectedClientData clientData; - private final List prevWarnings; @Override public void validate() { @@ -224,15 +187,14 @@ public void validate() { } @Override - public Step6 nextStep() { - return new Step6(clientData, allWarnings()); + public Step10 nextStep() { + return new Step10(clientData); } } @Value - class Step6 implements Step { + class Step10 implements Step { private final CollectedClientData clientData; - private final List prevWarnings; @Override public void validate() { @@ -240,23 +202,21 @@ public void validate() { } @Override - public Step7 nextStep() { - return new Step7(allWarnings()); + public Step11 nextStep() { + return new Step11(); } } @Value - class Step7 implements Step { - private final List prevWarnings; - + class Step11 implements Step { @Override public void validate() { assure(clientDataJsonHash().size() == 32, "Failed to compute hash of client data"); } @Override - public Step8 nextStep() { - return new Step8(clientDataJsonHash(), allWarnings()); + public Step12 nextStep() { + return new Step12(clientDataJsonHash()); } public ByteArray clientDataJsonHash() { @@ -265,9 +225,8 @@ public ByteArray clientDataJsonHash() { } @Value - class Step8 implements Step { + class Step12 implements Step { private final ByteArray clientDataJsonHash; - private final List prevWarnings; @Override public void validate() { @@ -275,8 +234,8 @@ public void validate() { } @Override - public Step9 nextStep() { - return new Step9(clientDataJsonHash, attestation(), allWarnings()); + public Step13 nextStep() { + return new Step13(clientDataJsonHash, attestation()); } public AttestationObject attestation() { @@ -285,10 +244,9 @@ public AttestationObject attestation() { } @Value - class Step9 implements Step { + class Step13 implements Step { private final ByteArray clientDataJsonHash; private final AttestationObject attestation; - private final List prevWarnings; @Override public void validate() { @@ -299,16 +257,15 @@ public void validate() { } @Override - public Step10 nextStep() { - return new Step10(clientDataJsonHash, attestation, allWarnings()); + public Step14 nextStep() { + return new Step14(clientDataJsonHash, attestation); } } @Value - class Step10 implements Step { + class Step14 implements Step { private final ByteArray clientDataJsonHash; private final AttestationObject attestation; - private final List prevWarnings; @Override public void validate() { @@ -318,22 +275,21 @@ public void validate() { } @Override - public Step11 nextStep() { - return new Step11(clientDataJsonHash, attestation, allWarnings()); + public Step15 nextStep() { + return new Step15(clientDataJsonHash, attestation); } } @Value - class Step11 implements Step { + class Step15 implements Step { private final ByteArray clientDataJsonHash; private final AttestationObject attestation; - private final List prevWarnings; @Override public void validate() { if (request .getAuthenticatorSelection() - .map(AuthenticatorSelectionCriteria::getUserVerification) + .flatMap(AuthenticatorSelectionCriteria::getUserVerification) .orElse(UserVerificationRequirement.PREFERRED) == UserVerificationRequirement.REQUIRED) { assure( @@ -343,53 +299,62 @@ public void validate() { } @Override - public Step12 nextStep() { - return new Step12(clientDataJsonHash, attestation, allWarnings()); + public Step16 nextStep() { + return new Step16(clientDataJsonHash, attestation); } } @Value - class Step12 implements Step { + class Step16 implements Step { private final ByteArray clientDataJsonHash; private final AttestationObject attestation; - private final List prevWarnings; @Override public void validate() { - if (!allowUnrequestedExtensions) { - ExtensionsValidation.validate(request.getExtensions(), response); - } - } - - @Override - public List getWarnings() { + final ByteArray publicKeyCose = + response + .getResponse() + .getAttestation() + .getAuthenticatorData() + .getAttestedCredentialData() + .get() + .getCredentialPublicKey(); + CBORObject publicKeyCbor = CBORObject.DecodeFromBytes(publicKeyCose.getBytes()); + final int alg = publicKeyCbor.get(CBORObject.FromObject(3)).AsInt32(); + assure( + request.getPubKeyCredParams().stream() + .anyMatch(pkcparam -> pkcparam.getAlg().getId() == alg), + "Unrequested credential key algorithm: got %d, expected one of: %s", + alg, + request.getPubKeyCredParams().stream() + .map(pkcparam -> pkcparam.getAlg()) + .collect(Collectors.toList())); try { - ExtensionsValidation.validate(request.getExtensions(), response); - return Collections.emptyList(); - } catch (Exception e) { - return Collections.singletonList(e.getMessage()); + WebAuthnCodecs.importCosePublicKey(publicKeyCose); + } catch (CoseException | IOException | InvalidKeySpecException | NoSuchAlgorithmException e) { + throw wrapAndLog(log, "Failed to parse credential public key", e); } } @Override - public Step13 nextStep() { - return new Step13(clientDataJsonHash, attestation, allWarnings()); + public Step18 nextStep() { + return new Step18(clientDataJsonHash, attestation); } } + // Nothing to do for step 17 + @Value - class Step13 implements Step { + class Step18 implements Step { private final ByteArray clientDataJsonHash; private final AttestationObject attestation; - private final List prevWarnings; @Override public void validate() {} @Override - public Step14 nextStep() { - return new Step14( - clientDataJsonHash, attestation, attestationStatementVerifier(), allWarnings()); + public Step19 nextStep() { + return new Step19(clientDataJsonHash, attestation, attestationStatementVerifier()); } public String format() { @@ -415,11 +380,10 @@ public Optional attestationStatementVerifier() { } @Value - class Step14 implements Step { + class Step19 implements Step { private final ByteArray clientDataJsonHash; private final AttestationObject attestation; private final Optional attestationStatementVerifier; - private final List prevWarnings; @Override public void validate() { @@ -434,8 +398,8 @@ public void validate() { } @Override - public Step15 nextStep() { - return new Step15(attestation, attestationType(), attestationTrustPath(), allWarnings()); + public Step20 nextStep() { + return new Step20(attestation, attestationType(), attestationTrustPath()); } public AttestationType attestationType() { @@ -449,11 +413,7 @@ public AttestationType attestationType() { return AttestationType.BASIC; case "tpm": // TODO delete this once tpm attestation verification is implemented - if (attestation.getAttestationStatement().has("x5c")) { - return AttestationType.ATTESTATION_CA; - } else { - return AttestationType.ECDAA; - } + return AttestationType.ATTESTATION_CA; default: return AttestationType.UNKNOWN; } @@ -483,248 +443,160 @@ public Optional> attestationTrustPath() { } @Value - class Step15 implements Step { + class Step20 implements Step { private final AttestationObject attestation; private final AttestationType attestationType; private final Optional> attestationTrustPath; - private final List prevWarnings; + + private final Optional trustRoots; + + public Step20( + AttestationObject attestation, + AttestationType attestationType, + Optional> attestationTrustPath) { + this.attestation = attestation; + this.attestationType = attestationType; + this.attestationTrustPath = attestationTrustPath; + this.trustRoots = findTrustRoots(); + } @Override public void validate() {} @Override - public Step16 nextStep() { - return new Step16( - attestation, attestationType, attestationTrustPath, trustResolver(), allWarnings()); + public Step21 nextStep() { + return new Step21(attestation, attestationType, attestationTrustPath, trustRoots); } - public Optional trustResolver() { - switch (attestationType) { - case NONE: - case SELF_ATTESTATION: - case UNKNOWN: - return Optional.empty(); - - case ANONYMIZATION_CA: - case ATTESTATION_CA: - case BASIC: - switch (attestation.getFormat()) { - case "android-key": - case "android-safetynet": - case "apple": - case "fido-u2f": - case "packed": - case "tpm": - return metadataService.map(KnownX509TrustAnchorsTrustResolver::new); - default: - throw new UnsupportedOperationException( - String.format( - "Attestation type %s is not supported for attestation statement format \"%s\".", - attestationType, attestation.getFormat())); - } - - default: - throw new UnsupportedOperationException( - "Attestation type not implemented: " + attestationType); - } + private Optional findTrustRoots() { + return attestationTrustSource.flatMap( + attestationTrustSource -> + attestationTrustPath.map( + atp -> + attestationTrustSource.findTrustRoots( + atp, + Optional.of( + attestation + .getAuthenticatorData() + .getAttestedCredentialData() + .get() + .getAaguid()) + .filter(aaguid -> !aaguid.equals(ZERO_AAGUID))))); } } @Value - class Step16 implements Step { + class Step21 implements Step { private final AttestationObject attestation; private final AttestationType attestationType; private final Optional> attestationTrustPath; - private final Optional trustResolver; - private final List prevWarnings; - - @Override - public void validate() { - assure( - trustResolver.isPresent() || allowUntrustedAttestation, - "Failed to obtain attestation trust anchors."); - - switch (attestationType) { - case SELF_ATTESTATION: - assure(allowUntrustedAttestation, "Self attestation is not allowed."); - break; - - case ANONYMIZATION_CA: - case ATTESTATION_CA: - case BASIC: - assure( - allowUntrustedAttestation || attestationTrusted(), - "Failed to derive trust for attestation key."); - break; - - case NONE: - assure(allowUntrustedAttestation, "No attestation is not allowed."); - break; - - case UNKNOWN: - assure( - allowUntrustedAttestation, "Unknown attestation statement formats are not allowed."); - break; - - default: - throw new UnsupportedOperationException( - "Attestation type not implemented: " + attestationType); - } - } - - @Override - public Step17 nextStep() { - return new Step17( - attestationType, attestationMetadata(), attestationTrusted(), allWarnings()); - } - - public boolean attestationTrusted() { - switch (attestationType) { - case NONE: - case SELF_ATTESTATION: - case UNKNOWN: - return false; + private final Optional trustRoots; - case ANONYMIZATION_CA: - case ATTESTATION_CA: - case BASIC: - return attestationMetadata().filter(Attestation::isTrusted).isPresent(); - default: - throw new UnsupportedOperationException( - "Attestation type not implemented: " + attestationType); - } - } + private final boolean attestationTrusted; - public Optional attestationMetadata() { - return trustResolver.flatMap( - tr -> { - try { - return Optional.of( - tr.resolveTrustAnchor(attestationTrustPath.orElseGet(Collections::emptyList))); - } catch (CertificateEncodingException e) { - log.debug("Failed to resolve trust anchor for attestation: {}", attestation, e); - return Optional.empty(); - } - }); - } + public Step21( + AttestationObject attestation, + AttestationType attestationType, + Optional> attestationTrustPath, + Optional trustRoots) { + this.attestation = attestation; + this.attestationType = attestationType; + this.attestationTrustPath = attestationTrustPath; + this.trustRoots = trustRoots; - @Override - public List getWarnings() { - return trustResolver - .map( - tr -> { - try { - tr.resolveTrustAnchor(attestationTrustPath.orElseGet(Collections::emptyList)); - return Collections.emptyList(); - } catch (CertificateEncodingException e) { - return Collections.singletonList("Failed to resolve trust anchor: " + e); - } - }) - .orElseGet(Collections::emptyList); + this.attestationTrusted = attestationTrusted(); } - } - - @Value - class Step17 implements Step { - private final AttestationType attestationType; - private final Optional attestationMetadata; - private final boolean attestationTrusted; - private final List prevWarnings; @Override public void validate() { assure( - credentialRepository.lookupAll(response.getId()).isEmpty(), - "Credential ID is already registered: %s", - response.getId()); + allowUntrustedAttestation || attestationTrusted, + "Failed to derive trust for attestation key."); } @Override - public Step18 nextStep() { - return new Step18(attestationType, attestationMetadata, attestationTrusted, allWarnings()); + public Step22 nextStep() { + return new Step22(attestationType, attestationTrusted, attestationTrustPath); } - } - - @Value - class Step18 implements Step { - private final AttestationType attestationType; - private final Optional attestationMetadata; - private final boolean attestationTrusted; - private final List prevWarnings; - @Override - public void validate() {} + public boolean attestationTrusted() { + if (attestationTrustPath.isPresent() && attestationTrustSource.isPresent()) { + try { + if (!trustRoots.isPresent() || trustRoots.get().getTrustRoots().isEmpty()) { + return false; + + } else { + final CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); + final CertPathValidator cpv = CertPathValidator.getInstance("PKIX"); + final CertPath certPath = certFactory.generateCertPath(attestationTrustPath.get()); + final PKIXParameters pathParams = + new PKIXParameters( + trustRoots.get().getTrustRoots().stream() + .map(rootCert -> new TrustAnchor(rootCert, null)) + .collect(Collectors.toSet())); + pathParams.setDate(Date.from(clock.instant())); + pathParams.setRevocationEnabled(trustRoots.get().isEnableRevocationChecking()); + trustRoots.get().getCertStore().ifPresent(pathParams::addCertStore); + cpv.validate(certPath, pathParams); + return true; + } - @Override - public Step19 nextStep() { - return new Step19(attestationType, attestationMetadata, attestationTrusted, allWarnings()); - } - } + } catch (CertPathValidatorException e) { + log.info( + "Failed to derive trust in attestation statement: {} at cert index {}: {}", + e.getReason(), + e.getIndex(), + e.getMessage()); + return false; - @Value - class Step19 implements Step { - private final AttestationType attestationType; - private final Optional attestationMetadata; - private final boolean attestationTrusted; - private final List prevWarnings; + } catch (CertificateException e) { + log.warn("Failed to build attestation certificate path.", e); + return false; - @Override - public void validate() {} + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException( + "Failed to check attestation trust path. A JCA provider is likely missing in the runtime environment.", + e); - @Override - public CustomLastStep nextStep() { - return new CustomLastStep( - attestationType, attestationMetadata, attestationTrusted, allWarnings()); + } catch (InvalidAlgorithmParameterException e) { + throw new RuntimeException( + "Failed to initialize attestation trust path validator. This is likely a bug, please file a bug report.", + e); + } + } else { + return false; + } } } - /** Steps that aren't yet standardised in a stable edition of the spec */ @Value - class CustomLastStep implements Step { + class Step22 implements Step { private final AttestationType attestationType; - private final Optional attestationMetadata; private final boolean attestationTrusted; - private final List prevWarnings; + private final Optional> attestationTrustPath; @Override public void validate() { - ByteArray publicKeyCose = - response - .getResponse() - .getAttestation() - .getAuthenticatorData() - .getAttestedCredentialData() - .get() - .getCredentialPublicKey(); - CBORObject publicKeyCbor = CBORObject.DecodeFromBytes(publicKeyCose.getBytes()); - int alg = publicKeyCbor.get(CBORObject.FromObject(3)).AsInt32(); assure( - request.getPubKeyCredParams().stream() - .anyMatch(pkcparam -> pkcparam.getAlg().getId() == alg), - "Unrequested credential key algorithm: got %d, expected one of: %s", - alg, - request.getPubKeyCredParams().stream() - .map(pkcparam -> pkcparam.getAlg()) - .collect(Collectors.toList())); - try { - WebAuthnCodecs.importCosePublicKey(publicKeyCose); - } catch (CoseException | IOException | InvalidKeySpecException | NoSuchAlgorithmException e) { - throw wrapAndLog(log, "Failed to parse credential public key", e); - } + credentialRepository.lookupAll(response.getId()).isEmpty(), + "Credential ID is already registered: %s", + response.getId()); } @Override public Finished nextStep() { - return new Finished(attestationType, attestationMetadata, attestationTrusted, allWarnings()); + return new Finished(attestationType, attestationTrusted, attestationTrustPath); } } + // Step 23 will be performed externally by library user + // Nothing to do for step 24 + @Value class Finished implements Step { private final AttestationType attestationType; - private final Optional attestationMetadata; private final boolean attestationTrusted; - private final List prevWarnings; + private final Optional> attestationTrustPath; @Override public void validate() { @@ -741,6 +613,14 @@ public Optional result() { return Optional.of( RegistrationResult.builder() .keyId(keyId()) + .aaguid( + response + .getResponse() + .getAttestation() + .getAuthenticatorData() + .getAttestedCredentialData() + .get() + .getAaguid()) .attestationTrusted(attestationTrusted) .attestationType(attestationType) .publicKeyCose( @@ -758,8 +638,7 @@ public Optional result() { AuthenticatorRegistrationExtensionOutputs.fromAuthenticatorData( response.getResponse().getParsedAuthenticatorData()) .orElse(null)) - .attestationMetadata(attestationMetadata) - .warnings(allWarnings()) + .attestationTrustPath(attestationTrustPath.orElse(null)) .build()); } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/KnownX509TrustAnchorsTrustResolver.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/KnownX509TrustAnchorsTrustResolver.java deleted file mode 100644 index 407be8dc3..000000000 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/KnownX509TrustAnchorsTrustResolver.java +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) 2018, Yubico AB -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, this -// list of conditions and the following disclaimer. -// -// 2. Redistributions in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -package com.yubico.webauthn; - -import com.yubico.webauthn.attestation.Attestation; -import com.yubico.webauthn.attestation.MetadataService; -import java.security.cert.CertificateEncodingException; -import java.security.cert.X509Certificate; -import java.util.List; -import lombok.AllArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -@Slf4j -@AllArgsConstructor -final class KnownX509TrustAnchorsTrustResolver implements AttestationTrustResolver { - - private final MetadataService metadataService; - - @Override - public Attestation resolveTrustAnchor(List certificateChain) - throws CertificateEncodingException { - return metadataService.getAttestation(certificateChain); - } -} 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 e5c63d91c..11055b963 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 @@ -27,6 +27,7 @@ import COSE.CoseException; import com.fasterxml.jackson.databind.JsonNode; import com.upokecenter.cbor.CBORObject; +import com.yubico.internal.util.CertificateParser; import com.yubico.internal.util.CollectionUtil; import com.yubico.internal.util.ExceptionUtil; import com.yubico.webauthn.data.AttestationObject; @@ -34,7 +35,6 @@ import com.yubico.webauthn.data.ByteArray; import com.yubico.webauthn.data.COSEAlgorithmIdentifier; import java.io.IOException; -import java.nio.ByteBuffer; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.PublicKey; @@ -63,8 +63,6 @@ final class PackedAttestationStatementVerifier public AttestationType getAttestationType(AttestationObject attestation) { if (attestation.getAttestationStatement().hasNonNull("x5c")) { return AttestationType.BASIC; - } else if (attestation.getAttestationStatement().hasNonNull("ecdaaKeyId")) { - return AttestationType.ECDAA; } else { return AttestationType.SELF_ATTESTATION; } @@ -81,19 +79,11 @@ public boolean verifyAttestationSignature( if (attestationObject.getAttestationStatement().has("x5c")) { return verifyX5cSignature(attestationObject, clientDataJsonHash); - } else if (attestationObject.getAttestationStatement().has("ecdaaKeyId")) { - return verifyEcdaaSignature(attestationObject, clientDataJsonHash); } else { return verifySelfAttestationSignature(attestationObject, clientDataJsonHash); } } - private boolean verifyEcdaaSignature( - AttestationObject attestationObject, ByteArray clientDataJsonHash) { - throw new UnsupportedOperationException( - "ECDAA signature verification is not (yet) implemented."); - } - private boolean verifySelfAttestationSignature( AttestationObject attestationObject, ByteArray clientDataJsonHash) { final PublicKey pubkey; @@ -215,7 +205,7 @@ private boolean verifyX5cSignature( final String signatureAlgorithmName = WebAuthnCodecs.getJavaAlgorithmName(sigAlg); Signature signatureVerifier; try { - signatureVerifier = Crypto.getSignature(signatureAlgorithmName); + signatureVerifier = Signature.getInstance(signatureAlgorithmName); } catch (NoSuchAlgorithmException e) { throw ExceptionUtil.wrapAndLog( log, "Failed to get a Signature instance for " + signatureAlgorithmName, e); @@ -282,7 +272,6 @@ public boolean verifyX5cRequirements(X509Certificate cert, ByteArray aaguid) { } final String ouValue = "Authenticator Attestation"; - final String idFidoGenCeAaguid = "1.3.6.1.4.1.45724.1.1.4"; final Set countries = CollectionUtil.immutableSet(new HashSet<>(Arrays.asList(Locale.getISOCountries()))); @@ -301,19 +290,12 @@ public boolean verifyX5cRequirements(X509Certificate cert, ByteArray aaguid) { ouValue, getDnField("OU", cert)); - Optional.ofNullable(cert.getExtensionValue(idFidoGenCeAaguid)) - .map(ext -> new ByteArray(parseAaguid(ext))) + CertificateParser.parseFidoAaguidExtension(cert) .ifPresent( - (ByteArray value) -> { + extensionAaguid -> { ExceptionUtil.assure( - value.equals(aaguid), - "X.509 extension %s (id-fido-gen-ce-aaguid) is present but does not match the authenticator AAGUID.", - idFidoGenCeAaguid); - - ExceptionUtil.assure( - !cert.getCriticalExtensionOIDs().contains(idFidoGenCeAaguid), - "X.509 extension %s (id-fido-gen-ce-aaguid) must not be marked critical.", - idFidoGenCeAaguid); + Arrays.equals(aaguid.getBytes(), extensionAaguid), + "X.509 extension \"id-fido-gen-ce-aaguid\" is present but does not match the authenticator AAGUID."); }); ExceptionUtil.assure( @@ -321,33 +303,4 @@ public boolean verifyX5cRequirements(X509Certificate cert, ByteArray aaguid) { return true; } - - /** - * Parses an AAGUID into bytes. Refer to Packed - * Attestation Statement Certificate Requirements on the W3C web site for details of the ASN.1 - * structure that this method parses. - * - * @param bytes the bytes making up value of the extension - * @return the bytes of the AAGUID - */ - private byte[] parseAaguid(byte[] bytes) { - - if (bytes != null && bytes.length == 20) { - ByteBuffer buffer = ByteBuffer.wrap(bytes); - - if (buffer.get() == (byte) 0x04 - && buffer.get() == (byte) 0x12 - && buffer.get() == (byte) 0x04 - && buffer.get() == (byte) 0x10) { - byte[] aaguidBytes = new byte[16]; - buffer.get(aaguidBytes); - - return aaguidBytes; - } - } - - throw new IllegalArgumentException( - "X.509 extension 1.3.6.1.4.1.45724.1.1.4 (id-fido-gen-ce-aaguid) is not valid."); - } } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/RegistrationResult.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/RegistrationResult.java index c07b7772b..afa059e9f 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/RegistrationResult.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/RegistrationResult.java @@ -25,18 +25,23 @@ package com.yubico.webauthn; import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; -import com.yubico.internal.util.CollectionUtil; -import com.yubico.webauthn.attestation.Attestation; +import com.yubico.internal.util.CertificateParser; +import com.yubico.webauthn.RelyingParty.RelyingPartyBuilder; +import com.yubico.webauthn.attestation.AttestationTrustSource; import com.yubico.webauthn.data.AttestationType; import com.yubico.webauthn.data.AuthenticatorRegistrationExtensionOutputs; import com.yubico.webauthn.data.ByteArray; import com.yubico.webauthn.data.ClientRegistrationExtensionOutputs; import com.yubico.webauthn.data.PublicKeyCredential; import com.yubico.webauthn.data.PublicKeyCredentialDescriptor; -import java.util.Collections; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; import java.util.List; import java.util.Optional; +import java.util.stream.Collectors; import lombok.Builder; import lombok.NonNull; import lombok.Value; @@ -49,8 +54,8 @@ public class RegistrationResult { /** * The credential * ID and transportsof - * the created credential. + * href="https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#dom-publickeycredentialdescriptor-transports">transports + * of the created credential. * * @see Credential * ID @@ -61,10 +66,24 @@ public class RegistrationResult { */ @NonNull private final PublicKeyCredentialDescriptor keyId; + /** + * The aaguid + * reported in the of the + * created credential. + * + *

This MAY be an AAGUID consisting of only zeroes. + */ + @NonNull private final ByteArray aaguid; + /** * true if and only if the attestation signature was successfully linked to a trusted * attestation root. * + *

This will always be false unless the {@link + * RelyingPartyBuilder#attestationTrustSource(AttestationTrustSource) attestationTrustSource} + * setting was configured on the {@link RelyingParty} instance. + * *

You can ignore this if authenticator attestation is not relevant to your application. */ private final boolean attestationTrusted; @@ -82,6 +101,23 @@ public class RegistrationResult { */ @NonNull private final AttestationType attestationType; + /** + * The attestation + * trust path for the created credential, if any. + * + *

If present, this may be useful for looking up attestation metadata from external sources. + * The attestation trust path has been successfully verified as trusted if and only if {@link + * #isAttestationTrusted()} is true. + * + *

You can ignore this if authenticator attestation is not relevant to your application. + * + * @see Attestation + * trust path + */ + private final List attestationTrustPath; + /** * The public key of the created credential. * @@ -102,47 +138,27 @@ public class RegistrationResult { */ private final long signatureCount; - /** Zero or more human-readable messages about non-critical issues. */ - @NonNull @Builder.Default private final List warnings = Collections.emptyList(); - - /** - * Additional information about the authenticator, identified based on the attestation - * certificate. - * - *

This will be absent unless you set a {@link - * com.yubico.webauthn.RelyingParty.RelyingPartyBuilder#metadataService(Optional) metadataService} - * in {@link RelyingParty}. - * - * @see §6.4. - * Attestation - * @see com.yubico.webauthn.RelyingParty.RelyingPartyBuilder#metadataService(Optional) - */ - private final Attestation attestationMetadata; - private final ClientRegistrationExtensionOutputs clientExtensionOutputs; private final AuthenticatorRegistrationExtensionOutputs authenticatorExtensionOutputs; - @JsonCreator private RegistrationResult( - @NonNull @JsonProperty("keyId") PublicKeyCredentialDescriptor keyId, - @JsonProperty("attestationTrusted") boolean attestationTrusted, - @NonNull @JsonProperty("attestationType") AttestationType attestationType, - @NonNull @JsonProperty("publicKeyCose") ByteArray publicKeyCose, - @JsonProperty("signatureCount") Long signatureCount, - @NonNull @JsonProperty("warnings") List warnings, - @JsonProperty("attestationMetadata") Attestation attestationMetadata, - @JsonProperty("clientExtensionOutputs") - ClientRegistrationExtensionOutputs clientExtensionOutputs, - @JsonProperty("authenticatorExtensionOutputs") - AuthenticatorRegistrationExtensionOutputs authenticatorExtensionOutputs) { + @NonNull PublicKeyCredentialDescriptor keyId, + @NonNull ByteArray aaguid, + boolean attestationTrusted, + @NonNull AttestationType attestationType, + List attestationTrustPath, + @NonNull ByteArray publicKeyCose, + Long signatureCount, + ClientRegistrationExtensionOutputs clientExtensionOutputs, + AuthenticatorRegistrationExtensionOutputs authenticatorExtensionOutputs) { this.keyId = keyId; + this.aaguid = aaguid; this.attestationTrusted = attestationTrusted; this.attestationType = attestationType; + this.attestationTrustPath = attestationTrustPath; this.publicKeyCose = publicKeyCose; this.signatureCount = signatureCount == null ? 0 : signatureCount; - this.warnings = CollectionUtil.immutableList(warnings); - this.attestationMetadata = attestationMetadata; this.clientExtensionOutputs = clientExtensionOutputs == null || clientExtensionOutputs.getExtensionIds().isEmpty() ? null @@ -150,8 +166,60 @@ private RegistrationResult( this.authenticatorExtensionOutputs = authenticatorExtensionOutputs; } - public Optional getAttestationMetadata() { - return Optional.ofNullable(attestationMetadata); + @JsonCreator + private static RegistrationResult fromJson( + @NonNull @JsonProperty("keyId") PublicKeyCredentialDescriptor keyId, + @NonNull @JsonProperty("aaguid") ByteArray aaguid, + @JsonProperty("attestationTrusted") boolean attestationTrusted, + @NonNull @JsonProperty("attestationType") AttestationType attestationType, + @JsonProperty("attestationTrustPath") List attestationTrustPath, + @NonNull @JsonProperty("publicKeyCose") ByteArray publicKeyCose, + @JsonProperty("signatureCount") Long signatureCount, + @JsonProperty("clientExtensionOutputs") + ClientRegistrationExtensionOutputs clientExtensionOutputs, + @JsonProperty("authenticatorExtensionOutputs") + AuthenticatorRegistrationExtensionOutputs authenticatorExtensionOutputs) { + return new RegistrationResult( + keyId, + aaguid, + attestationTrusted, + attestationType, + attestationTrustPath.stream() + .map( + pem -> { + try { + return CertificateParser.parsePem(pem); + } catch (CertificateException e) { + throw new RuntimeException(e); + } + }) + .collect(Collectors.toList()), + publicKeyCose, + signatureCount, + clientExtensionOutputs, + authenticatorExtensionOutputs); + } + + @JsonIgnore + public Optional> getAttestationTrustPath() { + return Optional.ofNullable(attestationTrustPath); + } + + @JsonProperty("attestationTrustPath") + private Optional> getAttestationTrustPathJson() { + return getAttestationTrustPath() + .map( + x5c -> + x5c.stream() + .map( + cert -> { + try { + return new ByteArray(cert.getEncoded()).getBase64(); + } catch (CertificateEncodingException e) { + throw new RuntimeException(e); + } + }) + .collect(Collectors.toList())); } /** @@ -225,60 +293,53 @@ Step2 keyId(PublicKeyCredentialDescriptor keyId) { } class Step2 { - Step3 attestationTrusted(boolean attestationTrusted) { - builder.attestationTrusted(attestationTrusted); + Step3 aaguid(ByteArray aaguid) { + builder.aaguid(aaguid); return new Step3(); } } class Step3 { - Step4 attestationType(AttestationType attestationType) { - builder.attestationType(attestationType); + Step4 attestationTrusted(boolean attestationTrusted) { + builder.attestationTrusted(attestationTrusted); return new Step4(); } } class Step4 { - Step5 publicKeyCose(ByteArray publicKeyCose) { - builder.publicKeyCose(publicKeyCose); + Step5 attestationType(AttestationType attestationType) { + builder.attestationType(attestationType); return new Step5(); } } class Step5 { - Step6 signatureCount(long signatureCount) { - builder.signatureCount(signatureCount); + Step6 publicKeyCose(ByteArray publicKeyCose) { + builder.publicKeyCose(publicKeyCose); return new Step6(); } } class Step6 { - Step7 clientExtensionOutputs(ClientRegistrationExtensionOutputs clientExtensionOutputs) { - builder.clientExtensionOutputs(clientExtensionOutputs); + Step7 signatureCount(long signatureCount) { + builder.signatureCount(signatureCount); return new Step7(); } } class Step7 { + Step8 clientExtensionOutputs(ClientRegistrationExtensionOutputs clientExtensionOutputs) { + builder.clientExtensionOutputs(clientExtensionOutputs); + return new Step8(); + } + } + + class Step8 { RegistrationResultBuilder authenticatorExtensionOutputs( AuthenticatorRegistrationExtensionOutputs authenticatorExtensionOutputs) { return builder.authenticatorExtensionOutputs(authenticatorExtensionOutputs); } } } - - RegistrationResultBuilder attestationMetadata( - @NonNull Optional attestationMetadata) { - this.attestationMetadata = attestationMetadata.orElse(null); - return this; - } - - /* - * Workaround, see: https://github.com/rzwitserloot/lombok/issues/2623#issuecomment-714816001 - * Consider reverting this workaround if Lombok fixes that issue. - */ - private RegistrationResultBuilder attestationMetadata(Attestation attestationMetadata) { - return this.attestationMetadata(Optional.ofNullable(attestationMetadata)); - } } } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/RelyingParty.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/RelyingParty.java index b0a3fb813..58ce3de51 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 @@ -26,7 +26,7 @@ import com.yubico.internal.util.CollectionUtil; import com.yubico.internal.util.OptionalUtil; -import com.yubico.webauthn.attestation.MetadataService; +import com.yubico.webauthn.attestation.AttestationTrustSource; import com.yubico.webauthn.data.AssertionExtensionInputs; import com.yubico.webauthn.data.AttestationConveyancePreference; import com.yubico.webauthn.data.AuthenticatorAssertionResponse; @@ -50,13 +50,18 @@ import com.yubico.webauthn.extension.appid.AppId; import java.net.MalformedURLException; import java.net.URL; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; +import java.security.Signature; +import java.time.Clock; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.stream.Collectors; import lombok.Builder; import lombok.NonNull; import lombok.Value; @@ -175,7 +180,7 @@ public class RelyingParty { * *

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

By default, this is not set. * @@ -186,9 +191,9 @@ public class RelyingParty { @NonNull private final Optional attestationConveyancePreference; /** - * A {@link MetadataService} instance to use for looking up device attestation metadata. This - * matters only if {@link #getAttestationConveyancePreference()} is non-empty and not set to - * {@link AttestationConveyancePreference#NONE}. + * An {@link AttestationTrustSource} instance to use for looking up trust roots for authenticator + * attestation. This matters only if {@link #getAttestationConveyancePreference()} is non-empty + * and not set to {@link AttestationConveyancePreference#NONE}. * *

By default, this is not set. * @@ -196,7 +201,7 @@ public class RelyingParty { * @see §6.4. * Attestation */ - @NonNull private final Optional metadataService; + @NonNull private final Optional attestationTrustSource; /** * The argument for the {@link PublicKeyCredentialCreationOptions#getPubKeyCredParams() @@ -310,27 +315,11 @@ public class RelyingParty { */ @Builder.Default private final boolean allowOriginSubdomain = false; - /** - * If true, {@link #finishRegistration(FinishRegistrationOptions) finishRegistration} - * and {@link #finishAssertion(FinishAssertionOptions) finishAssertion} will accept responses - * containing extension outputs for which there was no extension input. - * - *

The default is false. - * - * @see §9. WebAuthn - * Extensions - * @deprecated The false setting (default) is not compatible with WebAuthn Level 2 - * since authenticators are now always allowed to add unsolicited extensions. The next major - * version release will remove this option and always behave as if the option had been set to - * - * true. - */ - @Deprecated @Builder.Default private final boolean allowUnrequestedExtensions = false; - /** * If false, {@link #finishRegistration(FinishRegistrationOptions) * finishRegistration} will only allow registrations where the attestation signature can be linked - * to a trusted attestation root. This excludes self attestation and none attestation. + * to a trusted attestation root. This excludes none attestation, and self attestation unless the + * self attestation key is explicitly trusted. * *

Regardless of the value of this option, invalid attestation statements of supported formats * will always be rejected. For example, a "packed" attestation statement with an invalid @@ -350,19 +339,31 @@ public class RelyingParty { */ @Builder.Default private final boolean validateSignatureCounter = true; + /** + * A {@link Clock} which will be used to tell the current time while verifying attestation + * certificate chains. + * + *

This is intended primarily for testing, and relevant only if {@link + * RelyingPartyBuilder#attestationTrustSource(AttestationTrustSource)} is set. + * + *

The default is Clock.systemUTC(). + */ + @Builder.Default @NonNull private final Clock clock = Clock.systemUTC(); + + @Builder private RelyingParty( @NonNull RelyingPartyIdentity identity, Set origins, @NonNull CredentialRepository credentialRepository, @NonNull Optional appId, @NonNull Optional attestationConveyancePreference, - @NonNull Optional metadataService, + @NonNull Optional attestationTrustSource, List preferredPubkeyParams, boolean allowOriginPort, boolean allowOriginSubdomain, - boolean allowUnrequestedExtensions, boolean allowUntrustedAttestation, - boolean validateSignatureCounter) { + boolean validateSignatureCounter, + Clock clock) { this.identity = identity; this.origins = origins != null @@ -382,13 +383,13 @@ private RelyingParty( this.credentialRepository = credentialRepository; this.appId = appId; this.attestationConveyancePreference = attestationConveyancePreference; - this.metadataService = metadataService; - this.preferredPubkeyParams = preferredPubkeyParams; + this.attestationTrustSource = attestationTrustSource; + this.preferredPubkeyParams = filterAvailableAlgorithms(preferredPubkeyParams); this.allowOriginPort = allowOriginPort; this.allowOriginSubdomain = allowOriginSubdomain; - this.allowUnrequestedExtensions = allowUnrequestedExtensions; this.allowUntrustedAttestation = allowUntrustedAttestation; this.validateSignatureCounter = validateSignatureCounter; + this.clock = clock; } private static ByteArray generateChallenge() { @@ -397,6 +398,66 @@ private static ByteArray generateChallenge() { return new ByteArray(bytes); } + /** + * Filter pubKeyCredParams to only contain algorithms with a {@link KeyFactory} and a + * {@link Signature} available, and log a warning for every unsupported algorithm. + * + * @return a new {@link List} containing only the algorithms supported in the current JCA context. + */ + private static List filterAvailableAlgorithms( + List pubKeyCredParams) { + return Collections.unmodifiableList( + pubKeyCredParams.stream() + .filter( + param -> { + try { + switch (param.getAlg()) { + case EdDSA: + KeyFactory.getInstance("EdDSA"); + break; + + case ES256: + KeyFactory.getInstance("EC"); + break; + + case RS256: + case RS1: + KeyFactory.getInstance("RSA"); + break; + + default: + log.warn( + "Unknown algorithm: {}. Please file a bug report.", param.getAlg()); + } + } catch (NoSuchAlgorithmException e) { + log.warn( + "Unsupported algorithm in RelyingParty.preferredPubkeyParams: {}. No KeyFactory available; registrations with this key algorithm will fail. You may need to add a dependency and load a provider using java.security.Security.addProvider().", + param.getAlg()); + return false; + } + + final String signatureAlgName; + try { + signatureAlgName = WebAuthnCodecs.getJavaAlgorithmName(param.getAlg()); + } catch (IllegalArgumentException e) { + log.warn("Unknown algorithm: {}. Please file a bug report.", param.getAlg()); + return false; + } + + try { + Signature.getInstance(signatureAlgName); + } catch (NoSuchAlgorithmException e) { + log.warn( + "Unsupported algorithm in RelyingParty.preferredPubkeyParams: {}. No Signature available; registrations with this key algorithm will fail. You may need to add a dependency and load a provider using java.security.Security.addProvider().", + param.getAlg()); + return false; + } + + return true; + }) + .collect(Collectors.toList())); + } + public PublicKeyCredentialCreationOptions startRegistration( StartRegistrationOptions startRegistrationOptions) { PublicKeyCredentialCreationOptionsBuilder builder = @@ -456,9 +517,9 @@ FinishRegistrationSteps _finishRegistration( .rpId(identity.getId()) .allowOriginPort(allowOriginPort) .allowOriginSubdomain(allowOriginSubdomain) - .allowUnrequestedExtensions(allowUnrequestedExtensions) .allowUntrustedAttestation(allowUntrustedAttestation) - .metadataService(metadataService) + .attestationTrustSource(attestationTrustSource) + .clock(clock) .build(); } @@ -533,7 +594,6 @@ FinishAssertionSteps _finishAssertion( .credentialRepository(credentialRepository) .allowOriginPort(allowOriginPort) .allowOriginSubdomain(allowOriginSubdomain) - .allowUnrequestedExtensions(allowUnrequestedExtensions) .validateSignatureCounter(validateSignatureCounter) .build(); } @@ -546,7 +606,7 @@ public static class RelyingPartyBuilder { private @NonNull Optional appId = Optional.empty(); private @NonNull Optional attestationConveyancePreference = Optional.empty(); - private @NonNull Optional metadataService = Optional.empty(); + private @NonNull Optional attestationTrustSource = Optional.empty(); public static class MandatoryStages { private final RelyingPartyBuilder builder = new RelyingPartyBuilder(); @@ -645,7 +705,8 @@ public RelyingPartyBuilder appId(@NonNull AppId appId) { * *

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

By default, this is not set. * @@ -668,7 +729,8 @@ public RelyingPartyBuilder attestationConveyancePreference( * *

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

By default, this is not set. * @@ -682,9 +744,9 @@ public RelyingPartyBuilder attestationConveyancePreference( } /** - * A {@link MetadataService} instance to use for looking up device attestation metadata. This - * matters only if {@link #getAttestationConveyancePreference()} is non-empty and not set to - * {@link AttestationConveyancePreference#NONE}. + * An {@link AttestationTrustSource} instance to use for looking up trust roots for + * authenticator attestation. This matters only if {@link #getAttestationConveyancePreference()} + * is non-empty and not set to {@link AttestationConveyancePreference#NONE}. * *

By default, this is not set. * @@ -692,15 +754,16 @@ public RelyingPartyBuilder attestationConveyancePreference( * @see §6.4. * Attestation */ - public RelyingPartyBuilder metadataService(@NonNull Optional metadataService) { - this.metadataService = metadataService; + public RelyingPartyBuilder attestationTrustSource( + @NonNull Optional attestationTrustSource) { + this.attestationTrustSource = attestationTrustSource; return this; } /** - * A {@link MetadataService} instance to use for looking up device attestation metadata. This - * matters only if {@link #getAttestationConveyancePreference()} is non-empty and not set to - * {@link AttestationConveyancePreference#NONE}. + * An {@link AttestationTrustSource} instance to use for looking up trust roots for + * authenticator attestation. This matters only if {@link #getAttestationConveyancePreference()} + * is non-empty and not set to {@link AttestationConveyancePreference#NONE}. * *

By default, this is not set. * @@ -708,8 +771,9 @@ public RelyingPartyBuilder metadataService(@NonNull Optional me * @see §6.4. * Attestation */ - public RelyingPartyBuilder metadataService(@NonNull MetadataService metadataService) { - return this.metadataService(Optional.of(metadataService)); + public RelyingPartyBuilder attestationTrustSource( + @NonNull AttestationTrustSource attestationTrustSource) { + return this.attestationTrustSource(Optional.of(attestationTrustSource)); } } } 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 986061eb2..34d961bae 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 @@ -109,7 +109,7 @@ private static PublicKey importCoseRsaPublicKey(CBORObject cose) new RSAPublicKeySpec( new BigInteger(1, cose.get(CBORObject.FromObject(-1)).GetByteString()), new BigInteger(1, cose.get(CBORObject.FromObject(-2)).GetByteString())); - return Crypto.getKeyFactory("RSA").generatePublic(spec); + return KeyFactory.getInstance("RSA").generatePublic(spec); } private static ECPublicKey importCoseP256PublicKey(CBORObject cose) throws CoseException { @@ -136,7 +136,7 @@ private static PublicKey importCoseEd25519PublicKey(CBORObject cose) .concat(new ByteArray(new byte[] {0x03, (byte) (rawKey.size() + 1), 0})) .concat(rawKey); - KeyFactory kFact = Crypto.getKeyFactory("EdDSA"); + KeyFactory kFact = KeyFactory.getInstance("EdDSA"); return kFact.generatePublic(new X509EncodedKeySpec(x509Key.getBytes())); } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/attestation/AttestationTrustSource.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/attestation/AttestationTrustSource.java new file mode 100644 index 000000000..cc9fc17e9 --- /dev/null +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/attestation/AttestationTrustSource.java @@ -0,0 +1,125 @@ +// Copyright (c) 2018, Yubico AB +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package com.yubico.webauthn.attestation; + +import com.yubico.internal.util.CollectionUtil; +import com.yubico.webauthn.data.ByteArray; +import java.security.cert.CertStore; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import lombok.Builder; +import lombok.NonNull; +import lombok.Value; + +/** Abstraction of a repository which can look up trust roots for authenticator attestation. */ +public interface AttestationTrustSource { + + /** + * Attempt to look up attestation trust roots for an authenticator. + * + *

Note that it is possible for the same trust root to be used for different certificate + * chains. For example, an authenticator vendor may make two different authenticator models, each + * with its own attestation leaf certificate but both signed by the same attestation root + * certificate. If a Relying Party trusts one of those authenticators models but not the other, + * then its implementation of this method MUST return an empty set for the untrusted certificate + * chain. + * + * @param attestationCertificateChain the attestation certificate chain for the authenticator. + * @param aaguid the AAGUID of the authenticator, if available. + * @return A set of attestation root certificates trusted to attest for this authenticator, if any + * are available. If no trust roots are found, or if this authenticator is not trusted, return + * an empty result. Implementations MAY reuse the same result object, or parts of it, for + * multiple calls of this method, even with different arguments, but MUST return an empty set + * of trust roots for authenticators that should not be trusted. + */ + TrustRootsResult findTrustRoots( + List attestationCertificateChain, Optional aaguid); + + /** + * A result of looking up attestation trust roots for a particular attestation statement. This + * primarily consists of a set of trust root certificates, but may also include a {@link + * CertStore} of additional CRLs and/or intermediate certificate to use during certificate path + * validation, and may also disable certificate revocation checking for the relevant attestation + * statement. + */ + @Value + @Builder(toBuilder = true) + class TrustRootsResult { + + /** + * A set of attestation root certificates trusted to certify the relevant attestation statement. + * If the attestation statement is not trusted, or if no trust roots were found, this should be + * an empty set. + */ + @NonNull private final Set trustRoots; + + /** + * A {@link CertStore} of additional CRLs and/or intermediate certificates to use during + * certificate path validation, if any. This will not be used if {@link + * TrustRootsResultBuilder#trustRoots(Set) trustRoots} is empty. + * + *

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

The default is null. + */ + @Builder.Default private final CertStore certStore = null; + + /** + * Whether certificate revocation should be checked during certificate path validation. + * + *

The default is true. + */ + @Builder.Default private final boolean enableRevocationChecking = true; + + private TrustRootsResult( + @NonNull Set trustRoots, + CertStore certStore, + boolean enableRevocationChecking) { + this.trustRoots = CollectionUtil.immutableSet(trustRoots); + this.certStore = certStore; + this.enableRevocationChecking = enableRevocationChecking; + } + + public Optional getCertStore() { + return Optional.ofNullable(certStore); + } + + public static TrustRootsResultBuilder.Step1 builder() { + return new TrustRootsResultBuilder.Step1(); + } + + public static class TrustRootsResultBuilder { + public static class Step1 { + public TrustRootsResultBuilder trustRoots(@NonNull Set trustRoots) { + return new TrustRootsResultBuilder().trustRoots(trustRoots); + } + } + } + } +} diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/attestation/MetadataService.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/attestation/MetadataService.java deleted file mode 100644 index d0593d0d4..000000000 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/attestation/MetadataService.java +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (c) 2018, Yubico AB -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, this -// list of conditions and the following disclaimer. -// -// 2. Redistributions in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -package com.yubico.webauthn.attestation; - -import java.security.cert.CertificateEncodingException; -import java.security.cert.X509Certificate; -import java.util.List; - -/** - * Abstraction of a repository which can look up authenticator attestation metadata from an - * attestation certificate chain. - */ -public interface MetadataService { - - /** - * Attempt to look up attestation for a chain of certificates - * - * @param attestationCertificateChain a certificate chain, where each certificate in the list - * should be signed by the following certificate. - * @return Attestation metadata, if any is available. If the certificate chain is empty, or if - * there is no signature path from a trusted attestation root to the first certificate in - * attestationCertificateChange, return {@link Attestation#empty()}. - */ - Attestation getAttestation(List attestationCertificateChain) - throws CertificateEncodingException; -} diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/attestation/Transport.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/attestation/Transport.java deleted file mode 100644 index 0154b76c2..000000000 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/attestation/Transport.java +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright (c) 2018, Yubico AB -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, this -// list of conditions and the following disclaimer. -// -// 2. Redistributions in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -package com.yubico.webauthn.attestation; - -import java.util.Arrays; -import java.util.EnumSet; -import java.util.Set; - -/** Representations of communication modes supported by an authenticator. */ -public enum Transport { - /** The authenticator supports communication via classic Bluetooth. */ - BT_CLASSIC(1), - - /** The authenticator supports communication via Bluetooth Low Energy (BLE). */ - BLE(2), - - /** The authenticator supports communication via USB. */ - USB(4), - - /** The authenticator supports communication via Near Field Communication (NFC). */ - NFC(8), - - /** The authenticator supports communication via Lightning. */ - LIGHTNING(16); - - private final int bitpos; - - Transport(int bitpos) { - this.bitpos = bitpos; - } - - public static Set fromInt(int bits) { - EnumSet transports = EnumSet.noneOf(Transport.class); - for (Transport transport : Transport.values()) { - if ((transport.bitpos & bits) != 0) { - transports.add(transport); - } - } - - return transports; - } - - public static int toInt(Iterable transports) { - int transportsInt = 0; - for (Transport transport : transports) { - transportsInt |= transport.bitpos; - } - return transportsInt; - } - - public static int toInt(Transport... transports) { - return toInt(Arrays.asList(transports)); - } -} diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AttestationConveyancePreference.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AttestationConveyancePreference.java index 1d0034855..442849945 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AttestationConveyancePreference.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AttestationConveyancePreference.java @@ -25,9 +25,7 @@ package com.yubico.webauthn.data; import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import com.yubico.internal.util.json.JsonStringSerializable; -import com.yubico.internal.util.json.JsonStringSerializer; +import com.fasterxml.jackson.annotation.JsonValue; import java.util.Optional; import java.util.stream.Stream; import lombok.AccessLevel; @@ -42,9 +40,8 @@ * @see §5.4.6. * Attestation Conveyance Preference Enumeration (enum AttestationConveyancePreference) */ -@JsonSerialize(using = JsonStringSerializer.class) @AllArgsConstructor(access = AccessLevel.PRIVATE) -public enum AttestationConveyancePreference implements JsonStringSerializable { +public enum AttestationConveyancePreference { /** * Indicates that the Relying Party is not interested in authenticator attestation. @@ -78,7 +75,7 @@ public enum AttestationConveyancePreference implements JsonStringSerializable { */ DIRECT("direct"); - @Getter @NonNull private final String value; + @JsonValue @Getter @NonNull private final String value; private static Optional fromString(@NonNull String value) { return Stream.of(values()).filter(v -> v.value.equals(value)).findAny(); @@ -94,11 +91,4 @@ private static AttestationConveyancePreference fromJsonString(@NonNull String va "Unknown %s value: %s", AttestationConveyancePreference.class.getSimpleName(), value))); } - - @Override - @Deprecated - /** @deprecated Use {@link #getValue()} instead. */ - public String toJsonString() { - return value; - } } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AttestationType.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AttestationType.java index ada006c6c..84c295ba3 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AttestationType.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AttestationType.java @@ -100,25 +100,6 @@ public enum AttestationType { */ ANONYMIZATION_CA, - /** - * In this case, the Authenticator receives direct anonymous attestation (DAA) credentials from a - * single DAA-Issuer. These DAA credentials are used along with blinding to sign the attested - * credential data. The concept of blinding avoids the DAA credentials being misused as global - * correlation handle. WebAuthn supports DAA using elliptic curve cryptography and bilinear - * pairings, called ECDAA. See the FIDO - * ECDAA Algorithm for details. - * - * @see Elliptic Curve based - * Direct Anonymous Attestation (ECDAA) - * @see FIDO - * ECDAA Algorithm - * @deprecated ECDAA was removed in WebAuthn Level 2. - */ - @Deprecated - ECDAA, - /** * In this case, no attestation information is available. See also §8.7 None diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorAttachment.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorAttachment.java index af65ecd70..96c666659 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorAttachment.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorAttachment.java @@ -25,9 +25,7 @@ package com.yubico.webauthn.data; import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import com.yubico.internal.util.json.JsonStringSerializable; -import com.yubico.internal.util.json.JsonStringSerializer; +import com.fasterxml.jackson.annotation.JsonValue; import java.util.Optional; import java.util.stream.Stream; import lombok.AllArgsConstructor; @@ -52,9 +50,8 @@ * href="https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#enumdef-authenticatorattachment">§5.4.5. * Authenticator Attachment Enumeration (enum AuthenticatorAttachment) */ -@JsonSerialize(using = JsonStringSerializer.class) @AllArgsConstructor -public enum AuthenticatorAttachment implements JsonStringSerializable { +public enum AuthenticatorAttachment { /** * Indicates fromString(@NonNull String value) { return Stream.of(values()).filter(v -> v.value.equals(value)).findAny(); @@ -90,11 +87,4 @@ private static AuthenticatorAttachment fromJsonString(@NonNull String value) { "Unknown %s value: %s", AuthenticatorAttachment.class.getSimpleName(), value))); } - - @Override - @Deprecated - /** @deprecated Use {@link #getValue()} instead. */ - public String toJsonString() { - return value; - } } 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 230185eb7..ec3566bd0 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 @@ -53,7 +53,8 @@ public class AuthenticatorSelectionCriteria { * Specifies the extent to which the Relying Party desires to create a client-side discoverable * credential. For historical reasons the naming retains the deprecated “resident” terminology. * - *

The default is {@link ResidentKeyRequirement#DISCOURAGED}. + *

By default, this is not set. When not set, the default in the browser is {@link + * ResidentKeyRequirement#DISCOURAGED}. * * @see ResidentKeyRequirement * @see user * verification for the navigator.credentials.create() operation. Eligible * authenticators are filtered to only those capable of satisfying this requirement. + * + *

By default, this is not set. When not set, the default in the browser is {@link + * UserVerificationRequirement#PREFERRED}. + * + * @see UserVerificationRequirement + * @see §5.8.6. + * User Verification Requirement Enumeration (enum UserVerificationRequirement) + * @see User + * Verification */ private UserVerificationRequirement userVerification; @@ -83,17 +94,42 @@ public Optional getAuthenticatorAttachment() { } /** - * This member is retained for backwards compatibility with WebAuthn Level 1 and, for historical - * reasons, its naming retains the deprecated “resident” terminology for discoverable credentials. + * Specifies the extent to which the Relying Party desires to create a client-side discoverable + * credential. For historical reasons the naming retains the deprecated “resident” terminology. + * + *

By default, this is not set. When not set, the default in the browser is {@link + * ResidentKeyRequirement#DISCOURAGED}. + * + * @see ResidentKeyRequirement + * @see §5.4.6. + * Resident Key Requirement Enumeration (enum ResidentKeyRequirement) + * @see Client-side + * discoverable Credential + */ + public Optional getResidentKey() { + return Optional.ofNullable(residentKey); + } + + /** + * Describes the Relying Party's requirements regarding user + * verification for the navigator.credentials.create() operation. Eligible + * authenticators are filtered to only those capable of satisfying this requirement. + * + *

By default, this is not set. When not set, the default in the browser is {@link + * UserVerificationRequirement#PREFERRED}. * - * @return true if and only if {@link #getResidentKey()} is {@link - * ResidentKeyRequirement#REQUIRED}. - * @see #getResidentKey() - * @deprecated Use {@link #getResidentKey()} instead. + * @see UserVerificationRequirement + * @see §5.8.6. + * User Verification Requirement Enumeration (enum UserVerificationRequirement) + * @see User + * Verification */ - @Deprecated - public boolean isRequireResidentKey() { - return residentKey == ResidentKeyRequirement.REQUIRED; + public Optional getUserVerification() { + return Optional.ofNullable(userVerification); } @JsonCreator @@ -104,15 +140,16 @@ private AuthenticatorSelectionCriteria( @JsonProperty("userVerification") UserVerificationRequirement userVerification) { this.authenticatorAttachment = authenticatorAttachment; - if (residentKey == null && requireResidentKey != null) { + if (residentKey != null) { + this.residentKey = residentKey; + } else if (requireResidentKey != null) { this.residentKey = requireResidentKey ? ResidentKeyRequirement.REQUIRED : ResidentKeyRequirement.DISCOURAGED; } else { - this.residentKey = residentKey == null ? ResidentKeyRequirement.DISCOURAGED : residentKey; + this.residentKey = null; } - this.userVerification = - userVerification == null ? UserVerificationRequirement.PREFERRED : userVerification; + this.userVerification = userVerification; } /** For use by the builder. */ @@ -148,27 +185,5 @@ public AuthenticatorSelectionCriteriaBuilder authenticatorAttachment( this.authenticatorAttachment = authenticatorAttachment; return this; } - - /** - * This member is retained for backwards compatibility with WebAuthn Level 1 and, for historical - * reasons, its naming retains the deprecated “resident” terminology for discoverable - * credentials. - * - *

requireResidentKey(true) is an alias of residentKey(REQUIRED) - * . - * - *

requireResidentKey(false) is an alias of residentKey(DISCOURAGED) - * . - * - * @deprecated Use {@link #residentKey(ResidentKeyRequirement) residentKey} instead. - * @see #residentKey(ResidentKeyRequirement) - */ - @Deprecated - public AuthenticatorSelectionCriteriaBuilder requireResidentKey(boolean requireResidentKey) { - return residentKey( - requireResidentKey - ? ResidentKeyRequirement.REQUIRED - : ResidentKeyRequirement.DISCOURAGED); - } } } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorTransport.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorTransport.java index ee111974d..c8f47b154 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorTransport.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorTransport.java @@ -25,10 +25,7 @@ package com.yubico.webauthn.data; import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import com.yubico.internal.util.json.JsonStringSerializable; -import com.yubico.internal.util.json.JsonStringSerializer; -import com.yubico.webauthn.attestation.Transport; +import com.fasterxml.jackson.annotation.JsonValue; import java.util.stream.Stream; import lombok.AccessLevel; import lombok.AllArgsConstructor; @@ -55,13 +52,11 @@ * href="https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#enumdef-authenticatortransport">§5.10.4. * Authenticator Transport Enumeration (enum AuthenticatorTransport) */ -@JsonSerialize(using = JsonStringSerializer.class) @Value @AllArgsConstructor(access = AccessLevel.PRIVATE) -public class AuthenticatorTransport - implements Comparable, JsonStringSerializable { +public class AuthenticatorTransport implements Comparable { - @NonNull private final String id; + @JsonValue @NonNull private final String id; /** Indicates the respective authenticator can be contacted over removable USB. */ public static final AuthenticatorTransport USB = new AuthenticatorTransport("usb"); @@ -127,37 +122,6 @@ public static AuthenticatorTransport valueOf(String name) { } } - /** - * Convert a {@link Transport} from U2F metadata to a WebAuthn {@link AuthenticatorTransport} - * value. - * - * @throws IllegalArgumentException if transport has an unknown value. - */ - public static AuthenticatorTransport fromU2fTransport(Transport transport) { - switch (transport) { - case BT_CLASSIC: - case BLE: - return BLE; - - case USB: - case LIGHTNING: - return USB; - - case NFC: - return NFC; - - default: - throw new IllegalArgumentException("Unknown transport: " + transport); - } - } - - @Override - @Deprecated - /** @deprecated Use {@link #getId()} instead. */ - public String toJsonString() { - return id; - } - @Override public int compareTo(AuthenticatorTransport other) { return id.compareTo(other.id); diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/ByteArray.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/ByteArray.java index ffe494177..7899c39be 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/ByteArray.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/ByteArray.java @@ -25,11 +25,9 @@ package com.yubico.webauthn.data; import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.annotation.JsonValue; import com.google.common.primitives.Bytes; import com.yubico.internal.util.BinaryUtil; -import com.yubico.internal.util.json.JsonStringSerializable; -import com.yubico.internal.util.json.JsonStringSerializer; import com.yubico.webauthn.data.exception.Base64UrlException; import com.yubico.webauthn.data.exception.HexException; import java.util.Base64; @@ -38,10 +36,9 @@ import lombok.ToString; /** An immutable byte array with support for encoding/decoding to/from various encodings. */ -@JsonSerialize(using = JsonStringSerializer.class) @EqualsAndHashCode @ToString(includeFieldNames = false, onlyExplicitlyIncluded = true) -public final class ByteArray implements Comparable, JsonStringSerializable { +public final class ByteArray implements Comparable { private static final Base64.Encoder BASE64_ENCODER = Base64.getEncoder(); private static final Base64.Decoder BASE64_DECODER = Base64.getDecoder(); @@ -51,7 +48,7 @@ public final class ByteArray implements Comparable, JsonStringSeriali @NonNull private final byte[] bytes; - @NonNull private final String base64url; + @JsonValue @NonNull private final String base64url; /** Create a new instance by copying the contents of bytes. */ public ByteArray(@NonNull byte[] bytes) { @@ -112,34 +109,35 @@ public int size() { return this.bytes.length; } - /** @return a copy of the raw byte contents. */ + /** + * @return a copy of the raw byte contents. + */ public byte[] getBytes() { return BinaryUtil.copy(bytes); } - /** @return the content bytes encoded as classic Base64 data. */ + /** + * @return the content bytes encoded as classic Base64 data. + */ public String getBase64() { return BASE64_ENCODER.encodeToString(bytes); } - /** @return the content bytes encoded as Base64Url data, without padding. */ + /** + * @return the content bytes encoded as Base64Url data, without padding. + */ public String getBase64Url() { return base64url; } - /** @return the content bytes encoded as hexadecimal data. */ + /** + * @return the content bytes encoded as hexadecimal data. + */ @ToString.Include public String getHex() { return BinaryUtil.toHex(bytes); } - @Override - @Deprecated - /** @deprecated Use {@link #getBase64Url()} instead. */ - public String toJsonString() { - return base64url; - } - @Override public int compareTo(ByteArray other) { if (bytes.length != other.bytes.length) { 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 ec51c54b5..1ba31d5ca 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 @@ -25,9 +25,7 @@ package com.yubico.webauthn.data; import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import com.yubico.internal.util.json.JsonLongSerializable; -import com.yubico.internal.util.json.JsonLongSerializer; +import com.fasterxml.jackson.annotation.JsonValue; import java.util.Optional; import java.util.stream.Stream; import lombok.Getter; @@ -41,14 +39,13 @@ * href="https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#typedefdef-cosealgorithmidentifier">§5.10.5. * Cryptographic Algorithm Identifier (typedef COSEAlgorithmIdentifier) */ -@JsonSerialize(using = JsonLongSerializer.class) -public enum COSEAlgorithmIdentifier implements JsonLongSerializable { +public enum COSEAlgorithmIdentifier { EdDSA(-8), ES256(-7), RS256(-257), RS1(-65535); - @Getter private final long id; + @JsonValue @Getter private final long id; COSEAlgorithmIdentifier(long id) { this.id = id; @@ -64,11 +61,4 @@ private static COSEAlgorithmIdentifier fromJson(long id) { .orElseThrow( () -> new IllegalArgumentException("Unknown COSE algorithm identifier: " + id)); } - - @Override - @Deprecated - /** @deprecated Use {@link #getId()} instead. */ - public long toJsonNumber() { - return id; - } } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/ClientAssertionExtensionOutputs.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/ClientAssertionExtensionOutputs.java index 2cca28687..3c6579d66 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/ClientAssertionExtensionOutputs.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/ClientAssertionExtensionOutputs.java @@ -25,6 +25,7 @@ package com.yubico.webauthn.data; import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import java.util.HashSet; import java.util.Optional; @@ -49,6 +50,7 @@ */ @Value @Builder(toBuilder = true) +@JsonIgnoreProperties(ignoreUnknown = true) public class ClientAssertionExtensionOutputs implements ClientExtensionOutputs { /** diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/ClientRegistrationExtensionOutputs.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/ClientRegistrationExtensionOutputs.java index a0d087d4d..83525e942 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/ClientRegistrationExtensionOutputs.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/ClientRegistrationExtensionOutputs.java @@ -33,7 +33,6 @@ import lombok.Builder; import lombok.EqualsAndHashCode; import lombok.Value; -import lombok.extern.slf4j.Slf4j; /** * Contains Note: CTAP authenticators only support largeBlob in combination with - * {@link AuthenticatorSelectionCriteria#isRequireResidentKey()} set to true in + * {@link AuthenticatorSelectionCriteria#getResidentKey()} set to REQUIRED in * {@link StartRegistrationOptions#getAuthenticatorSelection()}. * * @see Note: CTAP authenticators only support largeBlob in combination with - * {@link AuthenticatorSelectionCriteria#isRequireResidentKey()} set to true in + * {@link AuthenticatorSelectionCriteria#getResidentKey()} set to REQUIRED in * {@link StartRegistrationOptions#getAuthenticatorSelection()}. * * @see navigator.credentials.create(). @@ -48,6 +54,7 @@ * href="https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#dictdef-publickeycredentialcreationoptions">§5.4. * Options for Credential Creation (dictionary PublicKeyCredentialCreationOptions) */ +@Slf4j @Value @Builder(toBuilder = true) public class PublicKeyCredentialCreationOptions { @@ -120,6 +127,7 @@ public class PublicKeyCredentialCreationOptions { */ @NonNull private final RegistrationExtensionInputs extensions; + @Builder @JsonCreator private PublicKeyCredentialCreationOptions( @NonNull @JsonProperty("rp") RelyingPartyIdentity rp, @@ -135,7 +143,7 @@ private PublicKeyCredentialCreationOptions( this.rp = rp; this.user = user; this.challenge = challenge; - this.pubKeyCredParams = CollectionUtil.immutableList(pubKeyCredParams); + this.pubKeyCredParams = filterAvailableAlgorithms(pubKeyCredParams); this.timeout = timeout; this.excludeCredentials = excludeCredentials == null @@ -351,4 +359,73 @@ public PublicKeyCredentialCreationOptionsBuilder authenticatorSelection( return this; } } + + /* + * Essentially a copy of RelyingParty.filterAvailableAlgorithms(List) because that method and WebAuthnCodecs are not visible here. + */ + private static List filterAvailableAlgorithms( + List pubKeyCredParams) { + return Collections.unmodifiableList( + pubKeyCredParams.stream() + .filter( + param -> { + try { + switch (param.getAlg()) { + case EdDSA: + KeyFactory.getInstance("EdDSA"); + break; + + case ES256: + KeyFactory.getInstance("EC"); + break; + + case RS256: + case RS1: + KeyFactory.getInstance("RSA"); + break; + + default: + log.warn( + "Unknown algorithm: {}. Please file a bug report.", param.getAlg()); + } + } catch (NoSuchAlgorithmException e) { + log.warn( + "Unsupported algorithm in PublicKeyCredentialCreationOptions.pubKeyCredParams: {}. No KeyFactory available; registrations with this key algorithm will fail. You may need to add a dependency and load a provider using java.security.Security.addProvider().", + param.getAlg()); + return false; + } + + try { + switch (param.getAlg()) { + case EdDSA: + Signature.getInstance("EDDSA"); + break; + + case ES256: + Signature.getInstance("SHA256withECDSA"); + break; + + case RS256: + Signature.getInstance("SHA256withRSA"); + break; + + case RS1: + Signature.getInstance("SHA1withRSA"); + break; + + default: + log.warn( + "Unknown algorithm: {}. Please file a bug report.", param.getAlg()); + } + } catch (NoSuchAlgorithmException e) { + log.warn( + "Unsupported algorithm in PublicKeyCredentialCreationOptions.pubKeyCredParams: {}. No Signature available; registrations with this key algorithm will fail. You may need to add a dependency and load a provider using java.security.Security.addProvider().", + param.getAlg()); + return false; + } + + return true; + }) + .collect(Collectors.toList())); + } } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialEntity.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialEntity.java index 4e9f31930..b6e260f7a 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialEntity.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialEntity.java @@ -24,9 +24,6 @@ package com.yubico.webauthn.data; -import java.net.URL; -import java.util.Optional; - /** * Describes a user account, or a WebAuthn Relying Party, which a public key credential is * associated with or scoped to, respectively. @@ -82,19 +79,4 @@ public interface PublicKeyCredentialEntity { * @see RFC 8265 */ String getName(); - - /** - * A serialized URL which resolves to an image associated with the entity. - * - *

For example, this could be a user's avatar or a Relying Party's logo. This URL MUST be an a - * priori authenticated URL. Authenticators MUST accept and store a 128-byte minimum length for an - * icon member's value. Authenticators MAY ignore an icon member's value if its length is greater - * than 128 bytes. The URL's scheme MAY be "data" to avoid fetches of the URL, at the cost of - * needing more storage. - * - * @deprecated The icon field has been removed from WebAuthn Level 2. This method - * will be removed in the next major version of this library. - */ - @Deprecated - Optional getIcon(); // TODO v2.0: delete this } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialRequestOptions.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialRequestOptions.java index 63dd9d1b5..d5a8baec9 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialRequestOptions.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialRequestOptions.java @@ -86,10 +86,18 @@ public class PublicKeyCredentialRequestOptions { * verification for the navigator.credentials.get() operation. * *

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

By default, this is not set. When not set, the default in the browser is {@link + * UserVerificationRequirement#PREFERRED}. + * + * @see UserVerificationRequirement + * @see §5.8.6. + * User Verification Requirement Enumeration (enum UserVerificationRequirement) + * @see User + * Verification */ - @NonNull @Builder.Default - private final UserVerificationRequirement userVerification = - UserVerificationRequirement.PREFERRED; + private final UserVerificationRequirement userVerification; /** * Additional parameters requesting additional processing by the client and authenticator. @@ -106,7 +114,7 @@ private PublicKeyCredentialRequestOptions( @JsonProperty("timeout") Long timeout, @JsonProperty("rpId") String rpId, @JsonProperty("allowCredentials") List allowCredentials, - @NonNull @JsonProperty("userVerification") UserVerificationRequirement userVerification, + @JsonProperty("userVerification") UserVerificationRequirement userVerification, @NonNull @JsonProperty("extensions") AssertionExtensionInputs extensions) { this.challenge = challenge; this.timeout = timeout; @@ -125,6 +133,10 @@ public Optional> getAllowCredentials() { return Optional.ofNullable(allowCredentials); } + public Optional getUserVerification() { + return Optional.ofNullable(userVerification); + } + /** * Serialize this {@link PublicKeyCredentialRequestOptions} value to JSON suitable for sending to * the client. diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialType.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialType.java index 33498820b..7a480eea2 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialType.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialType.java @@ -25,9 +25,7 @@ package com.yubico.webauthn.data; import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import com.yubico.internal.util.json.JsonStringSerializable; -import com.yubico.internal.util.json.JsonStringSerializer; +import com.fasterxml.jackson.annotation.JsonValue; import java.util.Optional; import java.util.stream.Stream; import lombok.AllArgsConstructor; @@ -47,12 +45,11 @@ * href="https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#enumdef-publickeycredentialtype">§5.10.2. * Credential Type Enumeration (enum PublicKeyCredentialType) */ -@JsonSerialize(using = JsonStringSerializer.class) @AllArgsConstructor -public enum PublicKeyCredentialType implements JsonStringSerializable { +public enum PublicKeyCredentialType { PUBLIC_KEY("public-key"); - @Getter @NonNull private final String id; + @JsonValue @Getter @NonNull private final String id; private static Optional fromString(@NonNull String id) { return Stream.of(values()).filter(v -> v.id.equals(id)).findAny(); @@ -68,11 +65,4 @@ private static PublicKeyCredentialType fromJsonString(@NonNull String id) { "Unknown %s value: %s", PublicKeyCredentialType.class.getSimpleName(), id))); } - - @Override - @Deprecated - /** @deprecated Use {@link #getId()} instead. */ - public String toJsonString() { - return id; - } } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/RelyingPartyIdentity.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/RelyingPartyIdentity.java index 865c0d280..d299c3804 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/RelyingPartyIdentity.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/RelyingPartyIdentity.java @@ -26,8 +26,6 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; -import java.net.URL; -import java.util.Optional; import lombok.Builder; import lombok.Getter; import lombok.NonNull; @@ -67,28 +65,11 @@ public class RelyingPartyIdentity implements PublicKeyCredentialEntity { */ @NonNull private final String id; - /** - * A URL which resolves to an image associated with the entity. For example, this could be the - * Relying Party's logo. - * - *

This URL MUST be an a priori authenticated URL. Authenticators MUST accept and store a - * 128-byte minimum length for an icon member’s value. Authenticators MAY ignore an icon member’s - * value if its length is greater than 128 bytes. The URL’s scheme MAY be "data" to avoid fetches - * of the URL, at the cost of needing more storage. - * - * @deprecated The icon property has been removed from WebAuthn Level 2. This field - * will be removed in the next major version of the library. - */ - @Deprecated private final URL icon; // TODO v2.0: delete this - @JsonCreator private RelyingPartyIdentity( - @NonNull @JsonProperty("name") String name, - @NonNull @JsonProperty("id") String id, - @JsonProperty("icon") URL icon) { + @NonNull @JsonProperty("name") String name, @NonNull @JsonProperty("id") String id) { this.name = name; this.id = id; - this.icon = icon; } public static RelyingPartyIdentityBuilder.MandatoryStages builder() { @@ -96,7 +77,6 @@ public static RelyingPartyIdentityBuilder.MandatoryStages builder() { } public static class RelyingPartyIdentityBuilder { - private URL icon = null; public static class MandatoryStages { private RelyingPartyIdentityBuilder builder = new RelyingPartyIdentityBuilder(); @@ -131,50 +111,5 @@ public RelyingPartyIdentityBuilder name(String name) { } } } - - /** - * A URL which resolves to an image associated with the entity. For example, this could be the - * Relying Party's logo. - * - *

This URL MUST be an a priori authenticated URL. Authenticators MUST accept and store a - * 128-byte minimum length for an icon member’s value. Authenticators MAY ignore an icon - * member’s value if its length is greater than 128 bytes. The URL’s scheme MAY be "data" to - * avoid fetches of the URL, at the cost of needing more storage. - * - * @deprecated The icon property has been removed from WebAuthn Level 2. This - * method will be removed in the next major version of the library. - */ - @Deprecated - public RelyingPartyIdentityBuilder icon(@NonNull Optional icon) { - return this.icon(icon.orElse(null)); - } - - /** - * A URL which resolves to an image associated with the entity. For example, this could be the - * Relying Party's logo. - * - *

This URL MUST be an a priori authenticated URL. Authenticators MUST accept and store a - * 128-byte minimum length for an icon member’s value. Authenticators MAY ignore an icon - * member’s value if its length is greater than 128 bytes. The URL’s scheme MAY be "data" to - * avoid fetches of the URL, at the cost of needing more storage. - * - * @deprecated The icon property has been removed from WebAuthn Level 2. This - * method will be removed in the next major version of the library. - */ - @Deprecated - public RelyingPartyIdentityBuilder icon(URL icon) { - this.icon = icon; - return this; - } - } - - /** - * @deprecated The icon property has been removed from WebAuthn Level 2. This method - * will be removed in the next major version of the library. - */ - @Deprecated - @Override - public Optional getIcon() { - return Optional.ofNullable(icon); } } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/ResidentKeyRequirement.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/ResidentKeyRequirement.java index 2a7426a9a..0af26831d 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/ResidentKeyRequirement.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/ResidentKeyRequirement.java @@ -25,9 +25,7 @@ package com.yubico.webauthn.data; import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import com.yubico.internal.util.json.JsonStringSerializable; -import com.yubico.internal.util.json.JsonStringSerializer; +import com.fasterxml.jackson.annotation.JsonValue; import java.util.Optional; import java.util.stream.Stream; import lombok.AllArgsConstructor; @@ -45,9 +43,8 @@ * href="https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#client-side-discoverable-credential">Client-side * discoverable Credential */ -@JsonSerialize(using = JsonStringSerializer.class) @AllArgsConstructor -public enum ResidentKeyRequirement implements JsonStringSerializable { +public enum ResidentKeyRequirement { /** * The client and authenticator will try to create a server-side credential if possible, and a @@ -97,7 +94,7 @@ public enum ResidentKeyRequirement implements JsonStringSerializable { */ REQUIRED("required"); - @Getter @NonNull private final String value; + @JsonValue @Getter @NonNull private final String value; private static Optional fromString(@NonNull String value) { return Stream.of(values()).filter(v -> v.value.equals(value)).findAny(); @@ -113,11 +110,4 @@ private static ResidentKeyRequirement fromJsonString(@NonNull String value) { "Unknown %s value: %s", ResidentKeyRequirement.class.getSimpleName(), value))); } - - @Override - @Deprecated - /** @deprecated Use {@link #getValue()} instead. */ - public String toJsonString() { - return value; - } } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/TokenBindingStatus.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/TokenBindingStatus.java index 2b177aa63..1e499751b 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/TokenBindingStatus.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/TokenBindingStatus.java @@ -25,9 +25,7 @@ package com.yubico.webauthn.data; import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import com.yubico.internal.util.json.JsonStringSerializable; -import com.yubico.internal.util.json.JsonStringSerializer; +import com.fasterxml.jackson.annotation.JsonValue; import java.util.Arrays; import java.util.Optional; import lombok.AllArgsConstructor; @@ -44,8 +42,7 @@ * @see TokenBindingInfo */ @AllArgsConstructor -@JsonSerialize(using = JsonStringSerializer.class) -public enum TokenBindingStatus implements JsonStringSerializable { +public enum TokenBindingStatus { /** * Indicates token binding was used when communicating with the Relying Party. In this case, the @@ -59,20 +56,14 @@ public enum TokenBindingStatus implements JsonStringSerializable { */ SUPPORTED("supported"); - @Getter @NonNull private final String value; + @JsonValue @Getter @NonNull private final String value; private static Optional fromString(@NonNull String value) { return Arrays.stream(values()).filter(v -> v.value.equals(value)).findAny(); } @JsonCreator - @Deprecated - /** - * @deprecated Use - * {@link CollectedClientData#getTokenBinding()}.{@link TokenBindingInfo#getStatus() getStatus()} - * instead. - */ - public static TokenBindingStatus fromJsonString(@NonNull String value) { + static TokenBindingStatus fromJsonString(@NonNull String value) { return fromString(value) .orElseThrow( () -> @@ -80,11 +71,4 @@ public static TokenBindingStatus fromJsonString(@NonNull String value) { String.format( "Unknown %s value: %s", TokenBindingStatus.class.getSimpleName(), value))); } - - @Override - @Deprecated - /** @deprecated Use {@link #getValue()} instead. */ - public String toJsonString() { - return value; - } } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/UserIdentity.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/UserIdentity.java index 2fd2913d4..9f23969a0 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/UserIdentity.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/UserIdentity.java @@ -26,8 +26,6 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; -import java.net.URL; -import java.util.Optional; import lombok.Builder; import lombok.Getter; import lombok.NonNull; @@ -103,30 +101,14 @@ public class UserIdentity implements PublicKeyCredentialEntity { */ @NonNull private final ByteArray id; - /** - * A URL which resolves to an image associated with the entity. For example, this could be the - * user’s avatar. - * - *

This URL MUST be an a priori authenticated URL. Authenticators MUST accept and store a - * 128-byte minimum length for an icon member’s value. Authenticators MAY ignore an icon member’s - * value if its length is greater than 128 bytes. The URL’s scheme MAY be "data" to avoid fetches - * of the URL, at the cost of needing more storage. - * - * @deprecated The icon property has been removed from WebAuthn Level 2. This field - * will be removed in the next major version of the library. - */ - @Deprecated private final URL icon; // TODO v2.0: delete this - @JsonCreator private UserIdentity( @NonNull @JsonProperty("name") String name, @NonNull @JsonProperty("displayName") String displayName, - @NonNull @JsonProperty("id") ByteArray id, - @JsonProperty("icon") URL icon) { + @NonNull @JsonProperty("id") ByteArray id) { this.name = name; this.displayName = displayName; this.id = id; - this.icon = icon; } public static UserIdentityBuilder.MandatoryStages builder() { @@ -134,7 +116,6 @@ public static UserIdentityBuilder.MandatoryStages builder() { } public static class UserIdentityBuilder { - private URL icon = null; public static class MandatoryStages { private UserIdentityBuilder builder = new UserIdentityBuilder(); @@ -172,50 +153,5 @@ public UserIdentityBuilder id(ByteArray id) { } } } - - /** - * A URL which resolves to an image associated with the entity. For example, this could be the - * user’s avatar. - * - *

This URL MUST be an a priori authenticated URL. Authenticators MUST accept and store a - * 128-byte minimum length for an icon member’s value. Authenticators MAY ignore an icon - * member’s value if its length is greater than 128 bytes. The URL’s scheme MAY be "data" to - * avoid fetches of the URL, at the cost of needing more storage. - * - * @deprecated The icon property has been removed from WebAuthn Level 2. This - * method will be removed in the next major version of the library. - */ - @Deprecated - public UserIdentityBuilder icon(@NonNull Optional icon) { - return this.icon(icon.orElse(null)); - } - - /** - * A URL which resolves to an image associated with the entity. For example, this could be the - * user’s avatar. - * - *

This URL MUST be an a priori authenticated URL. Authenticators MUST accept and store a - * 128-byte minimum length for an icon member’s value. Authenticators MAY ignore an icon - * member’s value if its length is greater than 128 bytes. The URL’s scheme MAY be "data" to - * avoid fetches of the URL, at the cost of needing more storage. - * - * @deprecated The icon property has been removed from WebAuthn Level 2. This - * method will be removed in the next major version of the library. - */ - @Deprecated - public UserIdentityBuilder icon(URL icon) { - this.icon = icon; - return this; - } - } - - /** - * @deprecated The icon property has been removed from WebAuthn Level 2. This method - * will be removed in the next major version of the library. - */ - @Deprecated - @Override - public Optional getIcon() { - return Optional.ofNullable(icon); } } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/UserVerificationRequirement.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/UserVerificationRequirement.java index 19b36f265..642f71bf3 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/UserVerificationRequirement.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/UserVerificationRequirement.java @@ -25,9 +25,7 @@ package com.yubico.webauthn.data; import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import com.yubico.internal.util.json.JsonStringSerializable; -import com.yubico.internal.util.json.JsonStringSerializer; +import com.fasterxml.jackson.annotation.JsonValue; import java.util.Optional; import java.util.stream.Stream; import lombok.AllArgsConstructor; @@ -44,9 +42,8 @@ * href="https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#enumdef-userverificationrequirement">§5.10.6. * User Verification Requirement Enumeration (enum UserVerificationRequirement) */ -@JsonSerialize(using = JsonStringSerializer.class) @AllArgsConstructor -public enum UserVerificationRequirement implements JsonStringSerializable { +public enum UserVerificationRequirement { /** * This value indicates that the Relying Party does not want user verification employed during the @@ -67,7 +64,7 @@ public enum UserVerificationRequirement implements JsonStringSerializable { */ REQUIRED("required"); - @Getter @NonNull private final String value; + @JsonValue @Getter @NonNull private final String value; private static Optional fromString(@NonNull String value) { return Stream.of(values()).filter(v -> v.value.equals(value)).findAny(); @@ -83,11 +80,4 @@ private static UserVerificationRequirement fromJsonString(@NonNull String value) "Unknown %s value: %s", UserVerificationRequirement.class.getSimpleName(), value))); } - - @Override - @Deprecated - /** @deprecated Use {@link #getValue()} instead. */ - public String toJsonString() { - return value; - } } diff --git a/webauthn-server-core/src/main/java/com/yubico/fido/metadata/KeyProtectionType.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/extension/uvm/KeyProtectionType.java similarity index 99% rename from webauthn-server-core/src/main/java/com/yubico/fido/metadata/KeyProtectionType.java rename to webauthn-server-core/src/main/java/com/yubico/webauthn/extension/uvm/KeyProtectionType.java index 51c9ddf00..7f4068b1b 100644 --- a/webauthn-server-core/src/main/java/com/yubico/fido/metadata/KeyProtectionType.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/extension/uvm/KeyProtectionType.java @@ -1,4 +1,4 @@ -package com.yubico.fido.metadata; +package com.yubico.webauthn.extension.uvm; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonValue; diff --git a/webauthn-server-core/src/main/java/com/yubico/fido/metadata/MatcherProtectionType.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/extension/uvm/MatcherProtectionType.java similarity index 98% rename from webauthn-server-core/src/main/java/com/yubico/fido/metadata/MatcherProtectionType.java rename to webauthn-server-core/src/main/java/com/yubico/webauthn/extension/uvm/MatcherProtectionType.java index c1e50614e..855a4053c 100644 --- a/webauthn-server-core/src/main/java/com/yubico/fido/metadata/MatcherProtectionType.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/extension/uvm/MatcherProtectionType.java @@ -1,4 +1,4 @@ -package com.yubico.fido.metadata; +package com.yubico.webauthn.extension.uvm; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonValue; diff --git a/webauthn-server-core/src/main/java/com/yubico/fido/metadata/UserVerificationMethod.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/extension/uvm/UserVerificationMethod.java similarity index 99% rename from webauthn-server-core/src/main/java/com/yubico/fido/metadata/UserVerificationMethod.java rename to webauthn-server-core/src/main/java/com/yubico/webauthn/extension/uvm/UserVerificationMethod.java index 9549e9067..e78de418a 100644 --- a/webauthn-server-core/src/main/java/com/yubico/fido/metadata/UserVerificationMethod.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/extension/uvm/UserVerificationMethod.java @@ -1,4 +1,4 @@ -package com.yubico.fido.metadata; +package com.yubico.webauthn.extension.uvm; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonValue; diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/meta/DocumentStatus.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/meta/DocumentStatus.java index 00f969d97..c67c014fc 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/meta/DocumentStatus.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/meta/DocumentStatus.java @@ -24,18 +24,15 @@ package com.yubico.webauthn.meta; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import com.yubico.internal.util.json.JsonStringSerializable; -import com.yubico.internal.util.json.JsonStringSerializer; +import com.fasterxml.jackson.annotation.JsonValue; import java.util.Optional; import java.util.stream.Stream; import lombok.AllArgsConstructor; import lombok.NonNull; /** A representation of Web Authentication specification document statuses. */ -@JsonSerialize(using = JsonStringSerializer.class) @AllArgsConstructor -public enum DocumentStatus implements JsonStringSerializable { +public enum DocumentStatus { /** An editor's draft is a changing work in progress. */ EDITORS_DRAFT("editors-draft"), @@ -51,16 +48,9 @@ public enum DocumentStatus implements JsonStringSerializable { /** A recommendation is a finished and released specification. */ RECOMMENDATION("recommendation"); - private final String id; + @JsonValue private final String id; static Optional fromString(@NonNull String id) { return Stream.of(values()).filter(v -> v.id.equals(id)).findAny(); } - - @Override - @Deprecated - /** @deprecated This will be removed in the next major version release. */ - public String toJsonString() { - return id; - } } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/meta/Specification.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/meta/Specification.java index 6d77e107c..05e645d1a 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/meta/Specification.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/meta/Specification.java @@ -24,8 +24,6 @@ package com.yubico.webauthn.meta; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import com.yubico.internal.util.json.LocalDateJsonSerializer; import java.net.URL; import java.time.LocalDate; import lombok.AccessLevel; @@ -49,7 +47,6 @@ public class Specification { private final DocumentStatus status; /** The release date of the specification document. */ - @JsonSerialize(using = LocalDateJsonSerializer.class) private final LocalDate releaseDate; static SpecificationBuilder builder() { diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/package-info.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/package-info.java index 6753e8fb8..e9e79f439 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/package-info.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/package-info.java @@ -107,13 +107,14 @@ * com.yubico.webauthn.RegistrationResult#getPublicKeyCose() publicKeyCose} as a new * credential for the user. The {@link com.yubico.webauthn.CredentialRepository} will need to * look these up for authentication. - *

  • Inspect the {@link com.yubico.webauthn.RegistrationResult#getWarnings() warnings} - ideally - * there should of course be none. - *
  • If you care about authenticator attestation, use the {@link - * com.yubico.webauthn.RegistrationResult#isAttestationTrusted() attestationTrusted}, {@link - * com.yubico.webauthn.RegistrationResult#getAttestationType() attestationType} and {@link - * com.yubico.webauthn.RegistrationResult#getAttestationMetadata() attestationMetadata} fields - * to enforce your attestation policy. + *
  • If you care about authenticator attestation, check that the {@link + * com.yubico.webauthn.RegistrationResult#isAttestationTrusted() attestationTrusted} field + * satisfies your attestation policy. For this you will likely need to configure the {@link + * com.yubico.webauthn.RelyingParty.RelyingPartyBuilder#attestationTrustSource(com.yubico.webauthn.attestation.AttestationTrustSource) + * attestationTrustSource} setting on your {@link com.yubico.webauthn.RelyingParty} instance. + * You may also want to consult some external data source to verify the authenticity of the + * {@link com.yubico.webauthn.data.AuthenticatorAttestationResponse#getAttestationObject() + * attestation object}. *
  • If you care about authenticator attestation, it is recommended to also store the raw {@link * com.yubico.webauthn.data.AuthenticatorAttestationResponse#getAttestationObject() * attestation object} as part of the credential. This enables you to retroactively inspect @@ -170,8 +171,6 @@ * com.yubico.webauthn.AssertionResult#getCredentialId() credentialId} result) to equal the * value returned in the {@link com.yubico.webauthn.AssertionResult#getSignatureCount() * signatureCount} result. - *
  • Inspect the {@link com.yubico.webauthn.RegistrationResult#getWarnings() warnings} - ideally - * there should of course be none. * */ package com.yubico.webauthn; diff --git a/webauthn-server-core/src/test/java/com/yubico/webauthn/RelyingPartyTest.java b/webauthn-server-core/src/test/java/com/yubico/webauthn/RelyingPartyTest.java index 11473d659..ed87a720a 100644 --- a/webauthn-server-core/src/test/java/com/yubico/webauthn/RelyingPartyTest.java +++ b/webauthn-server-core/src/test/java/com/yubico/webauthn/RelyingPartyTest.java @@ -1,35 +1,65 @@ package com.yubico.webauthn; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; -import com.yubico.webauthn.attestation.Attestation; -import com.yubico.webauthn.attestation.MetadataService; +import com.yubico.webauthn.attestation.AttestationTrustSource; import com.yubico.webauthn.data.AttestationConveyancePreference; import com.yubico.webauthn.data.ByteArray; +import com.yubico.webauthn.data.PublicKeyCredentialCreationOptions; import com.yubico.webauthn.data.PublicKeyCredentialDescriptor; +import com.yubico.webauthn.data.PublicKeyCredentialParameters; import com.yubico.webauthn.data.RelyingPartyIdentity; +import com.yubico.webauthn.data.UserIdentity; +import com.yubico.webauthn.data.exception.HexException; import com.yubico.webauthn.extension.appid.AppId; import com.yubico.webauthn.extension.appid.InvalidAppIdException; -import java.security.cert.CertificateEncodingException; +import java.security.Provider; +import java.security.Security; import java.security.cert.X509Certificate; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.junit.After; +import org.junit.Before; import org.junit.Test; +import uk.org.lidalia.slf4jext.Level; +import uk.org.lidalia.slf4jtest.TestLogger; +import uk.org.lidalia.slf4jtest.TestLoggerFactory; public class RelyingPartyTest { + private static final TestLogger testLog = TestLoggerFactory.getTestLogger(RelyingParty.class); + private List providersBefore; + + @Before + public void setUp() { + providersBefore = Stream.of(Security.getProviders()).collect(Collectors.toList()); + testLog.clearAll(); + } + + @After + public void tearDown() { + for (Provider prov : Security.getProviders()) { + Security.removeProvider(prov.getName()); + } + providersBefore.forEach(Security::addProvider); + testLog.clearAll(); + } + @Test(expected = NullPointerException.class) public void itHasTheseBuilderMethods() throws InvalidAppIdException { - final MetadataService metadataService = - new MetadataService() { + final AttestationTrustSource attestationTrustSource = + new AttestationTrustSource() { @Override - public Attestation getAttestation(List attestationCertificateChain) - throws CertificateEncodingException { + public TrustRootsResult findTrustRoots( + List attestationCertificateChain, Optional aaguid) { return null; } }; @@ -42,10 +72,9 @@ public Attestation getAttestation(List attestationCertificateCh .appId(Optional.of(new AppId("https://example.com"))) .attestationConveyancePreference(AttestationConveyancePreference.DIRECT) .attestationConveyancePreference(Optional.of(AttestationConveyancePreference.DIRECT)) - .metadataService(metadataService) - .metadataService(Optional.of(metadataService)) + .attestationTrustSource(attestationTrustSource) + .attestationTrustSource(Optional.of(attestationTrustSource)) .preferredPubkeyParams(Collections.emptyList()) - .allowUnrequestedExtensions(true) .allowUntrustedAttestation(true) .validateSignatureCounter(true); } @@ -57,35 +86,7 @@ public void originsIsImmutable() { RelyingParty rp = RelyingParty.builder() .identity(RelyingPartyIdentity.builder().id("localhost").name("Test").build()) - .credentialRepository( - new CredentialRepository() { - @Override - public Set getCredentialIdsForUsername( - String username) { - return null; - } - - @Override - public Optional getUserHandleForUsername(String username) { - return Optional.empty(); - } - - @Override - public Optional getUsernameForUserHandle(ByteArray userHandle) { - return Optional.empty(); - } - - @Override - public Optional lookup( - ByteArray credentialId, ByteArray userHandle) { - return Optional.empty(); - } - - @Override - public Set lookupAll(ByteArray credentialId) { - return null; - } - }) + .credentialRepository(unimplementedCredentialRepository()) .origins(origins) .build(); @@ -101,4 +102,154 @@ public Set lookupAll(ByteArray credentialId) { assertEquals(0, rp.getOrigins().size()); } } + + @Test + public void filtersAlgorithmsToThoseAvailable() throws HexException { + for (Provider prov : Security.getProviders()) { + if (prov.getName().contains("EC")) { + Security.removeProvider(prov.getName()); + } + } + + RelyingParty rp = + RelyingParty.builder() + .identity(RelyingPartyIdentity.builder().id("localhost").name("Test").build()) + .credentialRepository(unimplementedCredentialRepository()) + .preferredPubkeyParams( + Stream.of(PublicKeyCredentialParameters.ES256, PublicKeyCredentialParameters.RS256) + .collect(Collectors.toList())) + .build(); + PublicKeyCredentialCreationOptions pkcco = + rp.startRegistration( + StartRegistrationOptions.builder() + .user( + UserIdentity.builder() + .name("foo") + .displayName("Foo User") + .id(ByteArray.fromHex("00010203")) + .build()) + .build()); + + assertEquals( + Collections.singletonList(PublicKeyCredentialParameters.RS256), + pkcco.getPubKeyCredParams()); + } + + @Test + public void defaultSettingsDontFilterEs256OrRs256() throws HexException { + RelyingParty rp = + RelyingParty.builder() + .identity(RelyingPartyIdentity.builder().id("localhost").name("Test").build()) + .credentialRepository(unimplementedCredentialRepository()) + .preferredPubkeyParams( + Stream.of(PublicKeyCredentialParameters.ES256, PublicKeyCredentialParameters.RS256) + .collect(Collectors.toList())) + .build(); + PublicKeyCredentialCreationOptions pkcco = + rp.startRegistration( + StartRegistrationOptions.builder() + .user( + UserIdentity.builder() + .name("foo") + .displayName("Foo User") + .id(ByteArray.fromHex("00010203")) + .build()) + .build()); + + assertEquals( + Stream.of(PublicKeyCredentialParameters.ES256, PublicKeyCredentialParameters.RS256) + .collect(Collectors.toList()), + pkcco.getPubKeyCredParams()); + } + + @Test + public void defaultSettingsLogWarningIfSomeAlgorithmNotAvailable() { + for (Provider prov : Security.getProviders()) { + if (prov.getName().contains("EC")) { + Security.removeProvider(prov.getName()); + } + } + + RelyingParty.builder() + .identity(RelyingPartyIdentity.builder().id("localhost").name("Test").build()) + .credentialRepository(unimplementedCredentialRepository()) + .build(); + + assertTrue( + "Expected warning log containing \"ES256\" and (case-insensitive) \"unsupported\".", + testLog.getLoggingEvents().stream() + .anyMatch( + event -> + event.getLevel().compareTo(Level.WARN) >= 0 + && event.getArguments().stream() + .anyMatch(arg -> "ES256".equals(arg.toString())) + && event.getMessage().toLowerCase().contains("unsupported algorithm"))); + } + + @Test + public void logsWarningIfAlgorithmNotAvailable() { + for (Provider prov : Security.getProviders()) { + if (prov.getName().contains("EC")) { + Security.removeProvider(prov.getName()); + } + } + + RelyingParty.builder() + .identity(RelyingPartyIdentity.builder().id("localhost").name("Test").build()) + .credentialRepository(unimplementedCredentialRepository()) + .preferredPubkeyParams(Collections.singletonList(PublicKeyCredentialParameters.ES256)) + .build(); + + assertTrue( + "Expected warning log containing \"ES256\" and (case-insensitive) \"unsupported algorithm\".", + testLog.getLoggingEvents().stream() + .anyMatch( + event -> + event.getLevel().compareTo(Level.WARN) >= 0 + && event.getArguments().stream() + .anyMatch(arg -> "ES256".equals(arg.toString())) + && event.getMessage().toLowerCase().contains("unsupported algorithm"))); + } + + @Test + public void doesNotLogWarningIfAllAlgorithmsAvailable() { + RelyingParty.builder() + .identity(RelyingPartyIdentity.builder().id("localhost").name("Test").build()) + .credentialRepository(unimplementedCredentialRepository()) + .preferredPubkeyParams( + Stream.of(PublicKeyCredentialParameters.ES256, PublicKeyCredentialParameters.RS256) + .collect(Collectors.toList())) + .build(); + + assertEquals(0, testLog.getAllLoggingEvents().size()); + } + + private static CredentialRepository unimplementedCredentialRepository() { + return new CredentialRepository() { + @Override + public Set getCredentialIdsForUsername(String username) { + return null; + } + + @Override + public Optional getUserHandleForUsername(String username) { + return Optional.empty(); + } + + @Override + public Optional getUsernameForUserHandle(ByteArray userHandle) { + return Optional.empty(); + } + + @Override + public Optional lookup(ByteArray credentialId, ByteArray userHandle) { + return Optional.empty(); + } + + @Override + public Set lookupAll(ByteArray credentialId) { + return null; + } + }; + } } diff --git a/webauthn-server-core/src/test/java/com/yubico/webauthn/attestation/TransportTest.java b/webauthn-server-core/src/test/java/com/yubico/webauthn/attestation/TransportTest.java deleted file mode 100644 index a66615557..000000000 --- a/webauthn-server-core/src/test/java/com/yubico/webauthn/attestation/TransportTest.java +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (c) 2018, Yubico AB -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, this -// list of conditions and the following disclaimer. -// -// 2. Redistributions in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -package com.yubico.webauthn.attestation; - -import static org.junit.Assert.assertEquals; - -import java.util.EnumSet; -import org.junit.Test; - -public class TransportTest { - - @Test - public void testParsingSingleValuesFromInt() { - assertEquals(EnumSet.of(Transport.BT_CLASSIC), Transport.fromInt(1)); - assertEquals(EnumSet.of(Transport.BLE), Transport.fromInt(2)); - assertEquals(EnumSet.of(Transport.USB), Transport.fromInt(4)); - assertEquals(EnumSet.of(Transport.NFC), Transport.fromInt(8)); - } - - @Test - public void testParsingSetsFromInt() { - assertEquals(EnumSet.noneOf(Transport.class), Transport.fromInt(0)); - assertEquals(EnumSet.of(Transport.BLE, Transport.NFC), Transport.fromInt(10)); - assertEquals(EnumSet.of(Transport.USB, Transport.BT_CLASSIC), Transport.fromInt(5)); - assertEquals( - EnumSet.of(Transport.BT_CLASSIC, Transport.BLE, Transport.USB, Transport.NFC), - Transport.fromInt(15)); - } - - @Test - public void testEncodingSingleValuesToInt() { - assertEquals(1, Transport.toInt(Transport.BT_CLASSIC)); - assertEquals(2, Transport.toInt(Transport.BLE)); - assertEquals(4, Transport.toInt(Transport.USB)); - assertEquals(8, Transport.toInt(Transport.NFC)); - } - - @Test - public void testEncodingSetsToInt() { - assertEquals(0, Transport.toInt()); - assertEquals(10, Transport.toInt(Transport.BLE, Transport.NFC)); - assertEquals(5, Transport.toInt(Transport.USB, Transport.BT_CLASSIC)); - assertEquals( - 15, Transport.toInt(Transport.BT_CLASSIC, Transport.BLE, Transport.USB, Transport.NFC)); - } -} diff --git a/webauthn-server-core/src/test/java/com/yubico/webauthn/data/AuthenticatorSelectionCriteriaTest.java b/webauthn-server-core/src/test/java/com/yubico/webauthn/data/AuthenticatorSelectionCriteriaTest.java index 542d94530..4601f07f0 100644 --- a/webauthn-server-core/src/test/java/com/yubico/webauthn/data/AuthenticatorSelectionCriteriaTest.java +++ b/webauthn-server-core/src/test/java/com/yubico/webauthn/data/AuthenticatorSelectionCriteriaTest.java @@ -15,7 +15,6 @@ public void itHasTheseBuilderMethods() { AuthenticatorSelectionCriteria.builder() .authenticatorAttachment(AuthenticatorAttachment.CROSS_PLATFORM) .authenticatorAttachment(Optional.of(AuthenticatorAttachment.CROSS_PLATFORM)) - .requireResidentKey(false) .residentKey(ResidentKeyRequirement.REQUIRED) .userVerification(UserVerificationRequirement.PREFERRED) .build(); @@ -28,8 +27,7 @@ public void newResidentKeyOverridesOld() throws JsonProcessingException { json.readValue( "{\"requireResidentKey\": false, \"residentKey\": \"required\"}", AuthenticatorSelectionCriteria.class); - assertEquals(decoded.getResidentKey(), ResidentKeyRequirement.REQUIRED); - assertEquals(decoded.isRequireResidentKey(), true); + assertEquals(decoded.getResidentKey(), Optional.of(ResidentKeyRequirement.REQUIRED)); } @Test @@ -37,7 +35,6 @@ public void newResidentKeyFallsBackToOld() throws JsonProcessingException { ObjectMapper json = JacksonCodecs.json(); AuthenticatorSelectionCriteria decoded = json.readValue("{\"requireResidentKey\": true}", AuthenticatorSelectionCriteria.class); - assertEquals(decoded.getResidentKey(), ResidentKeyRequirement.REQUIRED); - assertEquals(decoded.isRequireResidentKey(), true); + assertEquals(decoded.getResidentKey(), Optional.of(ResidentKeyRequirement.REQUIRED)); } } diff --git a/webauthn-server-core/src/test/java/com/yubico/webauthn/data/PublicKeyCredentialCreationOptionsTest.java b/webauthn-server-core/src/test/java/com/yubico/webauthn/data/PublicKeyCredentialCreationOptionsTest.java index 7d42f8602..b42485a42 100644 --- a/webauthn-server-core/src/test/java/com/yubico/webauthn/data/PublicKeyCredentialCreationOptionsTest.java +++ b/webauthn-server-core/src/test/java/com/yubico/webauthn/data/PublicKeyCredentialCreationOptionsTest.java @@ -1,11 +1,44 @@ package com.yubico.webauthn.data; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import com.yubico.webauthn.data.exception.HexException; +import java.security.Provider; +import java.security.Security; import java.util.Collections; +import java.util.List; import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.junit.After; +import org.junit.Before; import org.junit.Test; +import uk.org.lidalia.slf4jext.Level; +import uk.org.lidalia.slf4jtest.TestLogger; +import uk.org.lidalia.slf4jtest.TestLoggerFactory; public class PublicKeyCredentialCreationOptionsTest { + private static final TestLogger testLog = + TestLoggerFactory.getTestLogger(PublicKeyCredentialCreationOptions.class); + private List providersBefore; + + @Before + public void setUp() { + providersBefore = Stream.of(Security.getProviders()).collect(Collectors.toList()); + testLog.clearAll(); + } + + @After + public void tearDown() { + for (Provider prov : Security.getProviders()) { + Security.removeProvider(prov.getName()); + } + providersBefore.forEach(Security::addProvider); + testLog.clearAll(); + } + @Test(expected = NullPointerException.class) public void itHasTheseBuilderMethods() { PublicKeyCredentialCreationOptions.builder() @@ -22,4 +55,105 @@ public void itHasTheseBuilderMethods() { .timeout(0) .timeout(Optional.of(0L)); } + + @Test + public void filtersAlgorithmsToThoseAvailable() throws HexException { + for (Provider prov : Security.getProviders()) { + if (prov.getName().contains("EC")) { + Security.removeProvider(prov.getName()); + } + } + + PublicKeyCredentialCreationOptions pkcco = + PublicKeyCredentialCreationOptions.builder() + .rp(RelyingPartyIdentity.builder().id("localhost").name("Test").build()) + .user( + UserIdentity.builder() + .name("foo") + .displayName("Foo User") + .id(ByteArray.fromHex("00010203")) + .build()) + .challenge(ByteArray.fromHex("04050607")) + .pubKeyCredParams( + Stream.of(PublicKeyCredentialParameters.ES256, PublicKeyCredentialParameters.RS256) + .collect(Collectors.toList())) + .build(); + + assertEquals( + Collections.singletonList(PublicKeyCredentialParameters.RS256), + pkcco.getPubKeyCredParams()); + } + + @Test + public void defaultProvidersDontFilterEs256OrRs256() throws HexException { + PublicKeyCredentialCreationOptions pkcco = + PublicKeyCredentialCreationOptions.builder() + .rp(RelyingPartyIdentity.builder().id("localhost").name("Test").build()) + .user( + UserIdentity.builder() + .name("foo") + .displayName("Foo User") + .id(ByteArray.fromHex("00010203")) + .build()) + .challenge(ByteArray.fromHex("04050607")) + .pubKeyCredParams( + Stream.of(PublicKeyCredentialParameters.ES256, PublicKeyCredentialParameters.RS256) + .collect(Collectors.toList())) + .build(); + + assertEquals( + Stream.of(PublicKeyCredentialParameters.ES256, PublicKeyCredentialParameters.RS256) + .collect(Collectors.toList()), + pkcco.getPubKeyCredParams()); + } + + @Test + public void logsWarningIfAlgorithmNotAvailable() throws HexException { + for (Provider prov : Security.getProviders()) { + if (prov.getName().contains("EC")) { + Security.removeProvider(prov.getName()); + } + } + + PublicKeyCredentialCreationOptions.builder() + .rp(RelyingPartyIdentity.builder().id("localhost").name("Test").build()) + .user( + UserIdentity.builder() + .name("foo") + .displayName("Foo User") + .id(ByteArray.fromHex("00010203")) + .build()) + .challenge(ByteArray.fromHex("04050607")) + .pubKeyCredParams(Collections.singletonList(PublicKeyCredentialParameters.ES256)) + .build(); + + assertTrue( + "Expected warning log containing \"ES256\" and (case-insensitive) \"unsupported algorithm\".", + testLog.getLoggingEvents().stream() + .anyMatch( + event -> + event.getLevel().compareTo(Level.WARN) >= 0 + && event.getArguments().stream() + .anyMatch(arg -> "ES256".equals(arg.toString())) + && event.getMessage().toLowerCase().contains("unsupported algorithm"))); + } + + @Test + public void doesNotLogWarningIfAllAlgorithmsAvailable() throws HexException { + PublicKeyCredentialCreationOptions.builder() + .rp(RelyingPartyIdentity.builder().id("localhost").name("Test").build()) + .user( + UserIdentity.builder() + .name("foo") + .displayName("Foo User") + .id(ByteArray.fromHex("00010203")) + .build()) + .challenge(ByteArray.fromHex("04050607")) + .pubKeyCredParams( + Stream.of(PublicKeyCredentialParameters.ES256, PublicKeyCredentialParameters.RS256) + .collect(Collectors.toList())) + .build(); + + assertEquals(0, testLog.getAllLoggingEvents().size()); + } } diff --git a/webauthn-server-core/src/test/java/com/yubico/webauthn/data/RelyingPartyIdentityTest.java b/webauthn-server-core/src/test/java/com/yubico/webauthn/data/RelyingPartyIdentityTest.java index 1c9121090..380b87ca6 100644 --- a/webauthn-server-core/src/test/java/com/yubico/webauthn/data/RelyingPartyIdentityTest.java +++ b/webauthn-server-core/src/test/java/com/yubico/webauthn/data/RelyingPartyIdentityTest.java @@ -1,18 +1,11 @@ package com.yubico.webauthn.data; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.Optional; import org.junit.Test; public class RelyingPartyIdentityTest { @Test - public void itHasTheseBuilderMethods() throws MalformedURLException { - RelyingPartyIdentity.builder() - .id("") - .name("") - .icon(new URL("https://example.com")) - .icon(Optional.of(new URL("https://example.com"))); + public void itHasTheseBuilderMethods() { + RelyingPartyIdentity.builder().id("").name("").build(); } } diff --git a/webauthn-server-core/src/test/java/com/yubico/webauthn/data/UserIdentityTest.java b/webauthn-server-core/src/test/java/com/yubico/webauthn/data/UserIdentityTest.java index f88148ace..fbac7a75f 100644 --- a/webauthn-server-core/src/test/java/com/yubico/webauthn/data/UserIdentityTest.java +++ b/webauthn-server-core/src/test/java/com/yubico/webauthn/data/UserIdentityTest.java @@ -1,19 +1,11 @@ package com.yubico.webauthn.data; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.Optional; import org.junit.Test; public class UserIdentityTest { @Test - public void itHasTheseBuilderMethods() throws MalformedURLException { - UserIdentity.builder() - .name("") - .displayName("") - .id(new ByteArray(new byte[] {})) - .icon(new URL("https://example.com")) - .icon(Optional.of(new URL("https://example.com"))); + public void itHasTheseBuilderMethods() { + UserIdentity.builder().name("").displayName("").id(new ByteArray(new byte[] {})).build(); } } diff --git a/webauthn-server-core/src/test/resources/globalsign-root-r2.pem b/webauthn-server-core/src/test/resources/globalsign-root-r2.pem new file mode 100644 index 000000000..6f0f8db0d --- /dev/null +++ b/webauthn-server-core/src/test/resources/globalsign-root-r2.pem @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDujCCAqKgAwIBAgILBAAAAAABD4Ym5g0wDQYJKoZIhvcNAQEFBQAwTDEgMB4G +A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjIxEzARBgNVBAoTCkdsb2JhbFNp +Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDYxMjE1MDgwMDAwWhcNMjExMjE1 +MDgwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMjETMBEG +A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAKbPJA6+Lm8omUVCxKs+IVSbC9N/hHD6ErPL +v4dfxn+G07IwXNb9rfF73OX4YJYJkhD10FPe+3t+c4isUoh7SqbKSaZeqKeMWhG8 +eoLrvozps6yWJQeXSpkqBy+0Hne/ig+1AnwblrjFuTosvNYSuetZfeLQBoZfXklq +tTleiDTsvHgMCJiEbKjNS7SgfQx5TfC4LcshytVsW33hoCmEofnTlEnLJGKRILzd +C9XZzPnqJworc5HGnRusyMvo4KD0L5CLTfuwNhv2GXqF4G3yYROIXJ/gkwpRl4pa +zq+r1feqCapgvdzZX99yqWATXgAByUr6P6TqBwMhAo6CygPCm48CAwEAAaOBnDCB +mTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUm+IH +V2ccHsBqBt5ZtJot39wZhi4wNgYDVR0fBC8wLTAroCmgJ4YlaHR0cDovL2NybC5n +bG9iYWxzaWduLm5ldC9yb290LXIyLmNybDAfBgNVHSMEGDAWgBSb4gdXZxwewGoG +3lm0mi3f3BmGLjANBgkqhkiG9w0BAQUFAAOCAQEAmYFThxxol4aR7OBKuEQLq4Gs +J0/WwbgcQ3izDJr86iw8bmEbTUsp9Z8FHSbBuOmDAGJFtqkIk7mpM0sYmsL4h4hO +291xNBrBVNpGP+DTKqttVCL1OmLNIG+6KYnX3ZHu01yiPqFbQfXf5WRDLenVOavS +ot+3i9DAgBkcRcAtjOj4LaR0VknFBbVPFd5uRHg5h6h+u/N5GJG79G+dwfCMNYxd +AfvDbbnvRG15RjF+Cv6pgsH/76tuIMRQyV+dTZsXjAzlAcmgQWpzU/qlULRuJQ/7 +TBj0/VLZjmmx6BEP3ojY+x1J96relc8geMJgEtslQIxq/H5COEBkEveegeGTLg== +-----END CERTIFICATE----- diff --git a/webauthn-server-core/src/test/resources/slf4jtest.properties b/webauthn-server-core/src/test/resources/slf4jtest.properties new file mode 100644 index 000000000..eacb68e5f --- /dev/null +++ b/webauthn-server-core/src/test/resources/slf4jtest.properties @@ -0,0 +1 @@ +print.level=DEBUG diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/AppleAttestationStatementVerifierSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/AppleAttestationStatementVerifierSpec.scala index 287f700d3..726d1757a 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/AppleAttestationStatementVerifierSpec.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/AppleAttestationStatementVerifierSpec.scala @@ -70,7 +70,7 @@ class AppleAttestationStatementVerifierSpec it("a test-generated apple attestation statement.") { val (attestationMaker, _, _) = AttestationMaker.apple() - val (pkc, _) = TestAuthenticator.createBasicAttestedCredential( + val (pkc, _, _) = TestAuthenticator.createBasicAttestedCredential( attestationMaker = attestationMaker ) val result = verifier.verifyAttestationSignature( @@ -106,7 +106,7 @@ class AppleAttestationStatementVerifierSpec it("an attestation statement without the attestation cert extension 1.2.840.113635.100.8.2 .") { val (attestationMaker, _, _) = AttestationMaker.apple(addNonceExtension = false) - val (pkc, _) = TestAuthenticator.createBasicAttestedCredential( + val (pkc, _, _) = TestAuthenticator.createBasicAttestedCredential( attestationMaker = attestationMaker ) an[IllegalArgumentException] shouldBe thrownBy { @@ -121,7 +121,7 @@ class AppleAttestationStatementVerifierSpec forAll { incorrectNonce: ByteArray => val (attestationMaker, _, _) = AttestationMaker.apple(nonceValue = Some(incorrectNonce)) - val (pkc, _) = TestAuthenticator.createBasicAttestedCredential( + val (pkc, _, _) = TestAuthenticator.createBasicAttestedCredential( attestationMaker = attestationMaker ) @@ -140,7 +140,7 @@ class AppleAttestationStatementVerifierSpec AttestationMaker.apple(certSubjectPublicKey = Some(certSubjectKeypair.getPublic) ) - val (pkc, _) = TestAuthenticator.createBasicAttestedCredential( + val (pkc, _, _) = TestAuthenticator.createBasicAttestedCredential( attestationMaker = appleAttestationMaker ) 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 9f4b60bb7..cdf29f722 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,8 +1,5 @@ package com.yubico.webauthn -import com.yubico.scalacheck.gen.JavaGenerators._ -import com.yubico.webauthn.attestation.Attestation -import com.yubico.webauthn.attestation.Generators._ import com.yubico.webauthn.data.AssertionExtensionInputs import com.yubico.webauthn.data.AttestationType import com.yubico.webauthn.data.AuthenticatorAssertionExtensionOutputs @@ -13,11 +10,13 @@ import com.yubico.webauthn.data.ClientRegistrationExtensionOutputs import com.yubico.webauthn.data.Generators._ import com.yubico.webauthn.data.PublicKeyCredentialDescriptor import com.yubico.webauthn.data.UserVerificationRequirement +import org.bouncycastle.asn1.x500.X500Name import org.scalacheck.Arbitrary import org.scalacheck.Arbitrary.arbitrary import org.scalacheck.Gen -import java.util.Optional +import java.security.cert.X509Certificate +import scala.jdk.CollectionConverters.SeqHasAsJava object Generators { @@ -32,7 +31,6 @@ object Generators { success <- arbitrary[Boolean] userHandle <- arbitrary[ByteArray] username <- arbitrary[String] - warnings <- arbitrary[java.util.List[String]] } yield AssertionResult .builder() .success(success) @@ -43,15 +41,15 @@ object Generators { .signatureCounterValid(signatureCounterValid) .clientExtensionOutputs(clientExtensionOutputs) .assertionExtensionOutputs(authenticatorExtensionOutputs.orNull) - .warnings(warnings) .build() ) implicit val arbitraryRegistrationResult: Arbitrary[RegistrationResult] = Arbitrary( for { - attestationMetadata <- arbitrary[Optional[Attestation]] + aaguid <- byteArray(16) attestationTrusted <- arbitrary[Boolean] + attestationTrustPath <- generateAttestationCertificateChain attestationType <- arbitrary[AttestationType] authenticatorExtensionOutputs <- arbitrary[Option[AuthenticatorRegistrationExtensionOutputs]] @@ -59,18 +57,17 @@ object Generators { keyId <- arbitrary[PublicKeyCredentialDescriptor] publicKeyCose <- arbitrary[ByteArray] signatureCount <- arbitrary[Long] - warnings <- arbitrary[java.util.List[String]] } yield RegistrationResult .builder() .keyId(keyId) + .aaguid(aaguid) .attestationTrusted(attestationTrusted) .attestationType(attestationType) .publicKeyCose(publicKeyCose) .signatureCount(signatureCount) .clientExtensionOutputs(clientExtensionOutputs) .authenticatorExtensionOutputs(authenticatorExtensionOutputs.orNull) - .attestationMetadata(attestationMetadata) - .warnings(warnings) + .attestationTrustPath(attestationTrustPath.asJava) .build() ) @@ -110,4 +107,28 @@ object Generators { } ) + def generateAttestationCertificateChain: Gen[List[X509Certificate]] = + for { + dummy <- Gen.nonEmptyListOf[Int](arbitrary[Int]) + } yield { + if (dummy.length >= 2) { + val tail = dummy.tail.init.foldLeft( + List(TestAuthenticator.generateAttestationCaCertificate()) + )({ + case (chain, _) => + TestAuthenticator.generateAttestationCaCertificate( + name = new X500Name( + s"CN=Yubico WebAuthn unit tests intermediate CA ${chain.length}, O=Yubico, OU=Authenticator Attestation, C=SE" + ), + superCa = Some(chain.head), + ) +: chain + }) + (TestAuthenticator.generateAttestationCertificate(caCertAndKey = + Some(tail.head) + ) +: tail).map(_._1) + } else { + List(TestAuthenticator.generateAttestationCertificate()._1) + } + } + } diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/PackedAttestationStatementVerifierSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/PackedAttestationStatementVerifierSpec.scala index 3b2adf847..408166995 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/PackedAttestationStatementVerifierSpec.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/PackedAttestationStatementVerifierSpec.scala @@ -24,17 +24,19 @@ package com.yubico.webauthn +import com.yubico.internal.util.BinaryUtil +import com.yubico.internal.util.CertificateParser import com.yubico.webauthn.Crypto.isP256 import com.yubico.webauthn.TestAuthenticator.AttestationCert import com.yubico.webauthn.TestAuthenticator.AttestationMaker import com.yubico.webauthn.data.ByteArray import com.yubico.webauthn.data.COSEAlgorithmIdentifier -import com.yubico.webauthn.test.Util import org.junit.runner.RunWith import org.scalatest.FunSpec import org.scalatest.Matchers import org.scalatestplus.junit.JUnitRunner +import java.nio.charset.StandardCharsets import java.security.interfaces.ECPrivateKey import scala.util.Success import scala.util.Try @@ -54,8 +56,11 @@ class PackedAttestationStatementVerifierSpec it("which pass Klas's attestation certificate.") { - val cert = Util.importCertFromPem( - getClass.getResourceAsStream("klas-cert.pem") + val cert = CertificateParser.parsePem( + new String( + BinaryUtil.readAll(getClass.getResourceAsStream("klas-cert.pem")), + StandardCharsets.UTF_8, + ) ) val result = Try( @@ -74,11 +79,12 @@ class PackedAttestationStatementVerifierSpec describe("supports attestation certificates with the algorithm") { it("ECDSA.") { val (cert, key) = TestAuthenticator.generateAttestationCertificate() - val (credential, _) = TestAuthenticator.createBasicAttestedCredential( - attestationMaker = AttestationMaker.packed( - new AttestationCert(COSEAlgorithmIdentifier.ES256, (cert, key)) + val (credential, _, _) = + TestAuthenticator.createBasicAttestedCredential( + attestationMaker = AttestationMaker.packed( + new AttestationCert(COSEAlgorithmIdentifier.ES256, (cert, key)) + ) ) - ) val result = verifier.verifyAttestationSignature( credential.getResponse.getAttestation, @@ -92,11 +98,12 @@ class PackedAttestationStatementVerifierSpec it("RSA.") { val (cert, key) = TestAuthenticator.generateRsaCertificate() - val (credential, _) = TestAuthenticator.createBasicAttestedCredential( - attestationMaker = AttestationMaker.packed( - new AttestationCert(COSEAlgorithmIdentifier.RS256, (cert, key)) + val (credential, _, _) = + TestAuthenticator.createBasicAttestedCredential( + attestationMaker = AttestationMaker.packed( + new AttestationCert(COSEAlgorithmIdentifier.RS256, (cert, key)) + ) ) - ) val result = verifier.verifyAttestationSignature( credential.getResponse.getAttestation, 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 bb0e1253c..f343fcd0b 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 @@ -27,10 +27,8 @@ package com.yubico.webauthn import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.node.JsonNodeFactory import com.fasterxml.jackson.databind.node.ObjectNode -import com.yubico.internal.util.BinaryUtil import com.yubico.internal.util.CertificateParser import com.yubico.internal.util.JacksonCodecs -import com.yubico.internal.util.scala.JavaConverters._ import com.yubico.webauthn.TestAuthenticator.AttestationCert import com.yubico.webauthn.TestAuthenticator.AttestationMaker import com.yubico.webauthn.TestAuthenticator.AttestationSigner @@ -53,9 +51,13 @@ import com.yubico.webauthn.data.UserIdentity import org.bouncycastle.asn1.x500.X500Name import java.nio.charset.StandardCharsets +import java.security.KeyFactory import java.security.KeyPair +import java.security.PrivateKey import java.security.cert.X509Certificate +import java.security.spec.PKCS8EncodedKeySpec import scala.jdk.CollectionConverters._ +import scala.jdk.OptionConverters.RichOption import scala.util.Failure import scala.util.Success import scala.util.Try @@ -63,16 +65,44 @@ import scala.util.Try object RegistrationTestDataGenerator extends App { regenerateTestData() + def importAttestationCa( + certBase64: String, + keyAlgorithm: String, + keyBase64: String, + ): (X509Certificate, PrivateKey) = { + val cert: X509Certificate = CertificateParser.parsePem(certBase64) + + val kf = KeyFactory.getInstance(keyAlgorithm) + val key: PrivateKey = kf.generatePrivate( + new PKCS8EncodedKeySpec(ByteArray.fromBase64(keyBase64).getBytes) + ) + + (cert, key) + } + def printTestDataCode( testData: RegistrationTestData ): Unit = { println( - s"""attestationObject = ByteArray.fromHex("${testData.attestationObject.getHex}"), + s"""alg = COSEAlgorithmIdentifier.${testData.alg.name}, + |attestationObject = ByteArray.fromHex("${testData.attestationObject.getHex}"), |clientDataJson = \"\"\"${testData.clientDataJson}\"\"\", - |privateKey = Some(ByteArray.fromHex("${testData.privateKey.get.getHex}")), - """.stripMargin + |privateKey = Some(ByteArray.fromHex("${testData.privateKey.get.getHex}")),""".stripMargin ) + if (testData.attestationCertChain.nonEmpty) { + println(s"""attestationCertChain = List(${testData.attestationCertChain + .map({ + case (cert, key) => + s"""RegistrationTestDataGenerator.importAttestationCa("${new ByteArray( + cert.getEncoded + ).getBase64}", "${key.getAlgorithm}", "${new ByteArray( + key.getEncoded + ).getBase64}")""" + }) + .mkString(", ")}),""") + } + testData.assertion foreach { assertion => println(s"""|assertion = Some(AssertionTestData( | request = JacksonCodecs.json().readValue(\"\"\"${JacksonCodecs @@ -153,10 +183,24 @@ object RegistrationTestData { val BasicAttestation: RegistrationTestData = new RegistrationTestData( alg = COSEAlgorithmIdentifier.ES256, - attestationObject = new ByteArray( - BinaryUtil.fromHex("bf68617574684461746158a449960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97634100000539000102030405060708090a0b0c0d0e0f0020d787c0d88c8b258e0e147af3f4442103fc7d41718ea92dd2b1be925c6a06113aa52258200b159bf750abbfa9bf8d6a4a806eff06533e9ac9f3576113f57ce6919306a51b03260102215820b9e1f17d71fc5cfbc2e19528b2fa6c9dd9e9a8bd35e692f5e12d71233e91950f200163666d7471616e64726f69642d7361666574796e65746761747453746d74bf6376657268313437393930323168726573706f6e73655907e665794a68624763694f694a53557a49314e694973496e67315979493657794a4e53556c4463565244513046724b32644264306c435157644a51304a55613364445a316c4a53323961535870714d45564264306c335957704662553144555564424d565646515864335a466459566d6c685630353253555a6b62466c72526a466b5232683153556857645746595557646b52315a365a45684e5a31457752586845656b464f516d644f566b4a42623031436247777857573173616d4a3652576c4e513046485154465652554e33643170525746597759556457645752486247705a57464a3259326c43516d5249556d786a4d314a6f5a456473646d4a715255784e515774485154465652554a6f54554e564d4656335347686a546b31555a336450564545795456526a4d4531715158645861474e4f5456526e6430395551544a4e56474d775457704264316471516d5a4e556e4e335231465a52465a5255555245516b706f5a45685362474d7a5558565a567a567259323035634670444e5770694d6a423452487042546b4a6e546c5a435157394e516d78734d566c7462477069656b567054554e42523045785655564464336461555668574d474648566e566b5232787157566853646d4e70516b4a6b53464a73597a4e536147524862485a69616b564d54554672523045785655564361453144565442566432646e52576c4e5154424851314e7852314e4a596a4e4555555643515646565155453053554a45643046335a3264465330467653554a425555524f6332307761544248624535514d575a4455474e4757444e3361316444533370315a466f3051324a5051304a544d6d394b4c7a4a335a6e5a7152555a324f456c4f5630703364325a73554539794b3063784e4552744f466734517a4d7256574e5251565a5763305249625535435a32314b513346475257307654473936633278685a5574335333465052326c3461484e464f53744e53477830616c7079634564614f467069596b527a4f554e705132357a646e6b7754564a4b645464495a4456784e585a7357553947596a4d315545563055585977516b5972557a426b4c3146685344647857476b30646a645a616b6f7a4d485672515752544d306f795747593262544e786153395563537455656c52716245637a53565277545539426556597653304a6b64574934613170774f456f775556497a556a6868614374615a554a7064564630513146335155746b636d314e63456c43555870496455356f613231614c324e736554597956554a31533264714c33646e4d55773156554533527a5a4f645734785a4642326157354d56557868526b6f3251334633513231576248426a53314635636b4a59596d70476445527962475a5256335a4f536c526c5633647a564756706345466e54554a4251556471536c5242616b31445255644465584e4851564652516d643156574e4255555646516b4a4a5255564251554a425a303146516c465a53454e42613074446433644f5247633464304e6e57556c4c6231704a656d6f77525546335355525451554633556c464a5a314e74555456504b7a6c3152576c784d544e70546d6b78566e5a4a645538335430357564546c46516b597a534549794f45687562445661554774445356464563474e68626d3958526b4a784d3074684d7a6c71536e68554e32784457473153616b4e6c54307868567a4a715a6d39715547316a63575a435a7a3039496c31392e65794a68634778445a584a3061575a70593246305a5552705a32567a64464e6f595449314e69493657794a4d5132457759544a714c3368764c7a56744d46553453465243516b3543546b4e4d57454a725a7a63725a79745a63475670523070744e54593050534a644c434a756232356a5a534936496c427165585a6b524539304e554a365a56523561474643646b307a554552705a303158636b6732515652736256464c636c6730566d68314e47733949697769595842725547466a6132466e5a553568625755694f694a6a623230756558566961574e764c6e646c596d4631644768754c6e526c633351694c434a6959584e7059306c756447566e636d6c306553493664484a315a53776964476c745a584e305957317754584d694f6a45314e546b314e6a6b794d7a67314f446773496d4677613052705a32567a64464e6f595449314e694936496b7844595442684d6d6f76654738764e5730775654684956454a43546b4a4f51307859516d746e4e79746e4b316c775a576c48536d30314e6a5139496977695933527a55484a765a6d6c735a55316864474e6f496a7030636e566c66512e76546e4f4f6151514b6e7959356747394d58636c63455237513145347275705a76415f6d77494a4e47745f2d5039436e6d64736366597a7a512d67714b3668537467714c326f6453485f6b6d473441464a4d367a304843712d324d41756262515f38435346507855615f6a5674397334552d7446415a564b64424b796e495f4b3863456b66683364684a664138774c70363268485f6f6b5a63744c6b5f437549787446646f4b587854793675706f4a4e496a4342614a4a314855304b4c424c78704e4543494346363876375368413855466e6e6863726a47456575794d6d634e5f6179535570306a3858536d5651496d7a754c714c4763476c545f647143486b764a673773303850616c53534131726e30634f505a526b41656c37706e65627746623854497373444852475a696639493255787530474774644b31364164797059555242633278642d4769302d30566c616631587577ffff") + attestationObject = + ByteArray.fromHex("bf68617574684461746158a449960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97634100000539000102030405060708090a0b0c0d0e0f0020dd22f7c6b8aefa689d6ca9febe1ef85d31a0111353b9ba0129f3b3e0eb18cd5ca5010203262001215820eaddbc0baa43b09ba0c6c55c58cf71a3a70975b91a50af7f7dbc53f2b35e6b06225820fbfbc9c953a6e9d05a5b702c59cdc1d232b8449f1bdb7b41c5319fbc6bdaea6c63666d7471616e64726f69642d7361666574796e65746761747453746d74bf6376657268313437393930323168726573706f6e736559093c65794a68624763694f694a53557a49314e694973496e67315979493657794a4e53556c4559577044513046735332644264306c435157644a5130644d5933644555566c4b5332396153576832593035425555564d516c4642643246715257314e5131464851544656525546336432525857465a705956644f646b6c475a47785a613059785a45646f64556c49566e56685746466e5a456457656d5249545764524d45563452487042546b4a6e546c5a435157394e516d78734d566c7462477069656b567054554e42523045785655564464336461555668574d474648566e566b5232787157566853646d4e70516b4a6b53464a73597a4e536147524862485a69616b564d54554672523045785655564361453144565442566430686f5930354e56476433543152424d6b3155597a424e616b46335632686a546b31555a336450564556365456526a4d45317151586458616b4a6d54564a7a64306452575552575556464552454a4b61475249556d786a4d3146315756633161324e744f584261517a5671596a497765455236515535435a303557516b467654554a736244465a62577871596e704661553144515564424d56564651336433576c4659566a426852315a315a456473616c6c59556e5a6a61554a435a45685362474d7a556d686b52327832596d704654453142613064424d565646516d684e513155775658646e5a3056705455457752304e54635564545357497a52464646516b4652565546424e456c43524864426432646e5255744262306c43515646444d47744e5a33457263555a345958467861475a765a326835576d7048516e4e58546c4e48535570485347565556457468556e6b33574534324f5664335447354f576e70334e4652704c315a5363316c51537a566b53576c4a63337077574539794d6d5a754e6b466b596c46595557704b4d585a745958706d614664774d47646162546c4e4e55706e536c5648596c524b5755686957454a7461326c6856585a4f5631644c575739614d475259626c5a364d5868706232744f65574e344d6a56444e55314b54465a5253315249596b4e6f5455566d4b314e334e585652525564465a6e4676596c52504f484243547a4135546e4a755556686c5246564a51566834537a6852626d46355a327879654664796432564d566a5a7561324e7262554a6c4c326c4955484a48646b393157444a476356517262477075636e56774b315976613170614f55393061474e7961324e3352484d79533164335954646b5132706853305275524530764d315278535546305956686d53445a354e4459354f47784562577331575374534e323132556c6455626a637863544a775632393665456c3256564e455454567451307078655573784f4664516157564d4f446c715a6e68305747564b645770425a30314351554648616b70555157704e5130564851336c7a5230465255554a6e6456566a5156464652554a4353555646515546435157644e52554a52575568445157744c51336433546b526e4f48644555566c4b5332396153576832593035425555564d516c46425247646e52554a425230744d615774785446557a4e47637962586731656d6b724d30564e65574d3555574a334d30463062457479545668786155453162444a7264466c6c5a3070515132773252487070646c7048575731575647394b536e643251336f7754314e4f62446b7a4e6e4e34646a4e344e56527a626a63774e7a684a4e456c5a5a6c644d5a33517753454934656b46544f5652684c30786e6432565a527a466154566854645774524b316b795630637662484e6c596e524762585972566a637a566b5134564738345346564c51324a734b304d79635563775647744e5155353259304a35633170736355354f59544a6e64334e794f58564e6358704664446457596b30785332524b4e3263336358684d566d4a764d33424b5a5568475446424c4d6a4654556a5a53536b4a6963336443596d4a6f4b305a4c62445631556b633562454e6e566e4e3255564e455643746954316470596d31534e577859566d525465466855623234354d7a4652576a4670526a644e4e5746456146687a64584643576d4a506179745556477032596c49336169744d5a6b4a366145744d546e4259576d4a745a6c6f3261546c305445673059554979635738345247315154466871516c5932566e46496269393350534a6466512e65794a68634778445a584a3061575a70593246305a5552705a32567a64464e6f595449314e69493657794a4d5132457759544a714c3368764c7a56744d46553453465243516b3543546b4e4d57454a725a7a63725a79745a63475670523070744e54593050534a644c434a756232356a5a534936496d4531526e704551574e354f4374454e4564345a304e74536e644f636b783264484a784d6d4e6d656d4d774f465a5262323944566b313556556b3949697769595842725547466a6132466e5a553568625755694f694a6a623230756558566961574e764c6e646c596d4631644768754c6e526c633351694c434a6959584e7059306c756447566e636d6c306553493664484a315a53776964476c745a584e305957317754584d694f6a45324e4451344e7a51774d54497a4e445573496d4677613052705a32567a64464e6f595449314e694936496b7844595442684d6d6f76654738764e5730775654684956454a43546b4a4f51307859516d746e4e79746e4b316c775a576c48536d30314e6a5139496977695933527a55484a765a6d6c735a55316864474e6f496a7030636e566c66512e434d6e4e5a472d63686b30625952704c34337069564e6630476238767467656d75316a7773526f4c5f3443644573326f58615854534e416e63316c6233506f723738387277644a7444585a626753634159414967677032653552696e3036494861746f444b666d36487845564646567a626b6973736468373777417a734d6e4d50316164486e6a36324b65365437306641654446555579344664615a4b6e6838725053464d3430795132486e5671666a664d32335379676b7544716446516d707856594574595a57514356416f533833327756773272714f2d70346f53615170775f5356586d37324b45537a467a56743969347067784951687276514c65334d30434b7037436b4d754655594d5548357a31515a57696a666c52547a354d4571416d586f6c764250547652574c5239636350426a53764a50327938704873393177634a383948306b744f453577443837414164426367ffff"), + clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","type":"webauthn.create","tokenBinding":{"status":"supported"}}""", + privateKey = Some( + ByteArray.fromHex("308193020100301306072a8648ce3d020106082a8648ce3d0301070479307702010104205d03f530a58e5be4a9c8d757155c16d69441075bc4522379bc6d4ffbe34fc51fa00a06082a8648ce3d030107a14403420004eaddbc0baa43b09ba0c6c55c58cf71a3a70975b91a50af7f7dbc53f2b35e6b06fbfbc9c953a6e9d05a5b702c59cdc1d232b8449f1bdb7b41c5319fbc6bdaea6c") + ), + attestationCertChain = List( + RegistrationTestDataGenerator.importAttestationCa( + "MIIDajCCAlKgAwIBAgICGLcwDQYJKoZIhvcNAQELBQAwajEmMCQGA1UEAwwdWXViaWNvIFdlYkF1dGhuIHVuaXQgdGVzdHMgQ0ExDzANBgNVBAoMBll1YmljbzEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjELMAkGA1UEBhMCU0UwHhcNMTgwOTA2MTc0MjAwWhcNMTgwOTEzMTc0MjAwWjBfMRswGQYDVQQDDBJhdHRlc3QuYW5kcm9pZC5jb20xDzANBgNVBAoMBll1YmljbzEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjELMAkGA1UEBhMCU0UwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC0kMgq+qFxaqqhfoghyZjGBsWNSGIJGHeTTKaRy7XN69WwLnNZzw4Ti/VRsYPK5dIiIszpXOr2fn6AdbQXQjJ1vmazfhWp0gZm9M5JgJUGbTJYHbXBmkiaUvNWWKYoZ0dXnVz1xiokNycx25C5MJLVQKTHbChMEf+Sw5uQEGEfqobTO8pBO09NrnQXeDUIAXxK8QnayglrxWrweLV6nkckmBe/iHPrGvOuX2FqT+ljnrup+V/kZZ9OthcrkcwDs2KWwa7dCjaKDnDM/3TqIAtaXfH6y4698lDmk5Y+R7mvRWTn71q2pWozxIvUSDM5mCJqyK18WPieL89jfxtXeJujAgMBAAGjJTAjMCEGCysGAQQBguUcAQEEBBIEEAABAgMEBQYHCAkKCwwNDg8wDQYJKoZIhvcNAQELBQADggEBAGKLikqLU34g2mx5zi+3EMyc9Qbw3AtlKrMXqiA5l2ktYegJPCl6DzivZGYmVToJJwvCz0OSNl936sxv3x5Tsn7078I4IYfWLgt0HB8zAS9Ta/LgweYG1ZMXSukQ+Y2WG/lsebtFmv+V73VD8To8HUKCbl+C2qG0TkMANvcBysZlqNNa2gwsr9uMqzEt7VbM1KdJ7g7qxLVbo3pJeHFLPK21SR6RJBbswBbbh+FKl5uRG9lCgVsvQSDT+bOWibmR5lXVdSxXTon931QZ1iF7M5aDhXsuqBZbOk+TTjvbR7j+LfBzhKLNpXZbmfZ6i9tLH4aB2qo8DmPLXjBV6VqHn/w=", + "RSA", + "MIIEuwIBADANBgkqhkiG9w0BAQEFAASCBKUwggShAgEAAoIBAQC0kMgq+qFxaqqhfoghyZjGBsWNSGIJGHeTTKaRy7XN69WwLnNZzw4Ti/VRsYPK5dIiIszpXOr2fn6AdbQXQjJ1vmazfhWp0gZm9M5JgJUGbTJYHbXBmkiaUvNWWKYoZ0dXnVz1xiokNycx25C5MJLVQKTHbChMEf+Sw5uQEGEfqobTO8pBO09NrnQXeDUIAXxK8QnayglrxWrweLV6nkckmBe/iHPrGvOuX2FqT+ljnrup+V/kZZ9OthcrkcwDs2KWwa7dCjaKDnDM/3TqIAtaXfH6y4698lDmk5Y+R7mvRWTn71q2pWozxIvUSDM5mCJqyK18WPieL89jfxtXeJujAgMBAAECgf98Iw16ftR/JNYqMNNmZzKg8gbfGuRLXIbYvdnGYkabS4edmFG1bKRAy/fcMi5pT5cn4MT/quHelRhjOIiXdOs8B6qTvBsopTvBjIxF/CB4SppR+hr6/xbrAhxJQKj7HgXuNkGytopCW6iWnlzg9IP+GHMwpysNIVKTk+dfI+Oh4HA9SZfI93PKK1iXgrQNtDPz9PS2L1/11zxmj2HpBYzSoLTAmaEc7vr0vl6y+bJslM0UHvNk+BgGmW64e0B/hnDKA+jJqF3xX5gMnOsP6/c62AnPkvuIvOLfVaXMi1YyineJn3NsmwNjjukmgpPPxgIVMQ951i6JxnBn2B+p6QECgYEAurAFqe7rbIkAZUd4tG6cu7S/1sRgfN3gWoWFJS2gQ1zq2HWwrjN+mTG9P7k1djBpUPX64dzs2OW4MWnHWeHOBDo+UrfBqpiX6WNLrwEnBS6bFHLZo/y/n2J+MK1v0uF/pR7cykRaVE4auRgOLSlwF/kCk0Qth9EE01PQ3iXZSrkCgYEA95riAauuFggjrbSR+2pBeFvHJnqSJoZO9eKFVW9QQRjfYxllwzmzfY59SzrIa1ifRWEy0DBQysf7xr8Bwil+L1vZneMBDIY0Sylmur8GEwqVYmWZfYGazyx9XhlKnP9nsSA28dUe0BZJYyi0mfV22TLwE24jNhMMeOOh1S6p+zsCgYA10MAROHpNE0E18OBuwuQTiAs1Ee7uj9c4wPyctwZX5NUeCO8hiF6aMqhnUjCDHXl+iSoFKfZsn+v08pUw59LHjTKiDa6aStqfwKv0itSAveqefm0WxKlIfM/7oEN3+uEc7EShWgrf+pPhf3m2sxdJEdMYOLMXT72gXaz8HNUCoQKBgQDbPiuM6yV0oLRm5RK2GfnqxulqavHqZtaX5oHFipD3czyqFR0EZp1GOds7t8srMgeleVFzArUnOTj5XLwD3pW6/YuNwCl3m4XGX9x00xxf0+k+fVQRy6b2dyBzJ9XnekeokSvVqq5j9rf4s1xnTvBzliT6L3XCNc+/Y2Ay0eT1bQKBgGO382A6Da/9rOJGlAI/pp/RqzOWyYlkf6bqOTHgyORaOu+4b+sKNz0aXbmfhSHqLXhYrdHcSr2fkaIJDp7WBTsYyfu20I2x22LcQ2BMi8WBmXrRnN8UYhvtxijyBo7lZeTOQuKFDclWk0gmji6RWL7pK+x8citQevstb9xvbZIN", + ), + RegistrationTestDataGenerator.importAttestationCa( + "MIIDYzCCAkugAwIBAgICA5MwDQYJKoZIhvcNAQELBQAwajEmMCQGA1UEAwwdWXViaWNvIFdlYkF1dGhuIHVuaXQgdGVzdHMgQ0ExDzANBgNVBAoMBll1YmljbzEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjELMAkGA1UEBhMCU0UwHhcNMTgwOTA2MTc0MjAwWhcNMTgwOTEzMTc0MjAwWjBqMSYwJAYDVQQDDB1ZdWJpY28gV2ViQXV0aG4gdW5pdCB0ZXN0cyBDQTEPMA0GA1UECgwGWXViaWNvMSIwIAYDVQQLDBlBdXRoZW50aWNhdG9yIEF0dGVzdGF0aW9uMQswCQYDVQQGEwJTRTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK1Ei94EwmfSUSH5AAl1TVOY8qFzZb7W0G3r3hsucQvfbW5Q5dFHfViqPbpyg5EF6WT8Pmz3fsU437Yif8On7Q5xcA3586g/Arr/I0IdVjDGwrIXomkAfMzA8gvzy4I5tnedpVMD9GtrzAcqaq83OdmCYyNE70uNHkCR8YYkuAXFvDSTGhwb8IowOAIjpgo92qIoxGfbag3aPwnnfwejj5GiVhXn6CGIpLs9xtZ02kKy34BvXMfbjQqW/eGivmJTaaD3+jC+lO9yUQy/JN7tE/RFb9XkzyWcX6FYzUkyzkoWvk7ZRgpDQhOevAtkKk/Y4kIRqdjQkWXLMAN+0gedfb8CAwEAAaMTMBEwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAQMtEeceTZrToMIR9IBkxFxlaE7HbTrp2EZmPhKeKCFnYwGCiDw69qhX8/92pldQNr70rmqgIfhdI/qwCUp4Uvj/2r5teSEBdPqcvZ0mSGI8MHZF69onbmvxNMeKqFVWbf4B3G2PSpKEZ0RVbW5yiuVB/Lt8PsYSTPCsecS2QShlsXSMqZpX2Yr1P+JQtkGVCnTsT1KNnNJ53f91rlqfnEqM4EISnqaqoay5wvWdeNkNseJPjvz9hQbvtbSBjKzrKxDLAVWseXRdAELXUGxPxS+2ZMd44bqgvRdHGJhRz9iL9VhCyCvkyUC+zV0aIVuEDrvG4kdfDa13YeuE9NcQY0Q==", + "RSA", + "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCtRIveBMJn0lEh+QAJdU1TmPKhc2W+1tBt694bLnEL321uUOXRR31Yqj26coORBelk/D5s937FON+2In/Dp+0OcXAN+fOoPwK6/yNCHVYwxsKyF6JpAHzMwPIL88uCObZ3naVTA/Rra8wHKmqvNznZgmMjRO9LjR5AkfGGJLgFxbw0kxocG/CKMDgCI6YKPdqiKMRn22oN2j8J538Ho4+RolYV5+ghiKS7PcbWdNpCst+Ab1zH240Klv3hor5iU2mg9/owvpTvclEMvyTe7RP0RW/V5M8lnF+hWM1JMs5KFr5O2UYKQ0ITnrwLZCpP2OJCEanY0JFlyzADftIHnX2/AgMBAAECggEABy7pUYoG+UDp5iupibrYOtgDbxgWpsPHHleB/MR/IUvhAIrQDE4Xbz6Xkow+0htZorsmZ2QXWFvUQnvJqjXjCQ9A4wNyy43ZMiFzt8D5msoStklujUXc5qw1HLO9wydbXjgl63wlfPKaIc3rYFo8xry2GXc5KHuwPmMOjU4mZu7LOLrxPaT0tUnXuhfOGMZ16w+zYQHUhWuXiFc/0A5a6MwoBUmdZP7AXTlor8LoF5HUYz3sjQGk1mG/o528WuQfNidh87ica1YUGrG4WT56cZUzDTdlwWnYBTHxtIP/tAszPUG5Ic9VjfjGZ6Gqt0lwrNnYBbiJJ3zDvswfAqMYwQKBgQDVit5Udp08XKF1R9Z2ybM4z0m9Oq/yHeJqw3VYuiy1n9Uelg6/W5SZpHdeXG6VbHDZxDm1QEpKwzmMwgFRwg0gRJjWlmRCf2Vejk1XW8Dxk9L890YBfdGsbrFQBtr+FwCtoHfB/8EFOYLkeMf5wif+c55JwIsulV+V7HYUQdZ/3wKBgQDPt7rOEjVFog7OatKVncSzlRkG69rD2zqK4ga4+g/2802QTvtMJKHZJ2Eg2is3sf1u4TWUaL0WLj8x3dQeRNWun37hu3c8HLY4zfvtdPYUnVkLF60grrUtNgyxUgL+RRdzqj0cEH63mAdEK/7+zxDnvH/rNPS+fVMVFyQVa+E+IQKBgQDGg9OOF7qyi7Z5bfAc/ANFs8ZsSOuaHFgJQm2Lr3+y1MRuK7fIAx4Q+wkRSsJu3KHIgBfZvMuT1wtgJFbPp6NGNR8UljjcbMxS691QcfbbXb4N9t44srvCHiFuMQFSpxW1U3Ehg13wOnfJZ9MYB3vgm6EyFPIOu0Rh/rICwPXkZwKBgQCCNVAagXt3bQEPEBN1ynJViG8p0YtPHwvxp4JDTi3XxeinP3tz3bq/H1pZd6mDvkV5zh8CKy3sy4y9u6qOVuQEFOM6qYMy4WSw8x6rWZgwj/oTZAIY7KuR7cHDHf/WWIU88khgYU6t09UqPNIZ9L9KJPWjAY0yI+mC3QC3lOqbQQKBgBFbstg/DDnVeCnXnd6mrwz0k7oyXsByIUNWNoXfkcb3MJn7LIe3NchfF0jXSqHJmji+pRHRqiMTkohoe27DNIZSd0zFPFuxPlP3tNE3kcjI31n7CL4VTZCHXmzQwgRTGZnpsMA0xkROVVYn75x9yoEgZmH8svtKVpjhm98AP7p7", + ), ), - clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","type":"webauthn.create","tokenBinding":{"status":"supported"},"clientExtensions":{}}""", ) { override def regenerate() = TestAuthenticator.createBasicAttestedCredential(attestationMaker = @@ -173,10 +217,19 @@ object RegistrationTestData { val WrongHostname: RegistrationTestData = new RegistrationTestData( alg = COSEAlgorithmIdentifier.ES256, - attestationObject = new ByteArray( - BinaryUtil.fromHex("bf68617574684461746158a449960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97634100000539000102030405060708090a0b0c0d0e0f0020bdbf5179112add51874c0824f0d860083d1ee7cb7f44c25ffa26c39271c3ed0aa5225820c35b4c850d7c13334432f7f17a5f1f9a5b1c85bff9b407dc0c962f0eb95eb9d10326010221582041d565e8ca57023f4510f150481d551f84f2dd09894fbc20d1c4707896943638200163666d7471616e64726f69642d7361666574796e65746761747453746d74bf6376657268313437393930323168726573706f6e73655907f165794a68624763694f694a53557a49314e694973496e67315979493657794a4e53556c4463315244513046735a57644264306c435157644a51304a55613364445a316c4a53323961535870714d45564264306c335957704662553144555564424d565646515864335a466459566d6c685630353253555a6b62466c72526a466b5232683153556857645746595557646b52315a365a45684e5a31457752586845656b464f516d644f566b4a42623031436247777857573173616d4a3652576c4e513046485154465652554e33643170525746597759556457645752486247705a57464a3259326c43516d5249556d786a4d314a6f5a456473646d4a715255784e515774485154465652554a6f54554e564d4656335347686a546b31555a336450564545795456526a4d4531715158645861474e4f5456526e6430395551544a4e56474d775457704264316471516d354e553031335356465a52465a5255555245516e42615a46644b63466b794f4764574d6c5a70555668574d4746484e47646b567a56775a454e434d467059546a426a656b56515455457752304578565556445a3364485631685761574658546e5a4e55306c335355465a52465a5255557845516d78435a466853623170584e5442685630356f5a45633565556c46526a426b52315a365a4564474d4746584f58564e55584e335131465a52465a525555644664307055556c5244513046545358644555566c4b53323961535768325930354255555643516c46425247646e5256424252454e44515646765132646e52554a4254573432535539336255786d5356427253546c5256315236546e6c5a4e304d3464444e6956444a5254484179654374324b3342325a6c70424d47686b5a31457852586875656d78574e57314f4e334a744d4735354b3046695a334646546d397265545533646a45764f57644359334d314c7a464e4c315a36555464735743746c4e325275516a4233516c42755355524b5a4374304d3064345a6d597659544645596b4977555535364d567033516a4d325545744f4e5856594e3274364b314d35516b5668616b644f4e47786a5a466c425232644654454a33513168745a55465a62316c47567974475a4763345a473034626d6f76575778314e3168426248464d4c7a423661577053576b6c535530704c4e46705157454e6f626a42314e3246464d553578567a52696332566a536b6c70656e5a34534868546547567257544134656b4673656c4579636a5235626e70754e3235334f476f30566a6c3357575254526d347a5630396d616c5654565578326344453061546c324f4549324e6d677264565a6f4e4578504d3231365a586c5652445a444e587035516e5a5a5757706a6256565a516d383162585268556a527164334250517a6c4963585a4b536a5a4c4e46453364336c4651304633525546425955317354554e4e64306c525755784c64316c43516b4648517a565364304a4255564646525764525555464252554e4264314647516d646a53554e5262307845515442505248704253304a6e5a33466f61327050554646525245466e546b6c4252454a4751576c46515445354e4442744d6e426865485a615645355251584e4953544678516d68756255737253335a564e7939704d6b52714b306c50623239775956564453555a6c4d453945515739584f4735484f544a74654531694e6d314a4e324e354c304d355a6b4535565445335755687161334673656b6b7a623273695858302e65794a68634778445a584a3061575a70593246305a5552705a32567a64464e6f595449314e69493657794a4d5132457759544a714c3368764c7a56744d46553453465243516b3543546b4e4d57454a725a7a63725a79745a63475670523070744e54593050534a644c434a756232356a5a534936496b4e4c544856715958413157444a475346566e61537444636b353153473072556c4a7456484a4b566c5642656b46504f5667305569746c557a513949697769595842725547466a6132466e5a553568625755694f694a6a623230756558566961574e764c6e646c596d4631644768754c6e526c633351694c434a6959584e7059306c756447566e636d6c306553493664484a315a53776964476c745a584e305957317754584d694f6a45314e546b314e6a6b794d7a67334f545173496d4677613052705a32567a64464e6f595449314e694936496b7844595442684d6d6f76654738764e5730775654684956454a43546b4a4f51307859516d746e4e79746e4b316c775a576c48536d30314e6a5139496977695933527a55484a765a6d6c735a55316864474e6f496a7030636e566c66512e54672d627854616c77687a78794c52415837576c66432d595a4e3670484a56434b4b6a5952556d6331467166496e45396a335847636f32596b697974306c4456635266494934314d4c7134464733585f4c476773553649344d323572706e69796b5f64706278684e486655564c4c326d4b4f314546704d4b6d51787549644d51584f33635f31746a42386f6279334c524179545652366a337144463449495f676b466a69755a5a4b42715741746b66694f636f78506b3559474d452d6f525968694e6879457063446650376a5963365443414861682d714c5337696a7a5a48736c6a504a326f6c534e7a593673587550316b544650475744496d4e5647616f795f4a63576a626e476c705f585932506e46716f6247424a49714d43446354674f344330335934746879387343557849365f484f43754c694b6d5a3268536554673076544873635052744c444733755969464d65355051ffff") + attestationObject = + ByteArray.fromHex("bf68617574684461746158a449960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97634100000539000102030405060708090a0b0c0d0e0f0020086222018c6696a6f4dfaa0154926508e68bbf94e68c929e9092ab7d6cc18ccaa5010203262001215820261d97d11aecbe36d1c2ae680454a6ee4f43a662d1d3a52f2b024c6fc824549b2258202db58db4b22bdd57693de51703f32853418abd8402b89acb534811ddc259ec4c63666d7471616e64726f69642d7361666574796e65746761747453746d74bf6376657268313437393930323168726573706f6e736559094665794a68624763694f694a53557a49314e694973496e67315979493657794a4e53556c45596e7044513046735a57644264306c435157644a513064475658644555566c4b5332396153576832593035425555564d516c4642643170365257704e5130564851544656525546336432465857465a705956644f646b6c475a47785a613059785a45646f64556c49566e56685746466e5a456457656d524954586845656b464f516d644f566b4a42623031436247777857573173616d4a3652576c4e513046485154465652554e33643170525746597759556457645752486247705a57464a3259326c43516d5249556d786a4d314a6f5a456473646d4a715255784e515774485154465652554a6f54554e564d4656335347686a546b31555a336450564545795456526a4d4531715158645861474e4f5456526e643039555258704e56474d775457704264316471516d354e553031335356465a52465a5255555245516e42615a46644b63466b794f4764574d6c5a70555668574d4746484e47646b567a56775a454e434d467059546a426a656b56515455457752304578565556445a3364485631685761574658546e5a4e55306c335355465a52465a5255557845516d78435a466853623170584e5442685630356f5a45633565556c46526a426b52315a365a4564474d4746584f58564e55584e335131465a52465a525555644664307055556c5244513046545358644555566c4b53323961535768325930354255555643516c46425247646e5256424252454e44515646765132646e52554a42546938796256566f5a475677546e6c6c4e554e75537a424765484978636d4576635670595244426154565a7953545a4b596b677a515863344c7a6c6e55487036536d4a3663484a364e57565256576869656b4a42644664705132314b516d746a516c5172633039484e544a565655397462797450626e686962453975574646684e33517259574e3062465653546e56456130783357433968535642474b7939425a32706a566c5653616d567856575673546c5a52644777314b7a523162324a35536a46525155354765444d325132786f4e464643636d6b3362574576596a6c774e6e45764e454a6a54476c4b656e466f62484243545670784c31644a64475a48627a525062574e71626a6861545664574e473434544570354e6a5a51576c4d3452555a3661484a4a4d33527157477831556b744c57464a316330464c65555676645556595955746d4d3246795456426d55466b76626c423457455a324f57524b516a52584e6a497a596b49325469746b4e44417863335a6b6431427a533270785a3070465555783253485a304d6e644c5233684454544e334b7a6855623246464e5652486430644a563255354d3370704d57787a57474655536a56595a6d644f4d6e46735230343451304633525546425955317354554e4e64306c525755784c64316c43516b4648517a565364304a4255564646525764525555464252554e4264314647516d646a53554e52623078455154425052487042546b4a6e6133466f61326c484f586377516b465263305a42515539445156464651584a6d537a467356554a6a64474d7a63314a75614664306247744861544a706546684a647a526156453833645752424d6b6f79646e4a5a646d3132533342584d4856705a6c52564f475279546e426f4f4845315655746e6457567352324a57596e4a496545525352444e735344464c5645746b59554a30626c557255576457566a4a31626e5a586245633153453152634556755354527464537334546d523062544a34566d4645633045306555684c61306c42526a4a525a5864755747525465545179596c704b523168504b305a7752325a5262335a4256456443616e5269656b786e52315235546d74796133704d527a4e6e596b5a3253455a4c574752574d4656595a3239575a564677626e493056555a50557a686e545539765244426c53486c3655444179546a524c614649784e486431626a424d4d335a68643146784c336c355a48553465485271626b70515133564a536e684d616939795658644f6345704256337042556a4978547a645a4d46704a4f57466b593064484f476c445457785a593056734d6b6c6c4e44563453564e704e4668754d6d56754f4746535a466f3463303079546d684b576c56336230566d4e334653576d30305232684f5554557962306c7255543039496c31392e65794a68634778445a584a3061575a70593246305a5552705a32567a64464e6f595449314e69493657794a4d5132457759544a714c3368764c7a56744d46553453465243516b3543546b4e4d57454a725a7a63725a79745a63475670523070744e54593050534a644c434a756232356a5a534936496e5a59546e4a4d53465572526c4932554734795332744a537a5a50525846364d575a6a52323556566d56615a314572576c5534526d56524d334d3949697769595842725547466a6132466e5a553568625755694f694a6a623230756558566961574e764c6e646c596d4631644768754c6e526c633351694c434a6959584e7059306c756447566e636d6c306553493664484a315a53776964476c745a584e305957317754584d694f6a45324e4451344e7a51774d5449304e445973496d4677613052705a32567a64464e6f595449314e694936496b7844595442684d6d6f76654738764e5730775654684956454a43546b4a4f51307859516d746e4e79746e4b316c775a576c48536d30314e6a5139496977695933527a55484a765a6d6c735a55316864474e6f496a7030636e566c66512e3079686a3746554e48595f755643314a73455a64686a56646e59492d31716d5f496b704c56773377443976744d325774736d58736b5041496f6f466c44433363484f7236386b625f6b5f774f7933325944657275366e4631307778563871565439617a2d515775357a7849727536795678744b344f51753557416a4553796e71524d4664795572706e3956776e39474652546275554c2d424a5a7a554f6e54365746564f42565a4f5a4f41375f656d4d72734f7137506e6d51415574704b703863376358676e39474f317742366b6c5174464847334a4f6b77757a2d377457694177434f4965756e3444377259734f77564b77507252427169585f66486c737974552d61385f61706c6772624851332d466a676d414f483659754c484d7536684746576f4461575a7459756c657556697a414b5a6a45505f70774d756866716c5a69553863505a772d4f5f767471416e6f7139524c51ffff"), + clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","type":"webauthn.create","tokenBinding":{"status":"supported"}}""", + privateKey = Some( + ByteArray.fromHex("308193020100301306072a8648ce3d020106082a8648ce3d030107047930770201010420f686e0434d6dc83eff5ac2f4288e79bdafeecb266ff76636e0cbff22f78790a9a00a06082a8648ce3d030107a14403420004261d97d11aecbe36d1c2ae680454a6ee4f43a662d1d3a52f2b024c6fc824549b2db58db4b22bdd57693de51703f32853418abd8402b89acb534811ddc259ec4c") + ), + attestationCertChain = List( + RegistrationTestDataGenerator.importAttestationCa( + "MIIDbzCCAlegAwIBAgICGFUwDQYJKoZIhvcNAQELBQAwZzEjMCEGA1UEAwwaWXViaWNvIFdlYkF1dGhuIHVuaXQgdGVzdHMxDzANBgNVBAoMBll1YmljbzEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjELMAkGA1UEBhMCU0UwHhcNMTgwOTA2MTc0MjAwWhcNMTgwOTEzMTc0MjAwWjBnMSMwIQYDVQQDDBpZdWJpY28gV2ViQXV0aG4gdW5pdCB0ZXN0czEPMA0GA1UECgwGWXViaWNvMSIwIAYDVQQLDBlBdXRoZW50aWNhdG9yIEF0dGVzdGF0aW9uMQswCQYDVQQGEwJTRTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN/2mUhdepNye5CnK0Fxr1ra/qZXD0ZMVrI6JbH3Aw8/9gPzzJbzprz5eQUhbzBAtWiCmJBkcBT+sOG52UUOmo+OnxblOnXQa7t+actlURNuDkLwX/aIPF+/AgjcVURjeqUelNVQtl5+4uobyJ1QANFx36Clh4QBri7ma/b9p6q/4BcLiJzqhlpBMZq/WItfGo4Omcjn8ZMWV4n8LJy66PZS8EFzhrI3tjXluRKKXRusAKyEouEXaKf3arMPfPY/nPxXFv9dJB4W623bB6N+d401svdwPsKjqgJEQLvHvt2wKGxCM3w+8ToaE5TGwGIWe93zi1lsXaTJ5XfgN2qlGN8CAwEAAaMlMCMwIQYLKwYBBAGC5RwBAQQEEgQQAAECAwQFBgcICQoLDA0ODzANBgkqhkiG9w0BAQsFAAOCAQEArfK1lUBctc3sRnhWtlkGi2ixXIw4ZTO7udA2J2vrYvmvKpW0uifTU8drNph8q5UKguelGbVbrHxDRD3lH1KTKdaBtnU+QgVV2unvWlG5HMQpEnI4mu+8Ndtm2xVaDsA4yHKkIAF2QewnXdSy42bZJGXO+FpGfQovATGBjtbzLgGTyNkrkzLG3gbFvHFKXdV0UXgoVeQpnr4UFOS8gMOoD0eHyzP02N4KhR14wun0L3vawQq/yydu8xtjnJPCuIJxLj/rUwNpJAWzAR21O7Y0ZI9adcGG8iCMlYcEl2Ie45xISi4Xn2en8aRdZ8sM2NhJZUwoEf7qRZm4GhNQ52oIkQ==", + "RSA", + "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDf9plIXXqTcnuQpytBca9a2v6mVw9GTFayOiWx9wMPP/YD88yW86a8+XkFIW8wQLVogpiQZHAU/rDhudlFDpqPjp8W5Tp10Gu7fmnLZVETbg5C8F/2iDxfvwII3FVEY3qlHpTVULZefuLqG8idUADRcd+gpYeEAa4u5mv2/aeqv+AXC4ic6oZaQTGav1iLXxqODpnI5/GTFleJ/Cycuuj2UvBBc4ayN7Y15bkSil0brACshKLhF2in92qzD3z2P5z8Vxb/XSQeFutt2wejfneNNbL3cD7Co6oCREC7x77dsChsQjN8PvE6GhOUxsBiFnvd84tZbF2kyeV34DdqpRjfAgMBAAECggEAR00qeqvsCMIzTZezAThQ0/ORi+J+pepK1Z4GfzR9QF8kExkMguhaJqKj4TrGO832Eyo0qQ+Y0U5OgOaaoc4m7dpBktfytyxeCAiUZOFCIRXyK2R8oK+5zN+yJaED8mxbUPM9/fWewdHSqyaiRVcBk6yVvf7E+IVSb3MDX1RdC1JHaHpZb/rxz2F/mcGznmoGmNrrteEH6dygsH5QY9nr51dWS2bX0EqAGBrAgA+F4360MrhRclOjQkHHrhPJMqkAUVKBvvUjASoLEIg77/d/Aj8QWawltZXL20olhyyCBRPbK0US6v6pMNcXNVlGhAD5pJJniqD0yViGJxwVLc9sWQKBgQDznU0wG5GrSNRp5tQN+u6qihGCfnmFRF5baa8EPpd66X2q37LjX3Wz1nfkyNXy3aC6JuT2noWuUD9X8NXvNpYCv/8jHBp4AL9OGTpmNTjnyqWmIoZv4sFgt7bic9tcTOjYgL5J6ZB/g3crq3LDmkiL5aWGdvUtP39wtiTYpSZYSQKBgQDrWYhV9h5mSzrE7T7MzIcsTpgP2gaysyObgFFE+n+5o+NLJttEQBLOaNd7XB10wEdv84wZy6acGvvbklMVXaAntohxBSgiMBb09Z60YofOQKHlJ6dYSq3JAtLnEK3J/2KCBYL9Z/BsA5/6n5Z0KSO/3cCvA96l0Y99wtPAqPX35wKBgQCRz6L1mmqz9KF+yXQ+8eSMGpukWYLuqx8246inh7cvEDXxYnc19FsEyudz/mlgNhsPkFwW6Ibm8I0ZW8MQrMFY4AYbw6RsEzZtzlfP+ScYRYikSaHhsf1AoHVMUUAInNf5TgWXQ78DM3LOpo3IWb32Tfum4eiZrpneooanTSIIUQKBgQDodXqwTXUhXNUjhaIt7ybkoIyZu6Q6Ba75/PhIxZQ67KGorSyOcSsiLXQJKKb/lpv4+/o50Gk0b4KtEg52YA+8qhKCb7GAczd5pNGpIlk5Y6WFDnHAR6L3lI50JIlDp7jI7GDBo1RZnAr8JX0aJzhkXsffFldoOWEdur4k4b5xqQKBgFGU5Ueoatl+PW09lyYvcyUY9bPg930dZ/lYNSmtPSAjSIJz2lQSqVG0v92frkaQwtAFBSwDpdGSP7jp6paHAOrQGKT2k9XN8njzklAjiPYGAnKu/ykk447bwOoz/+rS2gETL/i8vMmRVhbVVwthecor8wvLbM6x4qvKsYPeubTv", + ) ), - clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","type":"webauthn.create","tokenBinding":{"status":"supported"},"clientExtensions":{}}""", ) { override def regenerate() = TestAuthenticator.createBasicAttestedCredential(attestationMaker = @@ -188,10 +241,24 @@ object RegistrationTestData { val FalseCtsProfileMatch: RegistrationTestData = new RegistrationTestData( alg = COSEAlgorithmIdentifier.ES256, - attestationObject = new ByteArray( - BinaryUtil.fromHex("bf68617574684461746158a449960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97634100000539000102030405060708090a0b0c0d0e0f0020a3c7abed63a16885b1cbf572b1bb29c6614812a886c222a00e07bc7c600764e4a52258209ab2b20ef874f994a3f99ad8f1e61ba590cf67357e5c0997dafcb71edf42ff9b0326010221582027834a6fcb02f7d3182bad5f2b3d16856c93fb574703503a847a36e457996275200163666d7471616e64726f69642d7361666574796e65746761747453746d74bf6376657268313437393930323168726573706f6e73655907e765794a68624763694f694a53557a49314e694973496e67315979493657794a4e53556c4463565244513046724b32644264306c435157644a51304a55613364445a316c4a53323961535870714d45564264306c335957704662553144555564424d565646515864335a466459566d6c685630353253555a6b62466c72526a466b5232683153556857645746595557646b52315a365a45684e5a31457752586845656b464f516d644f566b4a42623031436247777857573173616d4a3652576c4e513046485154465652554e33643170525746597759556457645752486247705a57464a3259326c43516d5249556d786a4d314a6f5a456473646d4a715255784e515774485154465652554a6f54554e564d4656335347686a546b31555a336450564545795456526a4d4531715158645861474e4f5456526e6430395551544a4e56474d775457704264316471516d5a4e556e4e335231465a52465a5255555245516b706f5a45685362474d7a5558565a567a567259323035634670444e5770694d6a423452487042546b4a6e546c5a435157394e516d78734d566c7462477069656b567054554e42523045785655564464336461555668574d474648566e566b5232787157566853646d4e70516b4a6b53464a73597a4e536147524862485a69616b564d54554672523045785655564361453144565442566432646e52576c4e5154424851314e7852314e4a596a4e4555555643515646565155453053554a45643046335a3264465330467653554a425555524d5a47317863474a616244687162454d3254304e364d484e7052475a7a4d48525157455644536a4a6e5230465253304e504d474d7261303832597a4a6d536b354b53473479617a427661553534644468595a4859794e6b704a5479394c555538764f4649794f55394b595567305233677a6247463656336c7065574d765a6e5651614845775a6a645a4b31686c536d354f575374494d3039365a4770534d474674575667345430565763565645526b74316358467862444251536e6c7a4e6a4e31516c70505548563256585a5556793931626b395a5430706d563352725655393563446c6d593235725a6d354c5230646e51326477574756485547786861486b3165455a465333564b4e6a4673536e565063455a5361484a725545737a596d78424f5455785745466f576b4e744e31685252444e4856323578636d687361546834567a4a79546a424b4f585a7a4e6a524b4f453169526d46724d465673546d353561305261623342724d46706a5746464252556f30545664595231527a4f44463554465a574f544e425931597261464e48534642545a3370334f4731564c32784d4f456452553235455530686a527a4251526c7042595456524d32704661307852536d59345745466e54554a4251556471536c5242616b31445255644465584e4851564652516d643156574e4255555646516b4a4a5255564251554a425a303146516c465a53454e42613074446433644f5247633464304e6e57556c4c6231704a656d6f77525546335355525451554633556c464a5a324d7a56316c5856533936596e6c3151334e6d6345704f4f544a46575563766457644b4d6c70564e31564b4d57526b4b324d76576b645252576444535646456232733461336c565a6a4e32535374735456687a5a565a57645452754e5667315332314d636b3575566b68485757566d4d326c565632457251543039496c31392e65794a68634778445a584a3061575a70593246305a5552705a32567a64464e6f595449314e69493657794a4d5132457759544a714c3368764c7a56744d46553453465243516b3543546b4e4d57454a725a7a63725a79745a63475670523070744e54593050534a644c434a756232356a5a534936496b59354d5559354c30526c616b49345a6a4a31516c646b596e425764557076655764346257744865486c77576c52516256646d625746695a57733949697769595842725547466a6132466e5a553568625755694f694a6a623230756558566961574e764c6e646c596d4631644768754c6e526c633351694c434a6959584e7059306c756447566e636d6c306553493664484a315a53776964476c745a584e305957317754584d694f6a45314e546b314e6a6b794d7a6b794d545973496d4677613052705a32567a64464e6f595449314e694936496b7844595442684d6d6f76654738764e5730775654684956454a43546b4a4f51307859516d746e4e79746e4b316c775a576c48536d30314e6a5139496977695933527a55484a765a6d6c735a55316864474e6f496a706d5957787a5a58302e415a6938386e4264644c315a4d336c4c6a654943724332384675386a657063573937686e79446a4d327a366b7870322d49514c484d3779706468674d424c386b307a6637436572647272634c4e63345671494f7a694a694677334752704a656a4a61625f65734d7137324f71742d3855727a48506849476d494251744347547647646577654551715453656131784f6b356576786a4f667630564571563272497263562d46445f5568527437586e38654479536658744e4254784a4765374a727858436b537061374f65465932577a4d6d76536e316c745f482d507a35784b6a5f6665564e34317362425a4e70624649496c724879324e4361374e676c5a50347a6948373769766979316e564f4977517a5866545a496b6b6f7434426a775630484a6557776a447166577375726c467578346b32715047327a6a41574b755a575877617137584751565f53687068384f38652d4d4977ffff") + attestationObject = + ByteArray.fromHex("bf68617574684461746158a449960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97634100000539000102030405060708090a0b0c0d0e0f0020d98f9662aab53c0fef67d23b3503f26f1e4f79ee0a17b23c4b990321e488f9b8a501020326200121582086a6e783d0e3d9766b41403ed77291d5469adb69f8a47338759f3cd0d3831531225820fea6b90121fe5b3b5eb95f3e5839d306069b3a5bac790b9257810d7a56815bca63666d7471616e64726f69642d7361666574796e65746761747453746d74bf6376657268313437393930323168726573706f6e736559093d65794a68624763694f694a53557a49314e694973496e67315979493657794a4e53556c4559577044513046735332644264306c435157644a513052796333644555566c4b5332396153576832593035425555564d516c4642643246715257314e5131464851544656525546336432525857465a705956644f646b6c475a47785a613059785a45646f64556c49566e56685746466e5a456457656d5249545764524d45563452487042546b4a6e546c5a435157394e516d78734d566c7462477069656b567054554e42523045785655564464336461555668574d474648566e566b5232787157566853646d4e70516b4a6b53464a73597a4e536147524862485a69616b564d54554672523045785655564361453144565442566430686f5930354e56476433543152424d6b3155597a424e616b46335632686a546b31555a336450564556365456526a4d45317151586458616b4a6d54564a7a64306452575552575556464552454a4b61475249556d786a4d3146315756633161324e744f584261517a5671596a497765455236515535435a303557516b467654554a736244465a62577871596e704661553144515564424d56564651336433576c4659566a426852315a315a456473616c6c59556e5a6a61554a435a45685362474d7a556d686b52327832596d704654453142613064424d565646516d684e513155775658646e5a3056705455457752304e54635564545357497a52464646516b4652565546424e456c43524864426432646e5255744262306c4351564644545842706333707563474d7656484633525731694f4868574d5642724e575a3462546b32556a5276556b466f656c6c58634746745a5864564d6d4e4e4d564a52643070725754683552445230535339464d584d784b7a4d30624642744d6c425856546879636b355161334a31546c4a52526d4e694e326f3365484a33655856765931525562334a4d566a5636636e70794e48466a536d356162566c7a614442786246526e4f455255616d467663577468646c4e4352474a505755317a6130677863304a59565774304b304a565444425063454a32535568474f466c5062324e30566d56795a30745a656a6c4f6558466f536a4e43524749784d336c324d305235516a4e695443394c546e457963566377637a6c4c5a6a42304d577879546a4a5653486f3153444a794e797432616d6477656b7448575567325747733055324a33646c4e6d4b33646855574e715345564c5a314233565552554d5842584d3278354e6c4e745757677a5a4549345a3170695155704a5755707a4e575651636d4a5a5643743361335273535735734c79396c5755784d5332393551584e6956337046615664616255396b62325246545668364d46524b54444649526d4e35566a6c4c646b7875643152425a30314351554648616b70555157704e5130564851336c7a5230465255554a6e6456566a5156464652554a4353555646515546435157644e52554a52575568445157744c51336433546b526e4f48644555566c4b5332396153576832593035425555564d516c46425247646e52554a42526c6f3563334a73556a42535630733261474677633356315956704a656e6c525a7a6448654756434d305a355930744655456c7263453033635556335a54557252315635645735494c3342314e4535366448524a5a46646b5558647a5a6a68574e45557964335a4e555456474c335a6b556e6772513059784d6a4530536c55305232704d646e467a5a585232556e7033614652535a4764734e3231715547465563586c565457354656793945516e704e596b7777596d684352566c6b4d4777335432597a625339494d3351795a476b31524756695343396e566b46315a474976544856344e45315a56584270556e424b5446646a55445a72546a41764e6d706a547a644256574a5862575651564770585a6d46696132524361586849613163325a6d567653464a7356565a4e5446704d576a5a7565565a584e57396d6247465a62456c6c576b52506553744363465a4c596b3944526b633253467074546e463353316847636a5a4d636c6c784c30743352697372597a52755a79394d567a5231627a4231576e413461464e5656316c78566b565862474a345a464e4f4f55684953484a4a53577035646b35776443396c513264524d6b315152465a32524374545957566c52544a5650534a6466512e65794a68634778445a584a3061575a70593246305a5552705a32567a64464e6f595449314e69493657794a4d5132457759544a714c3368764c7a56744d46553453465243516b3543546b4e4d57454a725a7a63725a79745a63475670523070744e54593050534a644c434a756232356a5a534936496d7042644531686454677954564e6f5930646c526c6833534777314d5338764d444e45516a5254627a68524f4764784d6e4e57633356516232383949697769595842725547466a6132466e5a553568625755694f694a6a623230756558566961574e764c6e646c596d4631644768754c6e526c633351694c434a6959584e7059306c756447566e636d6c306553493664484a315a53776964476c745a584e305957317754584d694f6a45324e4451344e7a51774d5449314e6a6773496d4677613052705a32567a64464e6f595449314e694936496b7844595442684d6d6f76654738764e5730775654684956454a43546b4a4f51307859516d746e4e79746e4b316c775a576c48536d30314e6a5139496977695933527a55484a765a6d6c735a55316864474e6f496a706d5957787a5a58302e683351754d6c736e446b744648772d456c71615969683769776e352d3753494c4448316b487841476c316d636f717259694b5a4e647152383743564248346b7a4f6877535130777777724835775671705475564e4f364233726668664e4a387562586c4f78366b4d59615556567236674768573059696d536773486f61733250774e547958614f3644663874474143354f5a4449554b43484e3054676144467a4a6346735a764f584c436475596a4b43633653326f7249324e6256694b65797859636c42566342374e4d4a43667937666551643971384e7170727832522d4242687779484d4434644664394b573363626a49436d5876487059713266327644656d65363964582d5465705647327773363179594c35454b6e647a6a7a72626464797a63374173354a443446735452657043586d7a6f324863365935354c33325f74796c617a64464c456541386133643171786b613441ffff"), + clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","type":"webauthn.create","tokenBinding":{"status":"supported"}}""", + privateKey = Some( + ByteArray.fromHex("308193020100301306072a8648ce3d020106082a8648ce3d0301070479307702010104204afdd15339dbebb41b31fe65ed61b971f8e5aeab2ef9dc1f9ffcedd54d5b8145a00a06082a8648ce3d030107a1440342000486a6e783d0e3d9766b41403ed77291d5469adb69f8a47338759f3cd0d3831531fea6b90121fe5b3b5eb95f3e5839d306069b3a5bac790b9257810d7a56815bca") + ), + attestationCertChain = List( + RegistrationTestDataGenerator.importAttestationCa( + "MIIDajCCAlKgAwIBAgICDrswDQYJKoZIhvcNAQELBQAwajEmMCQGA1UEAwwdWXViaWNvIFdlYkF1dGhuIHVuaXQgdGVzdHMgQ0ExDzANBgNVBAoMBll1YmljbzEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjELMAkGA1UEBhMCU0UwHhcNMTgwOTA2MTc0MjAwWhcNMTgwOTEzMTc0MjAwWjBfMRswGQYDVQQDDBJhdHRlc3QuYW5kcm9pZC5jb20xDzANBgNVBAoMBll1YmljbzEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjELMAkGA1UEBhMCU0UwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCMpisznpc/TqwEmb8xV1Pk5fxm96R4oRAhzYWpamewU2cM1RQwJkY8yD4tI/E1s1+34lPm2PWU8rrNPkruNRQFcb7j7xrwyuocTTorLV5zrzr4qcJnZmYsh0qlTg8DTjaoqkavSBDbOYMskH1sBXUkt+BUL0OpBvIHF8YOoctVergKYz9NyqhJ3BDb13yv3DyB3bL/KNq2qW0s9Kf0t1lrN2UHz5H2r7+vjgpzKGYH6Xk4SbwvSf+waQcjHEKgPwUDT1pW3ly6SmYh3dB8gZbAJIYJs5ePrbYT+wktlInl//eYLLKoyAsbWzEiWZmOdodEMXz0TJL1HFcyV9KvLnwTAgMBAAGjJTAjMCEGCysGAQQBguUcAQEEBBIEEAABAgMEBQYHCAkKCwwNDg8wDQYJKoZIhvcNAQELBQADggEBAFZ9srlR0RWK6hapsuuaZIzyQg7GxeB3FycKEPIkpM7qEwe5+GUyunH/pu4NzttIdWdQwsf8V4E2wvMQ5F/vdRx+CF1214JU4GjLvqsetvRzwhTRdgl7mjPaTqyUMnEW/DBzMbL0bhBEYd0l7Of3m/H3t2di5DebH/gVAudb/Lux4MYUpiRpJLWcP6kN0/6jcO7AUbWmePTjWfabkdBixHkW6feoHRlUVMLZLZ6nyVW5oflaYlIeZDOy+BpVKbOCFG6HZmNqwKXFr6LrYq/KwF++c4ng/LW4uo0uZp8hSUWYqVEWlbxdSN9HHHrIIjyvNpt/eCgQ2MPDVvD+SaeeE2U=", + "RSA", + "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCMpisznpc/TqwEmb8xV1Pk5fxm96R4oRAhzYWpamewU2cM1RQwJkY8yD4tI/E1s1+34lPm2PWU8rrNPkruNRQFcb7j7xrwyuocTTorLV5zrzr4qcJnZmYsh0qlTg8DTjaoqkavSBDbOYMskH1sBXUkt+BUL0OpBvIHF8YOoctVergKYz9NyqhJ3BDb13yv3DyB3bL/KNq2qW0s9Kf0t1lrN2UHz5H2r7+vjgpzKGYH6Xk4SbwvSf+waQcjHEKgPwUDT1pW3ly6SmYh3dB8gZbAJIYJs5ePrbYT+wktlInl//eYLLKoyAsbWzEiWZmOdodEMXz0TJL1HFcyV9KvLnwTAgMBAAECggEAG9ETc0KW1DD5iXFk5FvKnlc0C6NHtonYOG8+06pVNoTQOTP9KaawNn49+cyFfKLst9/9ywa2z04QTt5WkHUT8B22bLsR33SqR42ohviSmRubdKmSZsPUSlM5mqbtBjDWU5ZVo34Dw2/E9y/edlo/+FKbfdrZLVlPzcJQa/1oyw9Ovo7xgwSXJX9dyNvNeHUso8Hx5uWYpyqZFmJOygXhkxZWYhBDASkSGg1cIAQPMS4r+N9HeeEsvb3HB6wLiqbp3U//RK/XIT6/UDL0iBX9isbKorO9jY24/gDiP0BDwsdavsy8Ji3A/ShUPZECNNfvjEv5OzB+BmdXS67shR8VFQKBgQC/NSE4py51kmWBs8wZ+cvNJNLm/TgJl1hwx4Qrf43RlPyrzgw48pQclJOVezfb94JbYieFWWHP87kM03RgtEuINsGQ4/Mu8SdeWbmim+UA1SJKwNKArvTL5gFedb2fVwYdBG15uRAJyhFy609Liu9iYMx3pe4WGFjNKd6n7kGjpwKBgQC8TzIV8E7o9J8IbAFYfrChv8PPM1FWiy4uFGnvcz43t+TWd/mpPEcafJZqZPhi2p/CQtE9KZ4l8TvqrezE4lVXrs1QUs/EsyqfmO598kIxjI3F0SHyKkdbTD7tj+Inep7Z/c8kfvutjl76Jp9WwaV0LuG6CuBONoirAa9AJJDhtQKBgQCrh1YOJKwg/PvipxDqHJUfq3EnlvG2aPcF9XY0L3FiGm2xEl8Ul0kXepIK/0bVJezjXeJmVhDRJKtVPjygpB0+TSDIgjWeXugaVBOcNI3zeUASH3i3yDwCzotb2fQKBV+OmHI9SC+DGKselMnF0xV9A6lpjIlRePXw1ybPL4Xi8QKBgALzbsJ/QI1QAAn+v0qmuZffTG87y6OCjNe2BC73bFstK43c1XG8exTELQs/x9CswmIl7+d4dnz7uceksBgpv9Ke76K5mX3onNthZyNcH4NtQ299Jn4IAZRBrp7EaXPa7RBXdN6KiuEeYQikgEy4viIC9hCXSQqQujWL0jY8HHUdAoGAOV0Ow6H9UORj0qWHwyTrpY8IPd4+Rsfj7Iqis/5rgGGB4sI0WqjkUs71mktE+TVFmKdxj3DnV4a5SFzP83Ti3dHZqT0Kea9phS5w6WB5SYry3tDB1vyTSHfIEPxmuEOO+vzQ/Jivg09kFhjFCU8buuapJQuMTOKa1gxjjffTkKg=", + ), + RegistrationTestDataGenerator.importAttestationCa( + "MIIDYzCCAkugAwIBAgICCNIwDQYJKoZIhvcNAQELBQAwajEmMCQGA1UEAwwdWXViaWNvIFdlYkF1dGhuIHVuaXQgdGVzdHMgQ0ExDzANBgNVBAoMBll1YmljbzEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjELMAkGA1UEBhMCU0UwHhcNMTgwOTA2MTc0MjAwWhcNMTgwOTEzMTc0MjAwWjBqMSYwJAYDVQQDDB1ZdWJpY28gV2ViQXV0aG4gdW5pdCB0ZXN0cyBDQTEPMA0GA1UECgwGWXViaWNvMSIwIAYDVQQLDBlBdXRoZW50aWNhdG9yIEF0dGVzdGF0aW9uMQswCQYDVQQGEwJTRTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMnR1N4c24tVbnocI95FlWBK80id6PPBxF9XKWCB1n8KSOAC/Bb6dhl591Pd/UrmUzwMwIO+VKMybrZ1WJM24ZRog2nrBggONi7zbr9AWE5kqaPBtrnWSrdHsc7R81x3XGmFWZDL2agVShgwDhj6UQGUObSp6poHQV/3iU58O9TOzOBsNwJ5IBxhqA+WWzDsE0LrVtjzzuS8DebFOsuH3ENpQQ+ht4JKrv3SMDAYcuJ8K4xmy0DcNycMZhEe2OxmCTrSLcCar5PcgzHrqRuEmtrjo2PNsZAyhomZXDSlWzdrbW50ntK56n9jMy6PmVQqtUEbS/9HHzhwNjgrU/fTTYsCAwEAAaMTMBEwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAeHXrscbkfGTIW4oBFJBh5nOI9JjjUzqCtrbvrloWGIDL6uPLVft0D4wTxQGJofuyOCEmcmMWgCbTtW4ic/Xy2F5ikAAF+74hCvrrzKjnEySxRg/AhK55WKssAEReUuwzzEEH8tnHcZVLlmO+roOAl4zdsbuiSKjoOald7BGdF7uTIDbFWh2hNf4Ser86xzICNdiUGxfvUq277tv44hDp34K0uAK6tTJ4ZPgp8E8KBEpWJKmKxZMu4sLyrCQpR+8+LfLDp6lBcTANkxFEc6qmiLtzWB3GwzZNaGKEfyk0iJR46Fd6qjFHqcwq7SGWAio+upJ5HjMdYb2n9SBqNS0nzg==", + "RSA", + "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDJ0dTeHNuLVW56HCPeRZVgSvNInejzwcRfVylggdZ/CkjgAvwW+nYZefdT3f1K5lM8DMCDvlSjMm62dViTNuGUaINp6wYIDjYu826/QFhOZKmjwba51kq3R7HO0fNcd1xphVmQy9moFUoYMA4Y+lEBlDm0qeqaB0Ff94lOfDvUzszgbDcCeSAcYagPllsw7BNC61bY887kvA3mxTrLh9xDaUEPobeCSq790jAwGHLifCuMZstA3DcnDGYRHtjsZgk60i3Amq+T3IMx66kbhJra46NjzbGQMoaJmVw0pVs3a21udJ7Suep/YzMuj5lUKrVBG0v/Rx84cDY4K1P3002LAgMBAAECggEAMN15aP4bujTJNw+xL5Qxgssi9KYnTT3RSjRUdGV7he77jiGq3/VsuuZpGAlb1tLFvHhp9HwVCh6f59WpyJs1KzXS+8ZIA5NNUzjly7DMsM52tIumroBQPuhBCQ6UmgLcgUAkW4bAAI6HDiw0fZ++FV/KSRALGZhAH/hTaolyAyhQs6KBDJmNSNZZIRUF6UDxt0jC+HOVk8ksaXvHVWF13189cjgVFIE5qwHbDvqzotb7d1I12hMuQPDvliqOG+Qk83a2Lv/yY1XJyGldrR4bt3GgGD5ohhxoN1xAp4fsqpPVHq1J2jK+0cXz3xhgmmZlWomZFQ+zaQMMbaTJ7P4oYQKBgQDPg3amcRbDNs+1vWmJqoWi3Qi8PQ839b1hHP/GDM3ROAWmTtG+1VUpwMLPmiGhKLQJ8IHZPXtm0NXa287IIwcwp0JjonXwNhXcJzhrBsyx0URyjX1ZV43TRXv86NXKVwUmiEsBA64iCsmbp6G+LGME0TsWCp1+H+nEoIu4PB1sZwKBgQD4+cknyCFZLgvAgty///9lvxfvlBuaXSv5N1gf/stqFld1jxkIioau7DVrBs2KdQM0rGM3YYOn8AG/bfAJdHla8CvNYZwssh3yYVD9krBpPQhSTKA5LDz1i2pb8aZTmE8M2A4/BNs8/hslez6nzq1Mp6YSvh6/eqOnVisBV9AfPQKBgEYhswbbb4r4SkisxC9PnyyEsUAVDsCl36QjjdncV+7elSI4vzBUnxymVfCdscHqpLY7P1cxLTR5Xd1Crmb7V6G81XYg4OUXElo+MxYQzTtHc2+XnAaGzZraf+XgtuhUcpwsMdUc7kv5A1wE0mgYTjrBj8uKOOH4XSQj8jSItJT/AoGBAJbUC25UKQ+ze18WZ9DQrtHeoAt9N/OdugPx6SsI2gXcnwMSu2GXOdxCMGIz7tSP6m9Ad6KXKoDUPtrPKPkxXEsg8Agtt8TD6qxpE/1pngFC/gWNcgrwp8VAviARFmfR/yGSyZ6XvJEIhz1/mgdih03Gyi7UiiAJlZbL9qWLowMpAoGADSJaHEF7RO9qylmQob1n3w6N6ao7nJd/7bDkJdwxJa8N0Oiv1eRkWevealnPKWF45RRUZcEtI5N1BsRa8C6ecplDmlZNfWAj+OnPn2pdev82yQ9wfc2SDiXaOe3jNuXZGS+PYaXzne4JUPXGJn3q6A06LsCEzqq50LkZFTWCb8w=", + ), ), - clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","type":"webauthn.create","tokenBinding":{"status":"supported"},"clientExtensions":{}}""", ) { override def regenerate() = TestAuthenticator.createBasicAttestedCredential(attestationMaker = @@ -211,26 +278,42 @@ object RegistrationTestData { val BasicAttestation: RegistrationTestData = new RegistrationTestData( alg = COSEAlgorithmIdentifier.ES256, - attestationObject = new ByteArray( - BinaryUtil.fromHex("bf68617574684461746158a449960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97634100000539000102030405060708090a0b0c0d0e0f0020e50fe8ab67d1e773463decf62cfe9a9d5928ece4fd98a013b80478301bb8e29ea5225820d06403b07cf09311ca10b2478979deaaad9c65751e749c503fe9fb935686fcae03260102215820bfa61c3ae256f6a887d2ae9b2075b5246896ba9f44a2a6874ab746acfe7db9e3200163666d74686669646f2d7532666761747453746d74bf63783563815901eb308201e73082018ca00302010202020539300a06082a8648ce3d040302306a3126302406035504030c1d59756269636f20576562417574686e20756e6974207465737473204341310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b3009060355040613025345301e170d3138303930363137343230305a170d3138303930363137343230305a30673123302106035504030c1a59756269636f20576562417574686e20756e6974207465737473310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b30090603550406130253453059301306072a8648ce3d020106082a8648ce3d030107034200040bd659232377a4f910fdcfccaec55511d00beacbdf417f49c9de938137f98df03971b3553bc11a2bd4ef5089ed290d15cc84e005443c794b13dc5e230916c591a32530233021060b2b0601040182e51c01010404120410000102030405060708090a0b0c0d0e0f300a06082a8648ce3d04030203490030460221008546464190caa7a603cd5c8dd60f30a23a9d227ca69603c1421c179092d8e4a1022100891b766c83b9def81518e354db14068d0ade9c8651927b347f4a63454b12add36373696758473045022100c88c93d88194e183f5522ec471a77f8a78d82fa7f99292f8d5f0c20cec6277d702203e289df8dd0568d9bd0b7d294fd30afcf3b264f5fb63f3163b46bb725c8fb31fffff") + attestationObject = + ByteArray.fromHex("bf68617574684461746158a449960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97634100000539000102030405060708090a0b0c0d0e0f00206c6bce2ee5169934d2d590abb976db458c0dcfcd9d82360fbc93c268668b56a6a5010203262001215820171d8294528e18ebcec47f0426f5aa9dcb4c8b7ab7d38609baf333c41c4d2c852258209d653007939949f6daee042081d6434a50d7eb8a4fafd142cc8189435b6a9f1563666d74686669646f2d7532666761747453746d74bf63783563815901ea308201e63082018ca0030201020202269d300a06082a8648ce3d040302306a3126302406035504030c1d59756269636f20576562417574686e20756e6974207465737473204341310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b3009060355040613025345301e170d3138303930363137343230305a170d3138303931333137343230305a30673123302106035504030c1a59756269636f20576562417574686e20756e6974207465737473310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b30090603550406130253453059301306072a8648ce3d020106082a8648ce3d03010703420004ea588dd17cbc7f702116e53a1caf6f40b51b30c89536bdcbe8ce70cd3c78dc61198ebeff86c5f012d57921c39f9b80342109c7a30c63bd341b15ad32076853bca32530233021060b2b0601040182e51c01010404120410000102030405060708090a0b0c0d0e0f300a06082a8648ce3d040302034800304502210091d462574131dd530219334938561c4a752c0177c7ff9e85af455248aca2debc02204ea8005deafd97acab96f509751f45ff12b57640162880dc6f7fa18ae44883d863736967584730450220362d1f5e267509d28401a6e9762c3d6ef22116c7f8e267cab9c5eb2ddb78673f022100bf8fc4e079570315669cf7580ea18ecc7bc15cd66125073c1d68f8ca220dc238ffff"), + clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","type":"webauthn.create","tokenBinding":{"status":"supported"}}""", + privateKey = Some( + ByteArray.fromHex("308193020100301306072a8648ce3d020106082a8648ce3d0301070479307702010104209c979b0f5035b44f2ac7ed29d4d20cc127895fbba82b69b0311dea9b2e57e8dea00a06082a8648ce3d030107a14403420004171d8294528e18ebcec47f0426f5aa9dcb4c8b7ab7d38609baf333c41c4d2c859d653007939949f6daee042081d6434a50d7eb8a4fafd142cc8189435b6a9f15") + ), + attestationCertChain = List( + RegistrationTestDataGenerator.importAttestationCa( + "MIIB5jCCAYygAwIBAgICJp0wCgYIKoZIzj0EAwIwajEmMCQGA1UEAwwdWXViaWNvIFdlYkF1dGhuIHVuaXQgdGVzdHMgQ0ExDzANBgNVBAoMBll1YmljbzEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjELMAkGA1UEBhMCU0UwHhcNMTgwOTA2MTc0MjAwWhcNMTgwOTEzMTc0MjAwWjBnMSMwIQYDVQQDDBpZdWJpY28gV2ViQXV0aG4gdW5pdCB0ZXN0czEPMA0GA1UECgwGWXViaWNvMSIwIAYDVQQLDBlBdXRoZW50aWNhdG9yIEF0dGVzdGF0aW9uMQswCQYDVQQGEwJTRTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABOpYjdF8vH9wIRblOhyvb0C1GzDIlTa9y+jOcM08eNxhGY6+/4bF8BLVeSHDn5uANCEJx6MMY700GxWtMgdoU7yjJTAjMCEGCysGAQQBguUcAQEEBBIEEAABAgMEBQYHCAkKCwwNDg8wCgYIKoZIzj0EAwIDSAAwRQIhAJHUYldBMd1TAhkzSThWHEp1LAF3x/+eha9FUkisot68AiBOqABd6v2XrKuW9Ql1H0X/ErV2QBYogNxvf6GK5EiD2A==", + "EC", + "MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgfMvSYwJCO1Mu7owbFmLYt9lQ2H8zqFoD4+kqeiybvnmgCgYIKoZIzj0DAQehRANCAATqWI3RfLx/cCEW5Tocr29AtRswyJU2vcvoznDNPHjcYRmOvv+GxfAS1Xkhw5+bgDQhCcejDGO9NBsVrTIHaFO8", + ), + RegistrationTestDataGenerator.importAttestationCa( + "MIIB1zCCAX2gAwIBAgICJTwwCgYIKoZIzj0EAwIwajEmMCQGA1UEAwwdWXViaWNvIFdlYkF1dGhuIHVuaXQgdGVzdHMgQ0ExDzANBgNVBAoMBll1YmljbzEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjELMAkGA1UEBhMCU0UwHhcNMTgwOTA2MTc0MjAwWhcNMTgwOTEzMTc0MjAwWjBqMSYwJAYDVQQDDB1ZdWJpY28gV2ViQXV0aG4gdW5pdCB0ZXN0cyBDQTEPMA0GA1UECgwGWXViaWNvMSIwIAYDVQQLDBlBdXRoZW50aWNhdG9yIEF0dGVzdGF0aW9uMQswCQYDVQQGEwJTRTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABB+mZAZb/g/+MPZiqGJw3BJ05x6Ku/Hqs3Po6dOVkUhcVUTMviUrKWcLY2m6/fAuEKNmrMc1nIs7HMryxYQK4C+jEzARMA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0EAwIDSAAwRQIhAPfhXdfTbgSsg9qNz/s1pPUZumWW7rgG790HGsL5o2H9AiAJiLmlnBZNR1hS1dNYXBOJorEWXUq3rMF0EjCh7sD8aw==", + "EC", + "MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQg712jkzFvwyWNpRxBkG/bao9vp2rHkHnYJl7V4Sz1SV2gCgYIKoZIzj0DAQehRANCAAQfpmQGW/4P/jD2YqhicNwSdOceirvx6rNz6OnTlZFIXFVEzL4lKylnC2Npuv3wLhCjZqzHNZyLOxzK8sWECuAv", + ), ), - clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","type":"webauthn.create","tokenBinding":{"status":"supported"},"clientExtensions":{}}""", transports = Set(AuthenticatorTransport.USB), ) { override def regenerate() = TestAuthenticator.createBasicAttestedCredential(attestationMaker = AttestationMaker.fidoU2f( - AttestationSigner.selfsigned(COSEAlgorithmIdentifier.ES256) + AttestationSigner.ca(COSEAlgorithmIdentifier.ES256) ) ) } val SelfAttestation: RegistrationTestData = new RegistrationTestData( alg = COSEAlgorithmIdentifier.ES256, - attestationObject = new ByteArray( - BinaryUtil.fromHex("bf68617574684461746158a449960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97634100000539000102030405060708090a0b0c0d0e0f00205558386f4ed61a6c98a3fed94060fff66808947953754a0dff2aea9ae2164635a52258208d05cb87cec921d5e6fbc22c32a07fb35ed89c19a3f0a2866fcf4a248194e650032601022158202bb1c0846fca809059b41272f0c2953d733b31b50c14453b7a9855b7bfc98229200163666d74686669646f2d7532666761747453746d74bf63783563815901e7308201e330820189a00302010202020539300a06082a8648ce3d04030230673123302106035504030c1a59756269636f20576562417574686e20756e6974207465737473310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b3009060355040613025345301e170d3138303930363137343230305a170d3138303930363137343230305a30673123302106035504030c1a59756269636f20576562417574686e20756e6974207465737473310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b30090603550406130253453059301306072a8648ce3d020106082a8648ce3d030107034200042bb1c0846fca809059b41272f0c2953d733b31b50c14453b7a9855b7bfc982298d05cb87cec921d5e6fbc22c32a07fb35ed89c19a3f0a2866fcf4a248194e650a32530233021060b2b0601040182e51c01010404120410000102030405060708090a0b0c0d0e0f300a06082a8648ce3d0403020348003045022100a91c5499a6518bc59648bde7e7467488736e1ae82b5eb85c14957a0f82d23dfc02205a4b9963f88dbabaa0fa298eae6f0876b9f5e65650c4bd29f1f3f7eeb1312c24637369675847304502205af7085152ec65cc5ee097c5890316e6cac286379c32925a969ab414b013aa59022100b9b9d56cf4314e10c13caa57fb1fb0a01e87ffdec623c62637fddf56a8c4c62cffff") + attestationObject = + ByteArray.fromHex("bf68617574684461746158a449960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97634100000539000102030405060708090a0b0c0d0e0f00205267098911e4511a52dfc89555f2f75d0ca770ed4069838e73e6053455c4335fa50102032620012158206d5e477a82ff2d85ff9ee7cd9b42cd5af679808b07fa16e3d66bdc5630a6841b2258201319661e863ff2bb0e8f681898263213bdcdec15df31919cfa3f981114e3b61863666d74686669646f2d7532666761747453746d74bf63783563815901e7308201e330820189a0030201020202183c300a06082a8648ce3d04030230673123302106035504030c1a59756269636f20576562417574686e20756e6974207465737473310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b3009060355040613025345301e170d3138303930363137343230305a170d3138303931333137343230305a30673123302106035504030c1a59756269636f20576562417574686e20756e6974207465737473310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b30090603550406130253453059301306072a8648ce3d020106082a8648ce3d030107034200046d5e477a82ff2d85ff9ee7cd9b42cd5af679808b07fa16e3d66bdc5630a6841b1319661e863ff2bb0e8f681898263213bdcdec15df31919cfa3f981114e3b618a32530233021060b2b0601040182e51c01010404120410000102030405060708090a0b0c0d0e0f300a06082a8648ce3d0403020348003045022100d6ec5292f53196d8ab6cb49dac3ef2df71d68fb4e9ab5c5be22633ea813504250220349be966dd47802e31128fc930c09917d84d84b2a6580122880293be1460ed556373696758473045022100f09eda7e673451cb8401b5694b56cbda454473253fb3aba7092fae11d64d409002203fd12750831ba78384a308a4692841fc3e21c0d702d0196ba9ee0e98bcb4da59ffff"), + clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","type":"webauthn.create","tokenBinding":{"status":"supported"}}""", + privateKey = Some( + ByteArray.fromHex("308193020100301306072a8648ce3d020106082a8648ce3d0301070479307702010104205e7c760543570afd43acd64e7d44ee1dfc3d0eec2f5909c8da58450d36dccb39a00a06082a8648ce3d030107a144034200046d5e477a82ff2d85ff9ee7cd9b42cd5af679808b07fa16e3d66bdc5630a6841b1319661e863ff2bb0e8f681898263213bdcdec15df31919cfa3f981114e3b618") ), - clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","type":"webauthn.create","tokenBinding":{"status":"supported"},"clientExtensions":{}}""", ) { override def regenerate() = TestAuthenticator.createSelfAttestedCredential( @@ -241,10 +324,12 @@ object RegistrationTestData { object NoneAttestation { val Default = new RegistrationTestData( alg = COSEAlgorithmIdentifier.ES256, - attestationObject = new ByteArray( - BinaryUtil.fromHex("bf68617574684461746158a449960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97634100000539000102030405060708090a0b0c0d0e0f002082e7622c8c35a5786e66815f44a82b954628df497361169e77af23bb9bea1b69a5225820ae947a15818d883351ac00b957ad794c4b0206e2df34ec7b52969016a215800e03260102215820763f33278817151fad81d172493b8826c3a736cb1acf884e38c26fbe65c2438a200163666d74646e6f6e656761747453746d74bfffff") + attestationObject = + ByteArray.fromHex("bf68617574684461746158a449960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97634100000539000102030405060708090a0b0c0d0e0f00205136465ab035f7cc9fb6cd6baa2588f25b6753f1bbffe0aea9453a854fcbe642a5010203262001215820a65616f668c6ba0eb0d819c3a0a9e61d44e5747f17ae7adfe31c290ff3b1c210225820b5d549b3164fd00756d8260a534f54e1d2bce7dd4b46df9b05231d8dd19f5b7f63666d74646e6f6e656761747453746d74bfffff"), + clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","type":"webauthn.create","tokenBinding":{"status":"supported"}}""", + privateKey = Some( + ByteArray.fromHex("308193020100301306072a8648ce3d020106082a8648ce3d030107047930770201010420228ec10ee79fc66580c844317a9404b3fb4badb7c46d5f00402a51b334b427f3a00a06082a8648ce3d030107a14403420004a65616f668c6ba0eb0d819c3a0a9e61d44e5747f17ae7adfe31c290ff3b1c210b5d549b3164fd00756d8260a534f54e1d2bce7dd4b46df9b05231d8dd19f5b7f") ), - clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","type":"webauthn.create","tokenBinding":{"status":"supported"},"clientExtensions":{}}""", ) { override def regenerate() = TestAuthenticator.createUnattestedCredential() } @@ -254,8 +339,23 @@ object RegistrationTestData { val BasicAttestation: RegistrationTestData = new RegistrationTestData( alg = COSEAlgorithmIdentifier.ES256, attestationObject = - ByteArray.fromHex("bf68617574684461746158a449960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97634100000539000102030405060708090a0b0c0d0e0f00206b1549b3cf2524c30089b001f8a0f100de9a97681910d2c8181337c516cb2eb6a503260102200121582079c229789b5a262e7b3b2057ef8636b7a20930f262fac3636682e70bdcd4d906225820ca5084617d404d831791a8281eba451aa165726267f9d480dfc315313c95408d63666d74667061636b65646761747453746d74bf63616c67266373696758483046022100f1b2138ab5e8dbce9d0e88862295f574c1b636aa740b57d6705646c799084dd5022100d87f9df13302b854a1c6a726481afbd96ddd2caeb51f4cba89bd248676e9af1063783563825901ed308201e93082018fa00302010202020539300a06082a8648ce3d040302306a3126302406035504030c1d59756269636f20576562417574686e20756e6974207465737473204341310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b3009060355040613025345301e170d3138303930363137343230305a170d3138303930363137343230305a306a3126302406035504030c1d59756269636f20576562417574686e20756e6974207465737473204341310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b30090603550406130253453059301306072a8648ce3d020106082a8648ce3d030107034200043c010b106a69efe039327ab79f57f8e43285f59ad56a50cfd0264b8ba88f79bf2291d561768bb686431aadce9dddf56858aac55b1638d5c03d2a2c426b64b64aa32530233021060b2b0601040182e51c01010404120410000102030405060708090a0b0c0d0e0f300a06082a8648ce3d0403020348003045022100aa1943235627b47852deace94c46e2499a4b2bcab17ffe5502d0c5d17f0f883d022076402b6fe8f66040e4f157e74f732e4a4d31268115e2880faa999f248a0485e05901db308201d73082017da00302010202020539300a06082a8648ce3d040302306a3126302406035504030c1d59756269636f20576562417574686e20756e6974207465737473204341310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b3009060355040613025345301e170d3138303930363137343230305a170d3138303930363137343230305a306a3126302406035504030c1d59756269636f20576562417574686e20756e6974207465737473204341310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b30090603550406130253453059301306072a8648ce3d020106082a8648ce3d0301070342000448f77f8679a4c7bfff4a3ec8291f18995444d21b8624aeefdf2821e69444ac66ced7c7c10ea30d9167836ee84042a9b944d2c239f2a493d5fb2896a2ca0b83d0a3133011300f0603551d130101ff040530030101ff300a06082a8648ce3d0403020348003045022100acc2e79b65faaa5206b27714102f8cdb95ee656c567b7ae7511467b6c324e8e802202a5ac41e505ac43f9efcf3985db215a7506244ba67eb19bdf17aabef8773e1c1ffff"), - clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","type":"webauthn.create","tokenBinding":{"status":"supported"},"clientExtensions":{}}""", + ByteArray.fromHex("bf68617574684461746158a449960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97634100000539000102030405060708090a0b0c0d0e0f0020086125e8b2570b01906cbbaa4e1fcd8e4847bb3ea362c4fe535af9f9e558279ea5010203262001215820ea1812b7dfd3593d9e211cad6bba5a26e10d2cdcf06daf19814c5398238996c12258206cb3b9405fd278ffeee82dff4f1d62780a94f232037530f5b2d94f279477295463666d74667061636b65646761747453746d74bf63616c67266373696758473045022071fd0b77039d2676e286270f63f95d3bb2a0c336faa8e4d4b0805c540e36520a022100c23284f7cbcb83f38e49c7b6ddcc4c251a5f0238055abab55fbed4c636b877d763783563815901e9308201e53082018ca0030201020202102c300a06082a8648ce3d040302306a3126302406035504030c1d59756269636f20576562417574686e20756e6974207465737473204341310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b3009060355040613025345301e170d3138303930363137343230305a170d3138303931333137343230305a30673123302106035504030c1a59756269636f20576562417574686e20756e6974207465737473310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b30090603550406130253453059301306072a8648ce3d020106082a8648ce3d03010703420004d87f24d800b1ed2d51e8b43b1655511036c8ec79c62a4e2ea47fac0825f0887a625bb0bc80ca0d81fc1710cf8d40cc8551ae3d8d7afd81ffa8889cab2b2bdefca32530233021060b2b0601040182e51c01010404120410000102030405060708090a0b0c0d0e0f300a06082a8648ce3d040302034700304402206daf4349fdb2e84c3a7c797e1bb96a4bcc84a465ca4af3ca5db2d38e638a241b02204aed881c7ba4921f705253fde0c093629d7f5593b6b026a2f3e894454806094dffff"), + clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","type":"webauthn.create","tokenBinding":{"status":"supported"}}""", + privateKey = Some( + ByteArray.fromHex("308193020100301306072a8648ce3d020106082a8648ce3d030107047930770201010420894eba4a6df7a4f2093443cdf65c8cca3dd38a79c6763a0105aa5690c69d7b61a00a06082a8648ce3d030107a14403420004ea1812b7dfd3593d9e211cad6bba5a26e10d2cdcf06daf19814c5398238996c16cb3b9405fd278ffeee82dff4f1d62780a94f232037530f5b2d94f2794772954") + ), + attestationCertChain = List( + RegistrationTestDataGenerator.importAttestationCa( + "MIIB5TCCAYygAwIBAgICECwwCgYIKoZIzj0EAwIwajEmMCQGA1UEAwwdWXViaWNvIFdlYkF1dGhuIHVuaXQgdGVzdHMgQ0ExDzANBgNVBAoMBll1YmljbzEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjELMAkGA1UEBhMCU0UwHhcNMTgwOTA2MTc0MjAwWhcNMTgwOTEzMTc0MjAwWjBnMSMwIQYDVQQDDBpZdWJpY28gV2ViQXV0aG4gdW5pdCB0ZXN0czEPMA0GA1UECgwGWXViaWNvMSIwIAYDVQQLDBlBdXRoZW50aWNhdG9yIEF0dGVzdGF0aW9uMQswCQYDVQQGEwJTRTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABNh/JNgAse0tUei0OxZVURA2yOx5xipOLqR/rAgl8Ih6YluwvIDKDYH8FxDPjUDMhVGuPY16/YH/qIicqysr3vyjJTAjMCEGCysGAQQBguUcAQEEBBIEEAABAgMEBQYHCAkKCwwNDg8wCgYIKoZIzj0EAwIDRwAwRAIgba9DSf2y6Ew6fHl+G7lqS8yEpGXKSvPKXbLTjmOKJBsCIErtiBx7pJIfcFJT/eDAk2Kdf1WTtrAmovPolEVIBglN", + "EC", + "MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgZRMJiDvUaoZYLRgOuFTJb3jzU6OOH+kT5RyZyFF5wLmgCgYIKoZIzj0DAQehRANCAATYfyTYALHtLVHotDsWVVEQNsjsecYqTi6kf6wIJfCIemJbsLyAyg2B/BcQz41AzIVRrj2Nev2B/6iInKsrK978", + ), + RegistrationTestDataGenerator.importAttestationCa( + "MIIB2DCCAX2gAwIBAgICAaswCgYIKoZIzj0EAwIwajEmMCQGA1UEAwwdWXViaWNvIFdlYkF1dGhuIHVuaXQgdGVzdHMgQ0ExDzANBgNVBAoMBll1YmljbzEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjELMAkGA1UEBhMCU0UwHhcNMTgwOTA2MTc0MjAwWhcNMTgwOTEzMTc0MjAwWjBqMSYwJAYDVQQDDB1ZdWJpY28gV2ViQXV0aG4gdW5pdCB0ZXN0cyBDQTEPMA0GA1UECgwGWXViaWNvMSIwIAYDVQQLDBlBdXRoZW50aWNhdG9yIEF0dGVzdGF0aW9uMQswCQYDVQQGEwJTRTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABLtJrr5PYSc4KhmUcwBzgZgNadDnCs/ow2oh2jiKYUqq1A6hFcFf1NPfXLQjP2I4fBI36T6/QR2iY9mbqyP5iVejEzARMA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0EAwIDSQAwRgIhANWaM2Tf2HPKc+ibCr8G4cxpQVr9Gib47a0CpqagCSCwAiEA3oKlX/ID94FKzgHvD2gyCKQU6RltAOMShVwoljj/5+E=", + "EC", + "MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgGEdS0qEf53TitZg2XniD2RIbYeNgAc3QOL3msfAb6PagCgYIKoZIzj0DAQehRANCAAS7Sa6+T2EnOCoZlHMAc4GYDWnQ5wrP6MNqIdo4imFKqtQOoRXBX9TT31y0Iz9iOHwSN+k+v0EdomPZm6sj+YlX", + ), + ), ) { override def regenerate() = TestAuthenticator.createBasicAttestedCredential(attestationMaker = @@ -268,10 +368,17 @@ object RegistrationTestData { val BasicAttestationEdDsa: RegistrationTestData = new RegistrationTestData( alg = COSEAlgorithmIdentifier.EdDSA, attestationObject = - ByteArray.fromHex("bf686175746844617461588149960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97634100000539000102030405060708090a0b0c0d0e0f002089b13dc1075db05f34ea0f2e2fd843ce0c0b262a4a852f5eb03d3b2668f437dfa403270101200621582051be73800d9386b8bcfa03f80143ed1279486f95acb714515616b849b588298963666d74667061636b65646761747453746d74bf63616c6726637369675846304402207ef99a22fb1d6fac37ce859f768a3b3d85477ef3825ea53fb7824bb292b12139022073ba899784179bcd06fb3e75657a99cd710ed84a98edbdc8370ac9df885eb8bb63783563815901e7308201e330820189a00302010202020539300a06082a8648ce3d04030230673123302106035504030c1a59756269636f20576562417574686e20756e6974207465737473310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b3009060355040613025345301e170d3138303930363137343230305a170d3138303930363137343230305a30673123302106035504030c1a59756269636f20576562417574686e20756e6974207465737473310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b30090603550406130253453059301306072a8648ce3d020106082a8648ce3d03010703420004ba072ce8a10f63a776c3ce83972e20259089b0d2072501678daedaea755175ee34c785c7cc47e06561fac2b48b1f22e795173c4b89cdfd651a661bb7b9b180f1a32530233021060b2b0601040182e51c01010404120410000102030405060708090a0b0c0d0e0f300a06082a8648ce3d0403020348003045022100b75626efe7b98fb81dcf8dbb301a2a2a0dea354c5b43592368bb0b7345e1e6ea022003deb0739996db0c3a3b40c116f070d10d03e7261459426378fa2896a92e5024ffff"), - clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","type":"webauthn.create","tokenBinding":{"status":"supported"},"clientExtensions":{}}""", + ByteArray.fromHex("bf686175746844617461588149960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97634100000539000102030405060708090a0b0c0d0e0f00206854a9bf7bf33e8820d2df695429e4d108ef68a8dad621b577139834ca8e14b9a401010327200621582083bf9038ed9a530d13b9d6ad40afcefbb637f10189c52496a2f3fa7f8217991663666d74667061636b65646761747453746d74bf63616c6726637369675848304602210096cea94d347ec08a1a0eb225290c83b377a39ec5e1584c4bb7743c8f65f639a3022100bfd1b29d8d7b7837cb0935aadd40fc62d936282229a7b051fc33f68e1754ee3763783563815901e7308201e330820189a0030201020202186d300a06082a8648ce3d04030230673123302106035504030c1a59756269636f20576562417574686e20756e6974207465737473310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b3009060355040613025345301e170d3138303930363137343230305a170d3138303931333137343230305a30673123302106035504030c1a59756269636f20576562417574686e20756e6974207465737473310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b30090603550406130253453059301306072a8648ce3d020106082a8648ce3d03010703420004c8921a7512d4d0c4f9b5bedcc7153cc4bfc44ea215daf98e96c237d18c830e2ee3ecf85e4fcad1cd17d6fc08d2325d3fa56a06a819b721473c568cc109c4d358a32530233021060b2b0601040182e51c01010404120410000102030405060708090a0b0c0d0e0f300a06082a8648ce3d0403020348003045022100b178e0f6f2e3412d938548fcb966abd24a6b4191f2f669696210e817ee1a309c02204090ef8e279ae7f1a8e30fcace260e08ac3ad7712ada8a18355a9361ceed316effff"), + clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","type":"webauthn.create","tokenBinding":{"status":"supported"}}""", privateKey = Some( - ByteArray.fromHex("3051020101300506032b657004220420098ff1cf173564547f5631f6db3f8dae75713b99d486604e8a09c755c53e11ee81210051be73800d9386b8bcfa03f80143ed1279486f95acb714515616b849b5882989") + ByteArray.fromHex("3051020101300506032b657004220420f3c025221210a3cd666ab9369a56b859fd8d2d0c5a160f20f0a0afb7d2b2756c81210083bf9038ed9a530d13b9d6ad40afcefbb637f10189c52496a2f3fa7f82179916") + ), + attestationCertChain = List( + RegistrationTestDataGenerator.importAttestationCa( + "MIIB4zCCAYmgAwIBAgICGG0wCgYIKoZIzj0EAwIwZzEjMCEGA1UEAwwaWXViaWNvIFdlYkF1dGhuIHVuaXQgdGVzdHMxDzANBgNVBAoMBll1YmljbzEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjELMAkGA1UEBhMCU0UwHhcNMTgwOTA2MTc0MjAwWhcNMTgwOTEzMTc0MjAwWjBnMSMwIQYDVQQDDBpZdWJpY28gV2ViQXV0aG4gdW5pdCB0ZXN0czEPMA0GA1UECgwGWXViaWNvMSIwIAYDVQQLDBlBdXRoZW50aWNhdG9yIEF0dGVzdGF0aW9uMQswCQYDVQQGEwJTRTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABMiSGnUS1NDE+bW+3McVPMS/xE6iFdr5jpbCN9GMgw4u4+z4Xk/K0c0X1vwI0jJdP6VqBqgZtyFHPFaMwQnE01ijJTAjMCEGCysGAQQBguUcAQEEBBIEEAABAgMEBQYHCAkKCwwNDg8wCgYIKoZIzj0EAwIDSAAwRQIhALF44Pby40Etk4VI/Llmq9JKa0GR8vZpaWIQ6BfuGjCcAiBAkO+OJ5rn8ajjD8rOJg4IrDrXcSraihg1WpNhzu0xbg==", + "EC", + "MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgxYFd/4ffTDkyuzLYMWhJpqsR+hwUkkiROFT8Q++TQ5ugCgYIKoZIzj0DAQehRANCAATIkhp1EtTQxPm1vtzHFTzEv8ROohXa+Y6WwjfRjIMOLuPs+F5PytHNF9b8CNIyXT+lagaoGbchRzxWjMEJxNNY", + ) ), assertion = Some( AssertionTestData( @@ -282,7 +389,7 @@ object RegistrationTestData { classOf[AssertionRequest], ), response = - PublicKeyCredential.parseAssertionResponseJson("""{"id":"ibE9wQddsF806g8uL9hDzgwLJipKhS9esD07Jmj0N98","response":{"authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MBAAAFOQ","clientDataJSON":"eyJjaGFsbGVuZ2UiOiJOM0xqSTJKNXlseVdlM0VENU9UNFhITFJxSHdtX0o0OF9EX2hvSk9GZjMwIiwib3JpZ2luIjoiaHR0cHM6Ly9sb2NhbGhvc3QiLCJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwidG9rZW5CaW5kaW5nIjp7InN0YXR1cyI6InN1cHBvcnRlZCJ9LCJjbGllbnRFeHRlbnNpb25zIjp7fX0","signature":"-8AKZkFZSNUemUihJhsUp8LqXFHgVTjfCuKVvf1kbIkuwz5ClZK2u562C8rkUnIorxtzD7ujYh1z4FstXKyRDg"},"clientExtensionResults":{},"type":"public-key"}"""), + PublicKeyCredential.parseAssertionResponseJson("""{"id":"aFSpv3vzPogg0t9pVCnk0QjvaKja1iG1dxOYNMqOFLk","response":{"authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MBAAAFOQ","clientDataJSON":"eyJjaGFsbGVuZ2UiOiJOM0xqSTJKNXlseVdlM0VENU9UNFhITFJxSHdtX0o0OF9EX2hvSk9GZjMwIiwib3JpZ2luIjoiaHR0cHM6Ly9sb2NhbGhvc3QiLCJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwidG9rZW5CaW5kaW5nIjp7InN0YXR1cyI6InN1cHBvcnRlZCJ9fQ","signature":"LySrkl9s9vF6F9tssQLrsXX2QVV87ZQQn8ZEwROt2tdmbkUprrEM08hkNX7Qmj70M8wYhSosy3jjyThrHn2fDg"},"clientExtensionResults":{},"type":"public-key"}"""), ) ), ) { @@ -298,8 +405,18 @@ object RegistrationTestData { val BasicAttestationRsa: RegistrationTestData = new RegistrationTestData( alg = COSEAlgorithmIdentifier.RS256, attestationObject = - ByteArray.fromHex("bf68617574684461746159016849960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97634100000539000102030405060708090a0b0c0d0e0f00202d027aec938e6fcf40460eb328f8596e43af1cbd99fddf61c1ffae6e7b0e404ba40339010001032059010100ab8f0b1c4ceef20c093cd67fe6cc264e3ba8208467f410e53b22415ef201ad1ad525ac1be334926ce4f565cfc777135924c1a9bfc3fad24e3d504d618602937b200fd1972ea0097ff9e7d33f68633263a8ce347550213de95228c9c093ca700042f782eb6c16da1b75ed2f481815b04c222cae865340592deeba809fee80e6c1199a3e36b50b400ef87570234754566b276a8fb0cbca7a6ffa1d24369878c8c831e415747b3142cce244ae8d4e0df921a4c9400ed615c1e9c98479af90be09fb2880512bf9d52f825ea031ff10daac369862df3da0d1a2782888415430d8040a0671a749269dcdc4ac22a66b42cf0ac3a3365a64c6ce82ff2548bfac493f6bad214301000163666d74667061636b65646761747453746d74bf63616c673901006373696759010054de4d2aae25f9bd1b9d0e20a9d4168a5feded7178fe1f47ee0fb9a8f19439c8cc1aeab7a7269e4d4edb29c7c9864fbd8202d8cc69584da0e73b4c1d731bff3ec29599964ebef12068a9791d0e52a0c9579d881c565e1ae8a0fc7f2de9ec8882d13919a164b362ab2a89faec3be869635f187b3ef30cd20986ec6f2ff667cb1a279871f77dd9d037f49a7da784cdf846e2d7220683aa928e3b422616be8b0609385a16e0509365a609e162a5239bdc1c4e7aa60c9a1860de753b99705173a72c9fc0390f42886ff9ff839f045cf6457ecb7cf26da34e95511fde6343e4812f40ceb8ff2e7dd24dafdd9c513225bf3418df4a7c1c0f5bc6a0155a31d9c2ddfe8c63783563815903733082036f30820257a00302010202020539300d06092a864886f70d01010b050030673123302106035504030c1a59756269636f20576562417574686e20756e6974207465737473310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b3009060355040613025345301e170d3138303930363137343230305a170d3138303930363137343230305a30673123302106035504030c1a59756269636f20576562417574686e20756e6974207465737473310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b300906035504061302534530820122300d06092a864886f70d01010105000382010f003082010a028201010097c332c418daf7cb31e27bf321f7f72f48c614650f6215db969d17a6ba24fc08d3140fa4d45ff2b2b0ce95fbd87b629e23ba84533dbf2ed90c4e2a770db459690ddfa433288a06fa0c2b1c012887926f1d366d2beac622788560d0a4197b4d90ba7bfd6f4b3250cc37f54e5f350160ad61136bca94b560ec783334cf0376cca042ff40b288049881f7fb3c265f6bbfd625c18efe5802c7dbd384b0b6f328ae9a1bbeb4a184b8eddf16ff419a76adef00d20b57e0927e997c2dfec964c24fb2f023848916c41b0de26636be72356b555d4d1090f2cbcf9003eff39d4b6f77498481d6fe8b2f2bfe2e895382494ca4495c8ac9a47c9fbc8832dc66f727852f814d0203010001a32530233021060b2b0601040182e51c01010404120410000102030405060708090a0b0c0d0e0f300d06092a864886f70d01010b050003820101002a801c27a9a78f74db8de08bc367a8877f53007c7edc01354716be772d7e1450ab99c7b9d4e1c30c05080e51cc69c98068f0130aeccea535e1eb4e7834413bba888633a0c3aad9b7286096084425500b8b442a30ffd52cb77520ee28e8341e2640c39b81be07d9fce48d49ee3bad11b6015c78505e2c1aeaeb829c167bd86bbb714310f6559f481bb9b970dbe8184c7b24d8a4ef2030331d6c8d41b966d5fb4bf08f8f736adedc918fe039100330a5c6a79c54c92351c907608abda0fc98f019ac182ed2858f3c65aeeb282562d1036a06573edac5bed696553b5d347620cf9412faddaab3319080263378085812b315357a3cbe3618ff81d2760c7276ded4afffff"), - clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","type":"webauthn.create","tokenBinding":{"status":"supported"},"clientExtensions":{}}""", + ByteArray.fromHex("bf68617574684461746159016849960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97634100000539000102030405060708090a0b0c0d0e0f00209b5d5b5702b6f70cf40c612063a9e327a427d5d1f6b7635e4d8fa9077e927249a40103033901002059010100a2e03644c82525d79a6467b1d5ebcdfcc23bf254a9746522075ee8b814e2603100edcf7828e8051f39d546a5d0ab2eef8d946785fa1c120fac3e22d6804de7d979b6d456a6be39ead335117218efc5ed2522db7bae615195c49da0fadc79c116e9717af465cf9af580d410a34ae07f9ac7828687fde92dcdeb557bdfe2778e8987a66e1026feb0bae5f2171e27d8635d9b54fda7030f920230264b0104345b18677385e60decd9e01f74d3ae477502029dfdc002ad8d0d24e68fd24c92870429adf7ace121855d336ed0db6b461e0cbdba8be257eabb0351130f948c7b5dde021bdc7cd960717fa5d46ff83c3e0a8d9c7ead172a6b1ccfeb6df170f437ee8745214301000163666d74667061636b65646761747453746d74bf63616c67390100637369675901009b93e7392c66341a64eb977791c1dc7d39f6d41c3b07fbc02f5d641f64e0b0ffe748f86eefd49f5695e07a3aefacf189e8adea0bdf178943203d44467028968fc85f5852926060d588366bd58df2b316e92c4a484f9a144fc2710a68da762050bda97496ce953590efca145c561d7ee264cdd8070e22be94b9c3be5a492b738085be311e9f56c90b5ddf844560e309f811e254f819c7da999ef922ab3ead4a883ef474374a77b100f258df5676f34b6ae253986a80939fdd6d4d2dc541830dfb416d4bde82a199cb90f8d5149a19cb5599366d9fd762ac41d0268cf59cf120eb6d2a4b56e250182e6904f4ef33f3ef662f58fc3a222eff7e978004eaa496ebac63783563815903733082036f30820257a003020102020224d6300d06092a864886f70d01010b050030673123302106035504030c1a59756269636f20576562417574686e20756e6974207465737473310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b3009060355040613025345301e170d3138303930363137343230305a170d3138303931333137343230305a30673123302106035504030c1a59756269636f20576562417574686e20756e6974207465737473310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b300906035504061302534530820122300d06092a864886f70d01010105000382010f003082010a0282010100e251e8399aa9b7c35f33f2f64de878dd3aaf67d33e731735166d207d3b0c4b5393244ec867c5b6ecf97bae6cb89926e027f12efe46c9ac02a2e1a0711f404c62895338ea1d4e44e70339522fa42f8a2603d53891d2ff3b80b684dae295c29fa1641d5b57860fbd414fdf9957588e8c7b3c6442d4702e90fc3e0950e8e8fc8bd1814f11bbc7135f320f8cacef0a3399d3a6d268123cd59e59ca9bbdec9b1ae2171d64b8938f5b9a1cfc4b7a3cd7d3975a87d64fc743981419cb57b7c7526840caac380c7d1cc2d3d24456c175508d415e37666abdb1b3371a0ae714ddd52017218e492231698a1ca99c072c5e304a3f110b1bc0ad940dd7f333d862b7527cf0590203010001a32530233021060b2b0601040182e51c01010404120410000102030405060708090a0b0c0d0e0f300d06092a864886f70d01010b050003820101000463b142ba46c9c0900ab2742aeb91497ff62d6a583b84d66f280847b90062ba32060df8bdd8d551cdd011d557707f0dd2f5c3ee3f60d16d4d78c0550f93261deaecc640c2b4b4933765897934d2bb2cab98544cdf679ad21d10a4b291f06e71a46bfebf800086abcea59a8fc6efeb861d6e62fcb46285673451b5c93b5172e07e0bd1115b11a989c4a935053c345f8e43c500b15defbe00cfc8ba0f4e4dccc71463cc681a7ff00df2e331be04ae2090eb8fe9e34c4b123eb49d942c6f5a6c34b4393ed89a6eb0d83c69c4459d867a0e02e2d4725fd88a8731508642cff2f829dc2dfb988229b34ec7c7c111e5984dfb05ef7ea2ef2e25284b53f0c7a1c41475ffff"), + clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","type":"webauthn.create","tokenBinding":{"status":"supported"}}""", + privateKey = Some( + ByteArray.fromHex("308204bc020100300d06092a864886f70d0101010500048204a6308204a20201000282010100a2e03644c82525d79a6467b1d5ebcdfcc23bf254a9746522075ee8b814e2603100edcf7828e8051f39d546a5d0ab2eef8d946785fa1c120fac3e22d6804de7d979b6d456a6be39ead335117218efc5ed2522db7bae615195c49da0fadc79c116e9717af465cf9af580d410a34ae07f9ac7828687fde92dcdeb557bdfe2778e8987a66e1026feb0bae5f2171e27d8635d9b54fda7030f920230264b0104345b18677385e60decd9e01f74d3ae477502029dfdc002ad8d0d24e68fd24c92870429adf7ace121855d336ed0db6b461e0cbdba8be257eabb0351130f948c7b5dde021bdc7cd960717fa5d46ff83c3e0a8d9c7ead172a6b1ccfeb6df170f437ee874502030100010282010005bb21edb5a528f9b723054b0a9deb7793560ca6d1f7987f640700e54946d5dfa38aba9c1dc45c39c70d2c19358870745964f9678b6f656b4bc23bf3943c29864b7415709f195e6c56d62d30f893a7413ae74915c703019de5772e6ce5491b7434ee9b46f23625dbb196c4a71a415ffc103d1582bf7a6ef429edae18289dd054141e94d5640cc2a92d28e4338300db089101e780ad8ee900005b0d7647c281af0035c99d74ce8f92ee6015e4b82dbd88ca3f3117294d1553c8ebfc47fe0654fabd67473a40c2185f29392f2d2fdfefae1e6cc126b5b0139d61e97cd08eed9d14791c3798043c80db94e908a0615d8a3a48f93c89f0f2d728833bc4f80636968102818100c06493326a52f78e63704a6a9263ebbcc9906ee1c09bef7890fdc0c39f00d07a0dfd2947324e19ec5b0854ba6e7ccbbd08bd5bdfe8b301308168d6113dcd720056987620ace63b0d58f856330820cb645b95116bf6f9277fdbbf5a65ed25b2b4a2e21fa9b9a1fceae34932fde85a177dd29951033f36896cc2eefa9bbf03b31102818100d8b96c4c6e7fe795c58d6a850bc181e7dd42d5fa34f420c56b4ef8a99c2d62ee11e19f175f80f4a12d8d8c682ed80cb1837dbeaf45285a74ec2bf559efe333753b9dc1927f3d89799d05fdf6679b2aeb02b995415723a10a1baf65a9cdb365054ab81efdef941b7a22988041e35a5bf09426b35f2e0874bfaddd5c641bd1a8f50281806baf5f9c565abde95acb1d3bed58343864f18cbe9b1a2cbd651a42ecbb70af3fc1d8b364004a2ac45a679d550446a19adaeb72232f9211d65e789968e918b6f86d7fc48ea177f411927cdd728ed81c3fbdeaffe7584338d29de2caec460255b6397d2b8fca315ae8f5f2a0b1f17d8bac8755fd3c3e037e83fbcfdb419576b2a1028180178fc923621c0bbc6fb6d92ecfc160f3294dfbdf70e45dafb8e3e40ae48cd6a59552172ebe5651c238269c6e33318fe7b8a8f213320c9a10fe2025537ace13a91a2b23815ecdfce538da0eeb3c06559b2937adef659edd023152575627a3ea46b201e474ad184808763c682d419f70416e89ea945d77d3e186f07afbf33e4f050281803c118e1f9314462087ae90a3ebaa238990e9ad0eb22ed83c7618e7929c234e0d75913ebf903e27ba0c2e340e2a2e847f1aaffb9efc6919eb7044df946d9d6dc835cad23b3b2d8e4dd49e10cfac52c773b362732086a40444c3acfe23a5b9101895cc2dc53e1ebe914a4f308ad12dd1211d0c091b3b91250f191866af8f90de8a") + ), + attestationCertChain = List( + RegistrationTestDataGenerator.importAttestationCa( + "MIIDbzCCAlegAwIBAgICJNYwDQYJKoZIhvcNAQELBQAwZzEjMCEGA1UEAwwaWXViaWNvIFdlYkF1dGhuIHVuaXQgdGVzdHMxDzANBgNVBAoMBll1YmljbzEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjELMAkGA1UEBhMCU0UwHhcNMTgwOTA2MTc0MjAwWhcNMTgwOTEzMTc0MjAwWjBnMSMwIQYDVQQDDBpZdWJpY28gV2ViQXV0aG4gdW5pdCB0ZXN0czEPMA0GA1UECgwGWXViaWNvMSIwIAYDVQQLDBlBdXRoZW50aWNhdG9yIEF0dGVzdGF0aW9uMQswCQYDVQQGEwJTRTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOJR6DmaqbfDXzPy9k3oeN06r2fTPnMXNRZtIH07DEtTkyROyGfFtuz5e65suJkm4CfxLv5GyawCouGgcR9ATGKJUzjqHU5E5wM5Ui+kL4omA9U4kdL/O4C2hNrilcKfoWQdW1eGD71BT9+ZV1iOjHs8ZELUcC6Q/D4JUOjo/IvRgU8Ru8cTXzIPjKzvCjOZ06bSaBI81Z5Zypu97Jsa4hcdZLiTj1uaHPxLejzX05dah9ZPx0OYFBnLV7fHUmhAyqw4DH0cwtPSRFbBdVCNQV43Zmq9sbM3GgrnFN3VIBchjkkiMWmKHKmcByxeMEo/EQsbwK2UDdfzM9hit1J88FkCAwEAAaMlMCMwIQYLKwYBBAGC5RwBAQQEEgQQAAECAwQFBgcICQoLDA0ODzANBgkqhkiG9w0BAQsFAAOCAQEABGOxQrpGycCQCrJ0KuuRSX/2LWpYO4TWbygIR7kAYroyBg34vdjVUc3QEdVXcH8N0vXD7j9g0W1NeMBVD5MmHersxkDCtLSTN2WJeTTSuyyrmFRM32ea0h0QpLKR8G5xpGv+v4AAhqvOpZqPxu/rhh1uYvy0YoVnNFG1yTtRcuB+C9ERWxGpicSpNQU8NF+OQ8UAsV3vvgDPyLoPTk3MxxRjzGgaf/AN8uMxvgSuIJDrj+njTEsSPrSdlCxvWmw0tDk+2JpusNg8acRFnYZ6DgLi1HJf2IqHMVCGQs/y+CncLfuYgimzTsfHwRHlmE37Be9+ou8uJShLU/DHocQUdQ==", + "RSA", + "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDiUeg5mqm3w18z8vZN6HjdOq9n0z5zFzUWbSB9OwxLU5MkTshnxbbs+XuubLiZJuAn8S7+RsmsAqLhoHEfQExiiVM46h1OROcDOVIvpC+KJgPVOJHS/zuAtoTa4pXCn6FkHVtXhg+9QU/fmVdYjox7PGRC1HAukPw+CVDo6PyL0YFPEbvHE18yD4ys7wozmdOm0mgSPNWeWcqbveybGuIXHWS4k49bmhz8S3o819OXWofWT8dDmBQZy1e3x1JoQMqsOAx9HMLT0kRWwXVQjUFeN2ZqvbGzNxoK5xTd1SAXIY5JIjFpihypnAcsXjBKPxELG8CtlA3X8zPYYrdSfPBZAgMBAAECggEAEAfBLSNZVhzOh310GLyYowLfHbmGuNwx9G6yWGxwSH2Y9H9oDoGgnVRmgEpPIPnx8qJQs84LxtVA+D5HBPGm92vGq0dZ4AtdWYsb2SgF/gEHUHj7Szis3EcRTfeyp+BqrA6wQ5jJUJxprerMlwcxyCDU8S7e6011sGc0herKyJRiNyfpq27OjQpzx6RMlaIwJSHyc2ZUkcTf7agDDPxxntLVL83RLdQ1VBiTZ6aSpRk35mCToQWRHJ/DEPHp2s4nYLuT6Q80tmzbUyiDJLPYWs/DFplgdhz6xPXcJT8AAJ4u/bx2rgn+Qs0hVLCWS5TndU8uUVMOga8nSolB69wx6QKBgQDn+jn3yo98DKfQq6okxaqk/JHEIfk+CRxqfT0M0jv1Yu2i+AcURXi9c1YbWj5mnmL1cbRvAqROPirSVbKthNr6f1LOBQNE28Wp6e0UhbpdCysAJQ42zIt5zpkjNR3sv7Ov6rNpd9rESprCrlZVGO6ASqyE4MLDyleeKvyWAcap3wKBgQD5wbL9tTyi2egTPrNKk2SNAbaSN3kWh5fyO9iFgaWJXcx9HLOxQw4eHKbd0yivf/p+rA2cxT43V+JGeYUabPAdqlxwnogiAFhkKHRK4uykpYtbcXKl1bcokPpjUIZhW0eBuTzLHW+TqtARUwTvJarXcMO97ElpjqyggOs71dWcxwKBgG2LeHxPJ1rJDyY3Km2a+m5W5u7brDtjSgvvgDjhvnaudNGUlqM3+0Bbirn376e9+7U0MKSLdtOL/+S7m1jdeBr6rTukmlqV/c2zLcWXMN5nO7MhzIiMJwEqUp6RosP98viLuJWBh8lSAasVcKdW0qm6maVWDiMvFhkW65ha1lm5AoGBAIlWkFye+rb6aHmcsu7BWjuHw6Nnybgv28giTJAtmJ+O6tGppM5G905rpv64DNlk4JQjfGDGvc2lEBJ85EqBuRA3DBoJswYvdmutKzW6zzJCrf0cx1SVzuAJyQYv05VZolqdR+bRSvfCqCO+qSnwZp+NrLWVZ1EaWabw/WmrzoklAoGAZU7ZG2qjp/XNKDvimqpwUMEFnj4KJXsbKEuL8CmLyhOOtmwvOeCva/Ks+Mf0rIaPX8Y7ZYzngxXvts2F3wMPwe1hrdJvlPtUnGAjSWd23z99klQ/LxUlmKhaILefG4oH0SaoaWq2/jdXUVno3MYgAm7r9jChhKNLoQVhBCpmOI4=", + ) + ), ) { override def regenerate() = TestAuthenticator.createBasicAttestedCredential( @@ -313,10 +430,17 @@ object RegistrationTestData { val BasicAttestationRs1: RegistrationTestData = new RegistrationTestData( alg = COSEAlgorithmIdentifier.RS1, attestationObject = - ByteArray.fromHex("bf68617574684461746159016849960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97634100000539000102030405060708090a0b0c0d0e0f00200dccf988353c4ba273b0cb871029fcfd3ea2ba0474a3c8aa34120cdf386d41f8a40339fffe010320590101009dc7b1317f0d4791afa7312dc4189fa272d7891853d48bd93064057c785248a0592cf2dcc3a31430218bd960f4ab7df989f142f4ab31dd4e481b28cf8e715656d07b3ecf1d88b6a2621f4bed5972e18be692ef155887cc0d03e200d0e5144995c1c1eee75cd68ad625c586dbfc2beeedf911615bbc7c0f14933a46c9bf4506f14337fec3ef57dccda236e4df1d83aad4bcd53ff9e754da7775bd45a09447483173ae265bee5560960fe581ab5a29b57ebbd10c2b07406ba259cbf20e7b22d96dafcdfb9b2d475853d3d5ccb6e619994f2ba6ef112165cae2db9d608f6c68dfaabae056dc19f933080d26c29dbe47dcf88a5435e9582df63e5f24dc35ac1fe88f214301000163666d74667061636b65646761747453746d74bf63616c6739fffe6373696759010008003116f6b02c14a059d8a0e92fdb5653b0c459528761cfbb2d34a192d12247ca9cfe7f164322ea38db77e9ae470d85ff00a892bab69dfb06b71bcda93b3b8c8beb1a530cfbbfa06f021e78230a31f5554f9547e34c1f9a47fb1cba3d76871796d92c5ee98ac367740d8ec36fe58dc9fdcb0e6a343880d83e1efa02895924278ecdf20a6803a2ac2c0309166346a8325ad6068a066fc12997df73ea0c0e32d05ecedc5d4c6de917fc1bc8e8cbc910a17e87159dc73552d8788477410d271e42fa261cc22c1d8edd464b3d082452b16dc5b19e81426b6bf7ab7de362faffc1697a9b23b971301f50fe38596b453bd614c04dd9a75f1e0aa1d38153e2e5a9268363783563815903733082036f30820257a00302010202020539300d06092a864886f70d010105050030673123302106035504030c1a59756269636f20576562417574686e20756e6974207465737473310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b3009060355040613025345301e170d3138303930363137343230305a170d3138303930363137343230305a30673123302106035504030c1a59756269636f20576562417574686e20756e6974207465737473310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b300906035504061302534530820122300d06092a864886f70d01010105000382010f003082010a0282010100d22a48cd3bdc9fa809a24a04dc158976b089296e54a949b29b7092dd5f16d2db81ffac4c814e2aaaf0be4f7e8214227ceb30cfe5da668d442999a40be8b2525b449084e7b5bdc3f29f16e303d3610500851e4d32053b1b0397ea285fa60a035df598618b5d67b2d1d8631575edc6d9de7873f4fc3156be00a59815adb226cfc274c86075bb3ff00d9e17bc1114220f91c23707ff415917ffaf34320845f50a01464a7b191385d8cac693ac68c26ed5589bec92f9db757df64bb025085bdee285f3b88e49d959f7ecaf0a70fbf5a3815bdb947bca995ac21c66d765ca380d8d348da06292375f7a8e9d5919a25f96168e61e67d097b7727eeca3645bf039fd4590203010001a32530233021060b2b0601040182e51c01010404120410000102030405060708090a0b0c0d0e0f300d06092a864886f70d0101050500038201010030d14a17813ccd2e2374300525cbbf6e55cb2f0b7e83fee0cc7345addca2cc139d40bba79846739630a9d60959d74eec2d2ea1015a6ec3fa9660be494b5efe80b3888287c6e275f4121f6b7dd076850de8e85538576ce44a71fd487f1dba264350dc1926eee25968c69556db43f4821272385e46c44715e3a7d603d5f6f3ae46abee46abb89070bd4628d4165b8c34bb77854b9b03a37efe3bf9220cc2ef1c4ae88c820eae5e984fbd54a280358d5198cdd3bd6bf54ab14c2253abaa59cd607769b71e8ed7b5a9b0a80d96002ab0cec9f0c5b387bcf44ea3a5b53f421a0ae8035be68e3c9afad1f6328afe05ad2a90407aa2778e1ea88062b0834dde1baf14d6ffff"), - clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","type":"webauthn.create","tokenBinding":{"status":"supported"},"clientExtensions":{}}""", + ByteArray.fromHex("bf68617574684461746159016849960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97634100000539000102030405060708090a0b0c0d0e0f0020b7462ea8d0c81456b6f15293642361bf5774a683dce4ed57ce9ed627e6ff18e6a401030339fffe2059010100c6e9a3c8649c81373f25650c4c92e54dcc3d7efbd7b601ee50479fca749fa44e2ed68de5d0ebaa83d3ee98ff457096bb9f02445c7a72ba2fe688fc4bebbd1a7d5e51d15a4b59315939b65e1823c6ef222dcee32c4ab2d971675ef8086d694e4fdcdd55c358eaf279eae7c9529239d07f236893bee9c74627a9b1c655aff3a486e3888a563155432e7c7f35fb7307c79c8408513a396d9d0b6094948a6b385c0143fe0645483dd04a775d17a884a78cd54c87f5b0b992664f5caed69d5f37544e0fe8ddc725f5ebf89e9e70bb98c6138749c5ab6e328eae4fd61d4898ad8219337ec008db674058842ea6a4d3740d8794ee799eff6a04bf75141499aa9a8dfdff214301000163666d74667061636b65646761747453746d74bf63616c6739fffe637369675901002ec4b2537e85566e913dc78f1e0a49141b88709e90e24f241d0a217d488194cdbe84a5a52e703d23116ad95d4c5540d514786b8215d2953d5a2dc6a8db68239b6cdf80dde7477a13ed92b7dd86e73a996b23607af3824e8703383a6f7c244c8c82a8a858dc44ac15e58fdfcf46b6bade67c4a04657932edf7a5fececc70a70817d8645113ed02650c4f38b32c89e5160f40b516db54e4e7677e109f07e7279f46643641623019c378ba733347a43290a22b37b71bcc172d208bf551e3109d6fd18f2214faf4263d236be2170296af5151d1e61f11beb2aebf5f40885ee2ab026c5212d9633f806d956714854ebeb8a27c56f37b7e10aa717273e9af84f95fd7163783563815903733082036f30820257a00302010202021750300d06092a864886f70d010105050030673123302106035504030c1a59756269636f20576562417574686e20756e6974207465737473310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b3009060355040613025345301e170d3138303930363137343230305a170d3138303931333137343230305a30673123302106035504030c1a59756269636f20576562417574686e20756e6974207465737473310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b300906035504061302534530820122300d06092a864886f70d01010105000382010f003082010a0282010100ae0beb5c73cd4445d3e589d30ab26bca92c33db6d18f4861d707757a7efda235f91c2ec93bbd728cdb286f0d911ac7eba2c9433a04d2e227af0c55d82dcbbbc82b96c3ed079eebd1502150c9e2ca128a08c499cd11f72058d7edd6db4937d5de67083a1d04c9637555ab8d9ba2702ca24db8b48a558e9721935b9415ee783e8d5d378bbd494a7a7226495dbe61b699d80479393d3e40fb2dcb90b66ce1edc8bb554f839c4f4621e06be32eecd434a8d26eecd011552e45b0563647f12a76d41209db7ea2cb0821723155bfcb4479f8911f503af5a7488a5e3f587915949537bd324ff5b81d13f99838d9e1daa1b47c7f9cf3900b74c62abd31345a0802545f5f0203010001a32530233021060b2b0601040182e51c01010404120410000102030405060708090a0b0c0d0e0f300d06092a864886f70d0101050500038201010049b713e2a6d40c6d03ea70015a2e5bdcd981dfaa95a1028c52439caa3c7a82a9c203c5da1370578fc0be2442cb96bc35d58ba9a56cf16a10f1c870d72831caaf26092ee131b8adbc0f1b88a2fb911ff91708aae28e057b1b5c78a67765e8c77aa5d7eb3caf77bc8d5b50e02bf290a605d6bb57d2ce2f1190862143b8c0897351d3ad886708a505565847301649f90edeece4a37a5983cc5f8bc1350ac6eb4f5a61ea7544a8c20887b5564b72b95b6dc1ef8f31581a77de7ed0b212a9c0d0c8027184d1ca4536a3d38c4d20f5cfc1b9191d9e1797676cfa229a61ee63755892e7714bf30065c4b68b31a8f65ecd65fd1a77a5db3f91845413afa5e2e47643f03effff"), + clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","type":"webauthn.create","tokenBinding":{"status":"supported"}}""", privateKey = Some( - ByteArray.fromHex("308204bc020100300d06092a864886f70d0101010500048204a6308204a202010002820101009dc7b1317f0d4791afa7312dc4189fa272d7891853d48bd93064057c785248a0592cf2dcc3a31430218bd960f4ab7df989f142f4ab31dd4e481b28cf8e715656d07b3ecf1d88b6a2621f4bed5972e18be692ef155887cc0d03e200d0e5144995c1c1eee75cd68ad625c586dbfc2beeedf911615bbc7c0f14933a46c9bf4506f14337fec3ef57dccda236e4df1d83aad4bcd53ff9e754da7775bd45a09447483173ae265bee5560960fe581ab5a29b57ebbd10c2b07406ba259cbf20e7b22d96dafcdfb9b2d475853d3d5ccb6e619994f2ba6ef112165cae2db9d608f6c68dfaabae056dc19f933080d26c29dbe47dcf88a5435e9582df63e5f24dc35ac1fe88f02030100010282010001c33bc5aba41e31d9be5cd7db5489609437010f38a8871124cb1b8c02db8753521e004a48825e505e31db9f1dcee8a5cc9690675a79ae46cf75fac8e119362e6d3eac5f1964a6e23dceaad77aad0d7291166e46c2a0dc07240d3fdfa7d65a53861c77865a714cd931071e506d665688453279cb88b5173c52e4bcc5ff5a4e7d1a25ec361c703f032a0f927a33348779bdc877b206d19c71b3851d572417206d76a605cdadd3f4701c76d98bb77d3c0daa516219cda1a1378ff1bc57e5a53d2ca4acaea01a4ba3daa1436d0b6e5773ad25711802d974e3bdcfafee572cd5cc4690a8aa861a64d110cdb108be6b5de2d2f575adaa8f8a6a4379581da7ad70f86902818100d71c1b9085370c186c92ecf0c9665c84caf7308b97c47b6e8c42b7e7de2ef0b904228d59a7285fec01f181f76039bba36177d89eff2ea5109e1d8267ab85b88e20d988d9ceee81d2d86f373fc52938549b944ac21eca3329e8b629afb557d28258753d19fd429cf84983fdc3335dab622330a4e0051de4ff707db659ed6fefcb02818100bbc5beeca7fe242ec3e4d1193f6df05bfe1516ccfd0577008863533d3b2be42d2ebd0a8d6bd172edf8eff30462b3beb31cbddb449853b6de0d58eb4269b7374ae80dd189c0243ca8da1d19097359784bf124dd92949b460e90b075469a676da2f00c94382cf9b5b6afa9828f4706331720bc12d37463812019812f6bded449cd028180429d12b02b80c37f20c85315b1d8c017e35eaf2adb61de337abe02838c5b8ef24ca4828f5be375e8f92517f14a5c368e3ed5c5405f97cb481d1ed84e506085a985e4b7ab73988a9d87a6d13e2f49378783f265403e16b1c76da853ba74f6f05aab180b46ec15dfd447b7d732c6ca60137100545e87571d9e38f0c5328e03d70702818049b32ce4006ffccdaa2fd66e7d79ee3c7d36d3d33380809be1ec725077381c002bf720fc2f146f72be219815e193c146d60222dd0298e10eb8d86cc68d6dcf33046fe00d9c2fdceb3d68ec59cc3f92bae3f45f4f582ab5cda3b6cee11e5b7829dae4650cc382637347f155805d152eda660bcbabd963f0dba3871410d7ce250502818039ee632c8dc74bae5b99b28d2c53c80934709101aa2c64f696d23f53842074814b023e579525105ffac163a6b22f25c5ff4b5a2961d12eca826f7ace40af34c0c5563ba6103d119e93cc900fbbe9f12677f6eba80598d305af3d706a625250c991a1bf3f8b149aec248777ecc0c52f56b44980a5c994d4b9d9f4f74aa4464b80") + ByteArray.fromHex("308204be020100300d06092a864886f70d0101010500048204a8308204a40201000282010100c6e9a3c8649c81373f25650c4c92e54dcc3d7efbd7b601ee50479fca749fa44e2ed68de5d0ebaa83d3ee98ff457096bb9f02445c7a72ba2fe688fc4bebbd1a7d5e51d15a4b59315939b65e1823c6ef222dcee32c4ab2d971675ef8086d694e4fdcdd55c358eaf279eae7c9529239d07f236893bee9c74627a9b1c655aff3a486e3888a563155432e7c7f35fb7307c79c8408513a396d9d0b6094948a6b385c0143fe0645483dd04a775d17a884a78cd54c87f5b0b992664f5caed69d5f37544e0fe8ddc725f5ebf89e9e70bb98c6138749c5ab6e328eae4fd61d4898ad8219337ec008db674058842ea6a4d3740d8794ee799eff6a04bf75141499aa9a8dfdff02030100010282010015cb0d606c4835640f2d6802794a9d8846389ff6b46ded8ce5b623375ef5b55e6cdaa1ab5740d56accc7d71d7371e021e84c559f12e4b4ff80864100df1ad5e7271eac62f489c37d5f745a9f7c73fef3ee0992718cdc46df44e5c3ce709104127deeb0c7240681d0826b28c492342162d1f1fd96022e6217ab0b9e364f29263d01071e09c724600996d64784208c66cbfe45c16855bf692997fce05cda8f78525e6368a9a764e21d6c8daf93f3ff78da2bd975dae066b6bb47f13d88142038b9e968ecb262e331ff6768af61f52f9ddf7d2dbdb82d7528ae6afbffac7e288564e9277ef8cad93c1ed4688318a2afd7d4a521e2fb927711d73fa60b68db726e7902818100de9c4f2236579c5aed66679b7ce5f33711bf41725d6167401fd167355055565a68b021c2c92c7eafcfb6da486b72d1725c0e992ab3f40937284a1d2e9dd4e4e38731050b86e2e33e66257c7d89f05e924efb305db4ff4ea77f89866b8916c223bf397ff604e7e2a5792e03da6648a2e00d825c852d995170cb66529d31f2dbf302818100e4bf63af4c2ac829c5b2b81ce8692297c6266b1bc1dd1da5164b7d52d0687cb3e46758d4c804a7dc8486fdbf6cffbec3306cc035822b3f5929fff6d0cd23a7d9999986a309220c4e97f8bddc53293740b76f3065eeafcd0e4e3e496d12efddbb25dee0d46560051bfa38be985ee8f7b43d03c203f590f61dce4c2ba3864554c502818100c72721e8a2a3389f6439b44137b8e52104408d858a2324e30a5425b85d992afb112359e0d0677d233e7a00c8bf4fe62f204a731ef00547e54fa7167a68fb589671911a4958b04ccabb4998191bb9ae71c83512ed128b41cbb9dec822167004d9442f65da2c4363d1d41aa599d2ddf2d0ed650dee9b7fc98b567cc1361ec47d9f0281800a7b469b12b767229adf7c963e840ac4bf9ca50dc98d533d6c4f1b37ff3aba7417c4308ad77b7721a0a4fadd99a6025cb94c5266614790088ae722ad20a94098b4f416fa4381dee47f0c33cef3b490c6936131eb89cb5e6f0860cc4686369d8764cdd8a982d7aa8444abf2f7d26984682adf9035543c473ac8682f1bb81c572d02818100c4b4a8466afcf8a822aa53570f25dbdd5424960ceba2899ca1853d8c56cdaed6b52091807be7f5d94bc2350b89e458a1ec4bb2ee8742c2acfea9cd35bae56eed38b6c4ce1f7b0e8c4c8a823fb8d6b7e0c2da3da21944d44821adcba757346c228d5bbb767b858e1adf1bde99fc838b119f0aceb9f2b79e55b32a0ffcab590c43") + ), + attestationCertChain = List( + RegistrationTestDataGenerator.importAttestationCa( + "MIIDbzCCAlegAwIBAgICF1AwDQYJKoZIhvcNAQEFBQAwZzEjMCEGA1UEAwwaWXViaWNvIFdlYkF1dGhuIHVuaXQgdGVzdHMxDzANBgNVBAoMBll1YmljbzEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjELMAkGA1UEBhMCU0UwHhcNMTgwOTA2MTc0MjAwWhcNMTgwOTEzMTc0MjAwWjBnMSMwIQYDVQQDDBpZdWJpY28gV2ViQXV0aG4gdW5pdCB0ZXN0czEPMA0GA1UECgwGWXViaWNvMSIwIAYDVQQLDBlBdXRoZW50aWNhdG9yIEF0dGVzdGF0aW9uMQswCQYDVQQGEwJTRTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK4L61xzzURF0+WJ0wqya8qSwz220Y9IYdcHdXp+/aI1+RwuyTu9cozbKG8NkRrH66LJQzoE0uInrwxV2C3Lu8grlsPtB57r0VAhUMniyhKKCMSZzRH3IFjX7dbbSTfV3mcIOh0EyWN1VauNm6JwLKJNuLSKVY6XIZNblBXueD6NXTeLvUlKenImSV2+YbaZ2AR5OT0+QPsty5C2bOHtyLtVT4OcT0Yh4GvjLuzUNKjSbuzQEVUuRbBWNkfxKnbUEgnbfqLLCCFyMVW/y0R5+JEfUDr1p0iKXj9YeRWUlTe9Mk/1uB0T+Zg42eHaobR8f5zzkAt0xiq9MTRaCAJUX18CAwEAAaMlMCMwIQYLKwYBBAGC5RwBAQQEEgQQAAECAwQFBgcICQoLDA0ODzANBgkqhkiG9w0BAQUFAAOCAQEASbcT4qbUDG0D6nABWi5b3NmB36qVoQKMUkOcqjx6gqnCA8XaE3BXj8C+JELLlrw11YuppWzxahDxyHDXKDHKryYJLuExuK28DxuIovuRH/kXCKrijgV7G1x4pndl6Md6pdfrPK93vI1bUOAr8pCmBda7V9LOLxGQhiFDuMCJc1HTrYhnCKUFVlhHMBZJ+Q7e7OSjelmDzF+LwTUKxutPWmHqdUSowgiHtVZLcrlbbcHvjzFYGnfeftCyEqnA0MgCcYTRykU2o9OMTSD1z8G5GR2eF5dnbPoimmHuY3VYkudxS/MAZcS2izGo9l7NZf0ad6XbP5GEVBOvpeLkdkPwPg==", + "RSA", + "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCuC+tcc81ERdPlidMKsmvKksM9ttGPSGHXB3V6fv2iNfkcLsk7vXKM2yhvDZEax+uiyUM6BNLiJ68MVdgty7vIK5bD7Qee69FQIVDJ4soSigjEmc0R9yBY1+3W20k31d5nCDodBMljdVWrjZuicCyiTbi0ilWOlyGTW5QV7ng+jV03i71JSnpyJkldvmG2mdgEeTk9PkD7LcuQtmzh7ci7VU+DnE9GIeBr4y7s1DSo0m7s0BFVLkWwVjZH8Sp21BIJ236iywghcjFVv8tEefiRH1A69adIil4/WHkVlJU3vTJP9bgdE/mYONnh2qG0fH+c85ALdMYqvTE0WggCVF9fAgMBAAECggEADxD0E6w97CI5ThOeDmfUjJQjd4+rA1gU3swuqhHH0Jy9zufpPpUkjzdCKvQwh/Ifc+zajMaxyfxScGMWqXGLSgV6nG+ZKx4nH+R1EwzhSN/kaL8W8UOvFqcWgnS4GVps/AafyoxJXKU3Fc3WglDAjwo4VNSWxGxRGT73tOWlnhByR8pDg0RY+IUywvTwrMkK1t5klMKn8iGXp4FoJH5+9g2cyThhZKsbFbX79ldxvp6ZPXztQutejFLUGgYjE8kc0WjYOpdktHb5vaA1Fb1EovMPVrw309TDoc7a7vd0/JWextsDilSeK9osOqSjtaK6lqDU60MpW88dXHqf2/KJUQKBgQDpiMPJgmMgS2Q4NDDjhh/pXOSzylB0C4Ut0orzV8XzhLBZK+2zpOfeshO0NEl5HKrD5+U6qRUutDnziN1XNAi9zaBxE59HHsBn/mEQAZ4BABh7yqoRzsPZyXHqH9I9nXH4E1ul7e8olEkTMxfoVpogvd6ouIHRzqMcdfVu+SIl8QKBgQC+yiecSUpUeqrYXamjJNMGffMcylE3TBRxnxsxb1Kc9K0fP4Kg96P0pd6n8ZVyIlwQbPHf0SO16bCmMWAFcW359Gwih7RsXtiVn7E3rCJh2v/xXYVTXaLVPPBTFmltEptHB9pdB898TXeD3HxEvK3T6pO3ikxi4Qhv5iA70ANKTwKBgB9i6i1jAL+OYmHLYeayWAedHH+taTKvea625UXfPhOEec8CQGgseZ1MgalufZFxcOHzgLNplhc40bERa+4UaDhaMn4ADSAZ2fIgFht4nCu2P5QjoMfT6i6TDGRS8oalanPU5jRezg2+IDQcYdIwEXblDc/PPGNeSj3A/MN1aEmBAoGAGTU2hJS+aGkIt6uwZSjRZPMxMuWcU3UO4nBGNILj5G8DjLRkewYdOI7N21y1BS57AYSTdKH6WOe2ek7dw/pXsIXV374UXZkufp5p2NQ6erlnxak4m2oswIXbru6wIUQrFfh4poAIrwDBQL/Z/276fJxqxf5U11+qMLa0HZL/TEsCgYEAyrcR+1DZJEcMeQXGc3bwDuCKcZRxSSnmYeWBL4aY0BV6/ktRKF4a0VZtXeNCUeXmdJZmr6yG0YdD4spUBlZZuWqEnEWbmhKTDzJKhEYktBxC+DGiYrkSZwWQAVjgKpPo0PL5I0j38UZfk2s2w0KAecGWhqMZmFQHh2DxwvQ9ECs=", + ) ), assertion = Some( AssertionTestData( @@ -327,7 +451,7 @@ object RegistrationTestData { classOf[AssertionRequest], ), response = - PublicKeyCredential.parseAssertionResponseJson("""{"id":"Dcz5iDU8S6JzsMuHECn8_T6iugR0o8iqNBIM3zhtQfg","response":{"authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MBAAAFOQ","clientDataJSON":"eyJjaGFsbGVuZ2UiOiJOM0xqSTJKNXlseVdlM0VENU9UNFhITFJxSHdtX0o0OF9EX2hvSk9GZjMwIiwib3JpZ2luIjoiaHR0cHM6Ly9sb2NhbGhvc3QiLCJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwidG9rZW5CaW5kaW5nIjp7InN0YXR1cyI6InN1cHBvcnRlZCJ9LCJjbGllbnRFeHRlbnNpb25zIjp7fX0","signature":"g3lWw0SG1AkAaxelbkPnrYvBBRg8VQZIkNHBp6Ogn-2E2zOan8Xe_FBItM_P1K_p49G9SpsljIrQxakH1kZMGBMflHYyaJC1duX0wqgUdFwz_p3sEfo9_vYpXt_Ytj6QYCOUjlJav_eGhtA_K-AWrw3Gz74nUrnjiBaFw-Iqno9ZucpRDo_0vKuTb7ARDSOWYo0eHWzcfY3CvXuEVxDlamUeA_JRtM2t4BKFaUo_91_D4XIvGO9KBWdM0d3KaU5hotO6kLjk0-EdQHrBNSweU0KeJEqBlceFj4AiPN8RFot5qXq1w_Zs9orLME-HwvkVykAGRZSdu2Pcjr2tNpQohg"},"clientExtensionResults":{},"type":"public-key"}"""), + PublicKeyCredential.parseAssertionResponseJson("""{"id":"t0YuqNDIFFa28VKTZCNhv1d0poPc5O1Xzp7WJ-b_GOY","response":{"authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MBAAAFOQ","clientDataJSON":"eyJjaGFsbGVuZ2UiOiJOM0xqSTJKNXlseVdlM0VENU9UNFhITFJxSHdtX0o0OF9EX2hvSk9GZjMwIiwib3JpZ2luIjoiaHR0cHM6Ly9sb2NhbGhvc3QiLCJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwidG9rZW5CaW5kaW5nIjp7InN0YXR1cyI6InN1cHBvcnRlZCJ9fQ","signature":"jZ9gU1TUKJ851-tmsBVtoE89Zt08UkOFSx7_LSQHYluLZ8Tj-LAfsxLp6TOHtNd82yg9oSFZZRUHCunZgtljBuLS6zWyhUd6kU9uoh4tUZ1ZPQuKj5F9YbjzB_qd3xRMsk_wKydErCr6S-TZ7sxAXuXs5JnjyUWTvDJILwv-bzIgaPaXZ_twXAe-h5sN14Vcf02cMo2kFm0wx86jOg25CEljJN-rQdRzSq4pHew8YhU_asXoMbGrkEtNhhjlCmyMoVamPd23kNMCjP05ofiM4av4BuZvi8RaeNhRJ1-oy-TQgMDG_Itpu-cSiD7zUOvNByjycicSevHvqLaOFyrhzw"},"clientExtensionResults":{},"type":"public-key"}"""), ) ), ) { @@ -370,8 +494,11 @@ object RegistrationTestData { new RegistrationTestData( alg = COSEAlgorithmIdentifier.ES256, attestationObject = - ByteArray.fromHex("bf68617574684461746158a449960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97634100000539000102030405060708090a0b0c0d0e0f00209e5f31be46db4229e201138428cc52ee5cf95649af97192b0c0bd95c6bf7da42a5032601022001215820edd3776120e992e2917f67dea1bd9ab796d4766e1c8d7f158d12b0b2c4932ba4225820393783915791af3e5b4ee9a8be59f9fc36b8c95b084d2d44c0095572ad8ac90263666d74667061636b65646761747453746d74bf63616c67266373696758473045022100e4653ca6e7334f6043e95636bafe4f4ca17eecfc21bd17d7b849e4fc723d07560220389513d56b8c030e964d4a286acd3cc5f74aaf1665a84a06421ef23cc2db0a3963783563815901c1308201bd30820162a00302010202020539300a06082a8648ce3d04030230673123302106035504030c1a59756269636f20576562417574686e20756e6974207465737473310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b3009060355040613025345301e170d3138303930363137343230305a170d3138303930363137343230305a30673123302106035504030c1a59756269636f20576562417574686e20756e6974207465737473310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b30090603550406130253453059301306072a8648ce3d020106082a8648ce3d03010703420004b83a0a452140254924a85f18868222eef921abf3859a6c4bc1704c4a3a55131be3191da5c8f2bde0e3f7fc0042e3ced4821112139a085ccd331ab0d9a36ba2a2300a06082a8648ce3d0403020349003046022100a9542f7287013fdefd29edadb84ad61f5b90c938d315c4dbf72005ed2808b149022100d4235ec51d66d892ff9447585167f728ce87733a29e41bac97b437b45ee1571dffff"), - clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","type":"webauthn.create","tokenBinding":{"status":"supported"},"clientExtensions":{}}""", + ByteArray.fromHex("bf68617574684461746158a449960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97634100000539000102030405060708090a0b0c0d0e0f002032b7598c61949137c693b792dd4d036546efeef8cd9998869f72e33b9989c429a501020326200121582026ee0679adcbdb11a2cf0e5fca991c0473644ff33eea4f917c570bfa58315fcf2258206135429534addbcbc9fd7b4f65cdb9f6b26082721c0b388b9b3076492e4e753f63666d74667061636b65646761747453746d74bf63616c67266373696758483046022100914467c73433609371460eb9ca3a12d70c5ecfe942621f830a04a402f284151102210082a3aee4e2cf2dec408b7db938f154ed35e027159e1643d85817e5583cd1bc4563783563815901c0308201bc30820162a003020102020224f1300a06082a8648ce3d04030230673123302106035504030c1a59756269636f20576562417574686e20756e6974207465737473310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b3009060355040613025345301e170d3138303930363137343230305a170d3138303931333137343230305a30673123302106035504030c1a59756269636f20576562417574686e20756e6974207465737473310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b30090603550406130253453059301306072a8648ce3d020106082a8648ce3d030107034200047cbe20ac36a8636bc7db9f09e982d2b42ac14dcfae2b4817779dc866e447567be05809d3cb1d86175066b11450e9e7f3c41b80738f47434e4943ca7262742054300a06082a8648ce3d04030203480030450220090e769ad19167008e077b2e59a4970eeb0f2a012ceea26b29d9839807d0c0fe022100dbe4ca02fbe9e2c35556d7d41ae37fba68a9ea1fdc629e49e1b31bffa1482ed4ffff"), + clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","type":"webauthn.create","tokenBinding":{"status":"supported"}}""", + privateKey = Some( + ByteArray.fromHex("308193020100301306072a8648ce3d020106082a8648ce3d030107047930770201010420d2b1178e0663be84ac03cc4b0a780932d6888505e96e028a8eb88316ce403e42a00a06082a8648ce3d030107a1440342000426ee0679adcbdb11a2cf0e5fca991c0473644ff33eea4f917c570bfa58315fcf6135429534addbcbc9fd7b4f65cdb9f6b26082721c0b388b9b3076492e4e753f") + ), ) { override def regenerate() = TestAuthenticator.createBasicAttestedCredential(attestationMaker = @@ -391,8 +518,18 @@ object RegistrationTestData { new RegistrationTestData( alg = COSEAlgorithmIdentifier.ES256, attestationObject = - ByteArray.fromHex("bf68617574684461746158a449960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d976341000005390f0e0d0c0b0a0908070605040302010000207b733c7c32c0303159eceb83a77e359de67b8aff51b4ae82af5e34e7b39b3a24a50326010220012158205d2702c2d02739b3a8bfbe84011cdf4c39b3dd1da73f92cb70c8ebe557ee277f225820d60faf92a4fcb6d49dbcbc59260d2fb031ce5c8a95f93d56553662bfa050ab0363666d74667061636b65646761747453746d74bf63616c6726637369675846304402201355a030930063732001ecbddf42e2b8de03ab3fbf96c492fd224929310c36e0022014704aa8426eb36229d5eb59db825f8184ad29ad1a3b6ab7f29a9a8304ea00de63783563815901e7308201e330820189a00302010202020539300a06082a8648ce3d04030230673123302106035504030c1a59756269636f20576562417574686e20756e6974207465737473310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b3009060355040613025345301e170d3138303930363137343230305a170d3138303930363137343230305a30673123302106035504030c1a59756269636f20576562417574686e20756e6974207465737473310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b30090603550406130253453059301306072a8648ce3d020106082a8648ce3d03010703420004e2a3cd9d0a55a1204a3eb3681b793cc3251a28d948428111241359d6c45f5af1ba36a50e0b5cd1c3fd81974cddd9fdb4aba0fd1352e1e107721433b32f34c717a32530233021060b2b0601040182e51c01010404120410000102030405060708090a0b0c0d0e0f300a06082a8648ce3d0403020348003045022100e5818e204920da08899fb97942c57b792bd769c2bfbe7ccd5c25d2169b1588b402207b7446fe3b419d0a4850a87abf3679611086f83df605e908ad3026cd8695f749ffff"), - clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","type":"webauthn.create","tokenBinding":{"status":"supported"},"clientExtensions":{}}""", + ByteArray.fromHex("bf68617574684461746158a449960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d976341000005390f0e0d0c0b0a090807060504030201000020be7b974862619d3b0cade8425a6d9f9e45d6fdcc81f3753746fb77239f3493d6a50102032620012158202441c80a3b195d4843e403e1099a754d3a535135eea95b930dc2edacaf615e1a2258202250729253ca17376e87c7c1129ce29a5385f3358fa902473c1e971cba531c7a63666d74667061636b65646761747453746d74bf63616c67266373696758483046022100bb0e0f781d9379a86140fec8daa4c264dd9f038a0f2bf114933b10fb65e12d050221008b7a33c94396e746f53c51f173825da83284a8b5ab22f8966cd8a16b6229dd8663783563815901e7308201e330820189a003020102020223f4300a06082a8648ce3d04030230673123302106035504030c1a59756269636f20576562417574686e20756e6974207465737473310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b3009060355040613025345301e170d3138303930363137343230305a170d3138303931333137343230305a30673123302106035504030c1a59756269636f20576562417574686e20756e6974207465737473310f300d060355040a0c0659756269636f31223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b30090603550406130253453059301306072a8648ce3d020106082a8648ce3d030107034200049ae973f617944d82164bd0ab6fef650e6f1f16ee9651b7a1bfc64a46a0ae0ba37a5ac0fda5e89a9056e7fb18af8f72c38434a834a49f6b4be69c3f5cac680216a32530233021060b2b0601040182e51c01010404120410000102030405060708090a0b0c0d0e0f300a06082a8648ce3d040302034800304502203092c9c29195223366d935be85e724ef83c85b819fa693bdc1873b49b73426400221008d9744429db78e1cc8fb338010c6f3d5bd87ee9dfc180e618007ebf3ad01b559ffff"), + clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","type":"webauthn.create","tokenBinding":{"status":"supported"}}""", + privateKey = Some( + ByteArray.fromHex("308193020100301306072a8648ce3d020106082a8648ce3d0301070479307702010104201e032ba8427464054d6b938d76bd08525610812af53256e23c417be37f9cd6dfa00a06082a8648ce3d030107a144034200042441c80a3b195d4843e403e1099a754d3a535135eea95b930dc2edacaf615e1a2250729253ca17376e87c7c1129ce29a5385f3358fa902473c1e971cba531c7a") + ), + attestationCertChain = List( + RegistrationTestDataGenerator.importAttestationCa( + "MIIB4zCCAYmgAwIBAgICI/QwCgYIKoZIzj0EAwIwZzEjMCEGA1UEAwwaWXViaWNvIFdlYkF1dGhuIHVuaXQgdGVzdHMxDzANBgNVBAoMBll1YmljbzEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjELMAkGA1UEBhMCU0UwHhcNMTgwOTA2MTc0MjAwWhcNMTgwOTEzMTc0MjAwWjBnMSMwIQYDVQQDDBpZdWJpY28gV2ViQXV0aG4gdW5pdCB0ZXN0czEPMA0GA1UECgwGWXViaWNvMSIwIAYDVQQLDBlBdXRoZW50aWNhdG9yIEF0dGVzdGF0aW9uMQswCQYDVQQGEwJTRTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJrpc/YXlE2CFkvQq2/vZQ5vHxbullG3ob/GSkagrgujelrA/aXompBW5/sYr49yw4Q0qDSkn2tL5pw/XKxoAhajJTAjMCEGCysGAQQBguUcAQEEBBIEEAABAgMEBQYHCAkKCwwNDg8wCgYIKoZIzj0EAwIDSAAwRQIgMJLJwpGVIjNm2TW+heck74PIW4GfppO9wYc7Sbc0JkACIQCNl0RCnbeOHMj7M4AQxvPVvYfunfwYDmGAB+vzrQG1WQ==", + "EC", + "MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgaztl9/kO098o7Rt1vnCg2bWkN83fEN0qlBzFDiTZYSWgCgYIKoZIzj0DAQehRANCAASa6XP2F5RNghZL0Ktv72UObx8W7pZRt6G/xkpGoK4Lo3pawP2l6JqQVuf7GK+PcsOENKg0pJ9rS+acP1ysaAIW", + ) + ), ) { override def regenerate() = TestAuthenticator.createBasicAttestedCredential( @@ -407,10 +544,12 @@ object RegistrationTestData { val SelfAttestation: RegistrationTestData = new RegistrationTestData( alg = COSEAlgorithmIdentifier.ES256, - attestationObject = new ByteArray( - BinaryUtil.fromHex("bf68617574684461746158a449960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97634100000539000102030405060708090a0b0c0d0e0f0020fa616cbe1c046d224524e773b386f9f3fd0d0fb6d4c20700023288034e48f093a52258208b02052aeec1d7cfaf1244d9b72296a6bfaf9542c132273c4be8fc01388ee8f30326010221582081906607ef7095eaa3dea2517cfc5a7c0c9768685e30ddb5865f2ada0f5cc63c200163666d74667061636b65646761747453746d74bf6373696758473045022010511b27bd566c7bcdf6e4f08ef2fe4ea20a56826b76761253bbcc31b0be1fa2022100b2659e3efc858fd4389dc48cd0651487f2e7bc4f5eba59db154bdcd0ae60c9d163616c6726ffff") + attestationObject = + ByteArray.fromHex("bf68617574684461746158a449960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97634100000539000102030405060708090a0b0c0d0e0f0020e3550a5097d3a496c37eb51b9eebaf1f2b26888f82d611a945d02f06a232035ea5010203262001215820bb79c99a09888bdc49eb4714aa674df11c41e95c5ce25ba55e9ea7b6dfa59430225820b50119d7bcf64573eb6f298196a86ec1fc698b98b68f0686a75ed871f352f15e63666d74667061636b65646761747453746d74bf63616c67266373696758463044022100848fd7ad4bd6d19b9f8278c2fa21be42337a8407b3a14332bd741628f4c96781021f2ce04061cce131be0e0b0839cbb8a262ebc7dbac9e732feb47e3c159999938ffff"), + clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","type":"webauthn.create","tokenBinding":{"status":"supported"}}""", + privateKey = Some( + ByteArray.fromHex("308193020100301306072a8648ce3d020106082a8648ce3d0301070479307702010104209ae0d69504d4a0ee4b1077aa05e82aa5883c1c9c9fba08a7b884995e1b07af2aa00a06082a8648ce3d030107a14403420004bb79c99a09888bdc49eb4714aa674df11c41e95c5ce25ba55e9ea7b6dfa59430b50119d7bcf64573eb6f298196a86ec1fc698b98b68f0686a75ed871f352f15e") ), - clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","type":"webauthn.create","tokenBinding":{"status":"supported"},"clientExtensions":{}}""", ) { override def regenerate() = TestAuthenticator.createSelfAttestedCredential( @@ -421,10 +560,10 @@ object RegistrationTestData { val SelfAttestationRs1: RegistrationTestData = new RegistrationTestData( alg = COSEAlgorithmIdentifier.RS1, attestationObject = - ByteArray.fromHex("bf68617574684461746159016849960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97634100000539000102030405060708090a0b0c0d0e0f0020a8330648c97e09686004be4c429ad3e46886f6033117632cb1aabf261c5d11cda40339fffe01032059010100cdb2b9448221d1432be58b681f27ad204e82fe4c6176c64aed49792fe57c5ca9ddf7ce6fe22f81b67205df310d96668c1c1107ea6e250f4107692842c555c13d6e3df41ca701ce153705269658a186d9a1abe013b127dd51483323f3c82e281962eccfd4f59c05d778ecbbfbfb5eb5902dc91e1e187aacd97d42373a3c3e05218d291989133cf32641d322e6e472c3e4812e613d9ddbb67e74580570d5ef173561c146d81c56bf7bc6353fde611b54cd1fe632a314ac7d3e74ac18c0b7886a70ddcce226dd836791444a88ac9323877adbc5978a51d2abca189651ad5b71169df782f065908edd8ab9edccdc997212c32071b577fd58d55b22557d303d070b77214301000163666d74667061636b65646761747453746d74bf63616c6739fffe63736967590100bbf85e350e87886d80591e44b1a8e8f7fe7a4b3c4748b112ac6bbd88096a7b83c5e2f268154eecec230784729d6418809ce1ea370c374fd3e6151790d0a7f5a7a9e57dcbfd2e0cad26b11002232087eaaa0baf7fdef65c30518237d4ae7d36b7c49cc96b499afb6c0eab2c6a728fa847595071b56515c049d909707fbea2ee22ce0a325939af3b9021e1371bfea19cd14fc9caa1d1a41d5408cba381197c5fddc4e33245411d720c3acb4e53b415b120581d8093e25d710e5acef7e77889a71e5dee935f02992a559eab33725c832f3f24bf3934de2f5ac2eb32a9cc23a652bf08fc7e94c342ef62b555524b733447a19b3307fb41257794e041e91d1e1fbb37ffff"), - clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","type":"webauthn.create","tokenBinding":{"status":"supported"},"clientExtensions":{}}""", + ByteArray.fromHex("bf68617574684461746159016849960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97634100000539000102030405060708090a0b0c0d0e0f0020e4b8a8b5c225031de6ff7b7de055afd8f6894db33a7dfef71f9905588931b427a401030339fffe2059010100aa235e49d81e92013f0d12d21e461d6289d48d955c480b7144ee71bedf96df015e20c35185a43813d2a09c1264d58a2463d602ed563c52e5b3cee29969cfd595cd3a8a84f1b3ed5b4cba4888690368844f9d0778bc3c56b9ea2531f2f19b776a6758fc60cc3c5869d73d68f1f381ae6a8440efa305d62f5b1ed3228112af989aa20517e37443f7725f7329e3a212a2a62a5d4dc1bf650706a6e5c7cb648c56a144ecb9c1ff29a6e83188fa83a9a2f0daed0dda05b0645bb742b3b40bea52c5c82c14ff014f363e3b6830007346ce30a21feb0547bfbc5fb3c64c793f036f743e050c172047fc9d4d805c377be2ec7d2a64cfb62ad11d0527b1b4ab61ccd60787214301000163666d74667061636b65646761747453746d74bf63616c6739fffe63736967590100316bd76326d49143881c7385acdb010903357b551c73eb85dc0b52de517bc6dd7f6e576e2ce6ca1220903e364d37da4296cb635afcf2eab7b318607998bd8a1443d428193748bcfeacad6f138667b39dc06986a7ef1470628083a104eadf70bfcc49a4b418453b3aecd78e9c593e08ffab3acae59f0bcab24c0f5881772c498344e2e5979d27cfc49307b5d20a847b8a93d1e8b87600e4f55efbe03c2386f47879ecb7528db69c2b1e667877284a9e9cbbac833ea15c950c3e7d0006252344bd760e19549ac2cf2cc4220a3886d187dc6fe538d79935947b41287420466ba53f2fcef6a6a43f95de913dcedbfc85caebd8a5de6b36d603dd26b3d9c84fc17885ffff"), + clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","type":"webauthn.create","tokenBinding":{"status":"supported"}}""", privateKey = Some( - ByteArray.fromHex("308204bd020100300d06092a864886f70d0101010500048204a7308204a30201000282010100cdb2b9448221d1432be58b681f27ad204e82fe4c6176c64aed49792fe57c5ca9ddf7ce6fe22f81b67205df310d96668c1c1107ea6e250f4107692842c555c13d6e3df41ca701ce153705269658a186d9a1abe013b127dd51483323f3c82e281962eccfd4f59c05d778ecbbfbfb5eb5902dc91e1e187aacd97d42373a3c3e05218d291989133cf32641d322e6e472c3e4812e613d9ddbb67e74580570d5ef173561c146d81c56bf7bc6353fde611b54cd1fe632a314ac7d3e74ac18c0b7886a70ddcce226dd836791444a88ac9323877adbc5978a51d2abca189651ad5b71169df782f065908edd8ab9edccdc997212c32071b577fd58d55b22557d303d070b7702030100010282010029573fb3fca17650d53c3399f01505cf05e87eda74062e9135827c483b8b94861155f217fb7207d456b34669b1ece5dc47f1c650ffe513dd427388836eecaec33d6a572b0107b45700315951832ba7920ad3a3dbe1517d420e4c34f0146dee6237c717781a0acb03c4ca73778fcc379a6c114d2bc848b37f9c9497cbecfa9c0607181290eda54e995999b2c18286ce3abba2d1a18f0785b76b163335fe1d7a805f1fe17ed592eabca18be4da7857bc384ec6398a5784e022e16dcd4e61dad8d285a475600d9f11d6e5aa7989a590ffbc99b45283282433e3e4f8d96d3f422c90c850ef3f906ba935dec95ce01f1192685d0e7ab3da7593aca13f4d2890cd112502818100fbe8098b0b37fd43c7b10bb49212e9e162e9be02d6c559d6a1e30a87d8dea7970ea76425226980c1d5bb63ddefcba787fcab8601e89d070dba758d3bf39f4407be8ad6e95abeb86c60be1939614c67720f75ab140837955e037812462733dd372e4751baa5fe87e074064e98d70c201342e9a4d47d6cb88fc6df5db6ac89e14302818100d10a744597179abf260100f7b295f24bed809f101f5a9b388bb04378665461b48c1016677768e6612690ce2f794428eba2a8fa0821f58f713be04b29aa83664f07b3b962c004a60286ad35c585ed4bacfe66682490f7ab7e62529232be325cebe52876e6dcef53373171861b7d40520f69b74c8620ffac0fe64623358a1effbd02818035f843bb277f2a62d030cd5a358599d83111f524b490f9ab7369aa42eaa2e1730aafb0540868642ea3350fb36801d0f5e09b7b0d83a1c8f61701c26d9ac77f92cd2effd6651bc1756ed0aba4d084c710f7e0f4f348c367dc09903b120eaa1cf60a933b1e6b1bfa4e8b6d227fba6b1da022d0de00ac929384324e7ecc7970dcf302818100cffcfdd92bcf419a04bf24ee4f53204469a7fb1bb886974078c4452d6b6b73d787308e8a1de652aac10b7d0b01364f1cbcb832269b5b4f8093d9c40f4de7f588969a3ccf434c9cbc90b19079da9a531c69f70c91ad67afcb4d1ae8f9f201fc307dce78179625cd7f720389329ab9bfac343c3bb88ce6b6950f4223d0268057650281800193dfb5d9612213bbdcbfd274061e5c02d439e2bcbecee0fc6cbe53b2c009b3c2b9438ee48e8c56af5703b12551bf3480761132fa483b26b024387397fd6e6e1f90717b84ce5a24bbccee01180ff113363e5c83c5fb49fa8475db93cd7fa79965853f5c196717ec2ef0047302a7943df5ba2cc462f5f5fc3068d1f72b15a565") + ByteArray.fromHex("308204bd020100300d06092a864886f70d0101010500048204a7308204a30201000282010100aa235e49d81e92013f0d12d21e461d6289d48d955c480b7144ee71bedf96df015e20c35185a43813d2a09c1264d58a2463d602ed563c52e5b3cee29969cfd595cd3a8a84f1b3ed5b4cba4888690368844f9d0778bc3c56b9ea2531f2f19b776a6758fc60cc3c5869d73d68f1f381ae6a8440efa305d62f5b1ed3228112af989aa20517e37443f7725f7329e3a212a2a62a5d4dc1bf650706a6e5c7cb648c56a144ecb9c1ff29a6e83188fa83a9a2f0daed0dda05b0645bb742b3b40bea52c5c82c14ff014f363e3b6830007346ce30a21feb0547bfbc5fb3c64c793f036f743e050c172047fc9d4d805c377be2ec7d2a64cfb62ad11d0527b1b4ab61ccd6078702030100010282010040474c80299ea31ac56f7304df5b2e0ee473e169e48b73873fbbb64d9ebba95522f2cdd826dd7c3241095cedb61ad72e1869ea81306b6a064e80832be2c61ab395ede0178a19a83b2e29d2ed767f4b2571cea9dbfb81f0621d0c206ae0cd13b8a782ff16b312b97483553828f10eb58e9898cff08f6bf44840c513ec1fdb2793e5220a1e3a679b9a56802bd022886912bd8c5a39771267676b1818a82f9baf14974528062ca92c27c8a0e5632d34049e85f29f49483d60a4d0805294ba3aab83d587b91243b962c5f3fc4e896ffb68fdfde5825b3572077f66078fc6369f2c64edfca3aa470c40bdfd19cc017bb8443929b0320f3baa654612678d03fed958c102818100ef759c2ee1751c085486aca0a4b757ff7ff8aaf97bddaa1e7f622b9e70cd1cc140355b1cfb068fe49f9eabbe4e87b00d420a628b4d87bd1f03059fd43f260bd5708998e7f930601ca2d2edbf1a15306bfb2fb282c24a585ab16da6856d63ce6e6b6db0781396a9a54c984e6a6e78560d0e44497a99d1a82264bece88e1ed7efb02818100b5e3f12814079e996a83ea316380b94a349e1fde55c4c0a42337ce21c872904ef47c7c0e6d9aaf9c8aa1205383ba35f10b08601cc23eb5910a71163bac0211b9271e656aad54cadacaf6fa53947a8aa90b77be4a53bea74d5c982015e95d960d873d716bcb9d5f53db2e44edf931128107b43f1e6a258da6d831570fe1eb83e502818100a290a9b37a04533fa482b97765dbc2b6065eff53d82fa86a83f855bc7ec001218141b7d578e5ff922a7b420534b311662ecb761334534ea55b1bda61f16d16e3943f15bb8684bcfb33df16e082089892f6386f6c2e12e2e0cf4bad9d2fa26e66b0300b79b972b341313ac521a455b5b5af55d4bc92e8aec88ab4aeea66d72139028180338eee5feef58df66ce401b1884407c1ca127c741899d20574da2fbb11c7afb241c93b9d17627e9f0008d651f608059c530547c13f2a696b38ab7fa2e08a219dfb41b97bb8b04d64219e4aad006c8ffb84fda75a084d05bd7f1ebc1199f9e63be576fc3c931603dc90479850608f917033cfdb08730c3c0fec3e68305f58d42502818040721fd4a13d7001227d4c54fd36344145263de906923bc6c950aa573a8ef9df2ec3f92670895bd19bb79c0aa2608dc2e5f004d95e93ba1b7ae9483ec0439767c93ed09667e08a962f0bf4ab237e499327698ac44b1d1fb8147bbd39698b680406dcffdf297a9c7437f90ef61b0022df6a995fe026775ed49c971ed5e80a99f0") ), assertion = Some( AssertionTestData( @@ -435,7 +574,7 @@ object RegistrationTestData { classOf[AssertionRequest], ), response = - PublicKeyCredential.parseAssertionResponseJson("""{"id":"qDMGSMl-CWhgBL5MQprT5GiG9gMxF2Mssaq_JhxdEc0","response":{"authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MBAAAFOQ","clientDataJSON":"eyJjaGFsbGVuZ2UiOiJOM0xqSTJKNXlseVdlM0VENU9UNFhITFJxSHdtX0o0OF9EX2hvSk9GZjMwIiwib3JpZ2luIjoiaHR0cHM6Ly9sb2NhbGhvc3QiLCJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwidG9rZW5CaW5kaW5nIjp7InN0YXR1cyI6InN1cHBvcnRlZCJ9LCJjbGllbnRFeHRlbnNpb25zIjp7fX0","signature":"YQuXZwJLOXeEfrOxzG42yJxShEGHFbfD2oYURkOiZOI2LSzfcv5t5KDq4dfJ9S-U5aaylfIlD72u8rQeMIVyf5e8jD5z0bnPP5STZzDsJneoPGOQ6BQfuYGSGSO_JxjU9O6KduNTXKrm2KqaCptOTJHyf9geA2wR7_XSmEdg_OSq7e164ZIK12jiG-RFdEEVpWhuoJPva0TeHfe2tAnQPNreV7v8DaIOWJiBblQTirP0oUn5LrCNhl_Tsgz2-F8R53k48JpesiMhEM6r-e7DI83CrNRZWJnmO-04hMEbdNqO3TmZ3Fmtw9ufpn3zygeK0jrIw3SamFe2NgVvbcIHTg"},"clientExtensionResults":{},"type":"public-key"}"""), + PublicKeyCredential.parseAssertionResponseJson("""{"id":"5LiotcIlAx3m_3t94FWv2PaJTbM6ff73H5kFWIkxtCc","response":{"authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MBAAAFOQ","clientDataJSON":"eyJjaGFsbGVuZ2UiOiJOM0xqSTJKNXlseVdlM0VENU9UNFhITFJxSHdtX0o0OF9EX2hvSk9GZjMwIiwib3JpZ2luIjoiaHR0cHM6Ly9sb2NhbGhvc3QiLCJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwidG9rZW5CaW5kaW5nIjp7InN0YXR1cyI6InN1cHBvcnRlZCJ9fQ","signature":"cFBvEhe-qgBaq7uqKrtgnb_9CwL3uejbeBDNcX9sii0PtEwDHLltnMFWAOSjPDyovQVPci8Quz65ft4eLIWdCnd_er4gN8GC8WG5eX5JEePBZbKZF5grkntIFMJieBX0CvremEmItKkEEa4U-k2Itn_pdh0zCtGGGQhXYIzHpGe4_iA2bkCIYjCYBy0xUw_tHccRhGp6WB88xLrUlkb5fF9mQulL3O3EzV8-J3SQoMBuh2kvUFNRx1SCXgA_ckaeMp9sHqCdDoreCjm0vd8hEDYiXMDW6I9fglVMivrskTE_Ddms2HFE-IkFxwnE2jJhEAysEooq4I7DXSKPL5q-ZA"},"clientExtensionResults":{},"type":"public-key"}"""), ) ), ) { @@ -456,6 +595,7 @@ case class RegistrationTestData( alg: COSEAlgorithmIdentifier, assertion: Option[AssertionTestData] = None, attestationObject: ByteArray, + attestationCertChain: List[(X509Certificate, PrivateKey)] = Nil, clientDataJson: String, authenticatorSelection: Option[AuthenticatorSelectionCriteria] = None, clientExtensionResults: ClientRegistrationExtensionOutputs = @@ -482,10 +622,11 @@ case class RegistrationTestData( ClientRegistrationExtensionOutputs, ], KeyPair, + List[(X509Certificate, PrivateKey)], ) = ??? def regenerateFull(): Try[RegistrationTestData] = Try({ - val (credential, keypair) = regenerate() + val (credential, keypair, attestationCertChain) = regenerate() val newValue = copy( attestationObject = credential.getResponse.getAttestationObject, clientDataJson = new String( @@ -493,6 +634,7 @@ case class RegistrationTestData( StandardCharsets.UTF_8, ), privateKey = Some(new ByteArray(keypair.getPrivate.getEncoded)), + attestationCertChain = attestationCertChain, ) newValue.copy( assertion = newValue.assertion.map(_.regenerate(newValue)) @@ -524,15 +666,6 @@ case class RegistrationTestData( .binaryValue ) - def attestationCaCert: Option[X509Certificate] = - Option( - new AttestationObject(attestationObject).getAttestationStatement.get( - "x5c" - ) - ) - .map(x5c => x5c.elements().asScala.toList.last) - .map(node => CertificateParser.parseDer(node.binaryValue())) - def editClientData(updater: ObjectNode => JsonNode): RegistrationTestData = copy( clientDataJson = JacksonCodecs.json.writeValueAsString( @@ -619,7 +752,7 @@ case class RegistrationTestData( ).asJava ) .extensions(requestedExtensions) - .authenticatorSelection(authenticatorSelection.asJava) + .authenticatorSelection(authenticatorSelection.toJava) .build() def response: PublicKeyCredential[ 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 e65eb6ffe..da17187b5 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 @@ -28,13 +28,10 @@ import com.fasterxml.jackson.core.`type`.TypeReference import com.fasterxml.jackson.databind.node.JsonNodeFactory import com.fasterxml.jackson.databind.node.ObjectNode import com.upokecenter.cbor.CBORObject -import com.yubico.fido.metadata.KeyProtectionType -import com.yubico.fido.metadata.MatcherProtectionType -import com.yubico.fido.metadata.UserVerificationMethod import com.yubico.internal.util.JacksonCodecs -import com.yubico.internal.util.scala.JavaConverters._ import com.yubico.webauthn.data.AssertionExtensionInputs import com.yubico.webauthn.data.AuthenticatorAssertionResponse +import com.yubico.webauthn.data.AuthenticatorTransport import com.yubico.webauthn.data.ByteArray import com.yubico.webauthn.data.ClientAssertionExtensionOutputs import com.yubico.webauthn.data.CollectedClientData @@ -44,6 +41,7 @@ import com.yubico.webauthn.data.Generators._ import com.yubico.webauthn.data.PublicKeyCredential import com.yubico.webauthn.data.PublicKeyCredentialCreationOptions import com.yubico.webauthn.data.PublicKeyCredentialDescriptor +import com.yubico.webauthn.data.PublicKeyCredentialParameters import com.yubico.webauthn.data.PublicKeyCredentialRequestOptions import com.yubico.webauthn.data.ReexportHelpers import com.yubico.webauthn.data.RelyingPartyIdentity @@ -51,10 +49,14 @@ import com.yubico.webauthn.data.UserIdentity import com.yubico.webauthn.data.UserVerificationRequirement import com.yubico.webauthn.exception.InvalidSignatureCountException import com.yubico.webauthn.extension.appid.AppId +import com.yubico.webauthn.extension.uvm.KeyProtectionType +import com.yubico.webauthn.extension.uvm.MatcherProtectionType +import com.yubico.webauthn.extension.uvm.UserVerificationMethod import com.yubico.webauthn.test.Helpers import com.yubico.webauthn.test.RealExamples import com.yubico.webauthn.test.Util.toStepWithUtilities import org.junit.runner.RunWith +import org.scalacheck.Arbitrary.arbitrary import org.scalacheck.Gen import org.scalatest.FunSpec import org.scalatest.Matchers @@ -68,6 +70,8 @@ import java.security.MessageDigest import java.security.interfaces.ECPublicKey import java.util.Optional import scala.jdk.CollectionConverters._ +import scala.jdk.OptionConverters.RichOption +import scala.jdk.OptionConverters.RichOptional import scala.util.Failure import scala.util.Success import scala.util.Try @@ -134,7 +138,7 @@ class RelyingPartyAssertionSpec userHandle: ByteArray, ): Optional[ByteArray] = if (username == Defaults.username) - Some(userHandle).asJava + Some(userHandle).toJava else ??? @@ -143,7 +147,7 @@ class RelyingPartyAssertionSpec username: String, ): Optional[String] = if (userHandle == Defaults.userHandle) - Some(username).asJava + Some(username).toJava else ??? @@ -164,7 +168,6 @@ class RelyingPartyAssertionSpec ), allowOriginPort: Boolean = false, allowOriginSubdomain: Boolean = false, - allowUnrequestedExtensions: Boolean = false, authenticatorData: ByteArray = Defaults.authenticatorData, callerTokenBindingId: Option[ByteArray] = None, challenge: ByteArray = Defaults.challenge, @@ -199,12 +202,12 @@ class RelyingPartyAssertionSpec .builder() .challenge(challenge) .rpId(rpId.getId) - .allowCredentials(allowCredentials.asJava) + .allowCredentials(allowCredentials.toJava) .userVerification(userVerificationRequirement) .extensions(requestedExtensions) .build() ) - .username(usernameForRequest.asJava) + .username(usernameForRequest.toJava) .build() val response = PublicKeyCredential @@ -220,7 +223,7 @@ class RelyingPartyAssertionSpec if (clientDataJsonBytes == null) null else clientDataJsonBytes ) .signature(if (signature == null) null else signature) - .userHandle(userHandleForResponse.asJava) + .userHandle(userHandleForResponse.toJava) .build() ) .clientExtensionResults(clientExtensionResults) @@ -244,9 +247,9 @@ class RelyingPartyAssertionSpec .build() ) else None - ).asJava + ).toJava override def lookupAll(credId: ByteArray) = - lookup(credId, null).asScala.toSet.asJava + lookup(credId, null).toScala.toSet.asJava override def getCredentialIdsForUsername(username: String) = ??? override def getUserHandleForUsername(username: String) : Optional[ByteArray] = @@ -266,14 +269,13 @@ class RelyingPartyAssertionSpec .allowOriginPort(allowOriginPort) .allowOriginSubdomain(allowOriginSubdomain) .allowUntrustedAttestation(false) - .allowUnrequestedExtensions(allowUnrequestedExtensions) .validateSignatureCounter(validateSignatureCounter) origins.map(_.asJava).foreach(builder.origins _) builder .build() - ._finishAssertion(request, response, callerTokenBindingId.asJava) + ._finishAssertion(request, response, callerTokenBindingId.toJava) } testWithEachProvider { it => @@ -282,10 +284,7 @@ class RelyingPartyAssertionSpec describe( "respects the userVerification parameter in StartAssertionOptions." ) { - - val default = UserVerificationRequirement.PREFERRED - - it(s"If the parameter is not set, or set to empty, the default of ${default} is used.") { + it(s"If the parameter is not set, or set to empty, it is also empty in the result.") { val rp = RelyingParty .builder() .identity(Defaults.rpId) @@ -300,11 +299,11 @@ class RelyingPartyAssertionSpec .build() ) - request1.getPublicKeyCredentialRequestOptions.getUserVerification should equal( - default + request1.getPublicKeyCredentialRequestOptions.getUserVerification.toScala should be( + None ) - request2.getPublicKeyCredentialRequestOptions.getUserVerification should equal( - default + request2.getPublicKeyCredentialRequestOptions.getUserVerification.toScala should be( + None ) } @@ -315,12 +314,15 @@ class RelyingPartyAssertionSpec .credentialRepository(Helpers.CredentialRepository.empty) .build() - forAll { uv: UserVerificationRequirement => + forAll { uv: Option[UserVerificationRequirement] => val request = rp.startAssertion( - StartAssertionOptions.builder().userVerification(uv).build() + StartAssertionOptions + .builder() + .userVerification(uv.toJava) + .build() ) - request.getPublicKeyCredentialRequestOptions.getUserVerification should equal( + request.getPublicKeyCredentialRequestOptions.getUserVerification.toScala should equal( uv ) } @@ -367,15 +369,15 @@ class RelyingPartyAssertionSpec username: String ): Optional[ByteArray] = if (username == user.getName) - Some(user.getId).asJava - else None.asJava + Some(user.getId).toJava + else None.toJava override def getUsernameForUserHandle( userHandle: ByteArray ): Optional[String] = if (userHandle == user.getId) - Some(user.getName).asJava - else None.asJava + Some(user.getName).toJava + else None.toJava override def lookup( credentialId: ByteArray, @@ -385,8 +387,8 @@ class RelyingPartyAssertionSpec if ( credentialId == credential.getCredentialId && userHandle == user.getId ) - Some(credential).asJava - else None.asJava + Some(credential).toJava + else None.toJava } override def lookupAll( @@ -422,8 +424,125 @@ class RelyingPartyAssertionSpec describe("§7.2. Verifying an authentication assertion: When verifying a given PublicKeyCredential structure (credential) and an AuthenticationExtensionsClientOutputs structure clientExtensionResults, as part of an authentication ceremony, the Relying Party MUST proceed as follows:") { - describe("1. If the allowCredentials option was given when this authentication ceremony was initiated, verify that credential.id identifies one of the public key credentials that were listed in allowCredentials.") { - it("Fails if returned credential ID is a requested one.") { + describe("1. Let options be a new PublicKeyCredentialRequestOptions structure configured to the Relying Party's needs for the ceremony.") { + it("If options.allowCredentials is present, the transports member of each item SHOULD be set to the value returned by credential.response.getTransports() when the corresponding credential was registered.") { + forAll( + Gen.nonEmptyContainerOf[Set, AuthenticatorTransport]( + arbitrary[AuthenticatorTransport] + ), + arbitrary[PublicKeyCredentialDescriptor], + arbitrary[PublicKeyCredentialDescriptor], + arbitrary[PublicKeyCredentialDescriptor], + ) { + ( + cred1Transports: Set[AuthenticatorTransport], + cred1: PublicKeyCredentialDescriptor, + cred2: PublicKeyCredentialDescriptor, + cred3: PublicKeyCredentialDescriptor, + ) => + val rp = RelyingParty + .builder() + .identity(Defaults.rpId) + .credentialRepository(new CredentialRepository { + override def getCredentialIdsForUsername( + username: String + ): java.util.Set[PublicKeyCredentialDescriptor] = + Set( + cred1.toBuilder + .transports(cred1Transports.asJava) + .build(), + cred2.toBuilder + .transports( + Optional.of( + Set.empty[AuthenticatorTransport].asJava + ) + ) + .build(), + cred3.toBuilder + .transports( + Optional + .empty[java.util.Set[AuthenticatorTransport]] + ) + .build(), + ).asJava + override def getUserHandleForUsername( + username: String + ): Optional[ByteArray] = ??? + override def getUsernameForUserHandle( + userHandleBase64: ByteArray + ): Optional[String] = ??? + override def lookup( + credentialId: ByteArray, + userHandle: ByteArray, + ): Optional[RegisteredCredential] = ??? + override def lookupAll( + credentialId: ByteArray + ): java.util.Set[RegisteredCredential] = ??? + }) + .preferredPubkeyParams( + List(PublicKeyCredentialParameters.ES256).asJava + ) + .build() + + val result = rp.startAssertion( + StartAssertionOptions + .builder() + .username(Defaults.username) + .build() + ) + + val requestCreds = + result.getPublicKeyCredentialRequestOptions.getAllowCredentials.get.asScala + requestCreds.head.getTransports.toScala should equal( + Some(cred1Transports.asJava) + ) + requestCreds(1).getTransports.toScala should equal( + Some(Set.empty.asJava) + ) + requestCreds(2).getTransports.toScala should equal(None) + } + } + } + + describe("2. Call navigator.credentials.get() and pass options as the publicKey option. Let credential be the result of the successfully resolved promise. If the promise is rejected, abort the ceremony with a user-visible error, or otherwise guide the user experience as might be determinable from the context available in the rejected promise. For information on different error contexts and the circumstances leading to them, see § 6.3.3 The authenticatorGetAssertion Operation.") { + it("Nothing to test: applicable only to client side.") {} + } + + it("3. Let response be credential.response. If response is not an instance of AuthenticatorAssertionResponse, abort the ceremony with a user-visible error.") { + val testData = + RegistrationTestData.Packed.BasicAttestationEdDsa.assertion.get + val faob = FinishAssertionOptions + .builder() + .request(testData.request) + "faob.response(testData.request)" shouldNot compile + faob.response(testData.response).build() should not be null + } + + describe("4. Let clientExtensionResults be the result of calling credential.getClientExtensionResults().") { + it( + "The PublicKeyCredential class has a clientExtensionResults field" + ) { + val pkc = PublicKeyCredential.parseAssertionResponseJson("""{ + "type": "public-key", + "id": "", + "response": { + "authenticatorData": "xGzvgq0bVGR3WR0Aiwh1nsPm0uy085R0v-ppaZJdA7cBAAAABQ", + "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiaHZGN1AxNGwxTjZUcEhnZXVBMjhDdnJaTE1yVjRSMjdZd2JrY2FSYlRPZyIsIm9yaWdpbiI6Imh0dHBzOi8vZGVtby55dWJpY28uY29tIiwiY3Jvc3NPcmlnaW4iOmZhbHNlfQ==", + "signature": "MEYCIQCi7u0ErVIGZIWOQbc_y7IYcNXBniczTgzHH_yE0WfzcQIhALDsITBJDPQMBFxB6pKd608lRVPcNeNnrX3olAxA3AmX" + }, + "clientExtensionResults": { + "appid": true, + "org.example.foo": "bar" + } + }""") + pkc.getClientExtensionResults.getExtensionIds should contain( + "appid" + ) + } + } + + describe("5. If options.allowCredentials is not empty, verify that credential.id identifies one of the public key credentials listed in options.allowCredentials.") { + it("Fails if returned credential ID is not a requested one.") { val steps = finishAssertion( allowCredentials = Some( List( @@ -435,9 +554,9 @@ class RelyingPartyAssertionSpec ), credentialId = new ByteArray(Array(0, 1, 2, 3)), ) - val step: FinishAssertionSteps#Step1 = steps.begin.next + val step: FinishAssertionSteps#Step5 = steps.begin - toStepWithUtilities(step).validations shouldBe a[Failure[_]] + step.validations shouldBe a[Failure[_]] step.validations.failed.get shouldBe an[IllegalArgumentException] step.tryNext shouldBe a[Failure[_]] } @@ -458,25 +577,25 @@ class RelyingPartyAssertionSpec ), credentialId = new ByteArray(Array(4, 5, 6, 7)), ) - val step: FinishAssertionSteps#Step1 = steps.begin.next + val step: FinishAssertionSteps#Step5 = steps.begin step.validations shouldBe a[Success[_]] step.tryNext shouldBe a[Success[_]] } - it("Succeeds if returned no credential IDs were requested.") { + it("Succeeds if no credential IDs were requested.") { val steps = finishAssertion( allowCredentials = None, credentialId = new ByteArray(Array(0, 1, 2, 3)), ) - val step: FinishAssertionSteps#Step1 = steps.begin.next + val step: FinishAssertionSteps#Step5 = steps.begin step.validations shouldBe a[Success[_]] step.tryNext shouldBe a[Success[_]] } } - describe("2. Identify the user being authenticated and verify that this user is the owner of the public key credential source credentialSource identified by credential.id:") { + describe("6. Identify the user being authenticated and verify that this user is the owner of the public key credential source credentialSource identified by credential.id:") { object owner { val username = "owner" val userHandle = new ByteArray(Array(4, 5, 6, 7)) @@ -496,7 +615,7 @@ class RelyingPartyAssertionSpec .publicKeyCose(getPublicKeyBytes(Defaults.credentialKey)) .signatureCount(0) .build() - ).asJava + ).toJava override def lookupAll(id: ByteArray) = ??? override def getCredentialIdsForUsername(username: String) = ??? override def getUserHandleForUsername( @@ -505,17 +624,17 @@ class RelyingPartyAssertionSpec Some( if (username == owner.username) owner.userHandle else nonOwner.userHandle - ).asJava + ).toJava override def getUsernameForUserHandle( userHandle: ByteArray ): Optional[String] = Some( if (userHandle == owner.userHandle) owner.username else nonOwner.username - ).asJava + ).toJava }) - describe("If the user was identified before the authentication ceremony was initiated, verify that the identified user is the owner of credentialSource. If credential.response.userHandle is present, verify that this value identifies the same user as was previously identified.") { + describe("If the user was identified before the authentication ceremony was initiated, e.g., via a username or cookie, verify that the identified user is the owner of credentialSource. If response.userHandle is present, let userHandle be its value. Verify that userHandle also maps to the same user.") { it( "Fails if credential ID is not owned by the given user handle." ) { @@ -525,7 +644,23 @@ class RelyingPartyAssertionSpec userHandleForUser = owner.userHandle, userHandleForResponse = Some(nonOwner.userHandle), ) - val step: FinishAssertionSteps#Step2 = steps.begin.next.next + 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 response.userHandle does not identify the same user as request.username." + ) { + val steps = finishAssertion( + credentialRepository = credentialRepository, + usernameForRequest = Some(nonOwner.username), + userHandleForUser = owner.userHandle, + userHandleForResponse = Some(owner.userHandle), + ) + val step: FinishAssertionSteps#Step6 = steps.begin.next step.validations shouldBe a[Failure[_]] step.validations.failed.get shouldBe an[IllegalArgumentException] @@ -539,14 +674,30 @@ class RelyingPartyAssertionSpec userHandleForUser = owner.userHandle, userHandleForResponse = Some(owner.userHandle), ) - val step: FinishAssertionSteps#Step2 = steps.begin.next.next + val step: FinishAssertionSteps#Step6 = steps.begin.next step.validations shouldBe a[Success[_]] step.tryNext shouldBe a[Success[_]] } } - describe("If the user was not identified before the authentication ceremony was initiated, verify that credential.response.userHandle is present, and that the user identified by this value is the owner of credentialSource.") { + describe("If the user was not identified before the authentication ceremony was initiated, verify that response.userHandle is present, and that the user identified by this value is the owner of credentialSource.") { + it( + "Fails if response.userHandle is not present." + ) { + val steps = finishAssertion( + credentialRepository = credentialRepository, + usernameForRequest = None, + userHandleForUser = owner.userHandle, + userHandleForResponse = 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 given user handle." ) { @@ -556,7 +707,7 @@ class RelyingPartyAssertionSpec userHandleForUser = owner.userHandle, userHandleForResponse = Some(nonOwner.userHandle), ) - val step: FinishAssertionSteps#Step2 = steps.begin.next.next + val step: FinishAssertionSteps#Step6 = steps.begin.next step.validations shouldBe a[Failure[_]] step.validations.failed.get shouldBe an[IllegalArgumentException] @@ -570,7 +721,7 @@ class RelyingPartyAssertionSpec userHandleForUser = owner.userHandle, userHandleForResponse = None, ) - val step: FinishAssertionSteps#Step0 = steps.begin + val step: FinishAssertionSteps#Step6 = steps.begin.next step.validations shouldBe a[Failure[_]] step.validations.failed.get shouldBe an[IllegalArgumentException] @@ -584,7 +735,7 @@ class RelyingPartyAssertionSpec userHandleForUser = owner.userHandle, userHandleForResponse = Some(owner.userHandle), ) - val step: FinishAssertionSteps#Step2 = steps.begin.next.next + val step: FinishAssertionSteps#Step6 = steps.begin.next step.validations shouldBe a[Success[_]] step.tryNext shouldBe a[Success[_]] @@ -592,16 +743,28 @@ class RelyingPartyAssertionSpec } } - describe("3. Using credential’s id attribute (or the corresponding rawId, if base64url encoding is inappropriate for your use case), look up the corresponding credential public key.") { + describe("7. Using credential.id (or credential.rawId, if base64url encoding is inappropriate for your use case), look up the corresponding credential public key and let credentialPublicKey be that credential public key.") { it("Fails if the credential ID is unknown.") { val steps = finishAssertion( - credentialRepository = Some(Helpers.CredentialRepository.empty) + credentialRepository = Some( + Helpers.CredentialRepository.withUser( + Defaults.user, + RegisteredCredential + .builder() + .credentialId( + Defaults.credentialId.concat(ByteArray.fromHex("00")) + ) + .userHandle(Defaults.userHandle) + .publicKeyCose(getPublicKeyBytes(Defaults.credentialKey)) + .signatureCount(0) + .build(), + ) + ) ) - val step: steps.Step3 = new steps.Step3( + val step: steps.Step7 = new steps.Step7( Defaults.username, Defaults.userHandle, - None.asJava, - Nil.asJava, + None.toJava, ) step.validations shouldBe a[Failure[_]] @@ -624,18 +787,17 @@ class RelyingPartyAssertionSpec ) ) ) - val step: FinishAssertionSteps#Step3 = steps.begin.next.next.next + val step: FinishAssertionSteps#Step7 = steps.begin.next.next step.validations shouldBe a[Success[_]] step.tryNext shouldBe a[Success[_]] } } - describe("4. Let cData, authData and sig denote the value of credential’s response's clientDataJSON, authenticatorData, and signature respectively.") { + describe("8. Let cData, authData and sig denote the value of response’s clientDataJSON, authenticatorData, and signature respectively.") { it("Succeeds if all three are present.") { val steps = finishAssertion() - val step: FinishAssertionSteps#Step4 = - steps.begin.next.next.next.next + val step: FinishAssertionSteps#Step8 = steps.begin.next.next.next step.validations shouldBe a[Success[_]] step.clientData should not be null @@ -663,11 +825,15 @@ class RelyingPartyAssertionSpec } } - describe("5. Let JSONtext be the result of running UTF-8 decode on the value of cData.") { - it("Nothing to test.") {} + describe("9. Let JSONtext be the result of running UTF-8 decode on the value of cData.") { + it("Fails if clientDataJSON is not valid UTF-8.") { + an[IOException] should be thrownBy new CollectedClientData( + new ByteArray(Array(-128)) + ) + } } - describe("6. Let C, the client data claimed as used for the signature, be the result of running an implementation-specific JSON parser on JSONtext.") { + describe("10. Let C, the client data claimed as used for the signature, be the result of running an implementation-specific JSON parser on JSONtext.") { it("Fails if cData is not valid JSON.") { an[IOException] should be thrownBy new CollectedClientData( new ByteArray("{".getBytes(Charset.forName("UTF-8"))) @@ -685,8 +851,8 @@ class RelyingPartyAssertionSpec "type": "" }""" ) - val step: FinishAssertionSteps#Step6 = - steps.begin.next.next.next.next.next.next + val step: FinishAssertionSteps#Step10 = + steps.begin.next.next.next.next step.validations shouldBe a[Success[_]] step.clientData should not be null @@ -695,12 +861,12 @@ class RelyingPartyAssertionSpec } describe( - "7. Verify that the value of C.type is the string webauthn.get." + "11. Verify that the value of C.type is the string webauthn.get." ) { it("The default test case succeeds.") { val steps = finishAssertion() - val step: FinishAssertionSteps#Step7 = - steps.begin.next.next.next.next.next.next.next + val step: FinishAssertionSteps#Step11 = + steps.begin.next.next.next.next.next step.validations shouldBe a[Success[_]] } @@ -714,8 +880,8 @@ class RelyingPartyAssertionSpec .set("type", jsonFactory.textNode(typeString)) ) ) - val step: FinishAssertionSteps#Step7 = - steps.begin.next.next.next.next.next.next.next + val step: FinishAssertionSteps#Step11 = + steps.begin.next.next.next.next.next step.validations shouldBe a[Failure[_]] step.validations.failed.get shouldBe an[IllegalArgumentException] @@ -733,20 +899,24 @@ class RelyingPartyAssertionSpec } } } + + it("""The string "webauthn.create" fails.""") { + assertFails("webauthn.create") + } } - it("8. Verify that the value of C.challenge matches the challenge that was sent to the authenticator in the PublicKeyCredentialRequestOptions passed to the get() call.") { + it("12. Verify that the value of C.challenge equals the base64url encoding of options.challenge.") { val steps = finishAssertion(challenge = new ByteArray(Array.fill(16)(0))) - val step: FinishAssertionSteps#Step8 = - steps.begin.next.next.next.next.next.next.next.next + val step: FinishAssertionSteps#Step12 = + steps.begin.next.next.next.next.next.next step.validations shouldBe a[Failure[_]] step.validations.failed.get shouldBe an[IllegalArgumentException] step.tryNext shouldBe a[Failure[_]] } - describe("9. Verify that the value of C.origin matches the Relying Party's origin.") { + describe("13. Verify that the value of C.origin matches the Relying Party's origin.") { def checkAccepted( origin: String, origins: Option[Set[String]] = None, @@ -763,8 +933,8 @@ class RelyingPartyAssertionSpec allowOriginPort = allowOriginPort, allowOriginSubdomain = allowOriginSubdomain, ) - val step: FinishAssertionSteps#Step9 = - steps.begin.next.next.next.next.next.next.next.next.next + val step: FinishAssertionSteps#Step13 = + steps.begin.next.next.next.next.next.next.next step.validations shouldBe a[Success[_]] step.tryNext shouldBe a[Success[_]] @@ -786,8 +956,8 @@ class RelyingPartyAssertionSpec allowOriginPort = allowOriginPort, allowOriginSubdomain = allowOriginSubdomain, ) - val step: FinishAssertionSteps#Step9 = - steps.begin.next.next.next.next.next.next.next.next.next + val step: FinishAssertionSteps#Step13 = + steps.begin.next.next.next.next.next.next.next step.validations shouldBe a[Failure[_]] step.validations.failed.get shouldBe an[IllegalArgumentException] @@ -961,11 +1131,11 @@ class RelyingPartyAssertionSpec } } - describe("10. Verify that the value of C.tokenBinding.status matches the state of Token Binding for the TLS connection over which the attestation was obtained.") { + describe("14. Verify that the value of C.tokenBinding.status matches the state of Token Binding for the TLS connection over which the attestation was obtained.") { it("Verification succeeds if neither side uses token binding ID.") { val steps = finishAssertion() - val step: FinishAssertionSteps#Step10 = - steps.begin.next.next.next.next.next.next.next.next.next.next + val step: FinishAssertionSteps#Step14 = + steps.begin.next.next.next.next.next.next.next.next step.validations shouldBe a[Success[_]] step.tryNext shouldBe a[Success[_]] @@ -975,8 +1145,8 @@ class RelyingPartyAssertionSpec val clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","hashAlgorithm":"SHA-256","type":"webauthn.get"}""" val steps = finishAssertion(clientDataJson = clientDataJson) - val step: FinishAssertionSteps#Step10 = - steps.begin.next.next.next.next.next.next.next.next.next.next + val step: FinishAssertionSteps#Step14 = + steps.begin.next.next.next.next.next.next.next.next step.validations shouldBe a[Success[_]] step.tryNext shouldBe a[Success[_]] @@ -986,8 +1156,8 @@ class RelyingPartyAssertionSpec val clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","hashAlgorithm":"SHA-256","tokenBinding":{"status":"supported"},"type":"webauthn.get"}""" val steps = finishAssertion(clientDataJson = clientDataJson) - val step: FinishAssertionSteps#Step10 = - steps.begin.next.next.next.next.next.next.next.next.next.next + val step: FinishAssertionSteps#Step14 = + steps.begin.next.next.next.next.next.next.next.next step.validations shouldBe a[Success[_]] step.tryNext shouldBe a[Success[_]] @@ -1001,8 +1171,8 @@ class RelyingPartyAssertionSpec Some(ByteArray.fromBase64Url("YELLOWSUBMARINE")), clientDataJson = clientDataJson, ) - val step: FinishAssertionSteps#Step10 = - steps.begin.next.next.next.next.next.next.next.next.next.next + val step: FinishAssertionSteps#Step14 = + steps.begin.next.next.next.next.next.next.next.next step.validations shouldBe a[Failure[_]] step.validations.failed.get shouldBe an[IllegalArgumentException] @@ -1016,12 +1186,13 @@ class RelyingPartyAssertionSpec callerTokenBindingId = None, clientDataJson = clientDataJson, ) - val step: FinishAssertionSteps#Step10 = - steps.begin.next.next.next.next.next.next.next.next.next.next + val step: FinishAssertionSteps#Step14 = + steps.begin.next.next.next.next.next.next.next.next step.validations shouldBe a[Success[_]] step.tryNext shouldBe a[Success[_]] } + it("Verification fails if client data specifies token binding ID but RP does not.") { val clientDataJson = """{"challenge":"AAEBAgMFCA0VIjdZEGl5Yls","origin":"https://localhost","hashAlgorithm":"SHA-256","tokenBinding":{"status":"present","id":"YELLOWSUBMARINE"},"type":"webauthn.get"}""" @@ -1029,8 +1200,8 @@ class RelyingPartyAssertionSpec callerTokenBindingId = None, clientDataJson = clientDataJson, ) - val step: FinishAssertionSteps#Step10 = - steps.begin.next.next.next.next.next.next.next.next.next.next + val step: FinishAssertionSteps#Step14 = + steps.begin.next.next.next.next.next.next.next.next step.validations shouldBe a[Failure[_]] step.validations.failed.get shouldBe an[IllegalArgumentException] @@ -1046,8 +1217,8 @@ class RelyingPartyAssertionSpec Some(ByteArray.fromBase64Url("YELLOWSUBMARINE")), clientDataJson = clientDataJson, ) - val step: FinishAssertionSteps#Step10 = - steps.begin.next.next.next.next.next.next.next.next.next.next + val step: FinishAssertionSteps#Step14 = + steps.begin.next.next.next.next.next.next.next.next step.validations shouldBe a[Success[_]] step.tryNext shouldBe a[Success[_]] @@ -1061,8 +1232,8 @@ class RelyingPartyAssertionSpec Some(ByteArray.fromBase64Url("YELLOWSUBMARINE")), clientDataJson = clientDataJson, ) - val step: FinishAssertionSteps#Step10 = - steps.begin.next.next.next.next.next.next.next.next.next.next + val step: FinishAssertionSteps#Step14 = + steps.begin.next.next.next.next.next.next.next.next step.validations shouldBe a[Failure[_]] step.validations.failed.get shouldBe an[IllegalArgumentException] @@ -1077,8 +1248,8 @@ class RelyingPartyAssertionSpec Some(ByteArray.fromBase64Url("YELLOWSUBMARINE")), clientDataJson = clientDataJson, ) - val step: FinishAssertionSteps#Step10 = - steps.begin.next.next.next.next.next.next.next.next.next.next + val step: FinishAssertionSteps#Step14 = + steps.begin.next.next.next.next.next.next.next.next step.validations shouldBe a[Failure[_]] step.validations.failed.get shouldBe an[IllegalArgumentException] @@ -1093,8 +1264,8 @@ class RelyingPartyAssertionSpec Some(ByteArray.fromBase64Url("YELLOWSUBMARINE")), clientDataJson = clientDataJson, ) - val step: FinishAssertionSteps#Step10 = - steps.begin.next.next.next.next.next.next.next.next.next.next + val step: FinishAssertionSteps#Step14 = + steps.begin.next.next.next.next.next.next.next.next step.validations shouldBe a[Failure[_]] step.validations.failed.get shouldBe an[IllegalArgumentException] @@ -1109,8 +1280,8 @@ class RelyingPartyAssertionSpec Some(ByteArray.fromBase64Url("ORANGESUBMARINE")), clientDataJson = clientDataJson, ) - val step: FinishAssertionSteps#Step10 = - steps.begin.next.next.next.next.next.next.next.next.next.next + val step: FinishAssertionSteps#Step14 = + steps.begin.next.next.next.next.next.next.next.next step.validations shouldBe a[Failure[_]] step.validations.failed.get shouldBe an[IllegalArgumentException] @@ -1119,14 +1290,14 @@ class RelyingPartyAssertionSpec } } - describe("11. Verify that the rpIdHash in authData is the SHA-256 hash of the RP ID expected by the Relying Party.") { + describe("15. Verify that the rpIdHash in authData is the SHA-256 hash of the RP ID expected by the Relying Party.") { it("Fails if RP ID is different.") { val steps = finishAssertion( rpId = Defaults.rpId.toBuilder.id("root.evil").build(), origins = Some(Set("https://localhost")), ) - val step: FinishAssertionSteps#Step11 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next + val step: FinishAssertionSteps#Step15 = + steps.begin.next.next.next.next.next.next.next.next.next step.validations shouldBe a[Failure[_]] step.validations.failed.get shouldBe an[IllegalArgumentException] @@ -1135,8 +1306,8 @@ class RelyingPartyAssertionSpec it("Succeeds if RP ID is the same.") { val steps = finishAssertion() - val step: FinishAssertionSteps#Step11 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next + val step: FinishAssertionSteps#Step15 = + steps.begin.next.next.next.next.next.next.next.next.next step.validations shouldBe a[Success[_]] step.tryNext shouldBe a[Success[_]] @@ -1146,7 +1317,7 @@ class RelyingPartyAssertionSpec val appid = new AppId("https://test.example.org/foo") val extensions = AssertionExtensionInputs .builder() - .appid(Some(appid).asJava) + .appid(Some(appid).toJava) .build() it("fails if RP ID is different.") { @@ -1157,8 +1328,8 @@ class RelyingPartyAssertionSpec .drop(32) ), ) - val step: FinishAssertionSteps#Step11 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next + val step: FinishAssertionSteps#Step15 = + steps.begin.next.next.next.next.next.next.next.next.next step.validations shouldBe a[Failure[_]] step.validations.failed.get shouldBe an[IllegalArgumentException] @@ -1167,8 +1338,8 @@ class RelyingPartyAssertionSpec it("succeeds if RP ID is the SHA-256 hash of the standard RP ID.") { val steps = finishAssertion(requestedExtensions = extensions) - val step: FinishAssertionSteps#Step11 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next + val step: FinishAssertionSteps#Step15 = + steps.begin.next.next.next.next.next.next.next.next.next step.validations shouldBe a[Success[_]] step.tryNext shouldBe a[Success[_]] @@ -1183,8 +1354,8 @@ class RelyingPartyAssertionSpec ).getBytes ++ Defaults.authenticatorData.getBytes.drop(32) ), ) - val step: FinishAssertionSteps#Step11 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next + val step: FinishAssertionSteps#Step15 = + steps.begin.next.next.next.next.next.next.next.next.next step.validations shouldBe a[Success[_]] step.tryNext shouldBe a[Success[_]] @@ -1228,7 +1399,7 @@ class RelyingPartyAssertionSpec (checkFailsWith(stepsToStep), checkSucceedsWith(stepsToStep)) } - describe("12. Verify that the User Present bit of the flags in authData is set.") { + describe("16. Verify that the User Present bit of the flags in authData is set.") { val flagOn: ByteArray = new ByteArray( Defaults.authenticatorData.getBytes.toVector .updated( @@ -1248,8 +1419,8 @@ class RelyingPartyAssertionSpec .toArray ) val (checkFails, checkSucceeds) = - checks[FinishAssertionSteps#Step13, FinishAssertionSteps#Step12]( - _.begin.next.next.next.next.next.next.next.next.next.next.next.next + checks[FinishAssertionSteps#Step17, FinishAssertionSteps#Step16]( + _.begin.next.next.next.next.next.next.next.next.next.next ) it("Fails if UV is discouraged and flag is not set.") { @@ -1277,7 +1448,7 @@ class RelyingPartyAssertionSpec } } - describe("13. If user verification is required for this assertion, verify that the User Verified bit of the flags in authData is set.") { + describe("17. If user verification is required for this assertion, verify that the User Verified bit of the flags in authData is set.") { val flagOn: ByteArray = new ByteArray( Defaults.authenticatorData.getBytes.toVector .updated( @@ -1297,8 +1468,8 @@ class RelyingPartyAssertionSpec .toArray ) val (checkFails, checkSucceeds) = - checks[FinishAssertionSteps#Step14, FinishAssertionSteps#Step13]( - _.begin.next.next.next.next.next.next.next.next.next.next.next.next.next + checks[FinishAssertionSteps#Step18, FinishAssertionSteps#Step17]( + _.begin.next.next.next.next.next.next.next.next.next.next.next ) it("Succeeds if UV is discouraged and flag is not set.") { @@ -1327,147 +1498,92 @@ class RelyingPartyAssertionSpec } } - describe("14. Verify that the values of the") { - - describe("client extension outputs in clientExtensionResults are as expected, considering the client extension input values that were given as the extensions option in the get() call. In particular, any extension identifier values in the clientExtensionResults MUST be also be present as extension identifier values in the extensions member of options, i.e., no extensions are present that were not requested. In the general case, the meaning of \"are as expected\" is specific to the Relying Party and which extensions are in use.") { - it("Fails if clientExtensionResults is not a subset of the extensions requested by the Relying Party.") { - - forAll(Extensions.unrequestedClientAssertionExtensions) { - case (extensionInputs, clientExtensionOutputs, _) => - val steps = finishAssertion( - requestedExtensions = extensionInputs, - clientExtensionResults = clientExtensionOutputs, - ) - val step: FinishAssertionSteps#Step14 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next - - step.validations shouldBe a[Failure[_]] - step.validations.failed.get shouldBe an[ - IllegalArgumentException - ] - step.tryNext shouldBe a[Failure[_]] - } - } - - it("Succeeds if clientExtensionResults is not a subset of the extensions requested by the Relying Party, but the Relying Party has enabled allowing unrequested extensions.") { - forAll(Extensions.unrequestedClientAssertionExtensions) { - case (extensionInputs, clientExtensionOutputs, _) => - val steps = finishAssertion( - allowUnrequestedExtensions = true, - requestedExtensions = extensionInputs, - clientExtensionResults = clientExtensionOutputs, - ) - val step: FinishAssertionSteps#Step14 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next - - step.validations shouldBe a[Success[_]] - step.tryNext shouldBe a[Success[_]] - } - } - - it("Succeeds if clientExtensionResults is a subset of the extensions requested by the Relying Party.") { - forAll(Extensions.subsetAssertionExtensions) { - case (extensionInputs, clientExtensionOutputs, _) => - val steps = finishAssertion( - requestedExtensions = extensionInputs, - clientExtensionResults = clientExtensionOutputs, - ) - val step: FinishAssertionSteps#Step14 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next + describe("18. Verify that the values of the client extension outputs in clientExtensionResults and the authenticator extension outputs in the extensions in authData are as expected, considering the client extension input values that were given in options.extensions and any specific policy of the Relying Party regarding unsolicited extensions, i.e., those that were not specified as part of options.extensions. In the general case, the meaning of \"are as expected\" is specific to the Relying Party and which extensions are in use.") { + it("Succeeds if clientExtensionResults is not a subset of the extensions requested by the Relying Party.") { + forAll(Extensions.unrequestedClientAssertionExtensions) { + case (extensionInputs, clientExtensionOutputs, _) => + val steps = finishAssertion( + requestedExtensions = extensionInputs, + clientExtensionResults = clientExtensionOutputs, + ) + val step: FinishAssertionSteps#Step18 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next - step.validations shouldBe a[Success[_]] - step.tryNext shouldBe a[Success[_]] - } + step.validations shouldBe a[Success[_]] + step.tryNext shouldBe a[Success[_]] } } - describe("authenticator extension outputs in the extensions in authData are as expected, considering the client extension input values that were given as the extensions option in the get() call. In particular, any extension identifier values in the extensions in authData MUST be also be present as extension identifier values in the extensions member of options, i.e., no extensions are present that were not requested. In the general case, the meaning of \"are as expected\" is specific to the Relying Party and which extensions are in use.") { - it("Fails if authenticator extensions is not a subset of the extensions requested by the Relying Party.") { - forAll(Extensions.unrequestedAuthenticatorAssertionExtensions) { - case ( - extensionInputs: AssertionExtensionInputs, - _, - authenticatorExtensionOutputs: CBORObject, - ) => - val steps = finishAssertion( - requestedExtensions = extensionInputs, - authenticatorData = TestAuthenticator.makeAuthDataBytes( - extensionsCborBytes = Some( - new ByteArray( - authenticatorExtensionOutputs.EncodeToBytes() - ) - ) - ), - ) - val step: FinishAssertionSteps#Step14 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next + it("Succeeds if clientExtensionResults is a subset of the extensions requested by the Relying Party.") { + forAll(Extensions.subsetAssertionExtensions) { + case (extensionInputs, clientExtensionOutputs, _) => + val steps = finishAssertion( + requestedExtensions = extensionInputs, + clientExtensionResults = clientExtensionOutputs, + ) + val step: FinishAssertionSteps#Step18 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next - step.validations shouldBe a[Failure[_]] - step.validations.failed.get shouldBe an[ - IllegalArgumentException - ] - step.tryNext shouldBe a[Failure[_]] - } + step.validations shouldBe a[Success[_]] + step.tryNext shouldBe a[Success[_]] } + } - it("Succeeds if authenticator extensions is not a subset of the extensions requested by the Relying Party, but the Relying Party has enabled allowing unrequested extensions.") { - forAll(Extensions.unrequestedAuthenticatorAssertionExtensions) { - case ( - extensionInputs: AssertionExtensionInputs, - _, - authenticatorExtensionOutputs: CBORObject, - ) => - val steps = finishAssertion( - allowUnrequestedExtensions = true, - requestedExtensions = extensionInputs, - authenticatorData = TestAuthenticator.makeAuthDataBytes( - extensionsCborBytes = Some( - new ByteArray( - authenticatorExtensionOutputs.EncodeToBytes() - ) + it("Succeeds if authenticator extensions is not a subset of the extensions requested by the Relying Party.") { + forAll(Extensions.unrequestedAuthenticatorAssertionExtensions) { + case ( + extensionInputs: AssertionExtensionInputs, + _, + authenticatorExtensionOutputs: CBORObject, + ) => + val steps = finishAssertion( + requestedExtensions = extensionInputs, + authenticatorData = TestAuthenticator.makeAuthDataBytes( + extensionsCborBytes = Some( + new ByteArray( + authenticatorExtensionOutputs.EncodeToBytes() ) - ), - ) - val step: FinishAssertionSteps#Step14 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next + ) + ), + ) + val step: FinishAssertionSteps#Step18 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next - step.validations shouldBe a[Success[_]] - step.tryNext shouldBe a[Success[_]] - } + step.validations shouldBe a[Success[_]] + step.tryNext shouldBe a[Success[_]] } + } - it("Succeeds if authenticator extensions is a subset of the extensions requested by the Relying Party.") { - forAll(Extensions.subsetAssertionExtensions) { - case ( - extensionInputs: AssertionExtensionInputs, - _, - authenticatorExtensionOutputs: CBORObject, - ) => - val steps = finishAssertion( - requestedExtensions = extensionInputs, - authenticatorData = TestAuthenticator.makeAuthDataBytes( - extensionsCborBytes = Some( - new ByteArray( - authenticatorExtensionOutputs.EncodeToBytes() - ) + it("Succeeds if authenticator extensions is a subset of the extensions requested by the Relying Party.") { + forAll(Extensions.subsetAssertionExtensions) { + case ( + extensionInputs: AssertionExtensionInputs, + _, + authenticatorExtensionOutputs: CBORObject, + ) => + val steps = finishAssertion( + requestedExtensions = extensionInputs, + authenticatorData = TestAuthenticator.makeAuthDataBytes( + extensionsCborBytes = Some( + new ByteArray( + authenticatorExtensionOutputs.EncodeToBytes() ) - ), - ) - val step: FinishAssertionSteps#Step14 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next + ) + ), + ) + val step: FinishAssertionSteps#Step18 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next - step.validations shouldBe a[Success[_]] - step.tryNext shouldBe a[Success[_]] - } + step.validations shouldBe a[Success[_]] + step.tryNext shouldBe a[Success[_]] } } - } - it("15. Let hash be the result of computing a hash over the cData using SHA-256.") { + it("19. Let hash be the result of computing a hash over the cData using SHA-256.") { val steps = finishAssertion() - val step: FinishAssertionSteps#Step15 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next + val step: FinishAssertionSteps#Step19 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next step.validations shouldBe a[Success[_]] step.tryNext shouldBe a[Success[_]] @@ -1480,11 +1596,11 @@ class RelyingPartyAssertionSpec ) } - describe("16. Using the credential public key looked up in step 3, verify that sig is a valid signature over the binary concatenation of authData and hash.") { + describe("20. Using credentialPublicKey, verify that sig is a valid signature over the binary concatenation of authData and hash.") { it("The default test case succeeds.") { val steps = finishAssertion() - val step: FinishAssertionSteps#Step16 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next + val step: FinishAssertionSteps#Step20 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next step.validations shouldBe a[Success[_]] step.tryNext shouldBe a[Success[_]] @@ -1500,8 +1616,8 @@ class RelyingPartyAssertionSpec .set("foo", jsonFactory.textNode("bar")) ) ) - val step: FinishAssertionSteps#Step16 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next + val step: FinishAssertionSteps#Step20 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next step.validations shouldBe a[Failure[_]] step.validations.failed.get shouldBe an[IllegalArgumentException] @@ -1519,8 +1635,8 @@ class RelyingPartyAssertionSpec rpId = Defaults.rpId.toBuilder.id(rpId).build(), origins = Some(Set("https://localhost")), ) - val step: FinishAssertionSteps#Step16 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next + val step: FinishAssertionSteps#Step20 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next step.validations shouldBe a[Failure[_]] step.validations.failed.get shouldBe an[IllegalArgumentException] @@ -1539,8 +1655,8 @@ class RelyingPartyAssertionSpec .toArray ) ) - val step: FinishAssertionSteps#Step16 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next + val step: FinishAssertionSteps#Step20 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next step.validations shouldBe a[Failure[_]] step.validations.failed.get shouldBe an[IllegalArgumentException] @@ -1555,8 +1671,8 @@ class RelyingPartyAssertionSpec .toArray ) ) - val step: FinishAssertionSteps#Step16 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next + val step: FinishAssertionSteps#Step20 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next step.validations shouldBe a[Failure[_]] step.validations.failed.get shouldBe an[IllegalArgumentException] @@ -1564,8 +1680,8 @@ class RelyingPartyAssertionSpec } } - describe("17. If the signature counter value authData.signCount is nonzero or the value stored in conjunction with credential’s id attribute is nonzero, then run the following sub-step:") { - describe("If the signature counter value authData.signCount is") { + describe("21. Let storedSignCount be the stored signature counter value associated with credential.id. If authData.signCount is nonzero or storedSignCount is nonzero, then run the following sub-step:") { + describe("If authData.signCount is") { def credentialRepository(signatureCount: Long) = Helpers.CredentialRepository.withUser( Defaults.user, @@ -1602,8 +1718,8 @@ class RelyingPartyAssertionSpec credentialRepository = Some(cr), validateSignatureCounter = true, ) - val step: FinishAssertionSteps#Step17 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next + val step: FinishAssertionSteps#Step21 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next step.validations shouldBe a[Success[_]] step.tryNext shouldBe a[Success[_]] @@ -1619,8 +1735,8 @@ class RelyingPartyAssertionSpec credentialRepository = Some(cr), validateSignatureCounter = true, ) - val step: FinishAssertionSteps#Step17 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next + val step: FinishAssertionSteps#Step21 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next step.validations shouldBe a[Failure[_]] step.tryNext shouldBe a[Failure[_]] @@ -1630,17 +1746,19 @@ class RelyingPartyAssertionSpec } } - describe("greater than the signature counter value stored in conjunction with credential’s id attribute.") { + describe("greater than storedSignCount:") { val cr = credentialRepository(1336) - describe("Update the stored signature counter value, associated with credential’s id attribute, to be the value of authData.signCount.") { + describe( + "Update storedSignCount to be the value of authData.signCount." + ) { it("An increasing signature counter always succeeds.") { val steps = finishAssertion( credentialRepository = Some(cr), validateSignatureCounter = true, ) - val step: FinishAssertionSteps#Step17 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next + val step: FinishAssertionSteps#Step21 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next step.validations shouldBe a[Success[_]] step.tryNext shouldBe a[Success[_]] @@ -1650,17 +1768,17 @@ class RelyingPartyAssertionSpec } } - describe("less than or equal to the signature counter value stored in conjunction with credential’s id attribute.") { + describe("less than or equal to storedSignCount:") { val cr = credentialRepository(1337) - describe("This is a signal that the authenticator may be cloned, i.e. at least two copies of the credential private key may exist and are being used in parallel. Relying Parties should incorporate this information into their risk scoring. Whether the Relying Party updates the stored signature counter value in this case, or not, or fails the authentication ceremony or not, is Relying Party-specific.") { + describe("This is a signal that the authenticator may be cloned, i.e. at least two copies of the credential private key may exist and are being used in parallel. Relying Parties should incorporate this information into their risk scoring. Whether the Relying Party updates storedSignCount in this case, or not, or fails the authentication ceremony or not, is Relying Party-specific.") { it("If signature counter validation is disabled, a nonincreasing signature counter succeeds.") { val steps = finishAssertion( credentialRepository = Some(cr), validateSignatureCounter = false, ) - val step: FinishAssertionSteps#Step17 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next + val step: FinishAssertionSteps#Step21 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next step.validations shouldBe a[Success[_]] step.tryNext shouldBe a[Success[_]] @@ -1673,8 +1791,8 @@ class RelyingPartyAssertionSpec credentialRepository = Some(cr), validateSignatureCounter = true, ) - val step: FinishAssertionSteps#Step17 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next + val step: FinishAssertionSteps#Step21 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next val result = Try(step.run()) step.validations shouldBe a[Failure[_]] @@ -1700,10 +1818,10 @@ class RelyingPartyAssertionSpec } } - it("18. If all the above steps are successful, continue with the authentication ceremony as appropriate. Otherwise, fail the authentication ceremony.") { + it("22. If all the above steps are successful, continue with the authentication ceremony as appropriate. Otherwise, fail the authentication ceremony.") { val steps = finishAssertion() val step: FinishAssertionSteps#Finished = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next step.validations shouldBe a[Success[_]] Try(steps.run) shouldBe a[Success[_]] @@ -1712,9 +1830,7 @@ class RelyingPartyAssertionSpec step.result.get.getCredentialId should equal(Defaults.credentialId) step.result.get.getUserHandle should equal(Defaults.userHandle) } - } - } describe("RelyingParty supports authenticating") { @@ -2137,10 +2253,10 @@ class RelyingPartyAssertionSpec .build() ) - result.getClientExtensionOutputs.get.getLargeBlob.get.getWritten.asScala should be( + result.getClientExtensionOutputs.get.getLargeBlob.get.getWritten.toScala should be( Some(true) ) - result.getClientExtensionOutputs.get.getLargeBlob.get.getBlob.asScala should be( + result.getClientExtensionOutputs.get.getLargeBlob.get.getBlob.toScala should be( None ) } @@ -2181,10 +2297,10 @@ class RelyingPartyAssertionSpec .build() ) - result.getClientExtensionOutputs.get.getLargeBlob.get.getBlob.asScala should be( + result.getClientExtensionOutputs.get.getLargeBlob.get.getBlob.toScala should be( Some(ByteArray.fromHex("00010203")) ) - result.getClientExtensionOutputs.get.getLargeBlob.get.getWritten.asScala should be( + result.getClientExtensionOutputs.get.getLargeBlob.get.getWritten.toScala should be( None ) } @@ -2236,7 +2352,7 @@ class RelyingPartyAssertionSpec .build() ) - result.getAuthenticatorExtensionOutputs.get.getUvm.asScala should equal( + result.getAuthenticatorExtensionOutputs.get.getUvm.toScala should equal( Some( List( new UvmEntry( 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 63c0f7fe5..cb907a3c6 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 @@ -51,11 +51,8 @@ class RelyingPartyCeremoniesSpec .credentialRepository(credentialRepo) .build() - private def createCheck( - modRp: RelyingParty => RelyingParty = identity - )(testData: RealExamples.Example): Unit = { - val registrationRp = - modRp(newRp(testData, Helpers.CredentialRepository.empty)) + private def createCheck(testData: RealExamples.Example): Unit = { + val registrationRp = newRp(testData, Helpers.CredentialRepository.empty) val registrationRequest = registrationRp .startRegistration( @@ -76,7 +73,6 @@ class RelyingPartyCeremoniesSpec testData.attestation.credential.getId ) registrationResult.isAttestationTrusted should be(false) - registrationResult.getAttestationMetadata.isPresent should be(false) val assertionRp = newRp( testData, @@ -84,9 +80,7 @@ class RelyingPartyCeremoniesSpec testData.user, Helpers.toRegisteredCredential(testData.user, registrationResult), ), - ).toBuilder - .allowUnrequestedExtensions(true) - .build() + ) val assertionResult = assertionRp.finishAssertion( FinishAssertionOptions @@ -125,7 +119,7 @@ class RelyingPartyCeremoniesSpec testWithEachProvider { it => describe("The default RelyingParty settings") { - val check = createCheck()(_) + val check = createCheck(_) describe("can register and then authenticate") { it("a YubiKey NEO.") { @@ -162,7 +156,7 @@ class RelyingPartyCeremoniesSpec check(RealExamples.SecurityKeyNfc) } - ignore("a YubiKey 5 NFC FIPS.") { // TODO Un-ignore when allowUnrequestedExtensions default changes to true + it("a YubiKey 5 NFC FIPS.") { check(RealExamples.YubikeyFips5Nfc) } @@ -182,18 +176,5 @@ class RelyingPartyCeremoniesSpec } } } - - describe("The default RelyingParty settings, but with allowUnrequestedExtensions(true)") { - - describe("can register and then authenticate") { - val check = createCheck(rp => - rp.toBuilder.allowUnrequestedExtensions(true).build() - )(_) - - it("a YubiKey 5 NFC FIPS.") { // TODO Delete when allowUnrequestedExtensions default changes to true - check(RealExamples.YubikeyFips5Nfc) - } - } - } } } 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 353d7d60d..ddae4cfa4 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 @@ -28,15 +28,13 @@ import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.node.JsonNodeFactory import com.fasterxml.jackson.databind.node.ObjectNode import com.upokecenter.cbor.CBORObject -import com.yubico.fido.metadata.KeyProtectionType -import com.yubico.fido.metadata.MatcherProtectionType -import com.yubico.fido.metadata.UserVerificationMethod +import com.yubico.internal.util.BinaryUtil +import com.yubico.internal.util.CertificateParser import com.yubico.internal.util.JacksonCodecs -import com.yubico.internal.util.scala.JavaConverters._ import com.yubico.webauthn.TestAuthenticator.AttestationCert import com.yubico.webauthn.TestAuthenticator.AttestationMaker -import com.yubico.webauthn.attestation.Attestation -import com.yubico.webauthn.attestation.MetadataService +import com.yubico.webauthn.attestation.AttestationTrustSource +import com.yubico.webauthn.attestation.AttestationTrustSource.TrustRootsResult import com.yubico.webauthn.data.AttestationObject import com.yubico.webauthn.data.AttestationType import com.yubico.webauthn.data.AuthenticatorData @@ -59,11 +57,15 @@ import com.yubico.webauthn.data.RelyingPartyIdentity import com.yubico.webauthn.data.UserIdentity import com.yubico.webauthn.data.UserVerificationRequirement import com.yubico.webauthn.exception.RegistrationFailedException +import com.yubico.webauthn.extension.uvm.KeyProtectionType +import com.yubico.webauthn.extension.uvm.MatcherProtectionType +import com.yubico.webauthn.extension.uvm.UserVerificationMethod import com.yubico.webauthn.test.Helpers import com.yubico.webauthn.test.RealExamples import com.yubico.webauthn.test.Util.toStepWithUtilities import org.bouncycastle.asn1.DEROctetString import org.bouncycastle.asn1.x500.X500Name +import org.bouncycastle.cert.jcajce.JcaX500NameUtil import org.junit.runner.RunWith import org.mockito.Mockito import org.scalacheck.Gen @@ -74,14 +76,27 @@ import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks import java.io.IOException import java.nio.charset.Charset +import java.nio.charset.StandardCharsets +import java.security.KeyFactory import java.security.KeyPair import java.security.MessageDigest import java.security.PrivateKey import java.security.SignatureException +import java.security.cert.CRL +import java.security.cert.CertStore +import java.security.cert.CollectionCertStoreParameters import java.security.cert.X509Certificate import java.security.interfaces.RSAPublicKey +import java.time.Clock +import java.time.Instant +import java.time.ZoneOffset +import java.util +import java.util.Collections +import java.util.Optional import javax.security.auth.x500.X500Principal import scala.jdk.CollectionConverters._ +import scala.jdk.OptionConverters.RichOption +import scala.jdk.OptionConverters.RichOptional import scala.util.Failure import scala.util.Success import scala.util.Try @@ -111,12 +126,11 @@ class RelyingPartyRegistrationSpec private def finishRegistration( allowOriginPort: Boolean = false, allowOriginSubdomain: Boolean = false, - allowUnrequestedExtensions: Boolean = false, allowUntrustedAttestation: Boolean = false, callerTokenBindingId: Option[ByteArray] = None, credentialRepository: CredentialRepository = Helpers.CredentialRepository.unimplemented, - metadataService: Option[MetadataService] = None, + attestationTrustSource: Option[AttestationTrustSource] = None, origins: Option[Set[String]] = None, preferredPubkeyParams: List[PublicKeyCredentialParameters] = Nil, rp: RelyingPartyIdentity = RelyingPartyIdentity @@ -125,6 +139,7 @@ class RelyingPartyRegistrationSpec .name("Test party") .build(), testData: RegistrationTestData, + clock: Clock = Clock.systemUTC(), ): FinishRegistrationSteps = { var builder = RelyingParty .builder() @@ -133,10 +148,12 @@ class RelyingPartyRegistrationSpec .preferredPubkeyParams(preferredPubkeyParams.asJava) .allowOriginPort(allowOriginPort) .allowOriginSubdomain(allowOriginSubdomain) - .allowUnrequestedExtensions(allowUnrequestedExtensions) .allowUntrustedAttestation(allowUntrustedAttestation) + .clock(clock) - metadataService.foreach { mds => builder = builder.metadataService(mds) } + attestationTrustSource.foreach { ats => + builder = builder.attestationTrustSource(ats) + } origins.map(_.asJava).foreach(builder.origins _) @@ -145,20 +162,39 @@ class RelyingPartyRegistrationSpec ._finishRegistration( testData.request, testData.response, - callerTokenBindingId.asJava, + callerTokenBindingId.toJava, ) } - class TestMetadataService(private val attestation: Option[Attestation] = None) - extends MetadataService { - override def getAttestation( - attestationCertificateChain: java.util.List[X509Certificate] - ): Attestation = - attestation match { - case None => Attestation.builder().trusted(false).build() - case Some(a) => a - } + val emptyTrustSource = new AttestationTrustSource { + override def findTrustRoots( + attestationCertificateChain: util.List[X509Certificate], + aaguid: Optional[ByteArray], + ): TrustRootsResult = + TrustRootsResult.builder().trustRoots(Collections.emptySet()).build() } + def trustSourceWith( + trustedCert: X509Certificate, + crls: Option[Set[CRL]] = None, + enableRevocationChecking: Boolean = true, + ): AttestationTrustSource = + (_: util.List[X509Certificate], _: Optional[ByteArray]) => { + TrustRootsResult + .builder() + .trustRoots(Collections.singleton(trustedCert)) + .certStore( + crls + .map(crls => + CertStore.getInstance( + "Collection", + new CollectionCertStoreParameters(crls.asJava), + ) + ) + .orNull + ) + .enableRevocationChecking(enableRevocationChecking) + .build() + } testWithEachProvider { it => describe("§7.1. Registering a new credential") { @@ -183,6 +219,11 @@ class RelyingPartyRegistrationSpec val testData = RegistrationTestData.Packed.BasicAttestationEdDsa.assertion.get "frob.response(testData.response)" shouldNot compile + frob + .response( + RegistrationTestData.Packed.BasicAttestationEdDsa.response + ) + .build() should not be null } } @@ -240,7 +281,7 @@ class RelyingPartyRegistrationSpec Some(RegistrationTestData.FidoU2f.BasicAttestation.request), ) ) - val step: FinishRegistrationSteps#Step2 = steps.begin.next + val step: FinishRegistrationSteps#Step6 = steps.begin step.validations shouldBe a[Success[_]] step.clientData should not be null @@ -253,7 +294,7 @@ class RelyingPartyRegistrationSpec val steps = finishRegistration(testData = RegistrationTestData.FidoU2f.BasicAttestation ) - val step: FinishRegistrationSteps#Step3 = steps.begin.next.next + val step: FinishRegistrationSteps#Step7 = steps.begin.next step.validations shouldBe a[Success[_]] } @@ -263,7 +304,7 @@ class RelyingPartyRegistrationSpec testData = RegistrationTestData.FidoU2f.BasicAttestation .editClientData("type", typeString) ) - val step: FinishRegistrationSteps#Step3 = steps.begin.next.next + val step: FinishRegistrationSteps#Step7 = steps.begin.next step.validations shouldBe a[Failure[_]] step.validations.failed.get shouldBe an[IllegalArgumentException] @@ -281,6 +322,10 @@ class RelyingPartyRegistrationSpec } } } + + it("""The string "webauthn.get" fails.""") { + assertFails("webauthn.get") + } } it("8. Verify that the value of C.challenge equals the base64url encoding of options.challenge.") { @@ -293,7 +338,7 @@ class RelyingPartyRegistrationSpec ) ) ) - val step: FinishRegistrationSteps#Step4 = steps.begin.next.next.next + val step: FinishRegistrationSteps#Step8 = steps.begin.next.next step.validations shouldBe a[Failure[_]] step.validations.failed.get shouldBe an[IllegalArgumentException] @@ -315,8 +360,7 @@ class RelyingPartyRegistrationSpec allowOriginPort = allowOriginPort, allowOriginSubdomain = allowOriginSubdomain, ) - val step: FinishRegistrationSteps#Step5 = - steps.begin.next.next.next.next + val step: FinishRegistrationSteps#Step9 = steps.begin.next.next.next step.validations shouldBe a[Success[_]] step.tryNext shouldBe a[Success[_]] @@ -335,8 +379,7 @@ class RelyingPartyRegistrationSpec allowOriginPort = allowOriginPort, allowOriginSubdomain = allowOriginSubdomain, ) - val step: FinishRegistrationSteps#Step5 = - steps.begin.next.next.next.next + val step: FinishRegistrationSteps#Step9 = steps.begin.next.next.next step.validations shouldBe a[Failure[_]] step.validations.failed.get shouldBe an[IllegalArgumentException] @@ -522,8 +565,8 @@ class RelyingPartyRegistrationSpec val steps = finishRegistration(testData = RegistrationTestData.FidoU2f.BasicAttestation ) - val step: FinishRegistrationSteps#Step6 = - steps.begin.next.next.next.next.next + val step: FinishRegistrationSteps#Step10 = + steps.begin.next.next.next.next step.validations shouldBe a[Success[_]] step.tryNext shouldBe a[Success[_]] @@ -534,8 +577,8 @@ class RelyingPartyRegistrationSpec RegistrationTestData.FidoU2f.BasicAttestation .editClientData(_.without[ObjectNode]("tokenBinding")) ) - val step: FinishRegistrationSteps#Step6 = - steps.begin.next.next.next.next.next + val step: FinishRegistrationSteps#Step10 = + steps.begin.next.next.next.next step.validations shouldBe a[Success[_]] step.tryNext shouldBe a[Success[_]] @@ -549,8 +592,8 @@ class RelyingPartyRegistrationSpec toJson(Map("status" -> "supported")), ) ) - val step: FinishRegistrationSteps#Step6 = - steps.begin.next.next.next.next.next + val step: FinishRegistrationSteps#Step10 = + steps.begin.next.next.next.next step.validations shouldBe a[Success[_]] step.tryNext shouldBe a[Success[_]] @@ -563,8 +606,8 @@ class RelyingPartyRegistrationSpec testData = RegistrationTestData.FidoU2f.BasicAttestation .editClientData(_.without[ObjectNode]("tokenBinding")), ) - val step: FinishRegistrationSteps#Step6 = - steps.begin.next.next.next.next.next + val step: FinishRegistrationSteps#Step10 = + steps.begin.next.next.next.next step.validations shouldBe a[Failure[_]] step.validations.failed.get shouldBe an[IllegalArgumentException] @@ -577,8 +620,8 @@ class RelyingPartyRegistrationSpec testData = RegistrationTestData.FidoU2f.BasicAttestation .editClientData(_.without[ObjectNode]("tokenBinding")), ) - val step: FinishRegistrationSteps#Step6 = - steps.begin.next.next.next.next.next + val step: FinishRegistrationSteps#Step10 = + steps.begin.next.next.next.next step.validations shouldBe a[Success[_]] step.tryNext shouldBe a[Success[_]] @@ -593,8 +636,8 @@ class RelyingPartyRegistrationSpec toJson(Map("status" -> "present", "id" -> "YELLOWSUBMARINE")), ), ) - val step: FinishRegistrationSteps#Step6 = - steps.begin.next.next.next.next.next + val step: FinishRegistrationSteps#Step10 = + steps.begin.next.next.next.next step.validations shouldBe a[Failure[_]] step.validations.failed.get shouldBe an[IllegalArgumentException] @@ -614,8 +657,8 @@ class RelyingPartyRegistrationSpec ), ), ) - val step: FinishRegistrationSteps#Step6 = - steps.begin.next.next.next.next.next + val step: FinishRegistrationSteps#Step10 = + steps.begin.next.next.next.next step.validations shouldBe a[Success[_]] step.tryNext shouldBe a[Success[_]] @@ -631,8 +674,8 @@ class RelyingPartyRegistrationSpec toJson(Map("status" -> "present")), ), ) - val step: FinishRegistrationSteps#Step6 = - steps.begin.next.next.next.next.next + val step: FinishRegistrationSteps#Step10 = + steps.begin.next.next.next.next step.validations shouldBe a[Failure[_]] step.validations.failed.get shouldBe an[IllegalArgumentException] @@ -646,8 +689,8 @@ class RelyingPartyRegistrationSpec testData = RegistrationTestData.FidoU2f.BasicAttestation .editClientData(_.without[ObjectNode]("tokenBinding")), ) - val step: FinishRegistrationSteps#Step6 = - steps.begin.next.next.next.next.next + val step: FinishRegistrationSteps#Step10 = + steps.begin.next.next.next.next step.validations shouldBe a[Failure[_]] step.validations.failed.get shouldBe an[IllegalArgumentException] @@ -664,8 +707,8 @@ class RelyingPartyRegistrationSpec toJson(Map("status" -> "supported")), ), ) - val step: FinishRegistrationSteps#Step6 = - steps.begin.next.next.next.next.next + val step: FinishRegistrationSteps#Step10 = + steps.begin.next.next.next.next step.validations shouldBe a[Failure[_]] step.validations.failed.get shouldBe an[IllegalArgumentException] @@ -684,8 +727,8 @@ class RelyingPartyRegistrationSpec ), ), ) - val step: FinishRegistrationSteps#Step6 = - steps.begin.next.next.next.next.next + val step: FinishRegistrationSteps#Step10 = + steps.begin.next.next.next.next step.validations shouldBe a[Failure[_]] step.validations.failed.get shouldBe an[IllegalArgumentException] @@ -698,8 +741,8 @@ class RelyingPartyRegistrationSpec val steps = finishRegistration(testData = RegistrationTestData.FidoU2f.BasicAttestation ) - val step: FinishRegistrationSteps#Step7 = - steps.begin.next.next.next.next.next.next + val step: FinishRegistrationSteps#Step11 = + steps.begin.next.next.next.next.next val digest = MessageDigest.getInstance("SHA-256") step.validations shouldBe a[Success[_]] @@ -717,8 +760,8 @@ class RelyingPartyRegistrationSpec val steps = finishRegistration(testData = RegistrationTestData.FidoU2f.BasicAttestation ) - val step: FinishRegistrationSteps#Step8 = - steps.begin.next.next.next.next.next.next.next + val step: FinishRegistrationSteps#Step12 = + steps.begin.next.next.next.next.next.next step.validations shouldBe a[Success[_]] step.tryNext shouldBe a[Success[_]] @@ -737,8 +780,8 @@ class RelyingPartyRegistrationSpec ) } ) - val step: FinishRegistrationSteps#Step9 = - steps.begin.next.next.next.next.next.next.next.next + val step: FinishRegistrationSteps#Step13 = + steps.begin.next.next.next.next.next.next.next step.validations shouldBe a[Failure[_]] step.validations.failed.get shouldBe an[IllegalArgumentException] @@ -749,8 +792,8 @@ class RelyingPartyRegistrationSpec val steps = finishRegistration(testData = RegistrationTestData.FidoU2f.BasicAttestation ) - val step: FinishRegistrationSteps#Step9 = - steps.begin.next.next.next.next.next.next.next.next + val step: FinishRegistrationSteps#Step13 = + steps.begin.next.next.next.next.next.next.next step.validations shouldBe a[Success[_]] step.tryNext shouldBe a[Success[_]] @@ -834,9 +877,9 @@ class RelyingPartyRegistrationSpec describe("14. Verify that the User Present bit of the flags in authData is set.") { val (checkFails, checkSucceeds) = checks[ - FinishRegistrationSteps#Step11, - FinishRegistrationSteps#Step10, - ](_.begin.next.next.next.next.next.next.next.next.next) + FinishRegistrationSteps#Step15, + FinishRegistrationSteps#Step14, + ](_.begin.next.next.next.next.next.next.next.next) it("Fails if UV is discouraged and flag is not set.") { checkFails(UserVerificationRequirement.DISCOURAGED, upOff) @@ -871,9 +914,9 @@ class RelyingPartyRegistrationSpec describe("15. If user verification is required for this registration, verify that the User Verified bit of the flags in authData is set.") { val (checkFails, checkSucceeds) = checks[ - FinishRegistrationSteps#Step12, - FinishRegistrationSteps#Step11, - ](_.begin.next.next.next.next.next.next.next.next.next.next) + FinishRegistrationSteps#Step16, + FinishRegistrationSteps#Step15, + ](_.begin.next.next.next.next.next.next.next.next.next) it("Succeeds if UV is discouraged and flag is not set.") { checkSucceeds(UserVerificationRequirement.DISCOURAGED, uvOff) @@ -940,182 +983,96 @@ class RelyingPartyRegistrationSpec } } - describe("17. Verify that the values of the") { - - describe("client extension outputs in clientExtensionResults are as expected, considering the client extension input values that were given in options.extensions and any specific policy of the Relying Party regarding unsolicited extensions, i.e., those that were not specified as part of options.extensions. In the general case, the meaning of \"are as expected\" is specific to the Relying Party and which extensions are in use.") { - it("Succeeds if clientExtensionResults is a subset of the extensions requested by the Relying Party.") { - forAll(Extensions.subsetRegistrationExtensions) { - case (extensionInputs, clientExtensionOutputs, _) => - val steps = finishRegistration( - testData = - RegistrationTestData.Packed.BasicAttestation.copy( - requestedExtensions = extensionInputs, - clientExtensionResults = clientExtensionOutputs, - ) - ) - val step: FinishRegistrationSteps#Step12 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next - - step.validations shouldBe a[Success[_]] - step.tryNext shouldBe a[Success[_]] - } - } - - it("Succeeds if clientExtensionResults is not a subset of the extensions requested by the Relying Party, but the Relying Party has enabled allowing unrequested extensions.") { - forAll(Extensions.unrequestedClientRegistrationExtensions) { - case (extensionInputs, clientExtensionOutputs, _) => - val steps = finishRegistration( - allowUnrequestedExtensions = true, - testData = - RegistrationTestData.Packed.BasicAttestation.copy( - requestedExtensions = extensionInputs, - clientExtensionResults = clientExtensionOutputs, - ), - ) - val step: FinishRegistrationSteps#Step12 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next - - step.validations shouldBe a[Success[_]] - step.tryNext shouldBe a[Success[_]] - } - } - - it("Fails if clientExtensionResults is not a subset of the extensions requested by the Relying Party.") { - forAll(Extensions.unrequestedClientRegistrationExtensions) { - case (extensionInputs, clientExtensionOutputs, _) => - val steps = finishRegistration( - testData = - RegistrationTestData.Packed.BasicAttestation.copy( - requestedExtensions = extensionInputs, - clientExtensionResults = clientExtensionOutputs, - ) + describe("17. Verify that the values of the client extension outputs in clientExtensionResults and the authenticator extension outputs in the extensions in authData are as expected, considering the client extension input values that were given in options.extensions and any specific policy of the Relying Party regarding unsolicited extensions, i.e., those that were not specified as part of options.extensions. In the general case, the meaning of \"are as expected\" is specific to the Relying Party and which extensions are in use.") { + it("Succeeds if clientExtensionResults is a subset of the extensions requested by the Relying Party.") { + forAll(Extensions.subsetRegistrationExtensions) { + case (extensionInputs, clientExtensionOutputs, _) => + val steps = finishRegistration( + testData = RegistrationTestData.Packed.BasicAttestation.copy( + requestedExtensions = extensionInputs, + clientExtensionResults = clientExtensionOutputs, ) + ) + val stepAfter: Try[FinishRegistrationSteps#Step18] = + steps.begin.next.next.next.next.next.next.next.next.next.next.tryNext - val step: FinishRegistrationSteps#Step12 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next - - step.validations shouldBe a[Failure[_]] - step.validations.failed.get shouldBe an[ - IllegalArgumentException - ] - step.tryNext shouldBe a[Failure[_]] - } - } - - ignore("TODO v2.0: Succeeds if clientExtensionResults is not a subset of the extensions requested by the Relying Party.") { - fail("TODO") - } - - ignore("TODO v2.0: Fails if clientExtensionResults is not a subset of the extensions requested by the Relying Party and the Relying Party has opted out of allowing unrequested extensions.") { - fail("TODO") + stepAfter shouldBe a[Success[_]] } } - describe("authenticator extension outputs in the extensions in authData are as expected, considering the client extension input values that were given in options.extensions and any specific policy of the Relying Party regarding unsolicited extensions, i.e., those that were not specified as part of options.extensions. In the general case, the meaning of \"are as expected\" is specific to the Relying Party and which extensions are in use.") { - it("Succeeds if authenticator extensions is a subset of the extensions requested by the Relying Party.") { - forAll(Extensions.subsetRegistrationExtensions) { - case ( - extensionInputs: RegistrationExtensionInputs, - _, - authenticatorExtensionOutputs: CBORObject, - ) => - val steps = finishRegistration( - testData = RegistrationTestData.Packed.BasicAttestation - .copy( - requestedExtensions = extensionInputs - ) - .editAuthenticatorData(authData => - new ByteArray( - authData.getBytes.updated( - 32, - (authData.getBytes()(32) | 0x80).toByte, - ) ++ authenticatorExtensionOutputs.EncodeToBytes() - ) - ) + it("Succeeds if clientExtensionResults is not a subset of the extensions requested by the Relying Party.") { + forAll(Extensions.unrequestedClientRegistrationExtensions) { + case (extensionInputs, clientExtensionOutputs, _) => + val steps = finishRegistration( + testData = RegistrationTestData.Packed.BasicAttestation.copy( + requestedExtensions = extensionInputs, + clientExtensionResults = clientExtensionOutputs, ) - val step: FinishRegistrationSteps#Step12 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next + ) + val stepAfter: Try[FinishRegistrationSteps#Step18] = + steps.begin.next.next.next.next.next.next.next.next.next.next.tryNext - step.validations shouldBe a[Success[_]] - step.tryNext shouldBe a[Success[_]] - } + stepAfter shouldBe a[Success[_]] } + } - it("Succeeds if authenticator extensions is not a subset of the extensions requested by the Relying Party, but the Relying Party has enabled allowing unrequested extensions.") { - forAll( - Extensions.unrequestedAuthenticatorRegistrationExtensions - ) { - case ( - extensionInputs: RegistrationExtensionInputs, - _, - authenticatorExtensionOutputs: CBORObject, - ) => - val steps = finishRegistration( - allowUnrequestedExtensions = true, - testData = RegistrationTestData.Packed.BasicAttestation - .copy( - requestedExtensions = extensionInputs + it("Succeeds if authenticator extensions is a subset of the extensions requested by the Relying Party.") { + forAll(Extensions.subsetRegistrationExtensions) { + case ( + extensionInputs: RegistrationExtensionInputs, + _, + authenticatorExtensionOutputs: CBORObject, + ) => + val steps = finishRegistration( + testData = RegistrationTestData.Packed.BasicAttestation + .copy( + requestedExtensions = extensionInputs + ) + .editAuthenticatorData(authData => + new ByteArray( + authData.getBytes.updated( + 32, + (authData.getBytes()(32) | 0x80).toByte, + ) ++ authenticatorExtensionOutputs.EncodeToBytes() ) - .editAuthenticatorData(authData => - new ByteArray( - authData.getBytes.updated( - 32, - (authData.getBytes()(32) | 0x80).toByte, - ) ++ authenticatorExtensionOutputs.EncodeToBytes() - ) - ), - ) - val step: FinishRegistrationSteps#Step12 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next + ) + ) + val stepAfter: Try[FinishRegistrationSteps#Step18] = + steps.begin.next.next.next.next.next.next.next.next.next.next.tryNext - step.validations shouldBe a[Success[_]] - step.tryNext shouldBe a[Success[_]] - } + stepAfter shouldBe a[Success[_]] } + } - it("Fails if authenticator extensions is not a subset of the extensions requested by the Relying Party.") { - forAll( - Extensions.unrequestedAuthenticatorRegistrationExtensions - ) { - case ( - extensionInputs: RegistrationExtensionInputs, - _, - authenticatorExtensionOutputs: CBORObject, - ) => - val steps = finishRegistration( - testData = RegistrationTestData.Packed.BasicAttestation - .copy( - requestedExtensions = extensionInputs - ) - .editAuthenticatorData(authData => - new ByteArray( - authData.getBytes.updated( - 32, - (authData.getBytes()(32) | 0x80).toByte, - ) ++ authenticatorExtensionOutputs.EncodeToBytes() - ) + it("Succeeds if authenticator extensions is not a subset of the extensions requested by the Relying Party.") { + forAll( + Extensions.unrequestedAuthenticatorRegistrationExtensions + ) { + case ( + extensionInputs: RegistrationExtensionInputs, + _, + authenticatorExtensionOutputs: CBORObject, + ) => + val steps = finishRegistration( + testData = RegistrationTestData.Packed.BasicAttestation + .copy( + requestedExtensions = extensionInputs + ) + .editAuthenticatorData(authData => + new ByteArray( + authData.getBytes.updated( + 32, + (authData.getBytes()(32) | 0x80).toByte, + ) ++ authenticatorExtensionOutputs.EncodeToBytes() ) - ) - val step: FinishRegistrationSteps#Step12 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next - - step.validations shouldBe a[Failure[_]] - step.validations.failed.get shouldBe an[ - IllegalArgumentException - ] - step.tryNext shouldBe a[Failure[_]] - } - } - - ignore("TODO v2.0: Succeeds if authenticator extensions is not a subset of the extensions requested by the Relying Party.") { - fail("TODO") - } + ) + ) + val stepAfter: Try[FinishRegistrationSteps#Step18] = + steps.begin.next.next.next.next.next.next.next.next.next.next.tryNext - ignore("TODO v2.0: Fails if authenticator extensions is not a subset of the extensions requested by the Relying Party and the Relying Party has opted out of allowing unrequested extensions.") { - fail("TODO") + stepAfter shouldBe a[Success[_]] } } - } describe("18. Determine the attestation statement format by performing a USASCII case-sensitive match on fmt against the set of supported WebAuthn Attestation Statement Format Identifier values. An up-to-date list of registered WebAuthn Attestation Statement Format Identifier values is maintained in the IANA \"WebAuthn Attestation Statement Format Identifiers\" registry established by RFC8809.") { @@ -1129,26 +1086,26 @@ class RelyingPartyRegistrationSpec def checkUnknown(format: String): Unit = { it(s"""Returns no known attestation statement verifier if fmt is "${format}".""") { val steps = setup(format) - val step: FinishRegistrationSteps#Step13 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next + val step: FinishRegistrationSteps#Step18 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next step.validations shouldBe a[Success[_]] step.tryNext shouldBe a[Success[_]] step.format should equal(format) - step.attestationStatementVerifier.asScala shouldBe empty + step.attestationStatementVerifier.toScala shouldBe empty } } def checkKnown(format: String): Unit = { it(s"""Returns a known attestation statement verifier if fmt is "${format}".""") { val steps = setup(format) - val step: FinishRegistrationSteps#Step13 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next + val step: FinishRegistrationSteps#Step18 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next step.validations shouldBe a[Success[_]] step.tryNext shouldBe a[Success[_]] step.format should equal(format) - step.attestationStatementVerifier.asScala should not be empty + step.attestationStatementVerifier.toScala should not be empty } } @@ -1185,8 +1142,8 @@ class RelyingPartyRegistrationSpec testData = testData, allowUntrustedAttestation = true, ) - val step: FinishRegistrationSteps#Step14 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next + val step: FinishRegistrationSteps#Step19 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next step.validations shouldBe a[Failure[_]] step.validations.failed.get.getCause shouldBe a[ @@ -1201,8 +1158,8 @@ class RelyingPartyRegistrationSpec val steps = finishRegistration(testData = RegistrationTestData.FidoU2f.BasicAttestation ) - val step: FinishRegistrationSteps#Step14 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next + val step: FinishRegistrationSteps#Step19 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next step.validations shouldBe a[Success[_]] step.attestationType should equal(AttestationType.BASIC) @@ -1213,8 +1170,8 @@ class RelyingPartyRegistrationSpec val steps = finishRegistration(testData = RegistrationTestData.FidoU2f.SelfAttestation ) - val step: FinishRegistrationSteps#Step14 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next + val step: FinishRegistrationSteps#Step19 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next step.validations shouldBe a[Success[_]] step.attestationType should equal( @@ -1228,7 +1185,7 @@ class RelyingPartyRegistrationSpec val steps = finishRegistration(testData = RegistrationTestData.FidoU2f.BasicAttestation ) - val step: FinishRegistrationSteps#Step14 = new steps.Step14( + val step: FinishRegistrationSteps#Step19 = new steps.Step19( Crypto.sha256( new ByteArray( testData.clientDataJsonBytes.getBytes.updated( @@ -1238,8 +1195,7 @@ class RelyingPartyRegistrationSpec ) ), new AttestationObject(testData.attestationObject), - Some(new FidoU2fAttestationStatementVerifier).asJava, - Nil.asJava, + Optional.of(new FidoU2fAttestationStatementVerifier), ) step.validations shouldBe a[Failure[_]] @@ -1254,11 +1210,10 @@ class RelyingPartyRegistrationSpec } val steps = finishRegistration(testData = testData) - val step: FinishRegistrationSteps#Step14 = new steps.Step14( + val step: FinishRegistrationSteps#Step19 = new steps.Step19( Crypto.sha256(testData.clientDataJsonBytes), new AttestationObject(testData.attestationObject), - Some(new FidoU2fAttestationStatementVerifier).asJava, - Nil.asJava, + Optional.of(new FidoU2fAttestationStatementVerifier), ) step.validations shouldBe a[Failure[_]] @@ -1300,11 +1255,10 @@ class RelyingPartyRegistrationSpec ) } val steps = finishRegistration(testData = testData) - val step: FinishRegistrationSteps#Step14 = new steps.Step14( + val step: FinishRegistrationSteps#Step19 = new steps.Step19( Crypto.sha256(testData.clientDataJsonBytes), new AttestationObject(testData.attestationObject), - Some(new FidoU2fAttestationStatementVerifier).asJava, - Nil.asJava, + Optional.of(new FidoU2fAttestationStatementVerifier), ) step.validations shouldBe a[Failure[_]] @@ -1319,7 +1273,7 @@ class RelyingPartyRegistrationSpec attestationAlg: COSEAlgorithmIdentifier, keypair: KeyPair, ): Unit = { - val (credential, _) = testAuthenticator + val (credential, _, _) = testAuthenticator .createBasicAttestedCredential(attestationMaker = AttestationMaker.fidoU2f( new AttestationCert( @@ -1343,8 +1297,8 @@ class RelyingPartyRegistrationSpec ), ) ) - val step: FinishRegistrationSteps#Step14 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next + val step: FinishRegistrationSteps#Step19 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next val standaloneVerification = Try { new FidoU2fAttestationStatementVerifier() @@ -1370,7 +1324,7 @@ class RelyingPartyRegistrationSpec attestationAlg: COSEAlgorithmIdentifier, keypair: KeyPair, ): Unit = { - val (credential, _) = testAuthenticator + val (credential, _, _) = testAuthenticator .createBasicAttestedCredential(attestationMaker = AttestationMaker.fidoU2f( new AttestationCert( @@ -1394,8 +1348,8 @@ class RelyingPartyRegistrationSpec ), ) ) - val step: FinishRegistrationSteps#Step14 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next + val step: FinishRegistrationSteps#Step19 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next val standaloneVerification = Try { new FidoU2fAttestationStatementVerifier() @@ -1452,11 +1406,10 @@ class RelyingPartyRegistrationSpec } val steps = finishRegistration(testData = testData) - val step: FinishRegistrationSteps#Step14 = new steps.Step14( + val step: FinishRegistrationSteps#Step19 = new steps.Step19( Crypto.sha256(testData.clientDataJsonBytes), new AttestationObject(testData.attestationObject), - Some(new NoneAttestationStatementVerifier).asJava, - Nil.asJava, + Optional.of(new NoneAttestationStatementVerifier), ) step.validations shouldBe a[Success[_]] @@ -1469,8 +1422,8 @@ class RelyingPartyRegistrationSpec val steps = finishRegistration(testData = RegistrationTestData.NoneAttestation.Default ) - val step: FinishRegistrationSteps#Step14 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next + val step: FinishRegistrationSteps#Step19 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next step.validations shouldBe a[Success[_]] step.attestationType should equal(AttestationType.NONE) @@ -1489,8 +1442,8 @@ class RelyingPartyRegistrationSpec val steps = finishRegistration(testData = RegistrationTestData.Packed.BasicAttestation ) - val step: FinishRegistrationSteps#Step14 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next + val step: FinishRegistrationSteps#Step19 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next step.getAttestationStatementVerifier.get shouldBe a[ PackedAttestationStatementVerifier @@ -1546,8 +1499,8 @@ class RelyingPartyRegistrationSpec val steps = finishRegistration(testData = RegistrationTestData.Packed.BasicAttestation ) - val step: FinishRegistrationSteps#Step14 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next + val step: FinishRegistrationSteps#Step19 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next step.validations shouldBe a[Success[_]] step.tryNext shouldBe a[Success[_]] @@ -1613,7 +1566,7 @@ class RelyingPartyRegistrationSpec "O=Yubico, C=AA, OU=Authenticator Attestation" ), ) - val (credential, _) = + val (credential, _, _) = authenticator.createBasicAttestedCredential( attestationMaker = AttestationMaker.packed( new AttestationCert(alg, (badCert, key)) @@ -1691,18 +1644,15 @@ class RelyingPartyRegistrationSpec it("5. If successful, return implementation-specific values representing attestation type Basic, AttCA or uncertainty, and attestation trust path x5c.") { val testData = RegistrationTestData.Packed.BasicAttestation val steps = finishRegistration(testData = testData) - val step: FinishRegistrationSteps#Step14 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next + val step: FinishRegistrationSteps#Step19 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next step.validations shouldBe a[Success[_]] step.tryNext shouldBe a[Success[_]] step.attestationType should be(AttestationType.BASIC) - step.attestationTrustPath.asScala should not be empty + step.attestationTrustPath.toScala should not be empty step.attestationTrustPath.get.asScala should be( - List( - testData.packedAttestationCert, - testData.attestationCaCert.get, - ) + List(testData.packedAttestationCert) ) } } @@ -1714,8 +1664,8 @@ class RelyingPartyRegistrationSpec it("The attestation type is identified as SelfAttestation.") { val steps = finishRegistration(testData = testDataBase) - val step: FinishRegistrationSteps#Step14 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next + val step: FinishRegistrationSteps#Step19 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next step.validations shouldBe a[Success[_]] step.tryNext shouldBe a[Success[_]] @@ -1902,15 +1852,15 @@ class RelyingPartyRegistrationSpec it("3. If successful, return implementation-specific values representing attestation type Self and an empty attestation trust path.") { val testData = RegistrationTestData.Packed.SelfAttestation val steps = finishRegistration(testData = testData) - val step: FinishRegistrationSteps#Step14 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next + val step: FinishRegistrationSteps#Step19 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next step.validations shouldBe a[Success[_]] step.tryNext shouldBe a[Success[_]] step.attestationType should be( AttestationType.SELF_ATTESTATION ) - step.attestationTrustPath.asScala shouldBe empty + step.attestationTrustPath.toScala shouldBe empty } } } @@ -2087,7 +2037,7 @@ class RelyingPartyRegistrationSpec it("The Basic Constraints extension MUST have the CA component set to false.") { val result = Try( verifier.verifyX5cRequirements( - testDataBase.attestationCaCert.get, + testDataBase.attestationCertChain.last._1, testDataBase.aaguid, ) ) @@ -2111,8 +2061,8 @@ class RelyingPartyRegistrationSpec ignore("The tpm statement format is supported.") { val steps = finishRegistration(testData = RegistrationTestData.Tpm.PrivacyCa) - val step: FinishRegistrationSteps#Step14 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next + val step: FinishRegistrationSteps#Step19 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next step.validations shouldBe a[Success[_]] step.tryNext shouldBe a[Success[_]] @@ -2122,8 +2072,8 @@ class RelyingPartyRegistrationSpec val steps = finishRegistration(testData = RegistrationTestData.AndroidKey.BasicAttestation ) - val step: FinishRegistrationSteps#Step14 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next + val step: FinishRegistrationSteps#Step19 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next step.validations shouldBe a[Success[_]] step.tryNext shouldBe a[Success[_]] @@ -2140,8 +2090,8 @@ class RelyingPartyRegistrationSpec allowUntrustedAttestation = false, rp = defaultTestData.rpId, ) - val step: FinishRegistrationSteps#Step14 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next + val step: FinishRegistrationSteps#Step19 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next step.getAttestationStatementVerifier.get shouldBe an[ AndroidSafetynetAttestationStatementVerifier @@ -2277,8 +2227,8 @@ class RelyingPartyRegistrationSpec testData = testDataContainer.RealExample, rp = testDataContainer.RealExample.rpId, ) - val step: FinishRegistrationSteps#Step14 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next + val step: FinishRegistrationSteps#Step19 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next step.validations shouldBe a[Success[_]] step.tryNext shouldBe a[Success[_]] @@ -2291,8 +2241,8 @@ class RelyingPartyRegistrationSpec val steps = finishRegistration(testData = testDataContainer.BasicAttestation ) - val step: FinishRegistrationSteps#Step14 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next + val step: FinishRegistrationSteps#Step19 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next step.validations shouldBe a[Success[_]] step.tryNext shouldBe a[Success[_]] @@ -2313,8 +2263,8 @@ class RelyingPartyRegistrationSpec .name("") .build(), ) - val step: FinishRegistrationSteps#Step14 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next + val step: FinishRegistrationSteps#Step19 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next step.validations shouldBe a[Success[_]] step.tryNext shouldBe a[Success[_]] @@ -2326,8 +2276,8 @@ class RelyingPartyRegistrationSpec RealExamples.AppleAttestationIos.asRegistrationTestData, rp = RealExamples.AppleAttestationIos.rp, ) - val step: FinishRegistrationSteps#Step14 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next + val step: FinishRegistrationSteps#Step19 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next step.validations shouldBe a[Success[_]] step.tryNext shouldBe a[Success[_]] @@ -2338,92 +2288,98 @@ class RelyingPartyRegistrationSpec RegistrationTestData.FidoU2f.BasicAttestation .setAttestationStatementFormat("urgel") ) - val step: FinishRegistrationSteps#Step14 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next + val step: FinishRegistrationSteps#Step19 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next step.validations shouldBe a[Success[_]] step.tryNext shouldBe a[Success[_]] step.attestationType should be(AttestationType.UNKNOWN) - step.attestationTrustPath.asScala shouldBe empty + step.attestationTrustPath.toScala shouldBe empty } - } - describe("20. If validation is successful, obtain a list of acceptable trust anchors (i.e. attestation root certificates) for that attestation type and attestation statement format fmt, from a trusted source or from policy. For example, the FIDO Metadata Service [FIDOMetadataService] provides one way to obtain such information, using the aaguid in the attestedCredentialData in authData.") { + it("(Deleted) If verification of the attestation statement failed, the Relying Party MUST fail the registration ceremony.") { + val steps = finishRegistration(testData = + RegistrationTestData.FidoU2f.BasicAttestation + .editClientData("foo", "bar") + ) + val step14: FinishRegistrationSteps#Step19 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next - describe("For the android-safetynet statement format") { - it("a trust resolver is returned.") { - val metadataService: MetadataService = new TestMetadataService() - val steps = finishRegistration( - testData = RegistrationTestData.AndroidSafetynet.RealExample, - metadataService = Some(metadataService), - rp = RegistrationTestData.AndroidSafetynet.RealExample.rpId, - ) - val step: FinishRegistrationSteps#Step15 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next + step14.validations shouldBe a[Failure[_]] + Try(step14.next) shouldBe a[Failure[_]] - step.validations shouldBe a[Success[_]] - step.trustResolver.get should not be null - step.tryNext shouldBe a[Success[_]] - } + Try(steps.run) shouldBe a[Failure[_]] + Try(steps.run).failed.get shouldBe an[IllegalArgumentException] } + } - describe("For the fido-u2f statement format") { - - it("with self attestation, no trust anchors are returned.") { - val steps = finishRegistration(testData = - RegistrationTestData.FidoU2f.SelfAttestation - ) - val step: FinishRegistrationSteps#Step15 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next - - step.validations shouldBe a[Success[_]] - step.trustResolver.asScala shouldBe empty - step.tryNext shouldBe a[Success[_]] - } - - it("with basic attestation, a trust resolver is returned.") { - val metadataService: MetadataService = new TestMetadataService() - val steps = finishRegistration( - testData = RegistrationTestData.FidoU2f.BasicAttestation, - metadataService = Some(metadataService), - ) - val step: FinishRegistrationSteps#Step15 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next + describe("20. If validation is successful, obtain a list of acceptable trust anchors (i.e. attestation root certificates) for that attestation type and attestation statement format fmt, from a trusted source or from policy. For example, the FIDO Metadata Service [FIDOMetadataService] provides one way to obtain such information, using the aaguid in the attestedCredentialData in authData.") { - step.validations shouldBe a[Success[_]] - step.trustResolver.get should not be null - step.tryNext shouldBe a[Success[_]] + val testData = RegistrationTestData.Packed.BasicAttestation + val (attestationRootCert, _) = + TestAuthenticator.generateAttestationCertificate() + + it("If an attestation trust source is set, it is used to get trust anchors.") { + val attestationTrustSource = new AttestationTrustSource { + override def findTrustRoots( + attestationCertificateChain: util.List[X509Certificate], + aaguid: Optional[ByteArray], + ): TrustRootsResult = + TrustRootsResult + .builder() + .trustRoots( + if ( + attestationCertificateChain + .get(0) + .equals( + CertificateParser.parseDer( + new AttestationObject( + testData.attestationObject + ).getAttestationStatement + .get("x5c") + .get(0) + .binaryValue() + ) + ) + ) { + Set(attestationRootCert).asJava + } else { + Set.empty[X509Certificate].asJava + } + ) + .build() } + val steps = finishRegistration( + testData = testData, + attestationTrustSource = Some(attestationTrustSource), + rp = testData.rpId, + ) + val step: FinishRegistrationSteps#Step20 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next + step.validations shouldBe a[Success[_]] + step.getTrustRoots.toScala.map( + _.getTrustRoots.asScala + ) should equal( + Some(Set(attestationRootCert)) + ) + step.tryNext shouldBe a[Success[_]] } - describe("For the none statement format") { - it("no trust anchors are returned.") { - val steps = finishRegistration(testData = - RegistrationTestData.NoneAttestation.Default - ) - val step: FinishRegistrationSteps#Step15 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next - - step.validations shouldBe a[Success[_]] - step.trustResolver.asScala shouldBe empty - step.tryNext shouldBe a[Success[_]] - } - } - - describe("For unknown attestation statement formats") { - it("no trust anchors are returned.") { - val steps = finishRegistration(testData = - RegistrationTestData.FidoU2f.BasicAttestation - .setAttestationStatementFormat("urgel") - ) - val step: FinishRegistrationSteps#Step15 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next + it( + "If an attestation trust source is not set, no trust anchors are returned." + ) { + val steps = finishRegistration( + testData = testData, + attestationTrustSource = None, + rp = testData.rpId, + ) + val step: FinishRegistrationSteps#Step20 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next - step.validations shouldBe a[Success[_]] - step.trustResolver.asScala shouldBe empty - step.tryNext shouldBe a[Success[_]] - } + step.validations shouldBe a[Success[_]] + step.getTrustRoots.toScala shouldBe empty + step.tryNext shouldBe a[Success[_]] } } @@ -2436,8 +2392,8 @@ class RelyingPartyRegistrationSpec testData = RegistrationTestData.NoneAttestation.Default, allowUntrustedAttestation = false, ) - val step: FinishRegistrationSteps#Step16 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next + val step: FinishRegistrationSteps#Step21 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next step.validations shouldBe a[Failure[_]] step.validations.failed.get shouldBe an[ @@ -2452,8 +2408,8 @@ class RelyingPartyRegistrationSpec testData = RegistrationTestData.NoneAttestation.Default, allowUntrustedAttestation = true, ) - val step: FinishRegistrationSteps#Step16 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next + val step: FinishRegistrationSteps#Step21 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next step.validations shouldBe a[Success[_]] step.attestationTrusted should be(false) @@ -2472,8 +2428,8 @@ class RelyingPartyRegistrationSpec testData = testData, allowUntrustedAttestation = false, ) - val step: FinishRegistrationSteps#Step16 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next + val step: FinishRegistrationSteps#Step21 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next step.validations shouldBe a[Failure[_]] step.validations.failed.get shouldBe an[ @@ -2488,8 +2444,8 @@ class RelyingPartyRegistrationSpec testData = testData, allowUntrustedAttestation = true, ) - val step: FinishRegistrationSteps#Step16 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next + val step: FinishRegistrationSteps#Step21 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next step.validations shouldBe a[Success[_]] step.attestationTrusted should be(false) @@ -2506,8 +2462,8 @@ class RelyingPartyRegistrationSpec testData = RegistrationTestData.FidoU2f.SelfAttestation, allowUntrustedAttestation = false, ) - val step: FinishRegistrationSteps#Step16 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next + val step: FinishRegistrationSteps#Step21 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next step.validations shouldBe a[Failure[_]] step.validations.failed.get shouldBe an[ @@ -2522,82 +2478,217 @@ class RelyingPartyRegistrationSpec testData = RegistrationTestData.FidoU2f.SelfAttestation, allowUntrustedAttestation = true, ) - val step: FinishRegistrationSteps#Step16 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next + val step: FinishRegistrationSteps#Step21 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next step.validations shouldBe a[Success[_]] step.attestationTrusted should be(false) step.tryNext shouldBe a[Success[_]] } + + it("is accepted if untrusted attestation is not allowed, but the self attestation key is a trust anchor.") { + val testData = RegistrationTestData.FidoU2f.SelfAttestation + val selfAttestationCert = CertificateParser.parseDer( + new AttestationObject( + testData.attestationObject + ).getAttestationStatement.get("x5c").get(0).binaryValue() + ) + val steps = finishRegistration( + testData = testData, + attestationTrustSource = Some( + trustSourceWith( + selfAttestationCert, + crls = Some( + Set( + TestAuthenticator.buildCrl( + JcaX500NameUtil.getX500Name( + selfAttestationCert.getSubjectX500Principal + ), + WebAuthnTestCodecs.importPrivateKey( + testData.privateKey.get, + testData.alg, + ), + "SHA256withECDSA", + currentTime = + TestAuthenticator.Defaults.certValidFrom, + nextUpdate = TestAuthenticator.Defaults.certValidTo, + ) + ) + ), + ) + ), + allowUntrustedAttestation = false, + clock = Clock.fixed( + TestAuthenticator.Defaults.certValidFrom, + ZoneOffset.UTC, + ), + ) + val step: FinishRegistrationSteps#Step21 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next + + step.validations shouldBe a[Success[_]] + step.attestationTrusted should be(true) + step.tryNext shouldBe a[Success[_]] + } } } describe("Otherwise, use the X.509 certificates returned as the attestation trust path from the verification procedure to verify that the attestation public key either correctly chains up to an acceptable root certificate, or is itself an acceptable certificate (i.e., it and the root certificate obtained in Step 20 may be the same).") { - def generateTests(testData: RegistrationTestData): Unit = { - it("is rejected if untrusted attestation is not allowed and the metadata service does not trust it.") { - val metadataService: MetadataService = new TestMetadataService() + def generateTests( + testData: RegistrationTestData, + clock: Clock, + trustedRootCert: Option[X509Certificate] = None, + enableRevocationChecking: Boolean = true, + ): Unit = { + it("is rejected if untrusted attestation is not allowed and the trust source does not trust it.") { val steps = finishRegistration( allowUntrustedAttestation = false, testData = testData, - metadataService = Some(metadataService), + attestationTrustSource = Some(emptyTrustSource), rp = testData.rpId, + clock = clock, ) - val step: FinishRegistrationSteps#Step16 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next + val step: FinishRegistrationSteps#Step21 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next step.validations shouldBe a[Failure[_]] step.attestationTrusted should be(false) - step.attestationMetadata.asScala should not be empty - step.attestationMetadata.get.getMetadataIdentifier.asScala shouldBe empty step.tryNext shouldBe a[Failure[_]] } - it("is accepted if untrusted attestation is allowed and the metadata service does not trust it.") { - val metadataService: MetadataService = new TestMetadataService() + it("is accepted if untrusted attestation is allowed and the trust source does not trust it.") { val steps = finishRegistration( allowUntrustedAttestation = true, testData = testData, - metadataService = Some(metadataService), + attestationTrustSource = Some(emptyTrustSource), rp = testData.rpId, + clock = clock, ) - val step: FinishRegistrationSteps#Step16 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next + val step: FinishRegistrationSteps#Step21 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next step.validations shouldBe a[Success[_]] step.attestationTrusted should be(false) - step.attestationMetadata.asScala should not be empty - step.attestationMetadata.get.getMetadataIdentifier.asScala shouldBe empty step.tryNext shouldBe a[Success[_]] } - it("is accepted if the metadata service trusts it.") { - val metadataService: MetadataService = new TestMetadataService( - Some( - Attestation - .builder() - .trusted(true) - .metadataIdentifier(Some("Test attestation CA").asJava) - .build() - ) - ) - + it("is accepted if the trust source trusts it.") { + val attestationTrustSource: Option[AttestationTrustSource] = + trustedRootCert + .orElse(testData.attestationCertChain.lastOption.map(_._1)) + .map( + trustSourceWith( + _, + crls = testData.attestationCertChain.lastOption + .map({ + case (cert, key) => + Set( + TestAuthenticator.buildCrl( + JcaX500NameUtil.getSubject(cert), + key, + "SHA256withECDSA", + clock.instant(), + clock.instant().plusSeconds(3600 * 24), + ) + ) + }), + enableRevocationChecking = enableRevocationChecking, + ) + ) val steps = finishRegistration( testData = testData, - metadataService = Some(metadataService), + attestationTrustSource = attestationTrustSource, rp = testData.rpId, + clock = clock, ) - val step: FinishRegistrationSteps#Step16 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next + val step: FinishRegistrationSteps#Step21 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next step.validations shouldBe a[Success[_]] step.attestationTrusted should be(true) - step.attestationMetadata.asScala should not be empty - step.attestationMetadata.get.getMetadataIdentifier.asScala should equal( - Some("Test attestation CA") - ) step.tryNext shouldBe a[Success[_]] } + + it("is rejected if the attestation root cert appears in getCertStore but not in findTrustRoots.") { + val rootCert = trustedRootCert.getOrElse( + testData.attestationCertChain.last._1 + ) + val crl: Option[CRL] = + testData.attestationCertChain.lastOption + .map({ + case (cert, key) => + TestAuthenticator.buildCrl( + JcaX500NameUtil.getSubject(cert), + key, + "SHA256withECDSA", + clock.instant(), + clock.instant().plusSeconds(3600 * 24), + ) + }) + val certStore = CertStore.getInstance( + "Collection", + new CollectionCertStoreParameters( + (List(rootCert) ++ crl).asJava + ), + ) + + { + // First, check that the attestation is not trusted if the root cert appears only in getCertStore. + val attestationTrustSource = new AttestationTrustSource { + override def findTrustRoots( + attestationCertificateChain: util.List[X509Certificate], + aaguid: Optional[ByteArray], + ): TrustRootsResult = + TrustRootsResult + .builder() + .trustRoots(Collections.emptySet()) + .certStore(certStore) + .enableRevocationChecking(enableRevocationChecking) + .build() + } + val steps = finishRegistration( + testData = testData, + attestationTrustSource = Some(attestationTrustSource), + rp = testData.rpId, + clock = clock, + ) + val step: FinishRegistrationSteps#Step21 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next + + step.validations shouldBe a[Failure[_]] + step.attestationTrusted should be(false) + step.tryNext shouldBe a[Failure[_]] + } + + { + // Since the above assertions would also pass if the cert chain happens to be broken, or CRL resolution fails, etc, make sure that the attestation is indeed trusted if the root cert appears in findTrustRoots. + val attestationTrustSource = new AttestationTrustSource { + override def findTrustRoots( + attestationCertificateChain: util.List[X509Certificate], + aaguid: Optional[ByteArray], + ): TrustRootsResult = + TrustRootsResult + .builder() + .trustRoots(Collections.singleton(rootCert)) + .certStore(certStore) + .enableRevocationChecking(enableRevocationChecking) + .build() + } + val steps = finishRegistration( + testData = testData, + attestationTrustSource = Some(attestationTrustSource), + rp = testData.rpId, + clock = clock, + ) + val step: FinishRegistrationSteps#Step21 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next + + step.validations shouldBe a[Success[_]] + step.attestationTrusted should be(true) + step.tryNext shouldBe a[Success[_]] + } + } } describe("An android-key basic attestation") { @@ -2607,20 +2698,43 @@ class RelyingPartyRegistrationSpec } describe("An android-safetynet basic attestation") { - generateTests(testData = - RegistrationTestData.AndroidSafetynet.RealExample + generateTests( + testData = RegistrationTestData.AndroidSafetynet.RealExample, + Clock + .fixed(Instant.parse("2019-01-01T00:00:00Z"), ZoneOffset.UTC), + trustedRootCert = Some( + CertificateParser.parsePem( + new String( + BinaryUtil.readAll( + getClass() + .getResourceAsStream("/globalsign-root-r2.pem") + ), + StandardCharsets.UTF_8, + ) + ) + ), + enableRevocationChecking = + false, // CRLs for this example are no longer available ) } describe("A fido-u2f basic attestation") { - generateTests(testData = - RegistrationTestData.FidoU2f.BasicAttestation + generateTests( + testData = RegistrationTestData.FidoU2f.BasicAttestation, + Clock.fixed( + TestAuthenticator.Defaults.certValidFrom, + ZoneOffset.UTC, + ), ) } describe("A packed basic attestation") { - generateTests(testData = - RegistrationTestData.Packed.BasicAttestation + generateTests( + testData = RegistrationTestData.Packed.BasicAttestation, + Clock.fixed( + TestAuthenticator.Defaults.certValidFrom, + ZoneOffset.UTC, + ), ) } } @@ -2651,8 +2765,8 @@ class RelyingPartyRegistrationSpec testData = testData, credentialRepository = credentialRepository, ) - val step: FinishRegistrationSteps#Step17 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next + val step: FinishRegistrationSteps#Step22 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next step.validations shouldBe a[Failure[_]] step.validations.failed.get shouldBe an[IllegalArgumentException] @@ -2665,8 +2779,8 @@ class RelyingPartyRegistrationSpec testData = testData, credentialRepository = Helpers.CredentialRepository.empty, ) - val step: FinishRegistrationSteps#Step17 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next + val step: FinishRegistrationSteps#Step22 = + steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next step.validations shouldBe a[Success[_]] step.tryNext shouldBe a[Success[_]] @@ -2677,12 +2791,30 @@ class RelyingPartyRegistrationSpec val testData = RegistrationTestData.FidoU2f.BasicAttestation val steps = finishRegistration( testData = testData, - metadataService = Some( - new TestMetadataService( - Some(Attestation.builder().trusted(true).build()) + attestationTrustSource = Some( + trustSourceWith( + testData.attestationCertChain.last._1, + crls = Some( + testData.attestationCertChain.tail + .map({ + case (cert, key) => + TestAuthenticator.buildCrl( + JcaX500NameUtil.getSubject(cert), + key, + "SHA256withECDSA", + TestAuthenticator.Defaults.certValidFrom, + TestAuthenticator.Defaults.certValidTo, + ) + }) + .toSet + ), ) ), credentialRepository = Helpers.CredentialRepository.empty, + clock = Clock.fixed( + TestAuthenticator.Defaults.certValidFrom, + ZoneOffset.UTC, + ), ) val result = steps.run() result.isAttestationTrusted should be(true) @@ -2702,7 +2834,7 @@ class RelyingPartyRegistrationSpec describe("It is RECOMMENDED to also:") { it("Associate the credentialId with the transport hints returned by calling credential.response.getTransports(). This value SHOULD NOT be modified before or after storing it. It is RECOMMENDED to use this value to populate the transports of the allowCredentials option in future get() calls to help the client know how to find a suitable authenticator.") { - result.getKeyId.getTransports.asScala should equal( + result.getKeyId.getTransports.toScala should equal( Some( testData.response.getResponse.getTransports ) @@ -2713,11 +2845,12 @@ class RelyingPartyRegistrationSpec describe("24. If the attestation statement attStmt successfully verified but is not trustworthy per step 21 above, the Relying Party SHOULD fail the registration ceremony.") { it("The test case with self attestation succeeds, but reports attestation is not trusted.") { - val testData = RegistrationTestData.FidoU2f.SelfAttestation + val testData = RegistrationTestData.Packed.SelfAttestation val steps = finishRegistration( testData = testData, allowUntrustedAttestation = true, credentialRepository = Helpers.CredentialRepository.empty, + attestationTrustSource = Some(emptyTrustSource), ) steps.run.getKeyId.getId should be(testData.response.getId) steps.run.isAttestationTrusted should be(false) @@ -2737,7 +2870,6 @@ class RelyingPartyRegistrationSpec result shouldBe a[Success[_]] result.get.isAttestationTrusted should be(false) result.get.getAttestationType should be(AttestationType.UNKNOWN) - result.get.getAttestationMetadata.asScala shouldBe empty } it("fails if the RP required trusted attestation.") { @@ -2755,11 +2887,11 @@ class RelyingPartyRegistrationSpec def testUntrusted(testData: RegistrationTestData): Unit = { val fmt = new AttestationObject(testData.attestationObject).getFormat - it(s"""A test case with good "${fmt}" attestation but no metadata service succeeds, but reports attestation as not trusted.""") { + it(s"""A test case with good "${fmt}" attestation but no attestation trust source succeeds, but reports attestation as not trusted.""") { val testData = RegistrationTestData.FidoU2f.BasicAttestation val steps = finishRegistration( testData = testData, - metadataService = None, + attestationTrustSource = None, allowUntrustedAttestation = true, credentialRepository = Helpers.CredentialRepository.empty, ) @@ -2774,25 +2906,6 @@ class RelyingPartyRegistrationSpec testUntrusted(RegistrationTestData.NoneAttestation.Default) testUntrusted(RegistrationTestData.Tpm.PrivacyCa) } - - it("(Deleted) If verification of the attestation statement failed, the Relying Party MUST fail the registration ceremony.") { - val steps = finishRegistration(testData = - RegistrationTestData.FidoU2f.BasicAttestation - .editClientData("foo", "bar") - ) - val step14: FinishRegistrationSteps#Step14 = - steps.begin.next.next.next.next.next.next.next.next.next.next.next.next.next - val step15: Try[FinishRegistrationSteps#Step15] = Try(step14.next) - - step14.validations shouldBe a[Failure[_]] - Try(step14.next) shouldBe a[Failure[_]] - - step15 shouldBe a[Failure[_]] - step15.failed.get shouldBe an[IllegalArgumentException] - - Try(steps.run) shouldBe a[Failure[_]] - Try(steps.run).failed.get shouldBe an[IllegalArgumentException] - } } } @@ -3006,17 +3119,6 @@ class RelyingPartyRegistrationSpec } describe("generate pubKeyCredParams which") { - val rp = RelyingParty - .builder() - .identity( - RelyingPartyIdentity - .builder() - .id("localhost") - .name("Test RP") - .build() - ) - .credentialRepository(Helpers.CredentialRepository.empty) - .build() val pkcco = rp.startRegistration( StartRegistrationOptions .builder() @@ -3043,13 +3145,50 @@ class RelyingPartyRegistrationSpec ) } - it("EdDSA.") { - pubKeyCredParams should contain( - PublicKeyCredentialParameters.EdDSA - ) - pubKeyCredParams map (_.getAlg) should contain( - COSEAlgorithmIdentifier.EdDSA + 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 + .builder() + .identity( + RelyingPartyIdentity + .builder() + .id("localhost") + .name("Test party") + .build() + ) + .credentialRepository(Helpers.CredentialRepository.empty) + .build() + + val pkcco = rp.startRegistration( + StartRegistrationOptions + .builder() + .user( + UserIdentity + .builder() + .name("foo") + .displayName("Foo") + .id(ByteArray.fromHex("aabbccdd")) + .build() + ) + .build() ) + val pubKeyCredParams = pkcco.getPubKeyCredParams.asScala + + if (Try(KeyFactory.getInstance("EdDSA")).isSuccess) { + pubKeyCredParams should contain( + PublicKeyCredentialParameters.EdDSA + ) + pubKeyCredParams map (_.getAlg) should contain( + COSEAlgorithmIdentifier.EdDSA + ) + } else { + pubKeyCredParams should not contain ( + PublicKeyCredentialParameters.EdDSA + ) + pubKeyCredParams map (_.getAlg) should not contain ( + COSEAlgorithmIdentifier.EdDSA + ) + } } it("RS256.") { @@ -3071,17 +3210,6 @@ class RelyingPartyRegistrationSpec } describe("expose the credProps extension output as RegistrationResult.isDiscoverable()") { - val rp = RelyingParty - .builder() - .identity( - RelyingPartyIdentity - .builder() - .id("localhost") - .name("Test RP") - .build() - ) - .credentialRepository(Helpers.CredentialRepository.empty) - .build() val testDataBase = RegistrationTestData.Packed.BasicAttestation val testData = testDataBase.copy(requestedExtensions = testDataBase.request.getExtensions.toBuilder.credProps().build() @@ -3107,7 +3235,7 @@ class RelyingPartyRegistrationSpec .build() ) - result.isDiscoverable.asScala should equal(Some(true)) + result.isDiscoverable.toScala should equal(Some(true)) } it("when set to false.") { @@ -3130,7 +3258,7 @@ class RelyingPartyRegistrationSpec .build() ) - result.isDiscoverable.asScala should equal(Some(false)) + result.isDiscoverable.toScala should equal(Some(false)) } it("when not available.") { @@ -3142,23 +3270,11 @@ class RelyingPartyRegistrationSpec .build() ) - result.isDiscoverable.asScala should equal(None) + result.isDiscoverable.toScala should equal(None) } } describe("support the largeBlob extension") { - val rp = RelyingParty - .builder() - .identity( - RelyingPartyIdentity - .builder() - .id("localhost") - .name("Test RP") - .build() - ) - .credentialRepository(Helpers.CredentialRepository.empty) - .build() - it("being enabled at registration time.") { val testData = RegistrationTestData.Packed.BasicAttestation val result = rp.finishRegistration( @@ -3196,18 +3312,6 @@ class RelyingPartyRegistrationSpec } describe("support the uvm extension") { - val rp = RelyingParty - .builder() - .identity( - RelyingPartyIdentity - .builder() - .id("localhost") - .name("Test RP") - .build() - ) - .credentialRepository(Helpers.CredentialRepository.empty) - .build() - it("at registration time.") { // Example from spec: https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#sctn-uvm-extension @@ -3226,7 +3330,7 @@ class RelyingPartyRegistrationSpec val uvmCborExample = ByteArray.fromHex("A16375766d828302040283040101") val challenge = TestAuthenticator.Defaults.challenge - val (cred, _) = TestAuthenticator.createUnattestedCredential( + val (cred, _, _) = TestAuthenticator.createUnattestedCredential( authenticatorExtensions = Some(JacksonCodecs.cbor().readTree(uvmCborExample.getBytes)), challenge = challenge, @@ -3269,7 +3373,7 @@ class RelyingPartyRegistrationSpec .build() ) - result.getAuthenticatorExtensionOutputs.get.getUvm.asScala should equal( + result.getAuthenticatorExtensionOutputs.get.getUvm.toScala should equal( Some( List( new UvmEntry( @@ -3331,7 +3435,6 @@ class RelyingPartyRegistrationSpec .build() ) .credentialRepository(Helpers.CredentialRepository.empty) - .allowUnrequestedExtensions(true) .build() val user = UserIdentity.builder .name("foo") @@ -3365,7 +3468,7 @@ class RelyingPartyRegistrationSpec .build() ) - result.getKeyId.getTransports.asScala.map(_.asScala) should equal( + result.getKeyId.getTransports.toScala.map(_.asScala) should equal( Some(Set(AuthenticatorTransport.USB, AuthenticatorTransport.NFC)) ) } @@ -3389,7 +3492,7 @@ class RelyingPartyRegistrationSpec .build() ) - result.getKeyId.getTransports.asScala.map(_.asScala) should equal( + result.getKeyId.getTransports.toScala.map(_.asScala) should equal( Some(Set.empty) ) } @@ -3412,11 +3515,60 @@ class RelyingPartyRegistrationSpec .build() ) - result.getKeyId.getTransports.asScala.map(_.asScala) should equal( + result.getKeyId.getTransports.toScala.map(_.asScala) should equal( Some(Set.empty) ) } } + + it("exposes getAttestationTrustPath() with the attestation trust path, if any.") { + val testData = RegistrationTestData.FidoU2f.BasicAttestation + val steps = finishRegistration( + testData = testData, + attestationTrustSource = Some( + trustSourceWith( + testData.attestationCertChain.last._1, + crls = Some( + testData.attestationCertChain + .map({ + case (cert, key) => + TestAuthenticator.buildCrl( + JcaX500NameUtil.getSubject(cert), + key, + "SHA256withECDSA", + TestAuthenticator.Defaults.certValidFrom, + TestAuthenticator.Defaults.certValidTo, + ) + }) + .toSet + ), + ) + ), + credentialRepository = Helpers.CredentialRepository.empty, + clock = Clock.fixed( + TestAuthenticator.Defaults.certValidFrom, + ZoneOffset.UTC, + ), + ) + val result = steps.run() + result.isAttestationTrusted should be(true) + result.getAttestationTrustPath.toScala.map(_.asScala) should equal( + Some(testData.attestationCertChain.init.map(_._1)) + ) + } + + it("exposes getAaguid() with the authenticator AAGUID.") { + val testData = RegistrationTestData.Packed.BasicAttestation + val steps = finishRegistration( + testData = testData, + credentialRepository = Helpers.CredentialRepository.empty, + allowUntrustedAttestation = true, + ) + val result = steps.run() + result.getAaguid should equal( + testData.response.getResponse.getAttestation.getAuthenticatorData.getAttestedCredentialData.get.getAaguid + ) + } } } 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 84602be6d..055348a73 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,7 +24,6 @@ package com.yubico.webauthn -import com.yubico.internal.util.scala.JavaConverters._ import com.yubico.webauthn.Generators._ import com.yubico.webauthn.data.AssertionExtensionInputs import com.yubico.webauthn.data.AttestationConveyancePreference @@ -52,6 +51,8 @@ import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks import java.util.Optional import scala.jdk.CollectionConverters._ +import scala.jdk.OptionConverters.RichOption +import scala.jdk.OptionConverters.RichOptional @RunWith(classOf[JUnitRunner]) class RelyingPartyStartOperationSpec @@ -73,8 +74,8 @@ class RelyingPartyStartOperationSpec override def getUsernameForUserHandle( userHandle: ByteArray ): Optional[String] = - if (userHandle == userId.getId) Some(userId.getName).asJava - else None.asJava + if (userHandle == userId.getId) Some(userId.getName).toJava + else None.toJava override def lookup( credentialId: ByteArray, userHandle: ByteArray, @@ -129,7 +130,7 @@ class RelyingPartyStartOperationSpec .build() ) - result.getExcludeCredentials.asScala.map(_.asScala) should equal( + result.getExcludeCredentials.toScala.map(_.asScala) should equal( Some(credentials) ) } @@ -154,7 +155,7 @@ class RelyingPartyStartOperationSpec val authnrSel = AuthenticatorSelectionCriteria .builder() .authenticatorAttachment(AuthenticatorAttachment.CROSS_PLATFORM) - .requireResidentKey(true) + .residentKey(ResidentKeyRequirement.REQUIRED) .build() val pkcco = relyingParty(userId = userId).startRegistration( @@ -164,14 +165,14 @@ class RelyingPartyStartOperationSpec .authenticatorSelection(authnrSel) .build() ) - pkcco.getAuthenticatorSelection.asScala should equal(Some(authnrSel)) + pkcco.getAuthenticatorSelection.toScala should equal(Some(authnrSel)) } it("allows setting authenticatorSelection with an Optional value.") { val authnrSel = AuthenticatorSelectionCriteria .builder() .authenticatorAttachment(AuthenticatorAttachment.CROSS_PLATFORM) - .requireResidentKey(true) + .residentKey(ResidentKeyRequirement.REQUIRED) .build() val pkccoWith = relyingParty(userId = userId).startRegistration( @@ -190,8 +191,8 @@ class RelyingPartyStartOperationSpec ) .build() ) - pkccoWith.getAuthenticatorSelection.asScala should equal(Some(authnrSel)) - pkccoWithout.getAuthenticatorSelection.asScala should equal(None) + pkccoWith.getAuthenticatorSelection.toScala should equal(Some(authnrSel)) + pkccoWithout.getAuthenticatorSelection.toScala should equal(None) } it("uses the RelyingParty setting for attestationConveyancePreference.") { @@ -218,7 +219,7 @@ class RelyingPartyStartOperationSpec .timeout(Optional.empty[java.lang.Long]) .build() ) - pkcco.getTimeout.asScala shouldBe empty + pkcco.getTimeout.toScala shouldBe empty } it("allows setting the timeout to a positive value.") { @@ -233,7 +234,7 @@ class RelyingPartyStartOperationSpec .build() ) - pkcco.getTimeout.asScala should equal(Some(timeout)) + pkcco.getTimeout.toScala should equal(Some(timeout)) } } @@ -281,7 +282,7 @@ class RelyingPartyStartOperationSpec .build() ) - result.getExtensions.getAppidExclude.asScala should equal(Some(appId)) + result.getExtensions.getAppidExclude.toScala should equal(Some(appId)) } } @@ -294,7 +295,7 @@ class RelyingPartyStartOperationSpec .build() ) - result.getExtensions.getAppidExclude.asScala should equal(None) + result.getExtensions.getAppidExclude.toScala should equal(None) } it("does not override the appidExclude extension with an empty value if already non-null in StartRegistrationOptions.") { @@ -313,7 +314,7 @@ class RelyingPartyStartOperationSpec .build() ) - result.getExtensions.getAppidExclude.asScala should equal( + result.getExtensions.getAppidExclude.toScala should equal( Some(requestAppId) ) } @@ -336,7 +337,7 @@ class RelyingPartyStartOperationSpec .build() ) - result.getExtensions.getAppidExclude.asScala should equal( + result.getExtensions.getAppidExclude.toScala should equal( Some(requestAppId) ) } @@ -401,159 +402,122 @@ class RelyingPartyStartOperationSpec } } - it("respects the requireResidentKey setting.") { + it("respects the residentKey setting.") { val rp = relyingParty(userId = userId) - val pkccoFalse = rp.startRegistration( - StartRegistrationOptions - .builder() - .user(userId) - .authenticatorSelection( - AuthenticatorSelectionCriteria - .builder() - .requireResidentKey(false) - .build() - ) - .build() - ) - val pkccoTrue = rp.startRegistration( + val pkccoDiscouraged = rp.startRegistration( StartRegistrationOptions .builder() .user(userId) .authenticatorSelection( AuthenticatorSelectionCriteria .builder() - .requireResidentKey(true) + .residentKey(ResidentKeyRequirement.DISCOURAGED) .build() ) .build() ) - pkccoFalse.getAuthenticatorSelection.get.isRequireResidentKey should be( - false - ) - pkccoFalse.getAuthenticatorSelection.get.getResidentKey should be( - ResidentKeyRequirement.DISCOURAGED - ) - pkccoTrue.getAuthenticatorSelection.get.isRequireResidentKey should be( - true - ) - pkccoTrue.getAuthenticatorSelection.get.getResidentKey should be( - ResidentKeyRequirement.REQUIRED - ) - } - - it("respects the authenticatorAttachment parameter.") { - val rp = relyingParty(userId = userId) - - val pkcco = rp.startRegistration( + val pkccoPreferred = rp.startRegistration( StartRegistrationOptions .builder() .user(userId) .authenticatorSelection( AuthenticatorSelectionCriteria .builder() - .authenticatorAttachment(AuthenticatorAttachment.CROSS_PLATFORM) + .residentKey(ResidentKeyRequirement.PREFERRED) .build() ) .build() ) - val pkccoWith = rp.startRegistration( + + val pkccoRequired = rp.startRegistration( StartRegistrationOptions .builder() .user(userId) .authenticatorSelection( AuthenticatorSelectionCriteria .builder() - .authenticatorAttachment( - Optional.of(AuthenticatorAttachment.PLATFORM) - ) + .residentKey(ResidentKeyRequirement.REQUIRED) .build() ) .build() ) - val pkccoWithout = rp.startRegistration( + + val pkccoUnspecified = rp.startRegistration( StartRegistrationOptions .builder() .user(userId) .authenticatorSelection( - AuthenticatorSelectionCriteria - .builder() - .authenticatorAttachment(Optional.empty[AuthenticatorAttachment]) - .build() + AuthenticatorSelectionCriteria.builder().build() ) .build() ) - pkcco.getAuthenticatorSelection.get.getAuthenticatorAttachment.asScala should be( - Some(AuthenticatorAttachment.CROSS_PLATFORM) + pkccoDiscouraged.getAuthenticatorSelection.get.getResidentKey.toScala should be( + Some(ResidentKeyRequirement.DISCOURAGED) ) - pkccoWith.getAuthenticatorSelection.get.getAuthenticatorAttachment.asScala should be( - Some(AuthenticatorAttachment.PLATFORM) + pkccoPreferred.getAuthenticatorSelection.get.getResidentKey.toScala should be( + Some(ResidentKeyRequirement.PREFERRED) + ) + pkccoRequired.getAuthenticatorSelection.get.getResidentKey.toScala should be( + Some(ResidentKeyRequirement.REQUIRED) ) - pkccoWithout.getAuthenticatorSelection.get.getAuthenticatorAttachment.asScala should be( + pkccoUnspecified.getAuthenticatorSelection.get.getResidentKey.toScala should be( None ) } - it("sets requireResidentKey to agree with residentKey.") { + it("respects the authenticatorAttachment parameter.") { val rp = relyingParty(userId = userId) - val pkccoDiscouraged = rp.startRegistration( + val pkcco = rp.startRegistration( StartRegistrationOptions .builder() .user(userId) .authenticatorSelection( AuthenticatorSelectionCriteria .builder() - .residentKey(ResidentKeyRequirement.DISCOURAGED) + .authenticatorAttachment(AuthenticatorAttachment.CROSS_PLATFORM) .build() ) .build() ) - val pkccoPreferred = rp.startRegistration( + val pkccoWith = rp.startRegistration( StartRegistrationOptions .builder() .user(userId) .authenticatorSelection( AuthenticatorSelectionCriteria .builder() - .residentKey(ResidentKeyRequirement.PREFERRED) + .authenticatorAttachment( + Optional.of(AuthenticatorAttachment.PLATFORM) + ) .build() ) .build() ) - val pkccoRequired = rp.startRegistration( + val pkccoWithout = rp.startRegistration( StartRegistrationOptions .builder() .user(userId) .authenticatorSelection( AuthenticatorSelectionCriteria .builder() - .residentKey(ResidentKeyRequirement.REQUIRED) + .authenticatorAttachment(Optional.empty[AuthenticatorAttachment]) .build() ) .build() ) - pkccoDiscouraged.getAuthenticatorSelection.get.isRequireResidentKey should be( - false - ) - pkccoPreferred.getAuthenticatorSelection.get.isRequireResidentKey should be( - false - ) - pkccoRequired.getAuthenticatorSelection.get.isRequireResidentKey should be( - true - ) - - pkccoDiscouraged.getAuthenticatorSelection.get.getResidentKey should equal( - ResidentKeyRequirement.DISCOURAGED + pkcco.getAuthenticatorSelection.get.getAuthenticatorAttachment.toScala should be( + Some(AuthenticatorAttachment.CROSS_PLATFORM) ) - pkccoPreferred.getAuthenticatorSelection.get.getResidentKey should equal( - ResidentKeyRequirement.PREFERRED + pkccoWith.getAuthenticatorSelection.get.getAuthenticatorAttachment.toScala should be( + Some(AuthenticatorAttachment.PLATFORM) ) - pkccoRequired.getAuthenticatorSelection.get.getResidentKey should equal( - ResidentKeyRequirement.REQUIRED + pkccoWithout.getAuthenticatorSelection.get.getAuthenticatorAttachment.toScala should be( + None ) } } @@ -565,7 +529,7 @@ class RelyingPartyStartOperationSpec val rp = relyingParty(credentials = credentials, userId = userId) val result = rp.startAssertion(StartAssertionOptions.builder().build()) - result.getPublicKeyCredentialRequestOptions.getAllowCredentials.asScala shouldBe empty + result.getPublicKeyCredentialRequestOptions.getAllowCredentials.toScala shouldBe empty } } @@ -579,7 +543,7 @@ class RelyingPartyStartOperationSpec .build() ) - result.getPublicKeyCredentialRequestOptions.getAllowCredentials.asScala + result.getPublicKeyCredentialRequestOptions.getAllowCredentials.toScala .map(_.asScala.toSet) should equal(Some(credentials)) } } @@ -594,7 +558,7 @@ class RelyingPartyStartOperationSpec .build() ) - result.getPublicKeyCredentialRequestOptions.getAllowCredentials.asScala + result.getPublicKeyCredentialRequestOptions.getAllowCredentials.toScala .map(_.asScala.toSet) should equal(Some(credentials)) } } @@ -639,13 +603,13 @@ class RelyingPartyStartOperationSpec val requestCreds = result.getPublicKeyCredentialRequestOptions.getAllowCredentials.get.asScala - requestCreds.head.getTransports.asScala should equal( + requestCreds.head.getTransports.toScala should equal( Some(cred1Transports.asJava) ) - requestCreds(1).getTransports.asScala should equal( + requestCreds(1).getTransports.toScala should equal( Some(Set.empty.asJava) ) - requestCreds(2).getTransports.asScala should equal(None) + requestCreds(2).getTransports.toScala should equal(None) } } @@ -670,7 +634,7 @@ class RelyingPartyStartOperationSpec .build() ) - result.getPublicKeyCredentialRequestOptions.getExtensions.getAppid.asScala should equal( + result.getPublicKeyCredentialRequestOptions.getExtensions.getAppid.toScala should equal( Some(appId) ) } @@ -685,7 +649,7 @@ class RelyingPartyStartOperationSpec .build() ) - result.getPublicKeyCredentialRequestOptions.getExtensions.getAppid.asScala should equal( + result.getPublicKeyCredentialRequestOptions.getExtensions.getAppid.toScala should equal( None ) } @@ -706,7 +670,7 @@ class RelyingPartyStartOperationSpec .build() ) - result.getPublicKeyCredentialRequestOptions.getExtensions.getAppid.asScala should equal( + result.getPublicKeyCredentialRequestOptions.getExtensions.getAppid.toScala should equal( Some(requestAppId) ) } @@ -729,7 +693,7 @@ class RelyingPartyStartOperationSpec .build() ) - result.getPublicKeyCredentialRequestOptions.getExtensions.getAppid.asScala should equal( + result.getPublicKeyCredentialRequestOptions.getExtensions.getAppid.toScala should equal( Some(requestAppId) ) } @@ -743,7 +707,7 @@ class RelyingPartyStartOperationSpec .timeout(Optional.empty[java.lang.Long]) .build() ) - req.getPublicKeyCredentialRequestOptions.getTimeout.asScala shouldBe empty + req.getPublicKeyCredentialRequestOptions.getTimeout.toScala shouldBe empty } it("allows setting the timeout to a positive value.") { @@ -757,7 +721,7 @@ class RelyingPartyStartOperationSpec .build() ) - req.getPublicKeyCredentialRequestOptions.getTimeout.asScala should equal( + req.getPublicKeyCredentialRequestOptions.getTimeout.toScala should equal( Some(timeout) ) } @@ -825,24 +789,24 @@ class RelyingPartyStartOperationSpec it("resets username when userHandle is set.") { forAll { (sao: StartAssertionOptions, userHandle: ByteArray) => val result = sao.toBuilder.userHandle(userHandle).build() - result.getUsername.asScala shouldBe empty + result.getUsername.toScala shouldBe empty } forAll { (sao: StartAssertionOptions, userHandle: ByteArray) => - val result = sao.toBuilder.userHandle(Some(userHandle).asJava).build() - result.getUsername.asScala shouldBe empty + val result = sao.toBuilder.userHandle(Some(userHandle).toJava).build() + result.getUsername.toScala shouldBe empty } } it("resets userHandle when username is set.") { forAll { (sao: StartAssertionOptions, username: String) => val result = sao.toBuilder.username(username).build() - result.getUserHandle.asScala shouldBe empty + result.getUserHandle.toScala shouldBe empty } forAll { (sao: StartAssertionOptions, username: String) => - val result = sao.toBuilder.username(Some(username).asJava).build() - result.getUserHandle.asScala shouldBe empty + val result = sao.toBuilder.username(Some(username).toJava).build() + result.getUserHandle.toScala shouldBe empty } } @@ -852,7 +816,7 @@ class RelyingPartyStartOperationSpec .username(username) .userHandle(Optional.empty[ByteArray]) .build() - result.getUsername.asScala should equal(Some(username)) + result.getUsername.toScala should equal(Some(username)) } forAll { (sao: StartAssertionOptions, username: String) => @@ -860,7 +824,7 @@ class RelyingPartyStartOperationSpec .username(username) .userHandle(null: ByteArray) .build() - result.getUsername.asScala should equal(Some(username)) + result.getUsername.toScala should equal(Some(username)) } } @@ -870,7 +834,7 @@ class RelyingPartyStartOperationSpec .userHandle(userHandle) .username(Optional.empty[String]) .build() - result.getUserHandle.asScala should equal(Some(userHandle)) + result.getUserHandle.toScala should equal(Some(userHandle)) } forAll { (sao: StartAssertionOptions, userHandle: ByteArray) => @@ -878,35 +842,35 @@ class RelyingPartyStartOperationSpec .userHandle(userHandle) .username(null: String) .build() - result.getUserHandle.asScala should equal(Some(userHandle)) + result.getUserHandle.toScala should equal(Some(userHandle)) } } it("allows unsetting username.") { forAll { (sao: StartAssertionOptions, username: String) => val preresult = sao.toBuilder.username(username).build() - preresult.getUsername.asScala should equal(Some(username)) + preresult.getUsername.toScala should equal(Some(username)) val result1 = preresult.toBuilder.username(Optional.empty[String]).build() - result1.getUsername.asScala shouldBe empty + result1.getUsername.toScala shouldBe empty val result2 = preresult.toBuilder.username(null: String).build() - result2.getUsername.asScala shouldBe empty + result2.getUsername.toScala shouldBe empty } } it("allows unsetting userHandle.") { forAll { (sao: StartAssertionOptions, userHandle: ByteArray) => val preresult = sao.toBuilder.userHandle(userHandle).build() - preresult.getUserHandle.asScala should equal(Some(userHandle)) + preresult.getUserHandle.toScala should equal(Some(userHandle)) val result1 = preresult.toBuilder.userHandle(Optional.empty[ByteArray]).build() - result1.getUserHandle.asScala shouldBe empty + result1.getUserHandle.toScala shouldBe empty val result2 = preresult.toBuilder.userHandle(null: ByteArray).build() - result2.getUserHandle.asScala shouldBe empty + result2.getUserHandle.toScala shouldBe empty } } } diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyUserIdentificationSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyUserIdentificationSpec.scala index be0863d8a..6eadda8c8 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyUserIdentificationSpec.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyUserIdentificationSpec.scala @@ -25,7 +25,6 @@ package com.yubico.webauthn import com.fasterxml.jackson.databind.node.ObjectNode -import com.yubico.internal.util.scala.JavaConverters._ import com.yubico.webauthn.data.AuthenticatorAssertionResponse import com.yubico.webauthn.data.ByteArray import com.yubico.webauthn.data.ClientAssertionExtensionOutputs @@ -42,6 +41,7 @@ import java.security.KeyPair import java.security.interfaces.ECPublicKey import java.util.Optional import scala.jdk.CollectionConverters._ +import scala.jdk.OptionConverters.RichOption import scala.util.Failure import scala.util.Success import scala.util.Try @@ -111,7 +111,7 @@ class RelyingPartyUserIdentificationSpec extends FunSpec with Matchers { .authenticatorData(authenticatorData) .clientDataJSON(clientDataJsonBytes) .signature(signature) - .userHandle(userHandle.asJava) + .userHandle(userHandle.toJava) .build() def defaultPublicKeyCredential( @@ -162,23 +162,23 @@ class RelyingPartyUserIdentificationSpec extends FunSpec with Matchers { ) .signatureCount(0) .build() - ).asJava + ).toJava else - None.asJava + None.toJava override def lookupAll(credId: ByteArray) = ??? override def getUserHandleForUsername(username: String) : Optional[ByteArray] = if (username == Defaults.username) - Some(Defaults.userHandle).asJava + Some(Defaults.userHandle).toJava else - None.asJava + None.toJava override def getUsernameForUserHandle(userHandle: ByteArray) : Optional[String] = if (userHandle == Defaults.userHandle) - Some(Defaults.username).asJava + Some(Defaults.username).toJava else - None.asJava + None.toJava } ) .preferredPubkeyParams(Nil.asJava) diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/TestAuthenticator.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/TestAuthenticator.scala index 0b9f28d52..88f06de19 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 @@ -30,7 +30,6 @@ import com.fasterxml.jackson.databind.node.ObjectNode import com.yubico.internal.util.BinaryUtil import com.yubico.internal.util.CertificateParser import com.yubico.internal.util.JacksonCodecs -import com.yubico.internal.util.scala.JavaConverters._ import com.yubico.webauthn.data.AuthenticatorAssertionResponse import com.yubico.webauthn.data.AuthenticatorAttestationResponse import com.yubico.webauthn.data.AuthenticatorData @@ -40,7 +39,6 @@ import com.yubico.webauthn.data.ClientAssertionExtensionOutputs import com.yubico.webauthn.data.ClientRegistrationExtensionOutputs import com.yubico.webauthn.data.PublicKeyCredential import com.yubico.webauthn.data.PublicKeyCredentialRequestOptions -import com.yubico.webauthn.test.Util import org.bouncycastle.asn1.ASN1ObjectIdentifier import org.bouncycastle.asn1.ASN1Primitive import org.bouncycastle.asn1.DEROctetString @@ -49,7 +47,9 @@ import org.bouncycastle.asn1.DERTaggedObject import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x509.BasicConstraints import org.bouncycastle.asn1.x509.Extension +import org.bouncycastle.asn1.x509.ReasonFlags import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo +import org.bouncycastle.cert.X509v2CRLBuilder import org.bouncycastle.cert.X509v3CertificateBuilder import org.bouncycastle.cert.jcajce.JcaX500NameUtil import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey @@ -58,14 +58,9 @@ import org.bouncycastle.jce.ECNamedCurveTable import org.bouncycastle.jce.provider.BouncyCastleProvider import org.bouncycastle.jce.spec.ECNamedCurveSpec import org.bouncycastle.math.ec.custom.sec.SecP256R1Curve -import org.bouncycastle.openssl.PEMKeyPair -import org.bouncycastle.openssl.PEMParser -import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder -import java.io.BufferedReader -import java.io.InputStream -import java.io.InputStreamReader +import java.io.ByteArrayInputStream import java.math.BigInteger import java.nio.charset.StandardCharsets import java.security.KeyFactory @@ -76,6 +71,7 @@ import java.security.PrivateKey import java.security.PublicKey import java.security.SecureRandom import java.security.Signature +import java.security.cert.CRL import java.security.cert.X509Certificate import java.security.interfaces.ECPublicKey import java.security.interfaces.RSAPublicKey @@ -87,10 +83,13 @@ import java.security.spec.X509EncodedKeySpec import java.time.Instant import java.util.Date import scala.jdk.CollectionConverters._ +import scala.jdk.OptionConverters.RichOption import scala.util.Try object TestAuthenticator { + private val random: SecureRandom = new SecureRandom() + object Defaults { val aaguid: ByteArray = new ByteArray( Array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15) @@ -110,12 +109,15 @@ object TestAuthenticator { } val credentialKey: KeyPair = generateEcKeypair() + + val certValidFrom: Instant = Instant.parse("2018-09-06T17:42:00Z") + val certValidTo: Instant = certValidFrom.plusSeconds(7 * 24 * 3600) } private def jsonFactory: JsonNodeFactory = JsonNodeFactory.instance private def toBytes(s: String): ByteArray = new ByteArray(s.getBytes("UTF-8")) - private def sha256(s: String): ByteArray = sha256(toBytes(s)) - private def sha256(b: ByteArray): ByteArray = + def sha256(s: String): ByteArray = sha256(toBytes(s)) + def sha256(b: ByteArray): ByteArray = new ByteArray(MessageDigest.getInstance("SHA-256").digest(b.getBytes)) sealed trait AttestationMaker { @@ -124,7 +126,7 @@ object TestAuthenticator { authDataBytes: ByteArray, clientDataJson: String, ): JsonNode - def attestationCert: Option[X509Certificate] = ??? + def certChain: List[(X509Certificate, PrivateKey)] = Nil def makeAttestationObjectBytes( authDataBytes: ByteArray, @@ -150,8 +152,7 @@ object TestAuthenticator { def packed(signer: AttestationSigner): AttestationMaker = new AttestationMaker { override val format = "packed" - override def attestationCert: Option[X509Certificate] = - Some(signer.cert) + override def certChain = signer.certChain override def makeAttestationStatement( authDataBytes: ByteArray, clientDataJson: String, @@ -161,8 +162,7 @@ object TestAuthenticator { def fidoU2f(signer: AttestationSigner): AttestationMaker = new AttestationMaker { override val format = "fido-u2f" - override def attestationCert: Option[X509Certificate] = - Some(signer.cert) + override def certChain = signer.certChain override def makeAttestationStatement( authDataBytes: ByteArray, clientDataJson: String, @@ -175,7 +175,7 @@ object TestAuthenticator { ): AttestationMaker = new AttestationMaker { override val format = "android-safetynet" - override def attestationCert: Option[X509Certificate] = Some(cert.cert) + override def certChain = cert.certChain override def makeAttestationStatement( authDataBytes: ByteArray, clientDataJson: String, @@ -225,7 +225,7 @@ object TestAuthenticator { def none(): AttestationMaker = new AttestationMaker { override val format = "none" - override def attestationCert: Option[X509Certificate] = None + override def certChain = Nil override def makeAttestationStatement( authDataBytes: ByteArray, clientDataJson: String, @@ -237,18 +237,21 @@ object TestAuthenticator { sealed trait AttestationSigner { def key: PrivateKey; def alg: COSEAlgorithmIdentifier; def cert: X509Certificate + def certChain: List[(X509Certificate, PrivateKey)] } case class SelfAttestation(keypair: KeyPair, alg: COSEAlgorithmIdentifier) extends AttestationSigner { - def key: PrivateKey = keypair.getPrivate - def cert: X509Certificate = + override def key: PrivateKey = keypair.getPrivate + override def cert: X509Certificate = { generateAttestationCertificate(alg = alg, keypair = Some(keypair))._1 + } + override def certChain = Nil } case class AttestationCert( - cert: X509Certificate, - key: PrivateKey, + override val cert: X509Certificate, + override val key: PrivateKey, alg: COSEAlgorithmIdentifier, - chain: List[X509Certificate], + override val certChain: List[(X509Certificate, PrivateKey)], ) extends AttestationSigner { def this( alg: COSEAlgorithmIdentifier, @@ -258,23 +261,44 @@ object TestAuthenticator { object AttestationSigner { def ca( alg: COSEAlgorithmIdentifier, + aaguid: ByteArray = Defaults.aaguid, certSubject: X500Name = new X500Name( - "CN=Yubico WebAuthn unit tests CA, O=Yubico, OU=Authenticator Attestation, C=SE" + "CN=Yubico WebAuthn unit tests, O=Yubico, OU=Authenticator Attestation, C=SE" ), + validFrom: Instant = Defaults.certValidFrom, + validTo: Instant = Defaults.certValidTo, ): AttestationCert = { val (caCert, caKey) = - generateAttestationCaCertificate(signingAlg = alg, name = certSubject) + generateAttestationCaCertificate( + signingAlg = alg, + validFrom = validFrom, + validTo = validTo, + ) val (cert, key) = generateAttestationCertificate( alg, caCertAndKey = Some((caCert, caKey)), name = certSubject, + extensions = List( + ( + "1.3.6.1.4.1.45724.1.1.4", + false, + new DEROctetString(aaguid.getBytes), + ) + ), + validFrom = validFrom, + validTo = validTo, + ) + AttestationCert( + cert, + key, + alg, + certChain = List((cert, key), (caCert, caKey)), ) - AttestationCert(cert, key, alg, List(caCert)) } def selfsigned(alg: COSEAlgorithmIdentifier): AttestationCert = { val (cert, key) = generateAttestationCertificate(alg = alg) - AttestationCert(cert, key, alg, Nil) + AttestationCert(cert, key, alg, certChain = List((cert, key))) } } @@ -297,6 +321,7 @@ object TestAuthenticator { ClientRegistrationExtensionOutputs, ], KeyPair, + List[(X509Certificate, PrivateKey)], ) = { val clientDataJson: String = @@ -367,6 +392,7 @@ object TestAuthenticator { .clientExtensionResults(clientExtensions) .build(), keypair, + attestationMaker.certChain, ) } @@ -380,6 +406,7 @@ object TestAuthenticator { ClientRegistrationExtensionOutputs, ], KeyPair, + List[(X509Certificate, PrivateKey)], ) = createCredential( aaguid = aaguid, @@ -396,6 +423,7 @@ object TestAuthenticator { ClientRegistrationExtensionOutputs, ], KeyPair, + List[(X509Certificate, PrivateKey)], ) = { val keypair = generateKeypair(keyAlgorithm) val signer = SelfAttestation(keypair, keyAlgorithm) @@ -415,6 +443,7 @@ object TestAuthenticator { ClientRegistrationExtensionOutputs, ], KeyPair, + List[(X509Certificate, PrivateKey)], ) = createCredential( attestationMaker = AttestationMaker.none(), @@ -516,7 +545,7 @@ object TestAuthenticator { alg, ) ) - .userHandle(userHandle.asJava) + .userHandle(userHandle.toJava) .build() PublicKeyCredential @@ -604,15 +633,11 @@ object TestAuthenticator { "sig" -> f.binaryNode(signature.getBytes), ) ++ (signer match { case _: SelfAttestation => Map.empty - case AttestationCert(cert, _, _, chain) => + case AttestationCert(cert, _, _, _) => Map( "x5c" -> f .arrayNode() - .addAll( - (cert +: chain) - .map(crt => f.binaryNode(crt.getEncoded)) - .asJava - ) + .add(cert.getEncoded) ) }) ).asJava @@ -632,16 +657,12 @@ object TestAuthenticator { val jwsHeader = f .objectNode() - .setAll( + .setAll[ObjectNode]( Map( "alg" -> f.textNode("RS256"), "x5c" -> f .arrayNode() - .addAll( - (cert.cert +: cert.chain) - .map(crt => f.textNode(new ByteArray(crt.getEncoded).getBase64)) - .asJava - ), + .add(new ByteArray(cert.cert.getEncoded).getBase64), ).asJava ) val jwsHeaderBase64 = new ByteArray( @@ -650,7 +671,7 @@ object TestAuthenticator { val jwsPayload = f .objectNode() - .setAll( + .setAll[ObjectNode]( Map( "nonce" -> f.textNode(nonce.getBase64), "timestampMs" -> f.numberNode(Instant.now().toEpochMilli), @@ -677,7 +698,7 @@ object TestAuthenticator { val attStmt = f .objectNode() - .setAll( + .setAll[ObjectNode]( Map( "ver" -> f.textNode("14799021"), "response" -> f.binaryNode( @@ -830,14 +851,16 @@ object TestAuthenticator { val g: KeyPairGenerator = KeyPairGenerator.getInstance("EC", new BouncyCastleProvider()) - g.initialize(ecSpec, new SecureRandom()) + g.initialize(ecSpec, random) g.generateKeyPair() } def generateEddsaKeypair(): KeyPair = { val alg = "Ed25519" - val keyPairGenerator = KeyPairGenerator.getInstance(alg) + // Need to use BouncyCastle provider here because JDK before 14 does not support EdDSA + val keyPairGenerator = + KeyPairGenerator.getInstance(alg, new BouncyCastleProvider()) keyPairGenerator.generateKeyPair() } @@ -855,7 +878,7 @@ object TestAuthenticator { def generateRsaKeypair(): KeyPair = { val g: KeyPairGenerator = KeyPairGenerator.getInstance("RSA") - g.initialize(2048, new SecureRandom()) + g.initialize(2048, random) g.generateKeyPair() } @@ -920,6 +943,8 @@ object TestAuthenticator { ), superCa: Option[(X509Certificate, PrivateKey)] = None, extensions: Iterable[(String, Boolean, ASN1Primitive)] = Nil, + validFrom: Instant = Defaults.certValidFrom, + validTo: Instant = Defaults.certValidTo, ): (X509Certificate, PrivateKey) = { val actualKeypair = keypair.getOrElse(generateKeypair(signingAlg)) ( @@ -932,6 +957,8 @@ object TestAuthenticator { signingAlg = signingAlg, isCa = true, extensions = extensions, + validFrom = validFrom, + validTo = validTo, ), actualKeypair.getPrivate, ) @@ -951,6 +978,8 @@ object TestAuthenticator { ) ), caCertAndKey: Option[(X509Certificate, PrivateKey)] = None, + validFrom: Instant = Defaults.certValidFrom, + validTo: Instant = Defaults.certValidTo, ): (X509Certificate, PrivateKey) = { val actualKeypair = keypair.getOrElse(generateKeypair(alg)) @@ -966,26 +995,30 @@ object TestAuthenticator { signingAlg = alg, isCa = false, extensions = extensions, + validFrom = validFrom, + validTo = validTo, ), actualKeypair.getPrivate, ) } - private def buildCertificate( + def buildCertificate( publicKey: PublicKey, issuerName: X500Name, subjectName: X500Name, signingKey: PrivateKey, signingAlg: COSEAlgorithmIdentifier, isCa: Boolean = false, - extensions: Iterable[(String, Boolean, ASN1Primitive)], + extensions: Iterable[(String, Boolean, ASN1Primitive)] = None, + validFrom: Instant = Defaults.certValidFrom, + validTo: Instant = Defaults.certValidTo, ): X509Certificate = { CertificateParser.parseDer({ val builder = new X509v3CertificateBuilder( issuerName, - new BigInteger("1337"), - Date.from(Instant.parse("2018-09-06T17:42:00Z")), - Date.from(Instant.parse("2018-09-06T17:42:00Z")), + BigInteger.valueOf(random.nextInt(10000)), + Date.from(validFrom), + Date.from(validTo), subjectName, SubjectPublicKeyInfo.getInstance(publicKey.getEncoded), ) @@ -1013,33 +1046,36 @@ object TestAuthenticator { }) } - def generateRsaCertificate(): (X509Certificate, PrivateKey) = - generateAttestationCertificate(COSEAlgorithmIdentifier.RS256) - - def importCertAndKeyFromPem( - certPem: InputStream, - keyPem: InputStream, - ): (X509Certificate, PrivateKey) = { - val cert: X509Certificate = Util.importCertFromPem(certPem) - - val priKeyParser = new PEMParser( - new BufferedReader(new InputStreamReader(keyPem)) - ) - priKeyParser.readObject() // Throw away the EC params part - - val converter = new JcaPEMKeyConverter() - - val key: PrivateKey = converter - .getKeyPair( - priKeyParser - .readObject() - .asInstanceOf[PEMKeyPair] - ) - .getPrivate + def buildCrl( + issuerName: X500Name, + signingKey: PrivateKey, + signingAlgJavaName: String, + currentTime: Instant, + nextUpdate: Instant, + revoked: Set[X509Certificate] = Set.empty, + ): CRL = { + java.security.cert.CertificateFactory + .getInstance("X.509") + .generateCRL(new ByteArrayInputStream({ + val builder = new X509v2CRLBuilder(issuerName, Date.from(currentTime)) + builder.setNextUpdate(Date.from(nextUpdate)) + + for { revoked <- revoked } { + builder.addCRLEntry( + revoked.getSerialNumber, + Date.from(currentTime), + ReasonFlags.cessationOfOperation, + ) + } - (cert, key) + val signerBuilder = new JcaContentSignerBuilder(signingAlgJavaName) + builder.build(signerBuilder.build(signingKey)).getEncoded + })) } + def generateRsaCertificate(): (X509Certificate, PrivateKey) = + generateAttestationCertificate(COSEAlgorithmIdentifier.RS256) + def coseAlgorithmOfJavaKey(key: PrivateKey): COSEAlgorithmIdentifier = Try(COSEAlgorithmIdentifier.valueOf(key.getAlgorithm)) getOrElse key match { diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/attestation/Generators.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/attestation/Generators.scala deleted file mode 100644 index e52015398..000000000 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/attestation/Generators.scala +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright (c) 2018, Yubico AB -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, this -// list of conditions and the following disclaimer. -// -// 2. Redistributions in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -package com.yubico.webauthn.attestation - -import com.yubico.scalacheck.gen.JavaGenerators._ -import org.scalacheck.Arbitrary -import org.scalacheck.Arbitrary.arbitrary - -import java.util.Optional - -object Generators { - - implicit val arbitraryAttestation: Arbitrary[Attestation] = Arbitrary( - for { - trusted <- arbitrary[Boolean] - deviceProperties <- arbitrary[Optional[java.util.Map[String, String]]] - metadataIdentifier <- arbitrary[Optional[String]] - transports <- arbitrary[Optional[java.util.Set[Transport]]] - vendorProperties <- arbitrary[Optional[java.util.Map[String, String]]] - } yield Attestation - .builder() - .trusted(trusted) - .deviceProperties(deviceProperties) - .metadataIdentifier(metadataIdentifier) - .transports(transports) - .vendorProperties(vendorProperties) - .build() - ) - -} diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/AuthenticatorAttestationResponseSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/AuthenticatorAttestationResponseSpec.scala index 344db8bac..b93b1321a 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/AuthenticatorAttestationResponseSpec.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/AuthenticatorAttestationResponseSpec.scala @@ -49,7 +49,7 @@ class AuthenticatorAttestationResponseSpec extends FunSpec with Matchers { "challenge":"${challenge.getBase64Url}", "clientExtensions":{"foo":"${fooExtension}"}, "origin":"${origin}", - "tokenBinding":{"status":"${tokenBindingStatus.toJsonString}","id":"${tokenBindingId.getBase64Url}"}, + "tokenBinding":{"status":"${tokenBindingStatus.getValue}","id":"${tokenBindingId.getBase64Url}"}, "type":"webauthn.get" }""".getBytes("UTF-8")) diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/AuthenticatorDataSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/AuthenticatorDataSpec.scala index 6fa18bf2e..13f943ed3 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/AuthenticatorDataSpec.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/AuthenticatorDataSpec.scala @@ -26,7 +26,6 @@ package com.yubico.webauthn.data import com.upokecenter.cbor.CBORObject import com.yubico.internal.util.BinaryUtil -import com.yubico.internal.util.scala.JavaConverters._ import com.yubico.webauthn.WebAuthnTestCodecs import com.yubico.webauthn.data.Generators.byteArray import org.junit.runner.RunWith @@ -38,6 +37,7 @@ import org.scalatestplus.junit.JUnitRunner import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks import java.security.interfaces.ECPublicKey +import scala.jdk.OptionConverters.RichOptional import scala.util.Failure import scala.util.Try @@ -127,7 +127,7 @@ class AuthenticatorDataSpec if (hasAttestation) { it("gets the correct attestation data from the raw bytes.") { - authData.getAttestedCredentialData.asScala shouldBe defined + authData.getAttestedCredentialData.toScala shouldBe defined authData.getAttestedCredentialData.get.getAaguid.getHex should equal( "000102030405060708090a0b0c0d0e0f" ) @@ -150,7 +150,7 @@ class AuthenticatorDataSpec if (hasExtensions) { it("gets the correct extension data from the raw bytes.") { - authData.getExtensions.asScala shouldBe defined + authData.getExtensions.toScala shouldBe defined new ByteArray( authData.getExtensions.get.EncodeToBytes() ) should equal(jsonToCbor("""{ "foo": "bar" }""")) diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/AuthenticatorTransportSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/AuthenticatorTransportSpec.scala index f763e8c66..2e56f8539 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/AuthenticatorTransportSpec.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/AuthenticatorTransportSpec.scala @@ -24,7 +24,6 @@ package com.yubico.webauthn.data -import com.yubico.webauthn.attestation.Transport import org.junit.runner.RunWith import org.scalatest.FunSpec import org.scalatest.Matchers @@ -96,23 +95,5 @@ class AuthenticatorTransportSpec ) should not equal constant } } - - it("has a fromU2fTransport(transport) function that can convert from Transport.") { - AuthenticatorTransport.fromU2fTransport( - Transport.BT_CLASSIC - ) should be theSameInstanceAs AuthenticatorTransport.BLE - AuthenticatorTransport.fromU2fTransport( - Transport.BLE - ) should be theSameInstanceAs AuthenticatorTransport.BLE - AuthenticatorTransport.fromU2fTransport( - Transport.USB - ) should be theSameInstanceAs AuthenticatorTransport.USB - AuthenticatorTransport.fromU2fTransport( - Transport.LIGHTNING - ) should be theSameInstanceAs AuthenticatorTransport.USB - AuthenticatorTransport.fromU2fTransport( - Transport.NFC - ) should be theSameInstanceAs AuthenticatorTransport.NFC - } } } diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/BuildersSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/BuildersSpec.scala index 2541603ad..31eb15301 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/BuildersSpec.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/BuildersSpec.scala @@ -29,8 +29,6 @@ import com.yubico.webauthn.AssertionRequest import com.yubico.webauthn.AssertionResult import com.yubico.webauthn.Generators._ import com.yubico.webauthn.RegistrationResult -import com.yubico.webauthn.attestation.Attestation -import com.yubico.webauthn.attestation.Generators._ import com.yubico.webauthn.data.Generators._ import org.junit.runner.RunWith import org.scalacheck.Arbitrary @@ -68,7 +66,6 @@ class BuildersSpec test(new TypeReference[AssertionExtensionInputs]() {}) test(new TypeReference[AssertionRequest]() {}) test(new TypeReference[AssertionResult]() {}) - test(new TypeReference[Attestation]() {}) test(new TypeReference[AttestedCredentialData]() {}) test(new TypeReference[AuthenticatorAssertionResponse]() {}) test(new TypeReference[AuthenticatorAttestationResponse]() {}) diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/EnumsSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/EnumsSpec.scala index f15a8792c..5a0d63d12 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/EnumsSpec.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/EnumsSpec.scala @@ -1,9 +1,9 @@ package com.yubico.webauthn.data -import com.yubico.fido.metadata.KeyProtectionType -import com.yubico.fido.metadata.MatcherProtectionType -import com.yubico.fido.metadata.UserVerificationMethod import com.yubico.internal.util.JacksonCodecs +import com.yubico.webauthn.extension.uvm.KeyProtectionType +import com.yubico.webauthn.extension.uvm.MatcherProtectionType +import com.yubico.webauthn.extension.uvm.UserVerificationMethod import org.junit.runner.RunWith import org.scalatest.FunSpec import org.scalatest.Matchers 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 73060d9cf..d83d942fc 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 @@ -28,12 +28,8 @@ import com.fasterxml.jackson.databind.node.JsonNodeFactory import com.fasterxml.jackson.databind.node.ObjectNode import com.upokecenter.cbor.CBOREncodeOptions import com.upokecenter.cbor.CBORObject -import com.yubico.fido.metadata.Generators.keyProtectionType -import com.yubico.fido.metadata.Generators.matcherProtectionType -import com.yubico.fido.metadata.Generators.userVerificationMethod import com.yubico.internal.util.BinaryUtil import com.yubico.internal.util.JacksonCodecs -import com.yubico.internal.util.scala.JavaConverters._ import com.yubico.scalacheck.gen.JacksonGenerators import com.yubico.scalacheck.gen.JacksonGenerators._ import com.yubico.scalacheck.gen.JavaGenerators._ @@ -49,6 +45,9 @@ import com.yubico.webauthn.data.Extensions.LargeBlob.LargeBlobRegistrationOutput import com.yubico.webauthn.data.Extensions.Uvm.UvmEntry import com.yubico.webauthn.extension.appid.AppId import com.yubico.webauthn.extension.appid.Generators._ +import com.yubico.webauthn.extension.uvm.Generators.keyProtectionType +import com.yubico.webauthn.extension.uvm.Generators.matcherProtectionType +import com.yubico.webauthn.extension.uvm.Generators.userVerificationMethod import org.scalacheck.Arbitrary import org.scalacheck.Arbitrary.arbitrary import org.scalacheck.Gen @@ -57,6 +56,8 @@ import java.net.URL import java.security.interfaces.ECPublicKey import java.util.Optional import scala.jdk.CollectionConverters._ +import scala.jdk.OptionConverters.RichOption +import scala.jdk.OptionConverters.RichOptional object Generators { @@ -229,7 +230,7 @@ object Generators { .authenticatorData(authenticatorData) .clientDataJSON(clientDataJson) .signature(signature) - .userHandle(userHandle.asJava) + .userHandle(userHandle.toJava) .build() implicit val arbitraryAuthenticatorAttestationResponse @@ -875,7 +876,7 @@ object Generators { : Arbitrary[Option[AuthenticatorAssertionExtensionOutputs]] = Arbitrary( Extensions.anyAssertionExtensions map { case (_, _, aaeo) => - AuthenticatorAssertionExtensionOutputs.fromCbor(aaeo).asScala + AuthenticatorAssertionExtensionOutputs.fromCbor(aaeo).toScala } ) implicit val arbitraryAuthenticatorRegistrationExtensionOutputs @@ -883,7 +884,7 @@ object Generators { Arbitrary( Extensions.anyRegistrationExtensions map { case (_, _, areo) => - AuthenticatorRegistrationExtensionOutputs.fromCbor(areo).asScala + AuthenticatorRegistrationExtensionOutputs.fromCbor(areo).toScala } ) @@ -907,7 +908,7 @@ object Generators { .set("type", jsonFactory.textNode(tpe)) .asInstanceOf[ObjectNode] - tokenBinding.asScala foreach { tb => + tokenBinding.toScala foreach { tb => json.set[ObjectNode]( "tokenBinding", JacksonCodecs @@ -916,7 +917,7 @@ object Generators { ) } - authenticatorExtensions.asScala foreach { ae => + authenticatorExtensions.toScala foreach { ae => json.set[ObjectNode]( "authenticatorExtensions", JacksonCodecs @@ -925,7 +926,7 @@ object Generators { ) } - clientExtensions.asScala foreach { ce => + clientExtensions.toScala foreach { ce => json.set[ObjectNode]( "clientExtensions", JacksonCodecs @@ -1062,14 +1063,12 @@ object Generators { implicit val arbitraryRelyingPartyIdentity: Arbitrary[RelyingPartyIdentity] = Arbitrary( for { - icon <- arbitrary[Optional[URL]] id <- arbitrary[String] name <- arbitrary[String] } yield RelyingPartyIdentity .builder() .id(id) .name(name) - .icon(icon) .build() ) @@ -1085,14 +1084,12 @@ object Generators { for { displayName <- arbitrary[String] name <- arbitrary[String] - icon <- arbitrary[Optional[URL]] id <- arbitrary[ByteArray] } yield UserIdentity .builder() .name(name) .displayName(displayName) .id(id) - .icon(icon) .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 cfc77a6c5..17b4b3b20 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 @@ -24,24 +24,17 @@ package com.yubico.webauthn.data -import com.fasterxml.jackson.annotation.JsonInclude.Include import com.fasterxml.jackson.core.`type`.TypeReference -import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.databind.SerializationFeature import com.fasterxml.jackson.databind.exc.ValueInstantiationException import com.fasterxml.jackson.databind.node.ObjectNode import com.fasterxml.jackson.databind.node.TextNode -import com.fasterxml.jackson.datatype.jdk8.Jdk8Module import com.yubico.internal.util.JacksonCodecs import com.yubico.webauthn.AssertionRequest import com.yubico.webauthn.AssertionResult import com.yubico.webauthn.Generators._ import com.yubico.webauthn.RegisteredCredential import com.yubico.webauthn.RegistrationResult -import com.yubico.webauthn.attestation.Attestation -import com.yubico.webauthn.attestation.Generators._ -import com.yubico.webauthn.attestation.Transport import com.yubico.webauthn.data.Generators._ import com.yubico.webauthn.extension.appid.AppId import com.yubico.webauthn.extension.appid.Generators._ @@ -60,12 +53,7 @@ class JsonIoSpec with Matchers with ScalaCheckDrivenPropertyChecks { - def json: ObjectMapper = - new ObjectMapper() - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true) - .configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false) - .setSerializationInclusion(Include.NON_ABSENT) - .registerModule(new Jdk8Module()) + def json: ObjectMapper = JacksonCodecs.json() describe("The class") { @@ -106,7 +94,6 @@ class JsonIoSpec test(new TypeReference[AssertionExtensionInputs]() {}) test(new TypeReference[AssertionRequest]() {}) test(new TypeReference[AssertionResult]() {}) - test(new TypeReference[Attestation]() {}) test(new TypeReference[AttestationConveyancePreference]() {}) test(new TypeReference[AttestationObject]() {}) test(new TypeReference[AttestationType]() {}) @@ -145,7 +132,6 @@ class JsonIoSpec test(new TypeReference[RelyingPartyIdentity]() {}) test(new TypeReference[TokenBindingInfo]() {}) test(new TypeReference[TokenBindingStatus]() {}) - test(new TypeReference[Transport]() {}) test(new TypeReference[UserIdentity]() {}) test(new TypeReference[UserVerificationRequirement]() {}) } @@ -372,7 +358,7 @@ class JsonIoSpec forAll( a.arbitrary, Gen.oneOf( - arbitrary[AuthenticatorAttachment].map(_.toJsonString), + arbitrary[AuthenticatorAttachment].map(_.getValue), arbitrary[String], ), ) { (value: P, authenticatorAttachment: String) => @@ -449,6 +435,17 @@ class JsonIoSpec } describe("The class PublicKeyCredentialRequestOptions") { + it("by default does not set a userVerification value.") { + forAll { challenge: ByteArray => + val pkcro = PublicKeyCredentialRequestOptions + .builder() + .challenge(challenge) + .build() + val jsonValue = JacksonCodecs.json.valueToTree[ObjectNode](pkcro) + jsonValue.get("userVerification") should be(null) + } + } + it("""has a toCredentialsGetJson() method which returns a JSON object with the PublicKeyCredentialGetOptions set as a top-level "publicKey" property.""") { forAll { pkcro: PublicKeyCredentialRequestOptions => val jsonValue = JacksonCodecs.json.readTree(pkcro.toCredentialsGetJson) @@ -461,6 +458,21 @@ class JsonIoSpec } } + describe("The class AuthenticatorSelectionCriteria") { + it("by default does not set a userVerification value.") { + val asc = AuthenticatorSelectionCriteria.builder().build() + val jsonValue = JacksonCodecs.json.valueToTree[ObjectNode](asc) + jsonValue.get("userVerification") should be(null) + } + + it("by default does not set a residentKey value.") { + val asc = AuthenticatorSelectionCriteria.builder().build() + val jsonValue = JacksonCodecs.json.valueToTree[ObjectNode](asc) + jsonValue.get("residentKey") should be(null) + jsonValue.get("requireResidentKey") should be(null) + } + } + describe("The class AssertionRequest") { it("""has a toCredentialsGetJson() method which returns a JSON object with the PublicKeyCredentialGetOptions set as a top-level "publicKey" property.""") { forAll { req: AssertionRequest => diff --git a/webauthn-server-core/src/test/scala/com/yubico/fido/metadata/Generators.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/extension/uvm/Generators.scala similarity index 89% rename from webauthn-server-core/src/test/scala/com/yubico/fido/metadata/Generators.scala rename to webauthn-server-core/src/test/scala/com/yubico/webauthn/extension/uvm/Generators.scala index b5bbc53a5..d3f20199c 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/fido/metadata/Generators.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/extension/uvm/Generators.scala @@ -1,4 +1,4 @@ -package com.yubico.fido.metadata +package com.yubico.webauthn.extension.uvm import org.scalacheck.Gen diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/test/Helpers.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/test/Helpers.scala index 994a8cf77..2a9395fe9 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/test/Helpers.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/test/Helpers.scala @@ -1,6 +1,5 @@ package com.yubico.webauthn.test -import com.yubico.internal.util.scala.JavaConverters._ import com.yubico.webauthn.CredentialRepository import com.yubico.webauthn.RegisteredCredential import com.yubico.webauthn.RegistrationResult @@ -10,6 +9,7 @@ import com.yubico.webauthn.data.UserIdentity import java.util.Optional import scala.jdk.CollectionConverters._ +import scala.jdk.OptionConverters.RichOption object Helpers { @@ -20,14 +20,14 @@ object Helpers { ): java.util.Set[PublicKeyCredentialDescriptor] = Set.empty.asJava override def getUserHandleForUsername( username: String - ): Optional[ByteArray] = None.asJava + ): Optional[ByteArray] = None.toJava override def getUsernameForUserHandle( userHandle: ByteArray - ): Optional[String] = None.asJava + ): Optional[String] = None.toJava override def lookup( credentialId: ByteArray, userHandle: ByteArray, - ): Optional[RegisteredCredential] = None.asJava + ): Optional[RegisteredCredential] = None.toJava override def lookupAll( credentialId: ByteArray ): java.util.Set[RegisteredCredential] = Set.empty.asJava @@ -71,14 +71,14 @@ object Helpers { username: String ): Optional[ByteArray] = if (username == user.getName) - Some(user.getId).asJava - else None.asJava + Some(user.getId).toJava + else None.toJava override def getUsernameForUserHandle( userHandle: ByteArray ): Optional[String] = if (userHandle == user.getId) - Some(user.getName).asJava - else None.asJava + Some(user.getName).toJava + else None.toJava override def lookup( credentialId: ByteArray, userHandle: ByteArray, @@ -86,8 +86,8 @@ object Helpers { if ( credentialId == credential.getCredentialId && userHandle == user.getId ) - Some(credential).asJava - else None.asJava + Some(credential).toJava + else None.toJava override def lookupAll( credentialId: ByteArray ): java.util.Set[RegisteredCredential] = diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/test/Util.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/test/Util.scala index 2b4ff3f7e..468171cfa 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/test/Util.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/test/Util.scala @@ -24,35 +24,20 @@ package com.yubico.webauthn.test -import com.yubico.internal.util.CertificateParser import com.yubico.webauthn.data.ByteArray import org.bouncycastle.asn1.sec.SECNamedCurves -import org.bouncycastle.cert.X509CertificateHolder import org.bouncycastle.jce.provider.BouncyCastleProvider import org.bouncycastle.jce.spec.ECParameterSpec import org.bouncycastle.jce.spec.ECPublicKeySpec -import org.bouncycastle.openssl.PEMParser -import java.io.BufferedReader -import java.io.InputStream -import java.io.InputStreamReader import java.security.GeneralSecurityException import java.security.KeyFactory import java.security.PublicKey -import java.security.cert.X509Certificate import scala.language.reflectiveCalls import scala.util.Try object Util { - def importCertFromPem(certPem: InputStream): X509Certificate = - CertificateParser.parseDer( - new PEMParser(new BufferedReader(new InputStreamReader(certPem))) - .readObject() - .asInstanceOf[X509CertificateHolder] - .getEncoded - ) - def decodePublicKey(encodedPublicKey: ByteArray): PublicKey = try { val curve = SECNamedCurves.getByName("secp256r1") diff --git a/webauthn-server-demo/build.gradle b/webauthn-server-demo/build.gradle index d4a5d86fc..ca28ec94a 100644 --- a/webauthn-server-demo/build.gradle +++ b/webauthn-server-demo/build.gradle @@ -13,13 +13,14 @@ dependencies { implementation( project(':webauthn-server-attestation'), - project(':webauthn-server-core-minimal'), + project(':webauthn-server-core'), project(':yubico-util'), - 'com.google.guava:guava', 'com.fasterxml.jackson.core:jackson-databind', + 'com.google.guava:guava', 'com.upokecenter:cbor', 'javax.ws.rs:javax.ws.rs-api', + 'org.bouncycastle:bcprov-jdk15on', 'org.eclipse.jetty:jetty-server', 'org.eclipse.jetty:jetty-servlet', 'org.glassfish.jersey.containers:jersey-container-servlet-core', @@ -33,7 +34,7 @@ dependencies { ) testImplementation( - project(':webauthn-server-core-minimal').sourceSets.test.output, + project(':webauthn-server-core').sourceSets.test.output, project(':yubico-util-scala'), 'junit:junit', diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/attestation/Attestation.java b/webauthn-server-demo/src/main/java/com/yubico/webauthn/attestation/Attestation.java similarity index 72% rename from webauthn-server-core/src/main/java/com/yubico/webauthn/attestation/Attestation.java rename to webauthn-server-demo/src/main/java/com/yubico/webauthn/attestation/Attestation.java index aa2d205b6..8aefd6e55 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/attestation/Attestation.java +++ b/webauthn-server-demo/src/main/java/com/yubico/webauthn/attestation/Attestation.java @@ -29,8 +29,6 @@ import java.io.Serializable; import java.util.Map; import java.util.Optional; -import java.util.Set; -import java.util.TreeSet; import lombok.Builder; import lombok.NonNull; import lombok.Value; @@ -42,12 +40,6 @@ @Builder(toBuilder = true) public class Attestation implements Serializable { - /** - * true if and only if the contained information has been verified to be - * cryptographically supported by a trusted attestation root. - */ - private final boolean trusted; - /** A unique identifier for a particular version of the data source of the data in this object. */ private final String metadataIdentifier; @@ -57,21 +49,14 @@ public class Attestation implements Serializable { /** Free-form information about the authenticator model. */ private final Map deviceProperties; - /** The set of communication modes supported by the authenticator. */ - private final Set transports; - @JsonCreator private Attestation( - @JsonProperty("trusted") boolean trusted, @JsonProperty("metadataIdentifier") String metadataIdentifier, @JsonProperty("vendorProperties") Map vendorProperties, - @JsonProperty("deviceProperties") Map deviceProperties, - @JsonProperty("transports") Set transports) { - this.trusted = trusted; + @JsonProperty("deviceProperties") Map deviceProperties) { this.metadataIdentifier = metadataIdentifier; this.vendorProperties = vendorProperties; this.deviceProperties = deviceProperties; - this.transports = transports == null ? null : new TreeSet<>(transports); } /** A unique identifier for a particular version of the data source of the data in this object. */ @@ -89,38 +74,14 @@ public Optional> getDeviceProperties() { return Optional.ofNullable(deviceProperties); } - /** The set of communication modes supported by the authenticator. */ - public Optional> getTransports() { - return Optional.ofNullable(transports); - } - public static Attestation empty() { - return builder().trusted(false).build(); - } - - public static AttestationBuilder.MandatoryStages builder() { - return new AttestationBuilder.MandatoryStages(); + return builder().build(); } public static class AttestationBuilder { - private boolean trusted; private String metadataIdentifier; private Map vendorProperties; private Map deviceProperties; - private Set transports; - - public static class MandatoryStages { - private final AttestationBuilder builder = new AttestationBuilder(); - - /** - * {@link AttestationBuilder#trusted(boolean) trusted} is a required parameter. - * - * @see AttestationBuilder#trusted(boolean) - */ - public AttestationBuilder trusted(boolean trusted) { - return builder.trusted(trusted); - } - } public AttestationBuilder metadataIdentifier(@NonNull Optional metadataIdentifier) { return this.metadataIdentifier(metadataIdentifier.orElse(null)); @@ -150,14 +111,5 @@ public AttestationBuilder deviceProperties(Map deviceProperties) this.deviceProperties = deviceProperties; return this; } - - public AttestationBuilder transports(@NonNull Optional> transports) { - return this.transports(transports.orElse(null)); - } - - public AttestationBuilder transports(Set transports) { - this.transports = transports; - return this; - } } } diff --git a/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/DeviceMatcher.java b/webauthn-server-demo/src/main/java/com/yubico/webauthn/attestation/DeviceMatcher.java similarity index 100% rename from webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/DeviceMatcher.java rename to webauthn-server-demo/src/main/java/com/yubico/webauthn/attestation/DeviceMatcher.java diff --git a/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/MetadataObject.java b/webauthn-server-demo/src/main/java/com/yubico/webauthn/attestation/MetadataObject.java similarity index 94% rename from webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/MetadataObject.java rename to webauthn-server-demo/src/main/java/com/yubico/webauthn/attestation/MetadataObject.java index 7d75da901..dbe3660fe 100644 --- a/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/MetadataObject.java +++ b/webauthn-server-demo/src/main/java/com/yubico/webauthn/attestation/MetadataObject.java @@ -87,7 +87,15 @@ public MetadataObject(JsonNode data) { } public static MetadataObject readDefault() { - InputStream is = MetadataObject.class.getResourceAsStream("/metadata.json"); + return readMetadata("/metadata.json"); + } + + public static MetadataObject readPreview() { + return readMetadata("/preview-metadata.json"); + } + + private static MetadataObject readMetadata(String path) { + InputStream is = MetadataObject.class.getResourceAsStream(path); try { return JacksonCodecs.json().readValue(is, MetadataObject.class); } catch (IOException e) { diff --git a/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/resolver/SimpleAttestationResolver.java b/webauthn-server-demo/src/main/java/com/yubico/webauthn/attestation/YubicoJsonMetadataService.java similarity index 50% rename from webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/resolver/SimpleAttestationResolver.java rename to webauthn-server-demo/src/main/java/com/yubico/webauthn/attestation/YubicoJsonMetadataService.java index a3e7a4d57..62a7acd19 100644 --- a/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/resolver/SimpleAttestationResolver.java +++ b/webauthn-server-demo/src/main/java/com/yubico/webauthn/attestation/YubicoJsonMetadataService.java @@ -1,4 +1,4 @@ -// Copyright (c) 2018, Yubico AB +// Copyright (c) 2015-2018, Yubico AB // All rights reserved. // // Redistribution and use in source and binary forms, with or without @@ -22,7 +22,7 @@ // 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.resolver; +package com.yubico.webauthn.attestation; import com.fasterxml.jackson.databind.JsonNode; import com.google.common.collect.ImmutableMap; @@ -30,92 +30,80 @@ import com.google.common.collect.Maps; import com.yubico.internal.util.CertificateParser; import com.yubico.internal.util.CollectionUtil; -import com.yubico.internal.util.ExceptionUtil; -import com.yubico.internal.util.OptionalUtil; -import com.yubico.webauthn.attestation.Attestation; -import com.yubico.webauthn.attestation.AttestationResolver; -import com.yubico.webauthn.attestation.DeviceMatcher; -import com.yubico.webauthn.attestation.MetadataObject; -import com.yubico.webauthn.attestation.Transport; -import com.yubico.webauthn.attestation.TrustResolver; 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; import java.util.Collection; -import java.util.HashMap; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; -public final class SimpleAttestationResolver implements AttestationResolver { +@Slf4j +public final class YubicoJsonMetadataService implements AttestationTrustSource { private static final String SELECTORS = "selectors"; private static final String SELECTOR_TYPE = "type"; private static final String SELECTOR_PARAMETERS = "parameters"; - private static final String TRANSPORTS = "transports"; - private static final String TRANSPORTS_EXT_OID = "1.3.6.1.4.1.45724.2.1.1"; - private static final Map DEFAULT_DEVICE_MATCHERS = ImmutableMap.of( ExtensionMatcher.SELECTOR_TYPE, new ExtensionMatcher(), FingerprintMatcher.SELECTOR_TYPE, new FingerprintMatcher()); - private final Map metadata = new HashMap<>(); - private final TrustResolver trustResolver; + private final Collection metadataObjects; private final Map matchers; - - public SimpleAttestationResolver( - @NonNull Collection objects, - @NonNull TrustResolver trustResolver, - @NonNull Map matchers) - throws CertificateException { - for (MetadataObject object : objects) { - for (String caPem : object.getTrustedCertificates()) { - X509Certificate trustAnchor = CertificateParser.parsePem(caPem); - metadata.put(trustAnchor, object); - } - } - - this.trustResolver = trustResolver; + private final Set trustRootCertificates; + + private YubicoJsonMetadataService( + @NonNull Collection metadataObjects, + @NonNull Map matchers) { + this.trustRootCertificates = + Collections.unmodifiableSet( + metadataObjects.stream() + .flatMap(metadataObject -> metadataObject.getTrustedCertificates().stream()) + .map( + pemEncodedCert -> { + try { + return CertificateParser.parsePem(pemEncodedCert); + } catch (CertificateException e) { + log.error("Failed to parse trusted certificate", e); + return null; + } + }) + .filter(Objects::nonNull) + .collect(Collectors.toSet())); + this.metadataObjects = metadataObjects; this.matchers = CollectionUtil.immutableMap(matchers); } - public SimpleAttestationResolver(Collection objects, TrustResolver trustResolver) - throws CertificateException { - this(objects, trustResolver, DEFAULT_DEVICE_MATCHERS); + public YubicoJsonMetadataService() { + this( + Stream.of(MetadataObject.readDefault(), MetadataObject.readPreview()) + .collect(Collectors.toList()), + DEFAULT_DEVICE_MATCHERS); } - private Optional lookupTrustAnchor(X509Certificate trustAnchor) { - return Optional.ofNullable(metadata.get(trustAnchor)); - } - - @Override - public Optional resolve( - X509Certificate attestationCertificate, List certificateChain) { - Optional trustAnchor = - trustResolver.resolveTrustAnchor(attestationCertificate, certificateChain); - - return trustAnchor - .flatMap(this::lookupTrustAnchor) + public Optional findMetadata(X509Certificate attestationCertificate) { + return metadataObjects.stream() .map( metadata -> { Map vendorProperties; Map deviceProperties = null; String identifier; - int metadataTransports = 0; identifier = metadata.getIdentifier(); vendorProperties = Maps.filterValues(metadata.getVendorInfo(), Objects::nonNull); for (JsonNode device : metadata.getDevices()) { if (deviceMatches(device.get(SELECTORS), attestationCertificate)) { - JsonNode transportNode = device.get(TRANSPORTS); - if (transportNode != null) { - metadataTransports |= transportNode.asInt(0); - } ImmutableMap.Builder devicePropertiesBuilder = ImmutableMap.builder(); for (Map.Entry deviceEntry : @@ -130,19 +118,18 @@ public Optional resolve( } } - return Attestation.builder() - .trusted(true) - .metadataIdentifier(Optional.ofNullable(identifier)) - .vendorProperties(Optional.of(vendorProperties)) - .deviceProperties(Optional.ofNullable(deviceProperties)) - .transports( - OptionalUtil.zipWith( - getTransports(attestationCertificate), - Optional.of(metadataTransports).filter(t -> t != 0), - (a, b) -> a | b) - .map(Transport::fromInt)) - .build(); - }); + return Optional.ofNullable(deviceProperties) + .map( + deviceProps -> + Attestation.builder() + .metadataIdentifier(Optional.ofNullable(identifier)) + .vendorProperties(Optional.of(vendorProperties)) + .deviceProperties(deviceProps) + .build()); + }) + .filter(Optional::isPresent) + .map(Optional::get) + .findAny(); } private boolean deviceMatches( @@ -161,42 +148,12 @@ private boolean deviceMatches( } } - private static Optional getTransports(X509Certificate cert) { - byte[] extensionValue = cert.getExtensionValue(TRANSPORTS_EXT_OID); - - if (extensionValue == null) { - return Optional.empty(); - } - - ExceptionUtil.assure( - extensionValue.length >= 4, - "Transports extension value must be at least 4 bytes (2 bytes octet string header, 2 bytes bit string header), was: %d", - extensionValue.length); - - // Mask out unused bits (shouldn't be needed as they should already be 0). - int unusedBitMask = 0xff; - for (int i = 0; i < extensionValue[3]; i++) { - unusedBitMask <<= 1; - } - extensionValue[extensionValue.length - 1] &= unusedBitMask; - - int transports = 0; - for (int i = extensionValue.length - 1; i >= 5; i--) { - byte b = extensionValue[i]; - for (int bi = 0; bi < 8; bi++) { - transports = (transports << 1) | (b & 1); - b >>= 1; - } - } - - return Optional.of(transports); - } - @Override - public Attestation untrustedFromCertificate(X509Certificate attestationCertificate) { - return Attestation.builder() - .trusted(false) - .transports(getTransports(attestationCertificate).map(Transport::fromInt)) + public TrustRootsResult findTrustRoots( + List attestationCertificateChain, Optional aaguid) { + return TrustRootsResult.builder() + .trustRoots(trustRootCertificates) + .enableRevocationChecking(false) .build(); } } diff --git a/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/matcher/ExtensionMatcher.java b/webauthn-server-demo/src/main/java/com/yubico/webauthn/attestation/matcher/ExtensionMatcher.java similarity index 100% rename from webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/matcher/ExtensionMatcher.java rename to webauthn-server-demo/src/main/java/com/yubico/webauthn/attestation/matcher/ExtensionMatcher.java diff --git a/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/matcher/FingerprintMatcher.java b/webauthn-server-demo/src/main/java/com/yubico/webauthn/attestation/matcher/FingerprintMatcher.java similarity index 100% rename from webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/matcher/FingerprintMatcher.java rename to webauthn-server-demo/src/main/java/com/yubico/webauthn/attestation/matcher/FingerprintMatcher.java diff --git a/webauthn-server-demo/src/main/java/com/yubico/webauthn/attestation/resolver/SimpleTrustResolverWithEquality.java b/webauthn-server-demo/src/main/java/com/yubico/webauthn/attestation/resolver/SimpleTrustResolverWithEquality.java deleted file mode 100644 index b243de4a9..000000000 --- a/webauthn-server-demo/src/main/java/com/yubico/webauthn/attestation/resolver/SimpleTrustResolverWithEquality.java +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright (c) 2018, Yubico AB -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, this -// list of conditions and the following disclaimer. -// -// 2. Redistributions in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -package com.yubico.webauthn.attestation.resolver; - -import com.google.common.collect.ArrayListMultimap; -import com.google.common.collect.Multimap; -import com.yubico.webauthn.attestation.TrustResolver; -import java.security.cert.X509Certificate; -import java.util.Collection; -import java.util.List; -import java.util.Optional; - -/** - * Resolves a metadata object whose associated certificate has signed the argument certificate, or - * is equal to the argument certificate. - */ -public class SimpleTrustResolverWithEquality implements TrustResolver { - - private final SimpleTrustResolver subresolver; - private final Multimap trustedCerts = ArrayListMultimap.create(); - - public SimpleTrustResolverWithEquality(Collection trustedCertificates) { - subresolver = new SimpleTrustResolver(trustedCertificates); - - for (X509Certificate cert : trustedCertificates) { - trustedCerts.put(cert.getSubjectDN().getName(), cert); - } - } - - @Override - public Optional resolveTrustAnchor( - X509Certificate attestationCertificate, List caCertificateChain) { - Optional subResult = - subresolver.resolveTrustAnchor(attestationCertificate, caCertificateChain); - - if (subResult.isPresent()) { - return subResult; - } else { - for (X509Certificate cert : - trustedCerts.get(attestationCertificate.getSubjectDN().getName())) { - if (cert.equals(attestationCertificate)) { - return Optional.of(cert); - } - } - - return Optional.empty(); - } - } -} diff --git a/webauthn-server-demo/src/main/java/demo/App.java b/webauthn-server-demo/src/main/java/demo/App.java index a6109b227..485e6d0f5 100644 --- a/webauthn-server-demo/src/main/java/demo/App.java +++ b/webauthn-server-demo/src/main/java/demo/App.java @@ -24,8 +24,18 @@ package demo; +import com.yubico.fido.metadata.FidoMetadataDownloaderException; +import com.yubico.fido.metadata.UnexpectedLegalHeader; +import com.yubico.webauthn.data.exception.Base64UrlException; import com.yubico.webauthn.extension.appid.InvalidAppIdException; import demo.webauthn.WebAuthnRestResource; +import java.io.IOException; +import java.security.DigestException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.SignatureException; +import java.security.cert.CertPathValidatorException; import java.security.cert.CertificateException; import java.util.Arrays; import java.util.HashSet; @@ -42,7 +52,18 @@ public Set> getClasses() { public Set getSingletons() { try { return new HashSet<>(Arrays.asList(new WebAuthnRestResource())); - } catch (InvalidAppIdException | CertificateException e) { + } catch (InvalidAppIdException + | CertificateException + | CertPathValidatorException + | InvalidAlgorithmParameterException + | Base64UrlException + | DigestException + | FidoMetadataDownloaderException + | UnexpectedLegalHeader + | IOException + | NoSuchAlgorithmException + | SignatureException + | InvalidKeyException e) { throw new RuntimeException(e); } } diff --git a/webauthn-server-demo/src/main/java/demo/webauthn/SessionManager.java b/webauthn-server-demo/src/main/java/demo/webauthn/SessionManager.java index 515034802..c3b972781 100644 --- a/webauthn-server-demo/src/main/java/demo/webauthn/SessionManager.java +++ b/webauthn-server-demo/src/main/java/demo/webauthn/SessionManager.java @@ -23,14 +23,18 @@ private static Cache newCache() { .build(); } - /** @return Create a new session for the given user, or return the existing one. */ + /** + * @return Create a new session for the given user, or return the existing one. + */ public ByteArray createSession(@NonNull ByteArray userHandle) throws ExecutionException { ByteArray sessionId = usersToSessionIds.get(userHandle, () -> generateRandom(32)); sessionIdsToUsers.put(sessionId, userHandle); return sessionId; } - /** @return the user handle of the given session, if any. */ + /** + * @return the user handle of the given session, if any. + */ public Optional getSession(@NonNull ByteArray token) { return Optional.ofNullable(sessionIdsToUsers.getIfPresent(token)); } diff --git a/webauthn-server-demo/src/main/java/demo/webauthn/WebAuthnRestResource.java b/webauthn-server-demo/src/main/java/demo/webauthn/WebAuthnRestResource.java index 550056c22..370a0eb89 100644 --- a/webauthn-server-demo/src/main/java/demo/webauthn/WebAuthnRestResource.java +++ b/webauthn-server-demo/src/main/java/demo/webauthn/WebAuthnRestResource.java @@ -29,9 +29,12 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.fasterxml.jackson.databind.node.ObjectNode; +import com.yubico.fido.metadata.FidoMetadataDownloaderException; +import com.yubico.fido.metadata.UnexpectedLegalHeader; import com.yubico.internal.util.JacksonCodecs; import com.yubico.util.Either; import com.yubico.webauthn.data.ByteArray; +import com.yubico.webauthn.data.ResidentKeyRequirement; import com.yubico.webauthn.data.exception.Base64UrlException; import com.yubico.webauthn.extension.appid.InvalidAppIdException; import com.yubico.webauthn.meta.VersionInfo; @@ -41,6 +44,12 @@ import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; +import java.security.DigestException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.SignatureException; +import java.security.cert.CertPathValidatorException; import java.security.cert.CertificateException; import java.util.Arrays; import java.util.List; @@ -74,7 +83,11 @@ public class WebAuthnRestResource { private final ObjectMapper jsonMapper = JacksonCodecs.json(); private final JsonNodeFactory jsonFactory = JsonNodeFactory.instance; - public WebAuthnRestResource() throws InvalidAppIdException, CertificateException { + public WebAuthnRestResource() + throws InvalidAppIdException, CertificateException, CertPathValidatorException, + InvalidAlgorithmParameterException, Base64UrlException, DigestException, + FidoMetadataDownloaderException, UnexpectedLegalHeader, IOException, + NoSuchAlgorithmException, SignatureException, InvalidKeyException { this(new WebAuthnServer()); } @@ -168,7 +181,9 @@ public Response startRegistration( username, displayName, Optional.ofNullable(credentialNickname), - requireResidentKey, + requireResidentKey + ? ResidentKeyRequirement.REQUIRED + : ResidentKeyRequirement.DISCOURAGED, Optional.ofNullable(sessionTokenBase64) .map( base64 -> { 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 e6bc6efd3..7f535e821 100644 --- a/webauthn-server-demo/src/main/java/demo/webauthn/WebAuthnServer.java +++ b/webauthn-server-demo/src/main/java/demo/webauthn/WebAuthnServer.java @@ -32,10 +32,10 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; -import com.google.common.io.Closeables; import com.upokecenter.cbor.CBORObject; +import com.yubico.fido.metadata.FidoMetadataDownloaderException; +import com.yubico.fido.metadata.UnexpectedLegalHeader; import com.yubico.internal.util.CertificateParser; -import com.yubico.internal.util.CollectionUtil; import com.yubico.internal.util.ExceptionUtil; import com.yubico.internal.util.JacksonCodecs; import com.yubico.util.Either; @@ -49,15 +49,7 @@ import com.yubico.webauthn.StartRegistrationOptions; import com.yubico.webauthn.U2fVerifier; import com.yubico.webauthn.attestation.Attestation; -import com.yubico.webauthn.attestation.AttestationResolver; -import com.yubico.webauthn.attestation.MetadataObject; -import com.yubico.webauthn.attestation.MetadataService; -import com.yubico.webauthn.attestation.StandardMetadataService; -import com.yubico.webauthn.attestation.TrustResolver; -import com.yubico.webauthn.attestation.resolver.CompositeAttestationResolver; -import com.yubico.webauthn.attestation.resolver.CompositeTrustResolver; -import com.yubico.webauthn.attestation.resolver.SimpleAttestationResolver; -import com.yubico.webauthn.attestation.resolver.SimpleTrustResolverWithEquality; +import com.yubico.webauthn.attestation.YubicoJsonMetadataService; import com.yubico.webauthn.data.AttestationConveyancePreference; import com.yubico.webauthn.data.AuthenticatorData; import com.yubico.webauthn.data.AuthenticatorSelectionCriteria; @@ -66,7 +58,9 @@ import com.yubico.webauthn.data.COSEAlgorithmIdentifier; import com.yubico.webauthn.data.PublicKeyCredentialDescriptor; import com.yubico.webauthn.data.RelyingPartyIdentity; +import com.yubico.webauthn.data.ResidentKeyRequirement; import com.yubico.webauthn.data.UserIdentity; +import com.yubico.webauthn.data.exception.Base64UrlException; import com.yubico.webauthn.exception.AssertionFailedException; import com.yubico.webauthn.exception.RegistrationFailedException; import com.yubico.webauthn.extension.appid.AppId; @@ -79,9 +73,13 @@ import demo.webauthn.data.U2fRegistrationResponse; import demo.webauthn.data.U2fRegistrationResult; import java.io.IOException; -import java.io.InputStream; +import java.security.DigestException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; -import java.security.cert.CertificateEncodingException; +import java.security.SignatureException; +import java.security.cert.CertPathValidatorException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.time.Clock; @@ -98,7 +96,6 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.function.Supplier; -import java.util.stream.Collectors; import lombok.AllArgsConstructor; import lombok.NonNull; import lombok.Value; @@ -109,31 +106,23 @@ public class WebAuthnServer { private static final Logger logger = LoggerFactory.getLogger(WebAuthnServer.class); private static final SecureRandom random = new SecureRandom(); - private static final String PREVIEW_METADATA_PATH = "/preview-metadata.json"; - private final Cache assertRequestStorage; private final Cache registerRequestStorage; private final InMemoryRegistrationStorage userStorage; private final SessionManager sessions = new SessionManager(); - private final TrustResolver trustResolver = - new CompositeTrustResolver( - Arrays.asList( - StandardMetadataService.createDefaultTrustResolver(), createExtraTrustResolver())); - - private final MetadataService metadataService = - new StandardMetadataService( - new CompositeAttestationResolver( - Arrays.asList( - StandardMetadataService.createDefaultAttestationResolver(trustResolver), - createExtraMetadataResolver(trustResolver)))); + private final YubicoJsonMetadataService metadataService = new YubicoJsonMetadataService(); private final Clock clock = Clock.systemDefaultZone(); private final ObjectMapper jsonMapper = JacksonCodecs.json(); private final RelyingParty rp; - public WebAuthnServer() throws InvalidAppIdException, CertificateException { + public WebAuthnServer() + throws InvalidAppIdException, CertificateException, CertPathValidatorException, + InvalidAlgorithmParameterException, Base64UrlException, DigestException, + FidoMetadataDownloaderException, UnexpectedLegalHeader, IOException, + NoSuchAlgorithmException, SignatureException, InvalidKeyException { this( new InMemoryRegistrationStorage(), newCache(), @@ -150,7 +139,10 @@ public WebAuthnServer( RelyingPartyIdentity rpIdentity, Set origins, Optional appId) - throws InvalidAppIdException, CertificateException { + throws InvalidAppIdException, CertificateException, CertPathValidatorException, + InvalidAlgorithmParameterException, Base64UrlException, DigestException, + FidoMetadataDownloaderException, UnexpectedLegalHeader, IOException, + NoSuchAlgorithmException, SignatureException, InvalidKeyException { this.userStorage = userStorage; this.registerRequestStorage = registerRequestStorage; this.assertRequestStorage = assertRequestStorage; @@ -161,10 +153,9 @@ public WebAuthnServer( .credentialRepository(this.userStorage) .origins(origins) .attestationConveyancePreference(Optional.of(AttestationConveyancePreference.DIRECT)) - .metadataService(Optional.of(metadataService)) + .attestationTrustSource(metadataService) .allowOriginPort(false) .allowOriginSubdomain(false) - .allowUnrequestedExtensions(true) .allowUntrustedAttestation(true) .validateSignatureCounter(true) .appId(appId) @@ -177,44 +168,6 @@ private static ByteArray generateRandom(int length) { return new ByteArray(bytes); } - private static MetadataObject readPreviewMetadata() { - InputStream is = WebAuthnServer.class.getResourceAsStream(PREVIEW_METADATA_PATH); - try { - return JacksonCodecs.json().readValue(is, MetadataObject.class); - } catch (IOException e) { - throw ExceptionUtil.wrapAndLog( - logger, "Failed to read metadata from " + PREVIEW_METADATA_PATH, e); - } finally { - Closeables.closeQuietly(is); - } - } - - /** - * Create a {@link TrustResolver} that accepts attestation certificates that are directly - * recognised as trust anchors. - */ - private static TrustResolver createExtraTrustResolver() { - try { - MetadataObject metadata = readPreviewMetadata(); - return new SimpleTrustResolverWithEquality(metadata.getParsedTrustedCertificates()); - } catch (CertificateException e) { - throw ExceptionUtil.wrapAndLog(logger, "Failed to read trusted certificate(s)", e); - } - } - - /** - * Create a {@link AttestationResolver} with additional metadata for unreleased YubiKey Preview - * devices. - */ - private static AttestationResolver createExtraMetadataResolver(TrustResolver trustResolver) { - try { - MetadataObject metadata = readPreviewMetadata(); - return new SimpleAttestationResolver(Collections.singleton(metadata), trustResolver); - } catch (CertificateException e) { - throw ExceptionUtil.wrapAndLog(logger, "Failed to read trusted certificate(s)", e); - } - } - private static Cache newCache() { return CacheBuilder.newBuilder() .maximumSize(100) @@ -226,7 +179,7 @@ public Either startRegistration( @NonNull String username, @NonNull String displayName, Optional credentialNickname, - boolean requireResidentKey, + ResidentKeyRequirement residentKeyRequirement, Optional sessionToken) throws ExecutionException { logger.trace( @@ -261,7 +214,7 @@ public Either startRegistration( .user(registrationUserId) .authenticatorSelection( AuthenticatorSelectionCriteria.builder() - .requireResidentKey(requireResidentKey) + .residentKey(residentKeyRequirement) .build()) .build()), Optional.of(sessions.createSession(registrationUserId.getId()))); @@ -486,16 +439,7 @@ public Either, SuccessfulU2fRegistrationResult> finishU2fRegistrati e); } - Optional attestation = Optional.empty(); - try { - if (attestationCert != null) { - attestation = - Optional.of( - metadataService.getAttestation(Collections.singletonList(attestationCert))); - } - } catch (CertificateEncodingException e) { - logger.error("Failed to resolve attestation", e); - } + Optional attestation = metadataService.findMetadata(attestationCert); final U2fRegistrationResult result = U2fRegistrationResult.builder() @@ -503,7 +447,7 @@ public Either, SuccessfulU2fRegistrationResult> finishU2fRegistrati PublicKeyCredentialDescriptor.builder() .id(response.getCredential().getU2fResponse().getKeyHandle()) .build()) - .attestationTrusted(attestation.map(Attestation::isTrusted).orElse(false)) + .attestationTrusted(attestation.isPresent()) .publicKeyCose( rawEcdaKeyToCose(response.getCredential().getU2fResponse().getPublicKey())) .attestationMetadata(attestation) @@ -560,23 +504,20 @@ public static final class SuccessfulAuthenticationResult { private final String username; private final ByteArray sessionToken; - private final List warnings; public SuccessfulAuthenticationResult( AssertionRequestWrapper request, AssertionResponse response, Collection registrations, String username, - ByteArray sessionToken, - List warnings) { + ByteArray sessionToken) { this( request, response, registrations, response.getCredential().getResponse().getParsedAuthenticatorData(), username, - sessionToken, - warnings); + sessionToken); } } @@ -624,8 +565,7 @@ public Either, SuccessfulAuthenticationResult> finishAuthentication response, userStorage.getRegistrationsByUsername(result.getUsername()), result.getUsername(), - sessions.createSession(result.getUserHandle()), - result.getWarnings())); + sessions.createSession(result.getUserHandle()))); } else { return Either.left(Collections.singletonList("Assertion failed: Invalid assertion.")); } @@ -709,7 +649,10 @@ private CredentialRegistration addRegistration( .signatureCount(result.getSignatureCount()) .build(), result.getKeyId().getTransports().orElseGet(TreeSet::new), - result.getAttestationMetadata()); + result + .getAttestationTrustPath() + .flatMap(x5c -> x5c.stream().findFirst()) + .flatMap(metadataService::findMetadata)); } private CredentialRegistration addRegistration( @@ -726,16 +669,7 @@ private CredentialRegistration addRegistration( .publicKeyCose(result.getPublicKeyCose()) .signatureCount(signatureCount) .build(), - result - .getAttestationMetadata() - .flatMap(Attestation::getTransports) - .map( - transports -> - CollectionUtil.immutableSortedSet( - transports.stream() - .map(AuthenticatorTransport::fromU2fTransport) - .collect(Collectors.toSet()))) - .orElse(Collections.emptySortedSet()), + Collections.emptySortedSet(), result.getAttestationMetadata()); } diff --git a/webauthn-server-attestation/src/main/resources/metadata.json b/webauthn-server-demo/src/main/resources/metadata.json old mode 100755 new mode 100644 similarity index 100% rename from webauthn-server-attestation/src/main/resources/metadata.json rename to webauthn-server-demo/src/main/resources/metadata.json diff --git a/webauthn-server-demo/src/main/webapp/index.html b/webauthn-server-demo/src/main/webapp/index.html index 1950e074e..103e35a1d 100644 --- a/webauthn-server-demo/src/main/webapp/index.html +++ b/webauthn-server-demo/src/main/webapp/index.html @@ -162,9 +162,24 @@ } function showDeviceInfo(params) { document.getElementById("device-info").style = undefined; - document.getElementById("device-name").textContent = params.displayName; - document.getElementById("device-nickname").textContent = params.nickname; - document.getElementById("device-icon").src = params.imageUrl; + + if (params.displayName) { + document.getElementById("device-name-row").style = undefined; + document.getElementById("device-name").textContent = params.displayName; + } else { + document.getElementById("device-name-row").style = "display: none"; + } + + if (params.nickname) { + document.getElementById("device-nickname-row").style = undefined; + document.getElementById("device-nickname").textContent = params.nickname; + } else { + document.getElementById("device-nickname-row").style = "display: none"; + } + + if (params.imageUrl) { + document.getElementById("device-icon").src = params.imageUrl; + } } function resetDisplays() { @@ -624,9 +639,13 @@

    Test your WebAuthn device

    +
    + Device: +
    +
    + Nickname: +
    - Device: - Nickname:
    Server response:
    
    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 6a92f8efd..247bdae7e 100644
    --- a/webauthn-server-demo/src/test/scala/demo/webauthn/WebAuthnServerSpec.scala
    +++ b/webauthn-server-demo/src/test/scala/demo/webauthn/WebAuthnServerSpec.scala
    @@ -27,7 +27,6 @@ package demo.webauthn
     import com.google.common.cache.Cache
     import com.google.common.cache.CacheBuilder
     import com.yubico.internal.util.JacksonCodecs
    -import com.yubico.internal.util.scala.JavaConverters._
     import com.yubico.webauthn.RegisteredCredential
     import com.yubico.webauthn.RegistrationTestData
     import com.yubico.webauthn.TestAuthenticator
    @@ -38,6 +37,7 @@ import com.yubico.webauthn.data.CollectedClientData
     import com.yubico.webauthn.data.Generators.arbitraryAuthenticatorTransport
     import com.yubico.webauthn.data.PublicKeyCredentialRequestOptions
     import com.yubico.webauthn.data.RelyingPartyIdentity
    +import com.yubico.webauthn.data.ResidentKeyRequirement
     import com.yubico.webauthn.extension.appid.AppId
     import demo.webauthn.data.AssertionRequestWrapper
     import demo.webauthn.data.CredentialRegistration
    @@ -54,6 +54,8 @@ import java.time.Instant
     import java.util.Optional
     import java.util.concurrent.TimeUnit
     import scala.jdk.CollectionConverters._
    +import scala.jdk.OptionConverters.RichOption
    +import scala.jdk.OptionConverters.RichOptional
     
     @RunWith(classOf[JUnitRunner])
     class WebAuthnServerSpec
    @@ -64,8 +66,8 @@ class WebAuthnServerSpec
       private val jsonMapper = JacksonCodecs.json()
       private val username = "foo-user"
       private val displayName = "Foo User"
    -  private val credentialNickname = Some("My Lovely Credential").asJava
    -  private val requireResidentKey = false
    +  private val credentialNickname = Some("My Lovely Credential").toJava
    +  private val residentKeyRequirement = ResidentKeyRequirement.DISCOURAGED
       private val requestId = ByteArray.fromBase64Url("request1")
       private val rpId =
         RelyingPartyIdentity.builder().id("localhost").name("Test party").build()
    @@ -82,7 +84,7 @@ class WebAuthnServerSpec
               username,
               displayName,
               credentialNickname,
    -          requireResidentKey,
    +          residentKeyRequirement,
               Optional.empty(),
             )
             val json = jsonMapper.writeValueAsString(request.right.get)
    @@ -182,13 +184,13 @@ class WebAuthnServerSpec
                 .startRegistration(
                   username,
                   displayName,
    -              None.asJava,
    -              false,
    -              None.asJava,
    +              None.toJava,
    +              ResidentKeyRequirement.DISCOURAGED,
    +              None.toJava,
                 )
                 .right
                 .get
    -          val (cred, keypair) =
    +          val (cred, keypair, _) =
                 TestAuthenticator.createUnattestedCredential(challenge =
                   request.getPublicKeyCredentialCreationOptions.getChallenge
                 )
    @@ -270,7 +272,7 @@ class WebAuthnServerSpec
                       .rpId(rpId.getId)
                       .build()
                   )
    -              .username(Some(testData.userId.getName).asJava)
    +              .username(Some(testData.userId.getName).toJava)
                   .build(),
               ),
             )
    @@ -317,7 +319,7 @@ class WebAuthnServerSpec
             val creds =
               assertionRequest.right.get.getPublicKeyCredentialRequestOptions.getAllowCredentials.get.asScala
             creds should have size 1
    -        creds.head.getTransports.asScala should equal(
    +        creds.head.getTransports.toScala should equal(
               Some(transports.asJava)
             )
           }
    diff --git a/yubico-util-scala/src/main/scala/com/yubico/internal/util/scala/JavaConverters.scala b/yubico-util-scala/src/main/scala/com/yubico/internal/util/scala/JavaConverters.scala
    deleted file mode 100644
    index 626156e58..000000000
    --- a/yubico-util-scala/src/main/scala/com/yubico/internal/util/scala/JavaConverters.scala
    +++ /dev/null
    @@ -1,58 +0,0 @@
    -// Copyright (c) 2018, Yubico AB
    -// All rights reserved.
    -//
    -// Redistribution and use in source and binary forms, with or without
    -// modification, are permitted provided that the following conditions are met:
    -//
    -// 1. Redistributions of source code must retain the above copyright notice, this
    -//    list of conditions and the following disclaimer.
    -//
    -// 2. Redistributions in binary form must reproduce the above copyright notice,
    -//    this list of conditions and the following disclaimer in the documentation
    -//    and/or other materials provided with the distribution.
    -//
    -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
    -// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
    -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
    -// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
    -// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
    -// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
    -// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
    -// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
    -// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
    -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
    -
    -package com.yubico.internal.util.scala
    -
    -import java.util.Optional
    -import java.util.function.Supplier
    -import scala.language.implicitConversions
    -
    -case class AsJavaOptional[A](a: Option[A]) {
    -  def asJava[B >: A]: Optional[B] =
    -    a match {
    -      case Some(value) => Optional.of(value)
    -      case None        => Optional.empty()
    -    }
    -}
    -case class AsScalaOption[A](a: Optional[A]) {
    -  def asScala: Option[A] = if (a.isPresent) Some(a.get()) else None
    -}
    -
    -case class AsJavaSupplier[A](a: () => A) {
    -  def asJava[B >: A]: Supplier[B] =
    -    new Supplier[B] {
    -      override def get(): B = a()
    -    }
    -}
    -
    -object JavaConverters {
    -
    -  implicit def asJavaOptionalConverter[A](a: Option[A]): AsJavaOptional[A] =
    -    AsJavaOptional(a)
    -  implicit def asJavaSupplierConverter[A](a: () => A): AsJavaSupplier[A] =
    -    AsJavaSupplier(a)
    -  implicit def asScalaOptionConverter[A](a: Optional[A]): AsScalaOption[A] =
    -    AsScalaOption(a)
    -
    -}
    diff --git a/yubico-util-scala/src/main/scala/com/yubico/scalacheck/gen/JavaGenerators.scala b/yubico-util-scala/src/main/scala/com/yubico/scalacheck/gen/JavaGenerators.scala
    index 9dc840291..885515aa4 100644
    --- a/yubico-util-scala/src/main/scala/com/yubico/scalacheck/gen/JavaGenerators.scala
    +++ b/yubico-util-scala/src/main/scala/com/yubico/scalacheck/gen/JavaGenerators.scala
    @@ -1,6 +1,5 @@
     package com.yubico.scalacheck.gen
     
    -import com.yubico.internal.util.scala.JavaConverters._
     import org.scalacheck.Arbitrary
     import org.scalacheck.Arbitrary.arbitrary
     import org.scalacheck.Gen
    @@ -8,13 +7,14 @@ import org.scalacheck.Gen
     import java.net.URL
     import java.util.Optional
     import scala.jdk.CollectionConverters._
    +import scala.jdk.OptionConverters.RichOption
     
     object JavaGenerators {
     
       implicit def arbitraryOptional[A](implicit
           a: Arbitrary[A]
       ): Arbitrary[Optional[A]] =
    -    Arbitrary(Gen.option(a.arbitrary).map(_.asJava))
    +    Arbitrary(Gen.option(a.arbitrary).map(_.toJava))
     
       implicit def arbitraryList[A](implicit
           a: Arbitrary[List[A]]
    diff --git a/yubico-util/build.gradle b/yubico-util/build.gradle
    index 807d3af16..e4249774e 100644
    --- a/yubico-util/build.gradle
    +++ b/yubico-util/build.gradle
    @@ -23,6 +23,7 @@ dependencies {
       implementation(
         'com.fasterxml.jackson.dataformat:jackson-dataformat-cbor',
         'com.fasterxml.jackson.datatype:jackson-datatype-jdk8',
    +    'com.fasterxml.jackson.datatype:jackson-datatype-jsr310',
         'com.google.guava:guava',
         'com.upokecenter:cbor',
         'org.slf4j:slf4j-api',
    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 c7b42eded..ed7eb5b78 100644
    --- a/yubico-util/src/main/java/com/yubico/internal/util/BinaryUtil.java
    +++ b/yubico-util/src/main/java/com/yubico/internal/util/BinaryUtil.java
    @@ -25,6 +25,8 @@
     package com.yubico.internal.util;
     
     import com.google.common.io.BaseEncoding;
    +import java.io.IOException;
    +import java.io.InputStream;
     import java.nio.ByteBuffer;
     import java.nio.ByteOrder;
     import java.util.Arrays;
    @@ -35,12 +37,16 @@ public static byte[] copy(byte[] bytes) {
         return Arrays.copyOf(bytes, bytes.length);
       }
     
    -  /** @param bytes Bytes to encode */
    +  /**
    +   * @param bytes Bytes to encode
    +   */
       public static String toHex(byte[] bytes) {
         return BaseEncoding.base16().encode(bytes).toLowerCase();
       }
     
    -  /** @param hex String of hexadecimal digits to decode as bytes. */
    +  /**
    +   * @param hex String of hexadecimal digits to decode as bytes.
    +   */
       public static byte[] fromHex(String hex) {
         return BaseEncoding.base16().decode(hex.toUpperCase());
       }
    @@ -126,4 +132,20 @@ public static byte[] encodeUint32(long value) {
         b.rewind();
         return Arrays.copyOfRange(b.array(), 4, 8);
       }
    +
    +  public static byte[] readAll(InputStream is) throws IOException {
    +    byte[] buffer = new byte[1024];
    +    int bufferLen = 0;
    +    while (true) {
    +      final int moreLen = is.read(buffer, bufferLen, buffer.length - bufferLen);
    +      if (moreLen <= 0) {
    +        return Arrays.copyOf(buffer, bufferLen);
    +      } else {
    +        bufferLen += moreLen;
    +        if (bufferLen == buffer.length) {
    +          buffer = Arrays.copyOf(buffer, buffer.length * 2);
    +        }
    +      }
    +    }
    +  }
     }
    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 63553fa51..bb03a8b32 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
    @@ -26,14 +26,21 @@
     
     import java.io.ByteArrayInputStream;
     import java.io.InputStream;
    +import java.nio.ByteBuffer;
    +import java.security.MessageDigest;
    +import java.security.NoSuchAlgorithmException;
    +import java.security.cert.Certificate;
     import java.security.cert.CertificateException;
     import java.security.cert.CertificateFactory;
     import java.security.cert.X509Certificate;
     import java.util.Arrays;
     import java.util.Base64;
     import java.util.List;
    +import java.util.Optional;
     
     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();
     
    @@ -89,4 +96,74 @@ public static X509Certificate parseDer(InputStream is) throws CertificateExcepti
         }
         return cert;
       }
    +
    +  /**
    +   * Compute a Subject Key Identifier as defined as method (1) in RFC 5280 section 4.2.1.2.
    +   *
    +   * @throws NoSuchAlgorithmException if the SHA-1 hash algorithm is not available.
    +   * @see Internet X.509
    +   *     Public Key Infrastructure Certificate and Certificate Revocation List (CRL) Profile,
    +   *     section 4.2.1.2. Subject Key Identifier
    +   */
    +  public static byte[] computeSubjectKeyIdentifier(final Certificate cert)
    +      throws NoSuchAlgorithmException {
    +    final byte[] spki = cert.getPublicKey().getEncoded();
    +
    +    // SubjectPublicKeyInfo  ::=  SEQUENCE  {
    +    //     algorithm            AlgorithmIdentifier,
    +    //     subjectPublicKey     BIT STRING  }
    +    final byte algLength = spki[2 + 1];
    +
    +    // BIT STRING begins with one octet specifying number of unused bits at end;
    +    // this is not included in the content to hash for a Subject Key Identifier.
    +    final int spkBitsStart = 2 + 2 + 2 + algLength + 1;
    +
    +    return MessageDigest.getInstance("SHA-1")
    +        .digest(Arrays.copyOfRange(spki, spkBitsStart, spki.length));
    +  }
    +
    +  /**
    +   * Parses an AAGUID into bytes. Refer to Packed
    +   * Attestation Statement Certificate Requirements on the W3C web site for details of the ASN.1
    +   * structure that this method parses.
    +   *
    +   * @param bytes the bytes making up value of the extension
    +   * @return the bytes of the AAGUID
    +   */
    +  private static byte[] parseAaguid(byte[] bytes) {
    +
    +    if (bytes != null && bytes.length == 20) {
    +      ByteBuffer buffer = ByteBuffer.wrap(bytes);
    +
    +      if (buffer.get() == (byte) 0x04
    +          && buffer.get() == (byte) 0x12
    +          && buffer.get() == (byte) 0x04
    +          && buffer.get() == (byte) 0x10) {
    +        byte[] aaguidBytes = new byte[16];
    +        buffer.get(aaguidBytes);
    +
    +        return aaguidBytes;
    +      }
    +    }
    +
    +    throw new IllegalArgumentException(
    +        "X.509 extension 1.3.6.1.4.1.45724.1.1.4 (id-fido-gen-ce-aaguid) is not valid.");
    +  }
    +
    +  public static Optional parseFidoAaguidExtension(X509Certificate cert) {
    +    Optional result =
    +        Optional.ofNullable(cert.getExtensionValue(ID_FIDO_GEN_CE_AAGUID))
    +            .map(CertificateParser::parseAaguid);
    +    result.ifPresent(
    +        aaguid -> {
    +          if (cert.getCriticalExtensionOIDs().contains(ID_FIDO_GEN_CE_AAGUID)) {
    +            throw new IllegalArgumentException(
    +                String.format(
    +                    "X.509 extension %s (id-fido-gen-ce-aaguid) must not be marked critical.",
    +                    ID_FIDO_GEN_CE_AAGUID));
    +          }
    +        });
    +    return result;
    +  }
     }
    diff --git a/yubico-util/src/main/java/com/yubico/internal/util/CollectionUtil.java b/yubico-util/src/main/java/com/yubico/internal/util/CollectionUtil.java
    index 457324504..2fc0021d4 100644
    --- a/yubico-util/src/main/java/com/yubico/internal/util/CollectionUtil.java
    +++ b/yubico-util/src/main/java/com/yubico/internal/util/CollectionUtil.java
    @@ -30,6 +30,13 @@ public static  List immutableList(List l) {
         return Collections.unmodifiableList(new ArrayList<>(l));
       }
     
    +  /**
    +   * Alias of s == null ? Collections.emptyList() : CollectionUtil.immutableList(s).
    +   */
    +  public static  List immutableListOrEmpty(List l) {
    +    return l == null ? Collections.emptyList() : immutableList(l);
    +  }
    +
       /**
        * Make an unmodifiable shallow copy of the argument.
        *
    @@ -39,6 +46,11 @@ public static  Set immutableSet(Set s) {
         return Collections.unmodifiableSet(new HashSet<>(s));
       }
     
    +  /** Alias of s == null ? Collections.emptySet() : CollectionUtil.immutableSet(s). */
    +  public static  Set immutableSetOrEmpty(Set s) {
    +    return s == null ? Collections.emptySet() : immutableSet(s);
    +  }
    +
       /**
        * Make an unmodifiable shallow copy of the argument.
        *
    diff --git a/yubico-util/src/main/java/com/yubico/internal/util/JacksonCodecs.java b/yubico-util/src/main/java/com/yubico/internal/util/JacksonCodecs.java
    index c8879cf24..f9c1027c9 100644
    --- a/yubico-util/src/main/java/com/yubico/internal/util/JacksonCodecs.java
    +++ b/yubico-util/src/main/java/com/yubico/internal/util/JacksonCodecs.java
    @@ -8,6 +8,7 @@
     import com.fasterxml.jackson.databind.node.ObjectNode;
     import com.fasterxml.jackson.dataformat.cbor.CBORFactory;
     import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
    +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
     import com.upokecenter.cbor.CBORObject;
     import java.io.IOException;
     
    @@ -23,7 +24,8 @@ public static ObjectMapper json() {
             .configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false)
             .setSerializationInclusion(Include.NON_ABSENT)
             .setBase64Variant(Base64Variants.MODIFIED_FOR_URL)
    -        .registerModule(new Jdk8Module());
    +        .registerModule(new Jdk8Module())
    +        .registerModule(new JavaTimeModule());
       }
     
       public static CBORObject deepCopy(CBORObject a) {
    diff --git a/yubico-util/src/main/java/com/yubico/internal/util/json/JsonLongSerializable.java b/yubico-util/src/main/java/com/yubico/internal/util/json/JsonLongSerializable.java
    deleted file mode 100644
    index b9454d922..000000000
    --- a/yubico-util/src/main/java/com/yubico/internal/util/json/JsonLongSerializable.java
    +++ /dev/null
    @@ -1,34 +0,0 @@
    -// Copyright (c) 2018, Yubico AB
    -// All rights reserved.
    -//
    -// Redistribution and use in source and binary forms, with or without
    -// modification, are permitted provided that the following conditions are met:
    -//
    -// 1. Redistributions of source code must retain the above copyright notice, this
    -//    list of conditions and the following disclaimer.
    -//
    -// 2. Redistributions in binary form must reproduce the above copyright notice,
    -//    this list of conditions and the following disclaimer in the documentation
    -//    and/or other materials provided with the distribution.
    -//
    -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
    -// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
    -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
    -// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
    -// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
    -// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
    -// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
    -// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
    -// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
    -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
    -
    -package com.yubico.internal.util.json;
    -
    -@Deprecated
    -/** @deprecated This will be removed in the next major version. */
    -public interface JsonLongSerializable {
    -
    -  @Deprecated
    -  /** @deprecated This will be removed in the next major version. */
    -  long toJsonNumber();
    -}
    diff --git a/yubico-util/src/main/java/com/yubico/internal/util/json/JsonLongSerializer.java b/yubico-util/src/main/java/com/yubico/internal/util/json/JsonLongSerializer.java
    deleted file mode 100644
    index 420b960ea..000000000
    --- a/yubico-util/src/main/java/com/yubico/internal/util/json/JsonLongSerializer.java
    +++ /dev/null
    @@ -1,41 +0,0 @@
    -// Copyright (c) 2018, Yubico AB
    -// All rights reserved.
    -//
    -// Redistribution and use in source and binary forms, with or without
    -// modification, are permitted provided that the following conditions are met:
    -//
    -// 1. Redistributions of source code must retain the above copyright notice, this
    -//    list of conditions and the following disclaimer.
    -//
    -// 2. Redistributions in binary form must reproduce the above copyright notice,
    -//    this list of conditions and the following disclaimer in the documentation
    -//    and/or other materials provided with the distribution.
    -//
    -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
    -// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
    -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
    -// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
    -// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
    -// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
    -// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
    -// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
    -// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
    -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
    -
    -package com.yubico.internal.util.json;
    -
    -import com.fasterxml.jackson.core.JsonGenerator;
    -import com.fasterxml.jackson.databind.JsonSerializer;
    -import com.fasterxml.jackson.databind.SerializerProvider;
    -import java.io.IOException;
    -
    -@Deprecated
    -/** @deprecated This will be removed in the next major version. */
    -public class JsonLongSerializer extends JsonSerializer {
    -
    -  @Override
    -  public void serialize(T t, JsonGenerator jsonGenerator, SerializerProvider serializerProvider)
    -      throws IOException {
    -    jsonGenerator.writeNumber(t.toJsonNumber());
    -  }
    -}
    diff --git a/yubico-util/src/main/java/com/yubico/internal/util/json/JsonStringSerializable.java b/yubico-util/src/main/java/com/yubico/internal/util/json/JsonStringSerializable.java
    deleted file mode 100644
    index 648963d3c..000000000
    --- a/yubico-util/src/main/java/com/yubico/internal/util/json/JsonStringSerializable.java
    +++ /dev/null
    @@ -1,34 +0,0 @@
    -// Copyright (c) 2018, Yubico AB
    -// All rights reserved.
    -//
    -// Redistribution and use in source and binary forms, with or without
    -// modification, are permitted provided that the following conditions are met:
    -//
    -// 1. Redistributions of source code must retain the above copyright notice, this
    -//    list of conditions and the following disclaimer.
    -//
    -// 2. Redistributions in binary form must reproduce the above copyright notice,
    -//    this list of conditions and the following disclaimer in the documentation
    -//    and/or other materials provided with the distribution.
    -//
    -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
    -// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
    -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
    -// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
    -// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
    -// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
    -// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
    -// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
    -// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
    -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
    -
    -package com.yubico.internal.util.json;
    -
    -@Deprecated
    -/** @deprecated This will be removed in the next major version. */
    -public interface JsonStringSerializable {
    -
    -  @Deprecated
    -  /** @deprecated This will be removed in the next major version. */
    -  String toJsonString();
    -}
    diff --git a/yubico-util/src/main/java/com/yubico/internal/util/json/JsonStringSerializer.java b/yubico-util/src/main/java/com/yubico/internal/util/json/JsonStringSerializer.java
    deleted file mode 100644
    index cb62ceb1a..000000000
    --- a/yubico-util/src/main/java/com/yubico/internal/util/json/JsonStringSerializer.java
    +++ /dev/null
    @@ -1,41 +0,0 @@
    -// Copyright (c) 2018, Yubico AB
    -// All rights reserved.
    -//
    -// Redistribution and use in source and binary forms, with or without
    -// modification, are permitted provided that the following conditions are met:
    -//
    -// 1. Redistributions of source code must retain the above copyright notice, this
    -//    list of conditions and the following disclaimer.
    -//
    -// 2. Redistributions in binary form must reproduce the above copyright notice,
    -//    this list of conditions and the following disclaimer in the documentation
    -//    and/or other materials provided with the distribution.
    -//
    -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
    -// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
    -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
    -// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
    -// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
    -// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
    -// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
    -// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
    -// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
    -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
    -
    -package com.yubico.internal.util.json;
    -
    -import com.fasterxml.jackson.core.JsonGenerator;
    -import com.fasterxml.jackson.databind.JsonSerializer;
    -import com.fasterxml.jackson.databind.SerializerProvider;
    -import java.io.IOException;
    -
    -@Deprecated
    -/** @deprecated This will be removed in the next major version. */
    -public class JsonStringSerializer extends JsonSerializer {
    -
    -  @Override
    -  public void serialize(T t, JsonGenerator jsonGenerator, SerializerProvider serializerProvider)
    -      throws IOException {
    -    jsonGenerator.writeString(t.toJsonString());
    -  }
    -}
    diff --git a/yubico-util/src/main/java/com/yubico/internal/util/json/LocalDateJsonSerializer.java b/yubico-util/src/main/java/com/yubico/internal/util/json/LocalDateJsonSerializer.java
    deleted file mode 100644
    index a3f73a0e7..000000000
    --- a/yubico-util/src/main/java/com/yubico/internal/util/json/LocalDateJsonSerializer.java
    +++ /dev/null
    @@ -1,41 +0,0 @@
    -// Copyright (c) 2018, Yubico AB
    -// All rights reserved.
    -//
    -// Redistribution and use in source and binary forms, with or without
    -// modification, are permitted provided that the following conditions are met:
    -//
    -// 1. Redistributions of source code must retain the above copyright notice, this
    -//    list of conditions and the following disclaimer.
    -//
    -// 2. Redistributions in binary form must reproduce the above copyright notice,
    -//    this list of conditions and the following disclaimer in the documentation
    -//    and/or other materials provided with the distribution.
    -//
    -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
    -// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
    -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
    -// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
    -// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
    -// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
    -// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
    -// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
    -// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
    -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
    -
    -package com.yubico.internal.util.json;
    -
    -import com.fasterxml.jackson.core.JsonGenerator;
    -import com.fasterxml.jackson.databind.JsonSerializer;
    -import com.fasterxml.jackson.databind.SerializerProvider;
    -import java.io.IOException;
    -import java.time.LocalDate;
    -
    -public class LocalDateJsonSerializer extends JsonSerializer {
    -
    -  @Override
    -  public void serialize(
    -      LocalDate t, JsonGenerator jsonGenerator, SerializerProvider serializerProvider)
    -      throws IOException {
    -    jsonGenerator.writeString(t.toString());
    -  }
    -}
    diff --git a/yubico-util/src/test/java/com/yubico/internal/util/CertificateParserTest.java b/yubico-util/src/test/java/com/yubico/internal/util/CertificateParserTest.java
    index 2ba576074..6ff852b00 100644
    --- a/yubico-util/src/test/java/com/yubico/internal/util/CertificateParserTest.java
    +++ b/yubico-util/src/test/java/com/yubico/internal/util/CertificateParserTest.java
    @@ -24,8 +24,10 @@
     
     package com.yubico.internal.util;
     
    +import static org.junit.Assert.assertEquals;
     import static org.junit.Assert.assertNotNull;
     
    +import java.security.NoSuchAlgorithmException;
     import java.security.cert.CertificateException;
     import org.junit.Test;
     
    @@ -36,8 +38,29 @@ public class CertificateParserTest {
       private static final String PEM_ATTESTATION_CERT =
           "-----BEGIN CERTIFICATE-----\n" + ATTESTATION_CERT + "\n-----END CERTIFICATE-----\n";
     
    +  private static final String SKY2_CERT =
    +      "-----BEGIN CERTIFICATE-----\n"
    +          + "MIICvjCCAaagAwIBAgIEdIb9wjANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZdWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAwMDBaGA8yMDUwMDkwNDAwMDAwMFowbzELMAkGA1UEBhMCU0UxEjAQBgNVBAoMCVl1YmljbyBBQjEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjEoMCYGA1UEAwwfWXViaWNvIFUyRiBFRSBTZXJpYWwgMTk1NTAwMzg0MjBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJVd8633JH0xde/9nMTzGk6HjrrhgQlWYVD7OIsuX2Unv1dAmqWBpQ0KxS8YRFwKE1SKE1PIpOWacE5SO8BN6+2jbDBqMCIGCSsGAQQBgsQKAgQVMS4zLjYuMS40LjEuNDE0ODIuMS4xMBMGCysGAQQBguUcAgEBBAQDAgUgMCEGCysGAQQBguUcAQEEBBIEEPigEfOMCk0VgAYXER+e3H0wDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAQEAMVxIgOaaUn44Zom9af0KqG9J655OhUVBVW+q0As6AIod3AH5bHb2aDYakeIyyBCnnGMHTJtuekbrHbXYXERIn4aKdkPSKlyGLsA/A+WEi+OAfXrNVfjhrh7iE6xzq0sg4/vVJoywe4eAJx0fS+Dl3axzTTpYl71Nc7p/NX6iCMmdik0pAuYJegBcTckE3AoYEg4K99AM/JaaKIblsbFh8+3LxnemeNf7UwOczaGGvjS6UzGVI0Odf9lKcPIwYhuTxM5CaNMXTZQ7xq4/yTfC3kPWtE4hFT34UJJflZBiLrxG4OsYxkHw/n5vKgmpspB3GfYuYTWhkDKiE8CYtyg87g==-----END CERTIFICATE-----";
    +  private static final String SKY_NFC_CERT =
    +      "-----BEGIN CERTIFICATE-----\n"
    +          + "MIICvTCCAaWgAwIBAgIEKudiYzANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZdWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAwMDBaGA8yMDUwMDkwNDAwMDAwMFowbjELMAkGA1UEBhMCU0UxEjAQBgNVBAoMCVl1YmljbyBBQjEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjEnMCUGA1UEAwweWXViaWNvIFUyRiBFRSBTZXJpYWwgNzE5ODA3MDc1MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEKgOGXmBD2Z4R/xCqJVRXhL8Jr45rHjsyFykhb1USGozZENOZ3cdovf5Ke8fj2rxi5tJGn/VnW4/6iQzKdIaeP6NsMGowIgYJKwYBBAGCxAoCBBUxLjMuNi4xLjQuMS40MTQ4Mi4xLjEwEwYLKwYBBAGC5RwCAQEEBAMCBDAwIQYLKwYBBAGC5RwBAQQEEgQQbUS6m/bsLkm5MAyP6SDLczAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4IBAQByV9A83MPhFWmEkNb4DvlbUwcjc9nmRzJjKxHc3HeK7GvVkm0H4XucVDB4jeMvTke0WHb/jFUiApvpOHh5VyMx5ydwFoKKcRs5x0/WwSWL0eTZ5WbVcHkDR9pSNcA/D/5AsUKOBcbpF5nkdVRxaQHuuIuwV4k1iK2IqtMNcU8vL6w21U261xCcWwJ6sMq4zzVO8QCKCQhsoIaWrwz828GDmPzfAjFsJiLJXuYivdHACkeJ5KHMt0mjVLpfJ2BCML7/rgbmvwL7wBW80VHfNdcKmKjkLcpEiPzwcQQhiN/qHV90t+p4iyr5xRSpurlP5zic2hlRkLKxMH2/kRjhqSn4-----END CERTIFICATE-----";
    +
       @Test
       public void parsePemDoesNotReturnNull() throws CertificateException {
         assertNotNull(CertificateParser.parsePem(PEM_ATTESTATION_CERT));
       }
    +
    +  @Test
    +  public void subjectPublicKeyIdentifierIsCorrect()
    +      throws CertificateException, NoSuchAlgorithmException {
    +    assertEquals(
    +        "bf12365afcb14d3dd820be7ec4be163cb7c85de0",
    +        BinaryUtil.toHex(
    +            CertificateParser.computeSubjectKeyIdentifier(CertificateParser.parsePem(SKY2_CERT))));
    +    assertEquals(
    +        "43c0f809b1d75616aa152c3cba57d73465057f21",
    +        BinaryUtil.toHex(
    +            CertificateParser.computeSubjectKeyIdentifier(
    +                CertificateParser.parsePem(SKY_NFC_CERT))));
    +  }
     }