From 0d1116cc3fcbf3a5282ed332a5d2dd5af4e63f31 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 17 May 2022 18:38:12 +0200 Subject: [PATCH 01/58] Add method FidoMetadataDownloader.refreshBlob() --- NEWS | 7 + webauthn-server-attestation/README.adoc | 36 +- .../doc/Migrating_from_v1.adoc | 2 + .../fido/metadata/FidoMetadataDownloader.java | 279 +- .../metadata/FidoMetadataDownloaderSpec.scala | 2871 +++++++++-------- 5 files changed, 1734 insertions(+), 1461 deletions(-) diff --git a/NEWS b/NEWS index 57dea19b5..b9b6cd622 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,10 @@ +== Version 2.1.0 (unreleased) == + +New features: + +- Added method `FidoMetadataDownloader.refreshBlob()`. + + == Version 2.0.0 == This release removes deprecated APIs and changes some defaults to better align diff --git a/webauthn-server-attestation/README.adoc b/webauthn-server-attestation/README.adoc index fdd8f88b6..e9d662a58 100644 --- a/webauthn-server-attestation/README.adoc +++ b/webauthn-server-attestation/README.adoc @@ -31,12 +31,19 @@ The link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.0.0/com/yubico/fido/metadata/FidoMetadataDownloader.html[`FidoMetadataDownloader`] class will attempt to download a new BLOB only when its link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.0.0/com/yubico/fido/metadata/FidoMetadataDownloader.html#loadCachedBlob()[`loadCachedBlob()`] -is executed, -and then only if the cache is empty or if the cached BLOB is invalid or out of date. +or +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.1.0/com/yubico/fido/metadata/FidoMetadataDownloader.html#refreshBlob()[`refreshBlob()`] +method is executed. +As the names suggest, +`loadCachedBlob()` downloads a new BLOB only if the cache is empty +or the cached BLOB is invalid or out of date, +while `refreshBlob()` always downloads a new BLOB and falls back +to the cached BLOB only when the new BLOB is invalid in some way. link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.0.0/com/yubico/fido/metadata/FidoMetadataService.html[`FidoMetadataService`] will never re-download a new BLOB once instantiated. + -You should use some external scheduling mechanism to re-run `loadCachedBlob()` periodically +You should use some external scheduling mechanism to re-run `loadCachedBlob()` +and/or `refreshBlob()` periodically and rebuild new `FidoMetadataService` instances with the updated metadata contents. You can do this with minimal disruption since the `FidoMetadataService` and `RelyingParty` classes keep no internal mutable state. @@ -95,11 +102,14 @@ Unlike other classes in this module and the core library, link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.0.0/com/yubico/fido/metadata/FidoMetadataDownloader.html[`FidoMetadataDownloader`] is NOT THREAD SAFE since its link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.0.0/com/yubico/fido/metadata/FidoMetadataDownloader.html#loadCachedBlob()[`loadCachedBlob()`] -method reads and writes caches. +and +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.1.0/com/yubico/fido/metadata/FidoMetadataDownloader.html#refreshBlob()[`refreshBlob()`] +methods read and write caches. link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.0.0/com/yubico/fido/metadata/FidoMetadataService.html[`FidoMetadataService`], on the other hand, is thread safe, -and `FidoMetadataDownloader` instances can be reused for subsequent `loadCachedBlob()` calls -as long as only one `loadCachedBlob()` call executes at a time. +and `FidoMetadataDownloader` instances can be reused +for subsequent `loadCachedBlob()` and `refreshBlob()` calls +as long as only one call executes at a time. ===== + [source,java] @@ -323,15 +333,19 @@ The library implements these as closely as possible, but with some slight depart ** Step 3 states "The `nextUpdate` field of the Metadata BLOB specifies a date when the download SHOULD occur at latest". `FidoMetadataDownloader` does not automatically re-download the BLOB. - Instead, each time its + Instead, each time the link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.0.0/com/yubico/fido/metadata/FidoMetadataDownloader.html#loadCachedBlob()[`loadCachedBlob()`] method is executed it checks whether a new BLOB should be downloaded. + The + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.1.0/com/yubico/fido/metadata/FidoMetadataDownloader.html#refreshBlob()[`refreshBlob()`] + method always attempts to download a new BLOB when executed, + but also does not trigger re-downloads automatically. + -If no BLOB exists in cache, or the cached BLOB is invalid, or if the current date is greater than or equal to `nextUpdate`, -then a new BLOB is downloaded. -If the new BLOB is valid, has a correct signature, and has a `no` field greater than the cached BLOB, +Whenever a newly downloaded BLOB is valid, has a correct signature, +and has a `no` field greater than the cached BLOB (if any), then the new BLOB replaces the cached one; -otherwise, the new BLOB is discarded and the cached one is kept until the next execution of `.loadCachedBlob()`. +otherwise, the new BLOB is discarded and the cached one is kept +until the next execution of `.loadCachedBlob()` or `.refreshBlob()`. * Metadata entries are not stored or cached individually, instead the BLOB is cached as a whole. In processing rules step 8, neither `FidoMetadataDownloader` nor diff --git a/webauthn-server-attestation/doc/Migrating_from_v1.adoc b/webauthn-server-attestation/doc/Migrating_from_v1.adoc index 06f54de16..5b99aa166 100644 --- a/webauthn-server-attestation/doc/Migrating_from_v1.adoc +++ b/webauthn-server-attestation/doc/Migrating_from_v1.adoc @@ -66,6 +66,8 @@ FidoMetadataService metadataService = FidoMetadataService.builder() You may also need to add external logic to occasionally re-run link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.0.0/com/yubico/fido/metadata/FidoMetadataDownloader.html#loadCachedBlob()[`loadCachedBlob()`] +and/or +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.1.0/com/yubico/fido/metadata/FidoMetadataDownloader.html#refreshBlob()[`refreshBlob()`] and reconstruct the `FidoMetadataService`, as `FidoMetadataService` will not automatically update the BLOB on its own. diff --git a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/FidoMetadataDownloader.java b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/FidoMetadataDownloader.java index d6e3a4913..0252ddc39 100644 --- a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/FidoMetadataDownloader.java +++ b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/FidoMetadataDownloader.java @@ -95,10 +95,10 @@ * *

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

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

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

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

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

NOTE: The {@link RegisteredCredential#getSignatureCount() signature count} in this object + * will reflect the signature counter state before the assertion operation, not the new + * counter value. When updating your database state, use the signature counter from {@link + * #getSignatureCount()} instead. */ - @NonNull private final ByteArray userHandle; + private final RegisteredCredential credential; /** * The username of the authenticated user. @@ -107,12 +100,33 @@ public class AssertionResult { private final AuthenticatorAssertionExtensionOutputs authenticatorExtensionOutputs; + private AssertionResult( + boolean success, + @NonNull @JsonProperty("credential") RegisteredCredential credential, + @NonNull String username, + long signatureCount, + boolean signatureCounterValid, + ClientAssertionExtensionOutputs clientExtensionOutputs, + AuthenticatorAssertionExtensionOutputs authenticatorExtensionOutputs) { + this( + success, + credential, + username, + null, + null, + signatureCount, + signatureCounterValid, + clientExtensionOutputs, + authenticatorExtensionOutputs); + } + @JsonCreator private AssertionResult( @JsonProperty("success") boolean success, - @NonNull @JsonProperty("credentialId") ByteArray credentialId, - @NonNull @JsonProperty("userHandle") ByteArray userHandle, + @NonNull @JsonProperty("credential") RegisteredCredential credential, @NonNull @JsonProperty("username") String username, + @JsonProperty("credentialId") ByteArray credentialId, // TODO: Delete in next major release + @JsonProperty("userHandle") ByteArray userHandle, // TODO: Delete in next major release @JsonProperty("signatureCount") long signatureCount, @JsonProperty("signatureCounterValid") boolean signatureCounterValid, @JsonProperty("clientExtensionOutputs") @@ -120,9 +134,20 @@ private AssertionResult( @JsonProperty("authenticatorExtensionOutputs") AuthenticatorAssertionExtensionOutputs authenticatorExtensionOutputs) { this.success = success; - this.credentialId = credentialId; - this.userHandle = userHandle; + this.credential = credential; this.username = username; + + if (credentialId != null) { + ExceptionUtil.assure( + credential.getCredentialId().equals(credentialId), + "Legacy credentialId is present and does not equal credential.credentialId"); + } + if (userHandle != null) { + ExceptionUtil.assure( + credential.getUserHandle().equals(userHandle), + "Legacy userHandle is present and does not equal credential.userHandle"); + } + this.signatureCount = signatureCount; this.signatureCounterValid = signatureCounterValid; this.clientExtensionOutputs = @@ -132,6 +157,36 @@ private AssertionResult( this.authenticatorExtensionOutputs = authenticatorExtensionOutputs; } + /** + * The credential + * ID of the credential used for the assertion. + * + * @see Credential + * ID + * @see PublicKeyCredentialRequestOptions#getAllowCredentials() + * @deprecated Use {@link #getCredential()}.{@link RegisteredCredential#getCredentialId() + * getCredentialId()} instead. + */ + @Deprecated + public ByteArray getCredentialId() { + return credential.getCredentialId(); + } + + /** + * The user handle + * of the authenticated user. + * + * @see User Handle + * @see UserIdentity#getId() + * @see #getUsername() + * @deprecated Use {@link #getCredential()}.{@link RegisteredCredential#getUserHandle()} () + * getUserHandle()} instead. + */ + @Deprecated + public ByteArray getUserHandle() { + return credential.getUserHandle(); + } + /** * The client @@ -180,49 +235,42 @@ public Step2 success(boolean success) { } public class Step2 { - public Step3 credentialId(ByteArray credentialId) { - builder.credentialId(credentialId); + public Step3 credential(RegisteredCredential credential) { + builder.credential(credential); return new Step3(); } } public class Step3 { - public Step4 userHandle(ByteArray userHandle) { - builder.userHandle(userHandle); + public Step4 username(String username) { + builder.username(username); return new Step4(); } } public class Step4 { - public Step5 username(String username) { - builder.username(username); + public Step5 signatureCount(long signatureCount) { + builder.signatureCount(signatureCount); return new Step5(); } } public class Step5 { - public Step6 signatureCount(long signatureCount) { - builder.signatureCount(signatureCount); + public Step6 signatureCounterValid(boolean signatureCounterValid) { + builder.signatureCounterValid(signatureCounterValid); return new Step6(); } } public class Step6 { - public Step7 signatureCounterValid(boolean signatureCounterValid) { - builder.signatureCounterValid(signatureCounterValid); - return new Step7(); - } - } - - public class Step7 { - public Step8 clientExtensionOutputs( + public Step7 clientExtensionOutputs( ClientAssertionExtensionOutputs clientExtensionOutputs) { builder.clientExtensionOutputs(clientExtensionOutputs); - return new Step8(); + return new Step7(); } } - public class Step8 { + public class Step7 { public AssertionResultBuilder assertionExtensionOutputs( AuthenticatorAssertionExtensionOutputs authenticatorExtensionOutputs) { return builder.authenticatorExtensionOutputs(authenticatorExtensionOutputs); diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishAssertionSteps.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishAssertionSteps.java index 543cd2d96..b0194b501 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishAssertionSteps.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishAssertionSteps.java @@ -203,7 +203,7 @@ class Step7 implements Step { @Override public Step8 nextStep() { - return new Step8(username, userHandle, credential.get()); + return new Step8(username, credential.get()); } @Override @@ -220,7 +220,6 @@ public void validate() { class Step8 implements Step { private final String username; - private final ByteArray userHandle; private final RegisteredCredential credential; @Override @@ -232,7 +231,7 @@ public void validate() { @Override public Step10 nextStep() { - return new Step10(username, userHandle, credential); + return new Step10(username, credential); } public ByteArray authenticatorData() { @@ -253,7 +252,6 @@ public ByteArray signature() { @Value class Step10 implements Step { private final String username; - private final ByteArray userHandle; private final RegisteredCredential credential; @Override @@ -263,7 +261,7 @@ public void validate() { @Override public Step11 nextStep() { - return new Step11(username, userHandle, credential, clientData()); + return new Step11(username, credential, clientData()); } public CollectedClientData clientData() { @@ -274,7 +272,6 @@ public CollectedClientData clientData() { @Value class Step11 implements Step { private final String username; - private final ByteArray userHandle; private final RegisteredCredential credential; private final CollectedClientData clientData; @@ -289,14 +286,13 @@ public void validate() { @Override public Step12 nextStep() { - return new Step12(username, userHandle, credential); + return new Step12(username, credential); } } @Value class Step12 implements Step { private final String username; - private final ByteArray userHandle; private final RegisteredCredential credential; @Override @@ -311,14 +307,13 @@ public void validate() { @Override public Step13 nextStep() { - return new Step13(username, userHandle, credential); + return new Step13(username, credential); } } @Value class Step13 implements Step { private final String username; - private final ByteArray userHandle; private final RegisteredCredential credential; @Override @@ -331,14 +326,13 @@ public void validate() { @Override public Step14 nextStep() { - return new Step14(username, userHandle, credential); + return new Step14(username, credential); } } @Value class Step14 implements Step { private final String username; - private final ByteArray userHandle; private final RegisteredCredential credential; @Override @@ -349,14 +343,13 @@ public void validate() { @Override public Step15 nextStep() { - return new Step15(username, userHandle, credential); + return new Step15(username, credential); } } @Value class Step15 implements Step { private final String username; - private final ByteArray userHandle; private final RegisteredCredential credential; @Override @@ -382,14 +375,13 @@ public void validate() { @Override public Step16 nextStep() { - return new Step16(username, userHandle, credential); + return new Step16(username, credential); } } @Value class Step16 implements Step { private final String username; - private final ByteArray userHandle; private final RegisteredCredential credential; @Override @@ -401,14 +393,13 @@ public void validate() { @Override public Step17 nextStep() { - return new Step17(username, userHandle, credential); + return new Step17(username, credential); } } @Value class Step17 implements Step { private final String username; - private final ByteArray userHandle; private final RegisteredCredential credential; @Override @@ -425,14 +416,13 @@ public void validate() { @Override public Step18 nextStep() { - return new Step18(username, userHandle, credential); + return new Step18(username, credential); } } @Value class Step18 implements Step { private final String username; - private final ByteArray userHandle; private final RegisteredCredential credential; @Override @@ -440,14 +430,13 @@ public void validate() {} @Override public Step19 nextStep() { - return new Step19(username, userHandle, credential); + return new Step19(username, credential); } } @Value class Step19 implements Step { private final String username; - private final ByteArray userHandle; private final RegisteredCredential credential; @Override @@ -457,7 +446,7 @@ public void validate() { @Override public Step20 nextStep() { - return new Step20(username, userHandle, credential, clientDataJsonHash()); + return new Step20(username, credential, clientDataJsonHash()); } public ByteArray clientDataJsonHash() { @@ -468,7 +457,6 @@ public ByteArray clientDataJsonHash() { @Value class Step20 implements Step { private final String username; - private final ByteArray userHandle; private final RegisteredCredential credential; private final ByteArray clientDataJsonHash; @@ -503,7 +491,7 @@ public void validate() { @Override public Step21 nextStep() { - return new Step21(username, userHandle, credential); + return new Step21(username, credential); } public ByteArray signedBytes() { @@ -514,13 +502,11 @@ public ByteArray signedBytes() { @Value class Step21 implements Step { private final String username; - private final ByteArray userHandle; private final RegisteredCredential credential; private final long storedSignatureCountBefore; - public Step21(String username, ByteArray userHandle, RegisteredCredential credential) { + public Step21(String username, RegisteredCredential credential) { this.username = username; - this.userHandle = userHandle; this.credential = credential; this.storedSignatureCountBefore = credential.getSignatureCount(); } @@ -540,7 +526,7 @@ private boolean signatureCounterValid() { @Override public Finished nextStep() { - return new Finished(username, userHandle, assertionSignatureCount(), signatureCounterValid()); + return new Finished(credential, username, assertionSignatureCount(), signatureCounterValid()); } private long assertionSignatureCount() { @@ -550,8 +536,8 @@ private long assertionSignatureCount() { @Value class Finished implements Step { + private final RegisteredCredential credential; private final String username; - private final ByteArray userHandle; private final long assertionSignatureCount; private final boolean signatureCounterValid; @@ -570,8 +556,7 @@ public Optional result() { return Optional.of( AssertionResult.builder() .success(true) - .credentialId(response.getId()) - .userHandle(userHandle) + .credential(credential) .username(username) .signatureCount(assertionSignatureCount) .signatureCounterValid(signatureCounterValid) diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/Generators.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/Generators.scala index cdf29f722..15d346337 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/Generators.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/Generators.scala @@ -25,17 +25,15 @@ object Generators { authenticatorExtensionOutputs <- arbitrary[Option[AuthenticatorAssertionExtensionOutputs]] clientExtensionOutputs <- arbitrary[ClientAssertionExtensionOutputs] - credentialId <- arbitrary[ByteArray] + credential <- arbitrary[RegisteredCredential] signatureCount <- arbitrary[Long] signatureCounterValid <- arbitrary[Boolean] success <- arbitrary[Boolean] - userHandle <- arbitrary[ByteArray] username <- arbitrary[String] } yield AssertionResult .builder() .success(success) - .credentialId(credentialId) - .userHandle(userHandle) + .credential(credential) .username(username) .signatureCount(signatureCount) .signatureCounterValid(signatureCounterValid) diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyAssertionSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyAssertionSpec.scala index da17187b5..fc546d5ef 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyAssertionSpec.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyAssertionSpec.scala @@ -1829,6 +1829,13 @@ class RelyingPartyAssertionSpec step.result.get.isSuccess should be(true) step.result.get.getCredentialId should equal(Defaults.credentialId) step.result.get.getUserHandle should equal(Defaults.userHandle) + step.result.get.getCredential.getCredentialId should equal( + step.result.get.getCredentialId + ) + step.result.get.getCredential.getUserHandle should equal( + step.result.get.getUserHandle + ) + step.result.get.getCredential.getPublicKeyCose should not be null } } } From 0828654f8b6a66f18a7b00df198f3f20b1f578db Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 31 May 2022 17:32:47 +0200 Subject: [PATCH 06/58] Compute assertionSignatureCount only once in FinishAssertionSteps --- .../com/yubico/webauthn/FinishAssertionSteps.java | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishAssertionSteps.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishAssertionSteps.java index b0194b501..c2388207e 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishAssertionSteps.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishAssertionSteps.java @@ -503,11 +503,14 @@ public ByteArray signedBytes() { class Step21 implements Step { private final String username; private final RegisteredCredential credential; + private final long assertionSignatureCount; private final long storedSignatureCountBefore; public Step21(String username, RegisteredCredential credential) { this.username = username; this.credential = credential; + this.assertionSignatureCount = + response.getResponse().getParsedAuthenticatorData().getSignatureCounter(); this.storedSignatureCountBefore = credential.getSignatureCount(); } @@ -515,22 +518,18 @@ public Step21(String username, RegisteredCredential credential) { public void validate() throws InvalidSignatureCountException { if (validateSignatureCounter && !signatureCounterValid()) { throw new InvalidSignatureCountException( - response.getId(), storedSignatureCountBefore + 1, assertionSignatureCount()); + response.getId(), storedSignatureCountBefore + 1, assertionSignatureCount); } } private boolean signatureCounterValid() { - return (assertionSignatureCount() == 0 && storedSignatureCountBefore == 0) - || assertionSignatureCount() > storedSignatureCountBefore; + return (assertionSignatureCount == 0 && storedSignatureCountBefore == 0) + || assertionSignatureCount > storedSignatureCountBefore; } @Override public Finished nextStep() { - return new Finished(credential, username, assertionSignatureCount(), signatureCounterValid()); - } - - private long assertionSignatureCount() { - return response.getResponse().getParsedAuthenticatorData().getSignatureCounter(); + return new Finished(credential, username, assertionSignatureCount, signatureCounterValid()); } } From 6843b800ad068da2490542f8bf27463a6b3aa3f4 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 31 May 2022 18:18:47 +0200 Subject: [PATCH 07/58] Collect archive and signature files in build/dist/ --- build.gradle | 14 ++++++++++++++ doc/releasing.md | 8 ++++---- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index cee16d713..05e4d0717 100644 --- a/build.gradle +++ b/build.gradle @@ -132,6 +132,12 @@ task assembleJavadoc(type: Sync) { destinationDir = file("${rootProject.buildDir}/javadoc") } +task collectSignatures(type: Sync) { + destinationDir = file("${rootProject.buildDir}/dist") + duplicatesStrategy DuplicatesStrategy.FAIL + include '*.jar', '*.jar.asc' +} + String getGitCommit() { def proc = "git rev-parse HEAD".execute(null, projectDir) proc.waitFor() @@ -270,6 +276,14 @@ subprojects { project -> useGpgCmd() sign publishing.publications.jars } + + tasks.withType(Sign) { Sign signTask -> + rootProject.tasks.collectSignatures { + from signTask.inputs.files + from signTask.outputs.files + } + signTask.finalizedBy rootProject.tasks.collectSignatures + } } } diff --git a/doc/releasing.md b/doc/releasing.md index 1aff7522a..df217dd38 100644 --- a/doc/releasing.md +++ b/doc/releasing.md @@ -60,9 +60,9 @@ Release candidate versions from ASCIIdoc to Markdown and remove line wraps. Include only changes/additions since the previous release or pre-release. - Attach the signature files from - `webauthn-server-attestation/build/libs/webauthn-server-attestation-X.Y.Z-RCN.jar.asc` + `build/dist/webauthn-server-attestation-X.Y.Z-RCN.jar.asc` and - `webauthn-server-core/build/libs/webauthn-server-core-X.Y.Z-RCN.jar.asc`. + `build/dist/webauthn-server-core-X.Y.Z-RCN.jar.asc`. - Note which JDK version was used to build the artifacts. @@ -147,8 +147,8 @@ Release versions from ASCIIdoc to Markdown and remove line wraps. Include all changes since the previous release (not just changes since the previous pre-release). - Attach the signature files from - `webauthn-server-attestation/build/libs/webauthn-server-attestation-X.Y.Z.jar.asc` + `build/dist/webauthn-server-attestation-X.Y.Z.jar.asc` and - `webauthn-server-core/build/libs/webauthn-server-core-X.Y.Z.jar.asc`. + `build/dist/webauthn-server-core-X.Y.Z.jar.asc`. - Note which JDK version was used to build the artifacts. From 82bea3faf34c27d17aaf2c942a3e73f0a0697ad3 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 16 Aug 2022 19:01:35 +0200 Subject: [PATCH 08/58] Modularize createCredential methods in TestAuthenticator --- .../yubico/webauthn/TestAuthenticator.scala | 125 +++++++++++------- 1 file changed, 80 insertions(+), 45 deletions(-) diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/TestAuthenticator.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/TestAuthenticator.scala index 88f06de19..00314e378 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/TestAuthenticator.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/TestAuthenticator.scala @@ -302,28 +302,50 @@ object TestAuthenticator { } } - private def createCredential( + private def createAuthenticatorData( aaguid: ByteArray = Defaults.aaguid, - attestationMaker: AttestationMaker, authenticatorExtensions: Option[JsonNode] = None, - challenge: ByteArray = Defaults.challenge, - clientData: Option[JsonNode] = None, - clientExtensions: ClientRegistrationExtensionOutputs = - ClientRegistrationExtensionOutputs.builder().build(), credentialKeypair: Option[KeyPair] = None, keyAlgorithm: COSEAlgorithmIdentifier = Defaults.keyAlgorithm, - origin: String = Defaults.origin, - tokenBindingStatus: String = Defaults.TokenBinding.status, - tokenBindingId: Option[String] = Defaults.TokenBinding.id, ): ( - data.PublicKeyCredential[ - data.AuthenticatorAttestationResponse, - ClientRegistrationExtensionOutputs, - ], + ByteArray, KeyPair, - List[(X509Certificate, PrivateKey)], ) = { + val keypair = + credentialKeypair.getOrElse(generateKeypair(algorithm = keyAlgorithm)) + val publicKeyCose = keypair.getPublic match { + case pub: ECPublicKey => WebAuthnTestCodecs.ecPublicKeyToCose(pub) + case pub: BCEdDSAPublicKey => WebAuthnTestCodecs.eddsaPublicKeyToCose(pub) + case pub: RSAPublicKey => + WebAuthnTestCodecs.rsaPublicKeyToCose(pub, keyAlgorithm) + } + val authDataBytes: ByteArray = makeAuthDataBytes( + rpId = Defaults.rpId, + attestedCredentialDataBytes = Some( + makeAttestedCredentialDataBytes( + aaguid = aaguid, + publicKeyCose = publicKeyCose, + ) + ), + extensionsCborBytes = authenticatorExtensions map (ext => + new ByteArray(JacksonCodecs.cbor().writeValueAsBytes(ext)) + ), + ) + + ( + authDataBytes, + keypair, + ) + } + + private def createClientData( + challenge: ByteArray = Defaults.challenge, + clientData: Option[JsonNode] = None, + origin: String = Defaults.origin, + tokenBindingStatus: String = Defaults.TokenBinding.status, + tokenBindingId: Option[String] = Defaults.TokenBinding.id, + ): String = { val clientDataJson: String = JacksonCodecs.json.writeValueAsString(clientData getOrElse { val json: ObjectNode = jsonFactory.objectNode() @@ -349,29 +371,27 @@ object TestAuthenticator { json }) - val clientDataJsonBytes = toBytes(clientDataJson) - val keypair = - credentialKeypair.getOrElse(generateKeypair(algorithm = keyAlgorithm)) - val publicKeyCose = keypair.getPublic match { - case pub: ECPublicKey => WebAuthnTestCodecs.ecPublicKeyToCose(pub) - case pub: BCEdDSAPublicKey => WebAuthnTestCodecs.eddsaPublicKeyToCose(pub) - case pub: RSAPublicKey => - WebAuthnTestCodecs.rsaPublicKeyToCose(pub, keyAlgorithm) - } + clientDataJson + } - val authDataBytes: ByteArray = makeAuthDataBytes( - rpId = Defaults.rpId, - attestedCredentialDataBytes = Some( - makeAttestedCredentialDataBytes( - aaguid = aaguid, - publicKeyCose = publicKeyCose, - ) - ), - extensionsCborBytes = authenticatorExtensions map (ext => - new ByteArray(JacksonCodecs.cbor().writeValueAsBytes(ext)) - ), - ) + private def createCredential( + authDataBytes: ByteArray, + clientDataJson: String, + credentialKeypair: KeyPair, + attestationMaker: AttestationMaker, + clientExtensions: ClientRegistrationExtensionOutputs = + ClientRegistrationExtensionOutputs.builder().build(), + ): ( + data.PublicKeyCredential[ + data.AuthenticatorAttestationResponse, + ClientRegistrationExtensionOutputs, + ], + KeyPair, + List[(X509Certificate, PrivateKey)], + ) = { + + val clientDataJsonBytes = toBytes(clientDataJson) val attestationObjectBytes = attestationMaker.makeAttestationObjectBytes(authDataBytes, clientDataJson) @@ -391,7 +411,7 @@ object TestAuthenticator { .response(response) .clientExtensionResults(clientExtensions) .build(), - keypair, + credentialKeypair, attestationMaker.certChain, ) } @@ -407,13 +427,20 @@ object TestAuthenticator { ], KeyPair, List[(X509Certificate, PrivateKey)], - ) = - createCredential( + ) = { + val (authData, credentialKeypair) = createAuthenticatorData( aaguid = aaguid, - attestationMaker = attestationMaker, keyAlgorithm = keyAlgorithm, ) + createCredential( + authDataBytes = authData, + credentialKeypair = credentialKeypair, + clientDataJson = createClientData(), + attestationMaker = attestationMaker, + ) + } + def createSelfAttestedCredential( attestationMaker: SelfAttestation => AttestationMaker, keyAlgorithm: COSEAlgorithmIdentifier = Defaults.keyAlgorithm, @@ -425,12 +452,15 @@ object TestAuthenticator { KeyPair, List[(X509Certificate, PrivateKey)], ) = { - val keypair = generateKeypair(keyAlgorithm) + val (authData, keypair) = createAuthenticatorData(credentialKeypair = + Some(generateKeypair(keyAlgorithm)) + ) val signer = SelfAttestation(keypair, keyAlgorithm) createCredential( + authDataBytes = authData, + clientDataJson = createClientData(), + credentialKeypair = keypair, attestationMaker = attestationMaker(signer), - credentialKeypair = Some(keypair), - keyAlgorithm = keyAlgorithm, ) } @@ -444,12 +474,17 @@ object TestAuthenticator { ], KeyPair, List[(X509Certificate, PrivateKey)], - ) = + ) = { + val (authData, keypair) = createAuthenticatorData( + authenticatorExtensions = authenticatorExtensions + ) createCredential( + authDataBytes = authData, + clientDataJson = createClientData(challenge = challenge), + credentialKeypair = keypair, attestationMaker = AttestationMaker.none(), - authenticatorExtensions = authenticatorExtensions, - challenge = challenge, ) + } def createAssertionFromTestData( testData: RegistrationTestData, From 24a7bbd27aa3b089182bfee638c43c9bc200cc27 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 16 May 2022 17:12:33 +0200 Subject: [PATCH 09/58] Add support for tpm attestation --- .../main/java/com/yubico/webauthn/Crypto.java | 9 + .../webauthn/FinishRegistrationSteps.java | 5 +- .../TpmAttestationStatementVerifier.java | 680 ++++++++++++++++++ .../webauthn/RegistrationTestData.scala | 31 +- .../RelyingPartyRegistrationSpec.scala | 130 ++-- 5 files changed, 804 insertions(+), 51 deletions(-) create mode 100644 webauthn-server-core/src/main/java/com/yubico/webauthn/TpmAttestationStatementVerifier.java diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/Crypto.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/Crypto.java index 5893f0dd6..60ddff1bc 100755 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/Crypto.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/Crypto.java @@ -96,9 +96,18 @@ public static boolean verifySignature( public static ByteArray sha256(ByteArray bytes) { //noinspection UnstableApiUsage + // TODO remove noinspection return new ByteArray(Hashing.sha256().hashBytes(bytes.getBytes()).asBytes()); } + public static ByteArray sha384(ByteArray bytes) { + return new ByteArray(Hashing.sha384().hashBytes(bytes.getBytes()).asBytes()); + } + + public static ByteArray sha512(ByteArray bytes) { + return new ByteArray(Hashing.sha512().hashBytes(bytes.getBytes()).asBytes()); + } + public static ByteArray sha256(String str) { return sha256(new ByteArray(str.getBytes(StandardCharsets.UTF_8))); } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishRegistrationSteps.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishRegistrationSteps.java index 868bb9b12..3c6cda6b2 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishRegistrationSteps.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishRegistrationSteps.java @@ -373,6 +373,8 @@ public Optional attestationStatementVerifier() { return Optional.of(new AndroidSafetynetAttestationStatementVerifier()); case "apple": return Optional.of(new AppleAttestationStatementVerifier()); + case "tpm": + return Optional.of(new TpmAttestationStatementVerifier()); default: return Optional.empty(); } @@ -411,9 +413,6 @@ public AttestationType attestationType() { case "android-key": // TODO delete this once android-key attestation verification is implemented return AttestationType.BASIC; - case "tpm": - // TODO delete this once tpm attestation verification is implemented - return AttestationType.ATTESTATION_CA; default: return AttestationType.UNKNOWN; } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/TpmAttestationStatementVerifier.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/TpmAttestationStatementVerifier.java new file mode 100644 index 000000000..b48386cfd --- /dev/null +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/TpmAttestationStatementVerifier.java @@ -0,0 +1,680 @@ +// Copyright (c) 2018, Yubico AB +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package com.yubico.webauthn; + +import COSE.CoseException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.upokecenter.cbor.CBORObject; +import com.yubico.internal.util.BinaryUtil; +import com.yubico.internal.util.ByteInputStream; +import com.yubico.internal.util.CertificateParser; +import com.yubico.internal.util.ExceptionUtil; +import com.yubico.webauthn.data.AttestationObject; +import com.yubico.webauthn.data.AttestationType; +import com.yubico.webauthn.data.ByteArray; +import com.yubico.webauthn.data.COSEAlgorithmIdentifier; +import java.io.IOException; +import java.math.BigInteger; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.cert.CertificateException; +import java.security.cert.CertificateParsingException; +import java.security.cert.X509Certificate; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.RSAPublicKeySpec; +import java.util.Arrays; +import java.util.List; +import javax.naming.InvalidNameException; +import javax.naming.directory.Attributes; +import javax.naming.ldap.LdapName; +import lombok.Value; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +final class TpmAttestationStatementVerifier + implements AttestationStatementVerifier, X5cAttestationStatementVerifier { + + private static final String TPM_VER = "2.0"; + private static final ByteArray TPM_GENERATED_VALUE = ByteArray.fromBase64("/1RDRw=="); + private static final ByteArray TPM_ST_ATTEST_CERTIFY = ByteArray.fromBase64("gBc="); + + private static final int TPM_ALG_NULL = 0x0010; + + private static final String OID_TCG_AT_TPM_MANUFACTURER = "2.23.133.2.1"; + private static final String OID_TCG_AT_TPM_MODEL = "2.23.133.2.2"; + private static final String OID_TCG_AT_TPM_VERSION = "2.23.133.2.3"; + + /** + * Object attributes + * + *

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

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

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

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

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

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

The default is the following list: + *

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

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

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

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

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

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

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

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

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

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

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

Consult the Java From ebb533e3fd6f75eb911bcb729ca95aeb679801d5 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Wed, 7 Sep 2022 15:28:19 +0200 Subject: [PATCH 16/58] Relax Guava version constraint to allow 31.x --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index b071b8243..f32e6e8d5 100644 --- a/build.gradle +++ b/build.gradle @@ -52,7 +52,7 @@ dependencies { api('com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:[2.13.2,3)') api('com.fasterxml.jackson.datatype:jackson-datatype-jdk8:[2.13.2,3)') api('com.fasterxml.jackson.datatype:jackson-datatype-jsr310:[2.13.2,3)') - api('com.google.guava:guava:[24.1.1,31)') + api('com.google.guava:guava:[24.1.1,32)') api('com.upokecenter:cbor:[4.5.1,5)') api('org.apache.httpcomponents:httpclient:[4.5.2,5)') api('org.bouncycastle:bcpkix-jdk15on:[1.62,2)') From 9f8fad7c55fef898173c1eba5d4a504af9cd34d0 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Wed, 7 Sep 2022 15:29:14 +0200 Subject: [PATCH 17/58] Use BouncyCastle jdk18on instead of jdk15on --- build.gradle | 4 ++-- .../build.gradle.kts | 2 +- webauthn-server-attestation/build.gradle.kts | 4 ++-- webauthn-server-core/build.gradle.kts | 4 ++-- webauthn-server-demo/build.gradle.kts | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/build.gradle b/build.gradle index f32e6e8d5..1060070ae 100644 --- a/build.gradle +++ b/build.gradle @@ -55,8 +55,8 @@ dependencies { api('com.google.guava:guava:[24.1.1,32)') api('com.upokecenter:cbor:[4.5.1,5)') api('org.apache.httpcomponents:httpclient:[4.5.2,5)') - api('org.bouncycastle:bcpkix-jdk15on:[1.62,2)') - api('org.bouncycastle:bcprov-jdk15on:[1.62,2)') + api('org.bouncycastle:bcpkix-jdk18on:[1.62,2)') + api('org.bouncycastle:bcprov-jdk18on:[1.62,2)') api('org.slf4j:slf4j-api:[1.7.25,2)') } } diff --git a/test-dependent-projects/java-dep-webauthn-server-core-and-bouncycastle/build.gradle.kts b/test-dependent-projects/java-dep-webauthn-server-core-and-bouncycastle/build.gradle.kts index af9cd1e75..801446db1 100644 --- a/test-dependent-projects/java-dep-webauthn-server-core-and-bouncycastle/build.gradle.kts +++ b/test-dependent-projects/java-dep-webauthn-server-core-and-bouncycastle/build.gradle.kts @@ -6,7 +6,7 @@ val coreTestsOutput = project(":webauthn-server-core").extensions.getByType(Sour dependencies { implementation(project(":webauthn-server-core")) - implementation("org.bouncycastle:bcprov-jdk15on:[1.62,2)") + implementation("org.bouncycastle:bcprov-jdk18on:[1.62,2)") testImplementation(coreTestsOutput) testImplementation("junit:junit:4.12") diff --git a/webauthn-server-attestation/build.gradle.kts b/webauthn-server-attestation/build.gradle.kts index 859012647..6fbfd417d 100644 --- a/webauthn-server-attestation/build.gradle.kts +++ b/webauthn-server-attestation/build.gradle.kts @@ -40,7 +40,7 @@ dependencies { implementation(project(":yubico-util")) implementation("com.google.guava:guava") implementation("com.fasterxml.jackson.core:jackson-databind") - implementation("org.bouncycastle:bcprov-jdk15on") + implementation("org.bouncycastle:bcprov-jdk18on") implementation("org.slf4j:slf4j-api") testImplementation(platform(project(":test-platform"))) @@ -48,7 +48,7 @@ dependencies { testImplementation(project(":yubico-util-scala")) testImplementation("com.fasterxml.jackson.datatype:jackson-datatype-jdk8") testImplementation("junit:junit") - testImplementation("org.bouncycastle:bcpkix-jdk15on") + testImplementation("org.bouncycastle:bcpkix-jdk18on") testImplementation("org.eclipse.jetty:jetty-server:[9.4.9.v20180320,10)") testImplementation("org.mockito:mockito-core") testImplementation("org.scala-lang:scala-library") diff --git a/webauthn-server-core/build.gradle.kts b/webauthn-server-core/build.gradle.kts index 61378da22..800d3aef5 100644 --- a/webauthn-server-core/build.gradle.kts +++ b/webauthn-server-core/build.gradle.kts @@ -35,8 +35,8 @@ dependencies { testImplementation("com.fasterxml.jackson.core:jackson-databind") testImplementation("com.upokecenter:cbor") testImplementation("junit:junit") - testImplementation("org.bouncycastle:bcpkix-jdk15on") - testImplementation("org.bouncycastle:bcprov-jdk15on") + testImplementation("org.bouncycastle:bcpkix-jdk18on") + testImplementation("org.bouncycastle:bcprov-jdk18on") testImplementation("org.mockito:mockito-core") testImplementation("org.scala-lang:scala-library") testImplementation("org.scalacheck:scalacheck_2.13") diff --git a/webauthn-server-demo/build.gradle.kts b/webauthn-server-demo/build.gradle.kts index eef456a6a..e3653bf6e 100644 --- a/webauthn-server-demo/build.gradle.kts +++ b/webauthn-server-demo/build.gradle.kts @@ -23,7 +23,7 @@ dependencies { implementation("com.fasterxml.jackson.core:jackson-databind") implementation("com.google.guava:guava") implementation("com.upokecenter:cbor") - implementation("org.bouncycastle:bcprov-jdk15on") + implementation("org.bouncycastle:bcprov-jdk18on") implementation("org.slf4j:slf4j-api") implementation("org.eclipse.jetty:jetty-servlet:9.4.9.v20180320") From fa04cff48844f39950701a798c6bc24ece163cbe Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Wed, 7 Sep 2022 15:29:51 +0200 Subject: [PATCH 18/58] Relax SLF4J version constraint to allow 2.x --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 1060070ae..ef002984a 100644 --- a/build.gradle +++ b/build.gradle @@ -57,7 +57,7 @@ dependencies { api('org.apache.httpcomponents:httpclient:[4.5.2,5)') api('org.bouncycastle:bcpkix-jdk18on:[1.62,2)') api('org.bouncycastle:bcprov-jdk18on:[1.62,2)') - api('org.slf4j:slf4j-api:[1.7.25,2)') + api('org.slf4j:slf4j-api:[1.7.25,3)') } } From a9a2aad0a5c27d2cc5cce199577dbdb0f6d92854 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 13 Sep 2022 19:30:31 +0200 Subject: [PATCH 19/58] Revert "Apply spotless plugin declaratively" This reverts commit ebfeb623b795e6066164d6d22d521df0a21daa68. --- build.gradle | 5 +++-- webauthn-server-attestation/build.gradle.kts | 1 - webauthn-server-core/build.gradle.kts | 1 - webauthn-server-demo/build.gradle.kts | 1 - yubico-util-scala/build.gradle.kts | 1 - yubico-util/build.gradle.kts | 1 - 6 files changed, 3 insertions(+), 7 deletions(-) diff --git a/build.gradle b/build.gradle index eeecba019..c02cc562b 100644 --- a/build.gradle +++ b/build.gradle @@ -94,7 +94,8 @@ subprojects { mavenCentral() } - if (project.plugins.hasPlugin('com.diffplug.spotless')) { + if (project !== project(':test-platform')) { + apply plugin: 'com.diffplug.spotless' spotless { java { googleJavaFormat() @@ -125,7 +126,7 @@ task collectSignatures(type: Sync) { subprojects { project -> - if (project.plugins.hasPlugin('scala') && project.plugins.hasPlugin('com.diffplug.spotless')) { + if (project.plugins.hasPlugin('scala')) { project.scalafix { configFile = rootProject.file('scalafix.conf') diff --git a/webauthn-server-attestation/build.gradle.kts b/webauthn-server-attestation/build.gradle.kts index 6fbfd417d..3225a235d 100644 --- a/webauthn-server-attestation/build.gradle.kts +++ b/webauthn-server-attestation/build.gradle.kts @@ -4,7 +4,6 @@ plugins { scala `maven-publish` signing - id("com.diffplug.spotless") id("info.solidsoft.pitest") id("io.github.cosmicsilence.scalafix") } diff --git a/webauthn-server-core/build.gradle.kts b/webauthn-server-core/build.gradle.kts index 800d3aef5..09ceca25f 100644 --- a/webauthn-server-core/build.gradle.kts +++ b/webauthn-server-core/build.gradle.kts @@ -4,7 +4,6 @@ plugins { scala `maven-publish` signing - id("com.diffplug.spotless") id("info.solidsoft.pitest") id("io.github.cosmicsilence.scalafix") } diff --git a/webauthn-server-demo/build.gradle.kts b/webauthn-server-demo/build.gradle.kts index e3653bf6e..c3c65b514 100644 --- a/webauthn-server-demo/build.gradle.kts +++ b/webauthn-server-demo/build.gradle.kts @@ -3,7 +3,6 @@ plugins { war application scala - id("com.diffplug.spotless") id("io.github.cosmicsilence.scalafix") } diff --git a/yubico-util-scala/build.gradle.kts b/yubico-util-scala/build.gradle.kts index d9baf98c6..7ffbc14c7 100644 --- a/yubico-util-scala/build.gradle.kts +++ b/yubico-util-scala/build.gradle.kts @@ -1,6 +1,5 @@ plugins { scala - id("com.diffplug.spotless") id("io.github.cosmicsilence.scalafix") } diff --git a/yubico-util/build.gradle.kts b/yubico-util/build.gradle.kts index 914805a33..83d58d030 100644 --- a/yubico-util/build.gradle.kts +++ b/yubico-util/build.gradle.kts @@ -3,7 +3,6 @@ plugins { scala `maven-publish` signing - id("com.diffplug.spotless") id("info.solidsoft.pitest") id("io.github.cosmicsilence.scalafix") } From 20194f1da3bcc0fc3053b912ed265110aa05dfad Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 13 Sep 2022 19:36:05 +0200 Subject: [PATCH 20/58] ./gradlew spotlessApply --- .../fido/metadata/FidoMetadataServiceIntegrationTest.scala | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/webauthn-server-attestation/src/integrationTest/scala/com/yubico/fido/metadata/FidoMetadataServiceIntegrationTest.scala b/webauthn-server-attestation/src/integrationTest/scala/com/yubico/fido/metadata/FidoMetadataServiceIntegrationTest.scala index 4c6a5a8fc..39e1f9311 100644 --- a/webauthn-server-attestation/src/integrationTest/scala/com/yubico/fido/metadata/FidoMetadataServiceIntegrationTest.scala +++ b/webauthn-server-attestation/src/integrationTest/scala/com/yubico/fido/metadata/FidoMetadataServiceIntegrationTest.scala @@ -186,7 +186,11 @@ class FidoMetadataServiceIntegrationTest } it("a YubiKey 5Ci.") { - check("YubiKey 5 .*Lightning", RealExamples.YubiKey5Ci, attachmentHintsUsb) + check( + "YubiKey 5 .*Lightning", + RealExamples.YubiKey5Ci, + attachmentHintsUsb, + ) } ignore("a Security Key by Yubico.") { // TODO: Investigate why this fails From c63ab59567403ebae6546965a824a1d3d94ad4a3 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 12 Sep 2022 15:38:07 +0200 Subject: [PATCH 21/58] Delete empty test case container --- .../fido/metadata/FidoMetadataServiceIntegrationTest.scala | 4 ---- 1 file changed, 4 deletions(-) diff --git a/webauthn-server-attestation/src/integrationTest/scala/com/yubico/fido/metadata/FidoMetadataServiceIntegrationTest.scala b/webauthn-server-attestation/src/integrationTest/scala/com/yubico/fido/metadata/FidoMetadataServiceIntegrationTest.scala index 39e1f9311..4a1e4dfbb 100644 --- a/webauthn-server-attestation/src/integrationTest/scala/com/yubico/fido/metadata/FidoMetadataServiceIntegrationTest.scala +++ b/webauthn-server-attestation/src/integrationTest/scala/com/yubico/fido/metadata/FidoMetadataServiceIntegrationTest.scala @@ -60,10 +60,6 @@ class FidoMetadataServiceIntegrationTest val attachmentHintsNfc = attachmentHintsUsb ++ Set(ATTACHMENT_HINT_WIRELESS, ATTACHMENT_HINT_NFC) - describe("by AAGUID") { - describe("correctly identifies") {} - } - describe("correctly identifies") { def check( expectedDescriptionRegex: String, From 43c2db294d8cff311a1f52b3c7f86d26b71d81ea Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 12 Sep 2022 15:45:54 +0200 Subject: [PATCH 22/58] Remove tpm attestation from list of unimplemented features --- webauthn-server-core/README | 1 - 1 file changed, 1 deletion(-) diff --git a/webauthn-server-core/README b/webauthn-server-core/README index a4d096156..4da98cd32 100644 --- a/webauthn-server-core/README +++ b/webauthn-server-core/README @@ -14,7 +14,6 @@ it. == Unimplemented features * Attestation statement formats: - ** https://www.w3.org/TR/webauthn/#sctn-tpm-attestation[`tpm`] ** https://www.w3.org/TR/webauthn/#sctn-android-key-attestation[`android-key`] From 0b2baba5d15dcbf6b94fe3983661695674fef2e8 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 12 Sep 2022 15:47:32 +0200 Subject: [PATCH 23/58] Move Windows Hello example from RegistrationTestData to RealExamples --- .../webauthn/RegistrationTestData.scala | 35 +------- .../webauthn/RelyingPartyCeremoniesSpec.scala | 8 +- .../RelyingPartyRegistrationSpec.scala | 15 ++-- .../yubico/webauthn/data/ExtensionsSpec.scala | 4 +- .../yubico/webauthn/test/RealExamples.scala | 81 ++++++++++++++----- 5 files changed, 75 insertions(+), 68 deletions(-) diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RegistrationTestData.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RegistrationTestData.scala index 0dddc9864..4497fcdfc 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RegistrationTestData.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RegistrationTestData.scala @@ -48,6 +48,7 @@ import com.yubico.webauthn.data.PublicKeyCredentialParameters import com.yubico.webauthn.data.RegistrationExtensionInputs import com.yubico.webauthn.data.RelyingPartyIdentity import com.yubico.webauthn.data.UserIdentity +import com.yubico.webauthn.test.RealExamples import org.bouncycastle.asn1.ASN1ObjectIdentifier import org.bouncycastle.asn1.DERSequence import org.bouncycastle.asn1.DERUTF8String @@ -178,11 +179,11 @@ object RegistrationTestData { Packed.BasicAttestationRsa, Packed.BasicAttestationRsaReal, Packed.SelfAttestation, - Tpm.RealExample, Tpm.ValidEs256, Tpm.ValidEs384, Tpm.ValidEs512, Tpm.ValidRs256, + RealExamples.WindowsHelloTpm.asRegistrationTestData, ) object AndroidKey { @@ -646,38 +647,6 @@ object RegistrationTestData { ), ) - val RealExample: RegistrationTestData = - new RegistrationTestData( - alg = COSEAlgorithmIdentifier.RS256, - // Real attestation object from Windows - attestationObject = - ByteArray.fromBase64Url("o2NmbXRjdHBtZ2F0dFN0bXSmY2FsZzn__mNzaWdZAQBFEaTe-uZvbZBNsIMtJa26eigMUxEM1mBtddR7gdEBH5Hyeo9hFCqiJwYVKUq_iP9hvFaiLzoGbAWDgiG-fa3F-S71c8w83756dyRBMXNHYEvYjfv0TqGyky73V4xyKpf1iHiO_g4t31UjQiyTfypdP_rRcm42KVKgVyRPZzx_AKweN9XKEFfT2Ym3fmqD_scaIeKSyGs9qwH1MbILLUVnRK6fKK6sAA4ZaDVz4gUiSUoK9ZycCC2hfLBq5GjiTLgQF_Q2O3gRTqmU8VfwVsmtN5OMaGOyaFrUk97-RvZVrARXhNzrUAJT7KjTLDZeIA96F3pB_F_q3xd_dgvwVpWHY3ZlcmMyLjBjeDVjglkFuzCCBbcwggOfoAMCAQICEHHcna7VCE3QpRyKgi2uvXYwDQYJKoZIhvcNAQELBQAwQTE_MD0GA1UEAxM2RVVTLU5UQy1LRVlJRC04ODJGMDQ3Qjg3MTIxQ0Y5ODg1RjMxMTYwQkM3QkI1NTg2QUY0NzFCMB4XDTIyMDEyMDE5NTQxNloXDTI3MDYwMzE3NTE0OFowADCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALtT5frDB-WUq6N0VYnlYEalJzSut0JQx3vP29_ub-kZ7csJrm8uGXQGUkPlf4EFehFTnQ1jX_oZ8jNPw1m3rV5ijcuCe3r5GICFD6gpbuErmGS2mDVfe3fl_p0gPvhtulqatb1uYkWfW5SIKix1XWRvm92s3lRQvd-6vX_ExPIP-pEf0tkeINpBNNWgdtx3VdW4KVFTcv-q2FKhqfqXiAdOMHmwmWyXYulppYqW2XC7Pw9QmHZR_C5Urpc5UMmABz4zWSAYOyBMkKsX8koAsk8RgLtus07wW3FhJqi-BYczIe0IxG0q9UL295lkaxreTkfWYZHMMcU4M-Tm1w7QvKsCAwEAAaOCAeowggHmMA4GA1UdDwEB_wQEAwIHgDAMBgNVHRMBAf8EAjAAMG0GA1UdIAEB_wRjMGEwXwYJKwYBBAGCNxUfMFIwUAYIKwYBBQUHAgIwRB5CAFQAQwBQAEEAIAAgAFQAcgB1AHMAdABlAGQAIAAgAFAAbABhAHQAZgBvAHIAbQAgACAASQBkAGUAbgB0AGkAdAB5MBAGA1UdJQQJMAcGBWeBBQgDMFAGA1UdEQEB_wRGMESkQjBAMT4wEAYFZ4EFAgIMB05QQ1Q3NXgwFAYFZ4EFAgEMC2lkOjRFNTQ0MzAwMBQGBWeBBQIDDAtpZDowMDA3MDAwMjAfBgNVHSMEGDAWgBSMmnF_AA0xD8rW7i0pqjSXJYwSHjAdBgNVHQ4EFgQUFmUMIda76eb7Whi8CweaWhe7yNMwgbIGCCsGAQUFBwEBBIGlMIGiMIGfBggrBgEFBQcwAoaBkmh0dHA6Ly9hemNzcHJvZGV1c2Fpa3B1Ymxpc2guYmxvYi5jb3JlLndpbmRvd3MubmV0L2V1cy1udGMta2V5aWQtODgyZjA0N2I4NzEyMWNmOTg4NWYzMTE2MGJjN2JiNTU4NmFmNDcxYi84ODIzMGNhMi0yN2U1LTQxNTEtOWJhMi01OWI1ODJjMzlhYWEuY2VyMA0GCSqGSIb3DQEBCwUAA4ICAQCxsTbR5V8qnw6H6HEWJvrqcRy8fkY_vFUSjUq27hRl0t9D6LuS20l65FFm48yLwCkQbIf-aOBjwWafAbSVnEMig3KP-2Ml8IFtH63Msq9lwDlnXx2PNi7ISOemHNzBNeOG7pd_Zs69XUTq9zCriw9gAILCVCYllBluycdT7wZdjf0Bb5QJtTMuhwNXnOWmjv0VBOfsclWo-SEnnufaIDi0Vcf_TzbgmNn408Ej7R4Njy4qLnhPk64ruuWNJt3xlLMjbJXe_VKdO3lhM7JVFWSNAn8zfvEIwrrgCPhp1k2mFUGxJEvpSTnuZtNF35z4_54K6cEqZiqO-qd4FKt4KYs1GYJDyxttuUySGtnYyZg2aYB6hamg3asRDjBMPqoURsdVJcWQh3dFnD88cbs7Qt4_ytqAY61qfPE7bJ6E33o0X7OtxmECPd3aBJk6nsyXEXNF2vIww1UCrRC0OEr1HsTqA4bQU8KCWV6kduUnvkUWPT8CF0d2ER4wnszb053Tlcf2ebcytTMf_Nd95g520Hhqb2FZALCErijBi04Bu6SNeND1NQ3nxDSKC-CamOYW0ODch05Xzi1V0_sq0zmdKTxMSpg1jOZ1Q9924D4lJkruCB3zcsIBTUxV0EgAM1zGuoqwWjwYXr_8tO4_kEO1Lw8DckZIrk1s3ySsMVC89TRrIVkG7zCCBuswggTToAMCAQICEzMAAAQI5W53M7IUDf4AAAAABAgwDQYJKoZIhvcNAQELBQAwgYwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xNjA0BgNVBAMTLU1pY3Jvc29mdCBUUE0gUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgMjAxNDAeFw0yMTA2MDMxNzUxNDhaFw0yNzA2MDMxNzUxNDhaMEExPzA9BgNVBAMTNkVVUy1OVEMtS0VZSUQtODgyRjA0N0I4NzEyMUNGOTg4NUYzMTE2MEJDN0JCNTU4NkFGNDcxQjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMEye3QdCUOGeseHj_QjLMJVbHBEJKkRbXFsmZi6Mob_3IsVfxO-mQQC-xfY9tDyB1jhxfFAG-rjUfRVBwYYPfaVo2W58Q09Kcpf-43Iw9SRxx2ThP-KPFJPFBofZroloNaTNz3DRaWZ2ha-_PUG2nwTXR7LoIpqMVW1PDzGxb47SNpRmKJxVZhQ2_wRhZZvHRHJpZmrCmHRpTRqWSzQT1jn7Zo9VuMYvp_OFj7-LFpkqi4BYyhi0kTBPDQTpYrBi7RtmF1MhZBmm1HGDhoXHcPSZkN5vq5at4g03R15KWyRDgBcckCAgtewd6Dtd_Zwaejlm57xyGqP6T-AE-N8udh1NPv_PZVlSc4CnCayUTPORuaJ7N-v7Y4wpNSIdipq29hw19WVuO_z7q6GpQbn17arYf6LSoDZfwO8GHXPrtBOYYSZCNKuZ_IK8nomBLJPtN5AzwEZNyLCZIkg0U0sJ-oVr2UEYxlwwZQm5RSDxProaKU-OXq4f_j_0pEu5_DbJx9syR3Nsv6Lt9Zkf3JSJTVtWXoM0-R_82vAJ669PX0LLr603PKWBZbW7zQvtGojT_Pc1FDGfwhcdckxd3MGpEjZwh_1D8elYcxj3Ndw5jClWosZKr33pUcjqeFtSZSur0lbm6vyCfS16XzSMn8IkHmbbXcpgGKHumUCFD8CHJIBAgMBAAGjggGOMIIBijAOBgNVHQ8BAf8EBAMCAoQwGwYDVR0lBBQwEgYJKwYBBAGCNxUkBgVngQUIAzAWBgNVHSAEDzANMAsGCSsGAQQBgjcVHzASBgNVHRMBAf8ECDAGAQH_AgEAMB0GA1UdDgQWBBSMmnF_AA0xD8rW7i0pqjSXJYwSHjAfBgNVHSMEGDAWgBR6jArOL0hiF-KU0a5VwVLscXSkVjBwBgNVHR8EaTBnMGWgY6Bhhl9odHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NybC9NaWNyb3NvZnQlMjBUUE0lMjBSb290JTIwQ2VydGlmaWNhdGUlMjBBdXRob3JpdHklMjAyMDE0LmNybDB9BggrBgEFBQcBAQRxMG8wbQYIKwYBBQUHMAKGYWh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY2VydHMvTWljcm9zb2Z0JTIwVFBNJTIwUm9vdCUyMENlcnRpZmljYXRlJTIwQXV0aG9yaXR5JTIwMjAxNC5jcnQwDQYJKoZIhvcNAQELBQADggIBAHG-1grb-6xpObMtxfFScl8PRLd_GjLFaeAd0kVPls0jzKplG2Am4O87Qg0OUY0VhQ-uD39590gGrWEWnOmdrVJ-R1lJc1yrIZFEASBEedJSvxw9YTNknD59uXtznIP_4Glk-4NpqpcYov2OkBdV59V4dTL5oWFH0vzkZQfvGFxEHwtB9O6Bh0Lk142zXAh5_vf_-hSw3t9adloBAnA0AtPUVkzmgNRGeTpfPm-Iud-MAoUXaccFn2EChjKb9ApbS1ww8ZvX4x2kFU6qctu32g7Vf6CgACc1i-UYDT_E-h6c1O4n7JK2OxVS-DVwybps-cALU0gj-ZMauNMej0_x_NerzvDuQ77eFMTDMY4ZTzYOzlg4Nj0K8y1Bx_KqeTBO0N9CdEG3dBxWCUUFQzAx-i38xJL-dtYTCCFATVhc9FFJ0CQgU07JAGAeuNm_GL8kN46bMXd_ApQYFzDQUWYYXvRIt9mCw0Zd45lpAMuiDKT9TgjUVDNu8LQ8FPK0KeiQVrGHFMhHgg2pbVH9Pvc1jNEeRpCo0BLpZQwuIgEt90mepkt6Va-C9krHsU4y2oalG2LUu-jOC3NWNK8LssYVUCFWtaKh5d-xdTQmjx4uO-1sq9GFntVJ94QnEDhldz0XMopQ8srTGLMqR3MT-GkSNb5X1UFC-X1udXI8YvB3ADr_Z3B1YkFyZWFZATYAAQALAAYEcgAgnf_L82w4OuaZ-5ho3G3LidcVOIS-KAOSLBJBWL-tIq4AEAAQCAAAAAAAAQC6zY02JuN_qf28iVCISa6d6aUM5sS2PsKvZMO0P_-28lLX_9-xbmbEcTPvCj5aIEqVUfCb07U4qCBBUdaWygAbhAckvzYrACm5I8hjMoGkxMkZLw5kX0SjtHx6VfgksElxnu4DcC2g9pqL_McgddZV0zNGrYNj1iamzoxTyOcYyvZjQv_4gU-t9mEc0uqHHv-H6k23p4mensyaGAhkGTV8odyBxsNpdnR8IPnXWPO7tBDTbk4mg3VtclqLhS0-TCh_QnZ6lcEl27wTE8FjwqVdQG9F1Ouyn-eNAAq0EufbzwjSNpuDrlj-Kj5xY_lS5BMEjQUXqP-U5nzW22TehX8VaGNlcnRJbmZvWKH_VENHgBcAIgALt-OVET9fzihC1dzyG5xG70MVdRkPpSEkWQ0nns4zaYkAFGo6XjXu3JY-wup-EHJYWEuLpb45AAAACMgZxo6-OwT1-cIZwQGjXsp0dUws1gAiAAvzCsernlTAewF0QwNOA-iWpyhGxC-k_xBRjTtGNz9m6gAiAAs54tyczU1aCAh_N3Vy3qcAnC9zGeOxtQQKCe8CAanbbGhhdXRoRGF0YVkBZ-MWwK8fdtoeGn4DEn0TAUu4IUP_PMiBiJd4lDRznbBDRQAAAAAImHBYytxLgbbhMN5Q3L6WACBxLUIzn9ngKAM11_UwWG7kCiAvVyO1mYGSsEhfWeyhDaQBAwM5AQAgWQEAus2NNibjf6n9vIlQiEmunemlDObEtj7Cr2TDtD__tvJS1__fsW5mxHEz7wo-WiBKlVHwm9O1OKggQVHWlsoAG4QHJL82KwApuSPIYzKBpMTJGS8OZF9Eo7R8elX4JLBJcZ7uA3AtoPaai_zHIHXWVdMzRq2DY9Ymps6MU8jnGMr2Y0L_-IFPrfZhHNLqhx7_h-pNt6eJnp7MmhgIZBk1fKHcgcbDaXZ0fCD511jzu7QQ025OJoN1bXJai4UtPkwof0J2epXBJdu8ExPBY8KlXUBvRdTrsp_njQAKtBLn288I0jabg65Y_io-cWP5UuQTBI0FF6j_lOZ81ttk3oV_FSFDAQAB"), - clientDataJson = new String( - ByteArray - .fromBase64Url( - "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoia0lnZElRbWFBbXM1NlVOencwREg4dU96M0JERjJVSllhSlA2eklRWDFhOCIsIm9yaWdpbiI6Imh0dHBzOi8vZGV2LmQydXJweXB2cmhiMDV4LmFtcGxpZnlhcHAuY29tIiwiY3Jvc3NPcmlnaW4iOmZhbHNlfQ" - ) - .getBytes, - StandardCharsets.UTF_8, - ), - rpId = RelyingPartyIdentity - .builder() - .id("d2urpypvrhb05x.amplifyapp.com") - .name("") - .build(), - userId = UserIdentity - .builder() - .name("foo") - .displayName("Foo Bar") - .id( - ByteArray.fromBase64Url("AAAA") - ) - .build(), - attestationRootCertificate = Some( - CertificateParser.parsePem("MIIF9TCCA92gAwIBAgIQXbYwTgy/J79JuMhpUB5dyzANBgkqhkiG9w0BAQsFADCBjDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjE2MDQGA1UEAxMtTWljcm9zb2Z0IFRQTSBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAyMDE0MB4XDTE0MTIxMDIxMzExOVoXDTM5MTIxMDIxMzkyOFowgYwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xNjA0BgNVBAMTLU1pY3Jvc29mdCBUUE0gUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgMjAxNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAJ+n+bnKt/JHIRC/oI/xgkgsYdPzP0gpvduDA2GbRtth+L4WUyoZKGBw7uz5bjjP8Aql4YExyjR3EZQ4LqnZChMpoCofbeDR4MjCE1TGwWghGpS0mM3GtWD9XiME4rE2K0VW3pdN0CLzkYbvZbs2wQTFfE62yNQiDjyHFWAZ4BQH4eWa8wrDMUxIAneUCpU6zCwM+l6Qh4ohX063BHzXlTSTc1fDsiPaKuMMjWjK9vp5UHFPa+dMAWr6OljQZPFIg3aZ4cUfzS9y+n77Hs1NXPBn6E4Db679z4DThIXyoKeZTv1aaWOWl/exsDLGt2mTMTyykVV8uD1eRjYriFpmoRDwJKAEMOfaURarzp7hka9TOElGyD2gOV4Fscr2MxAYCywLmOLzA4VDSYLuKAhPSp7yawET30AvY1HRfMwBxetSqWP2+yZRNYJlHpor5QTuRDgzR+Zej+aWx6rWNYx43kLthozeVJ3QCsD5iEI/OZlmWn5WYf7O8LB/1A7scrYv44FD8ck3Z+hxXpkklAsjJMsHZa9mBqh+VR1AicX4uZG8m16x65ZU2uUpBa3rn8CTNmw17ZHOiuSWJtS9+PrZVA8ljgf4QgA1g6NPOEiLG2fn8Gm+r5Ak+9tqv72KDd2FPBJ7Xx4stYj/WjNPtEUhW4rcLK3ktLfcy6ea7Rocw5y5AgMBAAGjUTBPMAsGA1UdDwQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR6jArOL0hiF+KU0a5VwVLscXSkVjAQBgkrBgEEAYI3FQEEAwIBADANBgkqhkiG9w0BAQsFAAOCAgEAW4ioo1+J9VWC0UntSBXcXRm1ePTVamtsxVy/GpP4EmJd3Ub53JzNBfYdgfUL51CppS3ZY6BoagB+DqoA2GbSL+7sFGHBl5ka6FNelrwsH6VVw4xV/8klIjmqOyfatPYsz0sUdZev+reeiGpKVoXrK6BDnUU27/mgPtem5YKWvHB/soofUrLKzZV3WfGdx9zBr8V0xW6vO3CKaqkqU9y6EsQw34n7eJCbEVVQ8VdFd9iV1pmXwaBAfBwkviPTKEP9Cm+zbFIOLr3V3CL9hJj+gkTUuXWlJJ6wVXEG5i4rIbLAV59UrW4LonP+seqvWMJYUFxu/niF0R3fSGM+NU11DtBVkhRZt1u0kFhZqjDz1dWyfT/N7Hke3WsDqUFsBi+8SEw90rWx2aUkLvKo83oU4Mx4na+2I3l9F2a2VNGk4K7l3a00g51miPiq0Da0jqw30PaLluTMTGY5+RnZVh50JD6nk+Ea3wRkU8aiYFnpIxfKBZ72whmYYa/egj9IKeqpR0vuLebbU0fJBf880K1jWD3Z5SFyJXo057Mv0OPw5mttytE585ZIy5JsaRXlsOoWGRXE3kUT/MKR1UoAgR54c8Bsh+9Dq2wqIK9mRn15zvBDeyHG6+czurLopziOUeWokxZN1syrEdKlhFoPYavm6t+PzIcpdxZwHA+V3jLJPfI=") - ), - ) - val ValidEs256: RegistrationTestData = new RegistrationTestData( alg = COSEAlgorithmIdentifier.ES256, attestationObject = diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyCeremoniesSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyCeremoniesSpec.scala index 04794e601..94adc5a50 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyCeremoniesSpec.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyCeremoniesSpec.scala @@ -91,12 +91,12 @@ class RelyingPartyCeremoniesSpec .publicKeyCredentialRequestOptions( PublicKeyCredentialRequestOptions .builder() - .challenge(testData.assertion.challenge) + .challenge(testData.assertion.get.challenge) .allowCredentials( List( PublicKeyCredentialDescriptor .builder() - .id(testData.assertion.id) + .id(testData.assertion.get.id) .build() ).asJava ) @@ -105,12 +105,12 @@ class RelyingPartyCeremoniesSpec .username(testData.user.getName) .build() ) - .response(testData.assertion.credential) + .response(testData.assertion.get.credential) .build() ) assertionResult.isSuccess should be(true) - assertionResult.getCredentialId should equal(testData.assertion.id) + assertionResult.getCredentialId should equal(testData.assertion.get.id) assertionResult.getUserHandle should equal(testData.user.getId) assertionResult.getUsername should equal(testData.user.getName) assertionResult.getSignatureCount should be >= testData.attestation.authenticatorData.getSignatureCounter diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyRegistrationSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyRegistrationSpec.scala index e0c11aa41..2e9698801 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyRegistrationSpec.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyRegistrationSpec.scala @@ -2085,7 +2085,7 @@ class RelyingPartyRegistrationSpec describe("The tpm statement format") { it("is supported.") { - val testData = RegistrationTestData.Tpm.RealExample + val testData = RealExamples.WindowsHelloTpm.asRegistrationTestData val steps = finishRegistration( testData = testData, @@ -3576,7 +3576,7 @@ class RelyingPartyRegistrationSpec } describe("A tpm attestation") { - val testData = RegistrationTestData.Tpm.RealExample + val testData = RealExamples.WindowsHelloTpm.asRegistrationTestData generateTests( testData = testData, clock = Clock.fixed( @@ -3594,7 +3594,8 @@ class RelyingPartyRegistrationSpec def init( policyTreeValidator: Option[Predicate[PolicyNode]] ): FinishRegistrationSteps#Step21 = { - val testData = RegistrationTestData.Tpm.RealExample + val testData = + RealExamples.WindowsHelloTpm.asRegistrationTestData val clock = Clock.fixed( Instant.parse("2022-08-25T16:00:00Z"), ZoneOffset.UTC, @@ -3813,7 +3814,7 @@ class RelyingPartyRegistrationSpec testUntrusted(RegistrationTestData.AndroidSafetynet.BasicAttestation) testUntrusted(RegistrationTestData.FidoU2f.BasicAttestation) testUntrusted(RegistrationTestData.NoneAttestation.Default) - testUntrusted(RegistrationTestData.Tpm.RealExample) + testUntrusted(RealExamples.WindowsHelloTpm.asRegistrationTestData) } } } @@ -3904,7 +3905,7 @@ class RelyingPartyRegistrationSpec } it("accept TPM attestations but report they're untrusted.") { - val testData = RegistrationTestData.Tpm.RealExample + val testData = RealExamples.WindowsHelloTpm.asRegistrationTestData val result = rp.toBuilder .identity(testData.rpId) .origins(Set("https://dev.d2urpypvrhb05x.amplifyapp.com").asJava) @@ -3921,7 +3922,7 @@ class RelyingPartyRegistrationSpec result.isAttestationTrusted should be(false) result.getKeyId.getId should equal( - RegistrationTestData.Tpm.RealExample.response.getId + RealExamples.WindowsHelloTpm.asRegistrationTestData.response.getId ) } @@ -4477,7 +4478,7 @@ class RelyingPartyRegistrationSpec } it("for a tpm attestation.") { - val testData = RegistrationTestData.Tpm.RealExample + val testData = RealExamples.WindowsHelloTpm.asRegistrationTestData val steps = finishRegistration( testData = testData, origins = Some(Set("https://dev.d2urpypvrhb05x.amplifyapp.com")), diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/ExtensionsSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/ExtensionsSpec.scala index 9abddb052..6ded9bce3 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/ExtensionsSpec.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/ExtensionsSpec.scala @@ -321,7 +321,7 @@ class ExtensionsSpec it("can deserialize a real largeBlob write example.") { val testData = RealExamples.LargeBlobWrite val registrationCred = testData.attestation.credential - val assertionCred = testData.assertion.credential + val assertionCred = testData.assertion.get.credential registrationCred.getClientExtensionResults.getExtensionIds.asScala should equal( Set("largeBlob") @@ -341,7 +341,7 @@ class ExtensionsSpec it("can deserialize a real largeBlob read example.") { val testData = RealExamples.LargeBlobRead val registrationCred = testData.attestation.credential - val assertionCred = testData.assertion.credential + val assertionCred = testData.assertion.get.credential registrationCred.getClientExtensionResults.getExtensionIds.asScala should equal( Set("largeBlob") diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/test/RealExamples.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/test/RealExamples.scala index dadc73de0..792b5301e 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/test/RealExamples.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/test/RealExamples.scala @@ -1,5 +1,6 @@ package com.yubico.webauthn.test +import com.yubico.internal.util.CertificateParser import com.yubico.internal.util.JacksonCodecs import com.yubico.webauthn.AssertionRequest import com.yubico.webauthn.AssertionTestData @@ -20,6 +21,7 @@ import com.yubico.webauthn.data.RelyingPartyIdentity import com.yubico.webauthn.data.UserIdentity import java.nio.charset.StandardCharsets +import java.security.cert.X509Certificate sealed trait HasClientData { def clientData: String @@ -43,6 +45,7 @@ object RealExamples { clientData: String, attestationObjectBytes: ByteArray, clientExtensionResultsJson: String = "{}", + attestationRootCertificate: Option[X509Certificate] = None, ) extends HasClientData { def attestationObject: AttestationObject = new AttestationObject(attestationObjectBytes) @@ -93,8 +96,15 @@ object RealExamples { rp: RelyingPartyIdentity, user: UserIdentity, attestation: AttestationExample, - assertion: AssertionExample, + assertion: Option[AssertionExample] = None, ) { + def this( + rp: RelyingPartyIdentity, + user: UserIdentity, + attestation: AttestationExample, + assertion: AssertionExample, + ) = this(rp, user, attestation, Some(assertion)) + def attestationCert: ByteArray = new ByteArray( attestation.attestationObject.getAttestationStatement @@ -115,7 +125,7 @@ object RealExamples { privateKey = None, rpId = rp, userId = user, - assertion = Some( + assertion = assertion.map({ assertion => AssertionTestData( request = AssertionRequest .builder() @@ -129,11 +139,12 @@ object RealExamples { .build(), response = assertion.credential, ) - ), + }), + attestationRootCertificate = attestation.attestationRootCertificate, ) } - val YubiKeyNeo = Example( + val YubiKeyNeo = new Example( RelyingPartyIdentity.builder().id("example.com").name("Example RP").build(), UserIdentity .builder() @@ -157,7 +168,7 @@ object RealExamples { ), ) - val YubiKey4 = Example( + val YubiKey4 = new Example( RelyingPartyIdentity.builder().id("example.com").name("Example RP").build(), UserIdentity .builder() @@ -181,7 +192,7 @@ object RealExamples { ), ) - val YubiKey5 = Example( + val YubiKey5 = new Example( RelyingPartyIdentity.builder().id("example.com").name("Example RP").build(), UserIdentity .builder() @@ -205,7 +216,7 @@ object RealExamples { ), ) - val YubiKey5Nfc = Example( + val YubiKey5Nfc = new Example( RelyingPartyIdentity .builder() .id("demo.yubico.com") @@ -235,7 +246,7 @@ object RealExamples { ), ) - val YubiKey5NfcPost5cNfc = Example( + val YubiKey5NfcPost5cNfc = new Example( RelyingPartyIdentity .builder() .id("demo.yubico.com") @@ -265,7 +276,7 @@ object RealExamples { ), ) - val YubiKey5cNfc = Example( + val YubiKey5cNfc = new Example( RelyingPartyIdentity .builder() .id("demo.yubico.com") @@ -295,7 +306,7 @@ object RealExamples { ), ) - val YubiKey5Nano = Example( + val YubiKey5Nano = new Example( RelyingPartyIdentity.builder().id("example.com").name("Example RP").build(), UserIdentity .builder() @@ -319,7 +330,7 @@ object RealExamples { ), ) - val YubiKey5Ci = Example( + val YubiKey5Ci = new Example( RelyingPartyIdentity.builder().id("example.com").name("Example RP").build(), UserIdentity .builder() @@ -343,7 +354,7 @@ object RealExamples { ), ) - val SecurityKey = Example( + val SecurityKey = new Example( RelyingPartyIdentity.builder().id("example.com").name("Example RP").build(), UserIdentity .builder() @@ -367,7 +378,7 @@ object RealExamples { ), ) - val SecurityKey2 = Example( + val SecurityKey2 = new Example( RelyingPartyIdentity.builder().id("example.com").name("Example RP").build(), UserIdentity .builder() @@ -391,7 +402,7 @@ object RealExamples { ), ) - val SecurityKeyNfc = Example( + val SecurityKeyNfc = new Example( RelyingPartyIdentity.builder().id("example.com").name("Example RP").build(), UserIdentity .builder() @@ -415,7 +426,7 @@ object RealExamples { ), ) - val AppleAttestationIos = Example( + val AppleAttestationIos = new Example( RelyingPartyIdentity .builder() .id("demo.yubico.com") @@ -445,7 +456,7 @@ object RealExamples { ), ) - val AppleAttestationMacos = Example( + val AppleAttestationMacos = new Example( RelyingPartyIdentity .builder() .id("demo.yubico.com") @@ -473,7 +484,7 @@ object RealExamples { ), ) - val YubikeyFips5Nfc = Example( + val YubikeyFips5Nfc = new Example( RelyingPartyIdentity.builder().id("demo.yubico.com").name("").build(), UserIdentity .builder() @@ -498,7 +509,7 @@ object RealExamples { ), ) - val Yubikey5ciFips = Example( + val Yubikey5ciFips = new Example( RelyingPartyIdentity.builder().id("demo.yubico.com").name("").build(), UserIdentity .builder() @@ -522,7 +533,7 @@ object RealExamples { ), ) - val YubikeyBio_5_5_4 = Example( + val YubikeyBio_5_5_4 = new Example( RelyingPartyIdentity .builder() .id("demo.yubico.com") @@ -551,7 +562,7 @@ object RealExamples { ), ) - val YubikeyBio_5_5_5 = Example( + val YubikeyBio_5_5_5 = new Example( RelyingPartyIdentity .builder() .id("demo.yubico.com") @@ -592,7 +603,7 @@ object RealExamples { clientExtensionResultsJson = """{"credProps":{"rk":true}}""", ) - val LargeBlobWrite = Example( + val LargeBlobWrite = new Example( RelyingPartyIdentity.builder().id("localhost").name("").build(), UserIdentity .builder() @@ -623,7 +634,7 @@ object RealExamples { ), ) - val LargeBlobRead = Example( + val LargeBlobRead = new Example( RelyingPartyIdentity.builder().id("localhost").name("").build(), UserIdentity .builder() @@ -654,4 +665,30 @@ object RealExamples { ), ) + val WindowsHelloTpm = + Example( + RelyingPartyIdentity + .builder() + .id("d2urpypvrhb05x.amplifyapp.com") + .name("") + .build(), + UserIdentity + .builder() + .name("foo") + .displayName("Foo Bar") + .id( + ByteArray.fromBase64Url("AAAA") + ) + .build(), + AttestationExample( + base64UrlToString( + "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoia0lnZElRbWFBbXM1NlVOencwREg4dU96M0JERjJVSllhSlA2eklRWDFhOCIsIm9yaWdpbiI6Imh0dHBzOi8vZGV2LmQydXJweXB2cmhiMDV4LmFtcGxpZnlhcHAuY29tIiwiY3Jvc3NPcmlnaW4iOmZhbHNlfQ" + ), + ByteArray.fromBase64Url("o2NmbXRjdHBtZ2F0dFN0bXSmY2FsZzn__mNzaWdZAQBFEaTe-uZvbZBNsIMtJa26eigMUxEM1mBtddR7gdEBH5Hyeo9hFCqiJwYVKUq_iP9hvFaiLzoGbAWDgiG-fa3F-S71c8w83756dyRBMXNHYEvYjfv0TqGyky73V4xyKpf1iHiO_g4t31UjQiyTfypdP_rRcm42KVKgVyRPZzx_AKweN9XKEFfT2Ym3fmqD_scaIeKSyGs9qwH1MbILLUVnRK6fKK6sAA4ZaDVz4gUiSUoK9ZycCC2hfLBq5GjiTLgQF_Q2O3gRTqmU8VfwVsmtN5OMaGOyaFrUk97-RvZVrARXhNzrUAJT7KjTLDZeIA96F3pB_F_q3xd_dgvwVpWHY3ZlcmMyLjBjeDVjglkFuzCCBbcwggOfoAMCAQICEHHcna7VCE3QpRyKgi2uvXYwDQYJKoZIhvcNAQELBQAwQTE_MD0GA1UEAxM2RVVTLU5UQy1LRVlJRC04ODJGMDQ3Qjg3MTIxQ0Y5ODg1RjMxMTYwQkM3QkI1NTg2QUY0NzFCMB4XDTIyMDEyMDE5NTQxNloXDTI3MDYwMzE3NTE0OFowADCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALtT5frDB-WUq6N0VYnlYEalJzSut0JQx3vP29_ub-kZ7csJrm8uGXQGUkPlf4EFehFTnQ1jX_oZ8jNPw1m3rV5ijcuCe3r5GICFD6gpbuErmGS2mDVfe3fl_p0gPvhtulqatb1uYkWfW5SIKix1XWRvm92s3lRQvd-6vX_ExPIP-pEf0tkeINpBNNWgdtx3VdW4KVFTcv-q2FKhqfqXiAdOMHmwmWyXYulppYqW2XC7Pw9QmHZR_C5Urpc5UMmABz4zWSAYOyBMkKsX8koAsk8RgLtus07wW3FhJqi-BYczIe0IxG0q9UL295lkaxreTkfWYZHMMcU4M-Tm1w7QvKsCAwEAAaOCAeowggHmMA4GA1UdDwEB_wQEAwIHgDAMBgNVHRMBAf8EAjAAMG0GA1UdIAEB_wRjMGEwXwYJKwYBBAGCNxUfMFIwUAYIKwYBBQUHAgIwRB5CAFQAQwBQAEEAIAAgAFQAcgB1AHMAdABlAGQAIAAgAFAAbABhAHQAZgBvAHIAbQAgACAASQBkAGUAbgB0AGkAdAB5MBAGA1UdJQQJMAcGBWeBBQgDMFAGA1UdEQEB_wRGMESkQjBAMT4wEAYFZ4EFAgIMB05QQ1Q3NXgwFAYFZ4EFAgEMC2lkOjRFNTQ0MzAwMBQGBWeBBQIDDAtpZDowMDA3MDAwMjAfBgNVHSMEGDAWgBSMmnF_AA0xD8rW7i0pqjSXJYwSHjAdBgNVHQ4EFgQUFmUMIda76eb7Whi8CweaWhe7yNMwgbIGCCsGAQUFBwEBBIGlMIGiMIGfBggrBgEFBQcwAoaBkmh0dHA6Ly9hemNzcHJvZGV1c2Fpa3B1Ymxpc2guYmxvYi5jb3JlLndpbmRvd3MubmV0L2V1cy1udGMta2V5aWQtODgyZjA0N2I4NzEyMWNmOTg4NWYzMTE2MGJjN2JiNTU4NmFmNDcxYi84ODIzMGNhMi0yN2U1LTQxNTEtOWJhMi01OWI1ODJjMzlhYWEuY2VyMA0GCSqGSIb3DQEBCwUAA4ICAQCxsTbR5V8qnw6H6HEWJvrqcRy8fkY_vFUSjUq27hRl0t9D6LuS20l65FFm48yLwCkQbIf-aOBjwWafAbSVnEMig3KP-2Ml8IFtH63Msq9lwDlnXx2PNi7ISOemHNzBNeOG7pd_Zs69XUTq9zCriw9gAILCVCYllBluycdT7wZdjf0Bb5QJtTMuhwNXnOWmjv0VBOfsclWo-SEnnufaIDi0Vcf_TzbgmNn408Ej7R4Njy4qLnhPk64ruuWNJt3xlLMjbJXe_VKdO3lhM7JVFWSNAn8zfvEIwrrgCPhp1k2mFUGxJEvpSTnuZtNF35z4_54K6cEqZiqO-qd4FKt4KYs1GYJDyxttuUySGtnYyZg2aYB6hamg3asRDjBMPqoURsdVJcWQh3dFnD88cbs7Qt4_ytqAY61qfPE7bJ6E33o0X7OtxmECPd3aBJk6nsyXEXNF2vIww1UCrRC0OEr1HsTqA4bQU8KCWV6kduUnvkUWPT8CF0d2ER4wnszb053Tlcf2ebcytTMf_Nd95g520Hhqb2FZALCErijBi04Bu6SNeND1NQ3nxDSKC-CamOYW0ODch05Xzi1V0_sq0zmdKTxMSpg1jOZ1Q9924D4lJkruCB3zcsIBTUxV0EgAM1zGuoqwWjwYXr_8tO4_kEO1Lw8DckZIrk1s3ySsMVC89TRrIVkG7zCCBuswggTToAMCAQICEzMAAAQI5W53M7IUDf4AAAAABAgwDQYJKoZIhvcNAQELBQAwgYwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xNjA0BgNVBAMTLU1pY3Jvc29mdCBUUE0gUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgMjAxNDAeFw0yMTA2MDMxNzUxNDhaFw0yNzA2MDMxNzUxNDhaMEExPzA9BgNVBAMTNkVVUy1OVEMtS0VZSUQtODgyRjA0N0I4NzEyMUNGOTg4NUYzMTE2MEJDN0JCNTU4NkFGNDcxQjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMEye3QdCUOGeseHj_QjLMJVbHBEJKkRbXFsmZi6Mob_3IsVfxO-mQQC-xfY9tDyB1jhxfFAG-rjUfRVBwYYPfaVo2W58Q09Kcpf-43Iw9SRxx2ThP-KPFJPFBofZroloNaTNz3DRaWZ2ha-_PUG2nwTXR7LoIpqMVW1PDzGxb47SNpRmKJxVZhQ2_wRhZZvHRHJpZmrCmHRpTRqWSzQT1jn7Zo9VuMYvp_OFj7-LFpkqi4BYyhi0kTBPDQTpYrBi7RtmF1MhZBmm1HGDhoXHcPSZkN5vq5at4g03R15KWyRDgBcckCAgtewd6Dtd_Zwaejlm57xyGqP6T-AE-N8udh1NPv_PZVlSc4CnCayUTPORuaJ7N-v7Y4wpNSIdipq29hw19WVuO_z7q6GpQbn17arYf6LSoDZfwO8GHXPrtBOYYSZCNKuZ_IK8nomBLJPtN5AzwEZNyLCZIkg0U0sJ-oVr2UEYxlwwZQm5RSDxProaKU-OXq4f_j_0pEu5_DbJx9syR3Nsv6Lt9Zkf3JSJTVtWXoM0-R_82vAJ669PX0LLr603PKWBZbW7zQvtGojT_Pc1FDGfwhcdckxd3MGpEjZwh_1D8elYcxj3Ndw5jClWosZKr33pUcjqeFtSZSur0lbm6vyCfS16XzSMn8IkHmbbXcpgGKHumUCFD8CHJIBAgMBAAGjggGOMIIBijAOBgNVHQ8BAf8EBAMCAoQwGwYDVR0lBBQwEgYJKwYBBAGCNxUkBgVngQUIAzAWBgNVHSAEDzANMAsGCSsGAQQBgjcVHzASBgNVHRMBAf8ECDAGAQH_AgEAMB0GA1UdDgQWBBSMmnF_AA0xD8rW7i0pqjSXJYwSHjAfBgNVHSMEGDAWgBR6jArOL0hiF-KU0a5VwVLscXSkVjBwBgNVHR8EaTBnMGWgY6Bhhl9odHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NybC9NaWNyb3NvZnQlMjBUUE0lMjBSb290JTIwQ2VydGlmaWNhdGUlMjBBdXRob3JpdHklMjAyMDE0LmNybDB9BggrBgEFBQcBAQRxMG8wbQYIKwYBBQUHMAKGYWh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY2VydHMvTWljcm9zb2Z0JTIwVFBNJTIwUm9vdCUyMENlcnRpZmljYXRlJTIwQXV0aG9yaXR5JTIwMjAxNC5jcnQwDQYJKoZIhvcNAQELBQADggIBAHG-1grb-6xpObMtxfFScl8PRLd_GjLFaeAd0kVPls0jzKplG2Am4O87Qg0OUY0VhQ-uD39590gGrWEWnOmdrVJ-R1lJc1yrIZFEASBEedJSvxw9YTNknD59uXtznIP_4Glk-4NpqpcYov2OkBdV59V4dTL5oWFH0vzkZQfvGFxEHwtB9O6Bh0Lk142zXAh5_vf_-hSw3t9adloBAnA0AtPUVkzmgNRGeTpfPm-Iud-MAoUXaccFn2EChjKb9ApbS1ww8ZvX4x2kFU6qctu32g7Vf6CgACc1i-UYDT_E-h6c1O4n7JK2OxVS-DVwybps-cALU0gj-ZMauNMej0_x_NerzvDuQ77eFMTDMY4ZTzYOzlg4Nj0K8y1Bx_KqeTBO0N9CdEG3dBxWCUUFQzAx-i38xJL-dtYTCCFATVhc9FFJ0CQgU07JAGAeuNm_GL8kN46bMXd_ApQYFzDQUWYYXvRIt9mCw0Zd45lpAMuiDKT9TgjUVDNu8LQ8FPK0KeiQVrGHFMhHgg2pbVH9Pvc1jNEeRpCo0BLpZQwuIgEt90mepkt6Va-C9krHsU4y2oalG2LUu-jOC3NWNK8LssYVUCFWtaKh5d-xdTQmjx4uO-1sq9GFntVJ94QnEDhldz0XMopQ8srTGLMqR3MT-GkSNb5X1UFC-X1udXI8YvB3ADr_Z3B1YkFyZWFZATYAAQALAAYEcgAgnf_L82w4OuaZ-5ho3G3LidcVOIS-KAOSLBJBWL-tIq4AEAAQCAAAAAAAAQC6zY02JuN_qf28iVCISa6d6aUM5sS2PsKvZMO0P_-28lLX_9-xbmbEcTPvCj5aIEqVUfCb07U4qCBBUdaWygAbhAckvzYrACm5I8hjMoGkxMkZLw5kX0SjtHx6VfgksElxnu4DcC2g9pqL_McgddZV0zNGrYNj1iamzoxTyOcYyvZjQv_4gU-t9mEc0uqHHv-H6k23p4mensyaGAhkGTV8odyBxsNpdnR8IPnXWPO7tBDTbk4mg3VtclqLhS0-TCh_QnZ6lcEl27wTE8FjwqVdQG9F1Ouyn-eNAAq0EufbzwjSNpuDrlj-Kj5xY_lS5BMEjQUXqP-U5nzW22TehX8VaGNlcnRJbmZvWKH_VENHgBcAIgALt-OVET9fzihC1dzyG5xG70MVdRkPpSEkWQ0nns4zaYkAFGo6XjXu3JY-wup-EHJYWEuLpb45AAAACMgZxo6-OwT1-cIZwQGjXsp0dUws1gAiAAvzCsernlTAewF0QwNOA-iWpyhGxC-k_xBRjTtGNz9m6gAiAAs54tyczU1aCAh_N3Vy3qcAnC9zGeOxtQQKCe8CAanbbGhhdXRoRGF0YVkBZ-MWwK8fdtoeGn4DEn0TAUu4IUP_PMiBiJd4lDRznbBDRQAAAAAImHBYytxLgbbhMN5Q3L6WACBxLUIzn9ngKAM11_UwWG7kCiAvVyO1mYGSsEhfWeyhDaQBAwM5AQAgWQEAus2NNibjf6n9vIlQiEmunemlDObEtj7Cr2TDtD__tvJS1__fsW5mxHEz7wo-WiBKlVHwm9O1OKggQVHWlsoAG4QHJL82KwApuSPIYzKBpMTJGS8OZF9Eo7R8elX4JLBJcZ7uA3AtoPaai_zHIHXWVdMzRq2DY9Ymps6MU8jnGMr2Y0L_-IFPrfZhHNLqhx7_h-pNt6eJnp7MmhgIZBk1fKHcgcbDaXZ0fCD511jzu7QQ025OJoN1bXJai4UtPkwof0J2epXBJdu8ExPBY8KlXUBvRdTrsp_njQAKtBLn288I0jabg65Y_io-cWP5UuQTBI0FF6j_lOZ81ttk3oV_FSFDAQAB"), + attestationRootCertificate = Some( + CertificateParser.parsePem("MIIF9TCCA92gAwIBAgIQXbYwTgy/J79JuMhpUB5dyzANBgkqhkiG9w0BAQsFADCBjDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjE2MDQGA1UEAxMtTWljcm9zb2Z0IFRQTSBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAyMDE0MB4XDTE0MTIxMDIxMzExOVoXDTM5MTIxMDIxMzkyOFowgYwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xNjA0BgNVBAMTLU1pY3Jvc29mdCBUUE0gUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgMjAxNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAJ+n+bnKt/JHIRC/oI/xgkgsYdPzP0gpvduDA2GbRtth+L4WUyoZKGBw7uz5bjjP8Aql4YExyjR3EZQ4LqnZChMpoCofbeDR4MjCE1TGwWghGpS0mM3GtWD9XiME4rE2K0VW3pdN0CLzkYbvZbs2wQTFfE62yNQiDjyHFWAZ4BQH4eWa8wrDMUxIAneUCpU6zCwM+l6Qh4ohX063BHzXlTSTc1fDsiPaKuMMjWjK9vp5UHFPa+dMAWr6OljQZPFIg3aZ4cUfzS9y+n77Hs1NXPBn6E4Db679z4DThIXyoKeZTv1aaWOWl/exsDLGt2mTMTyykVV8uD1eRjYriFpmoRDwJKAEMOfaURarzp7hka9TOElGyD2gOV4Fscr2MxAYCywLmOLzA4VDSYLuKAhPSp7yawET30AvY1HRfMwBxetSqWP2+yZRNYJlHpor5QTuRDgzR+Zej+aWx6rWNYx43kLthozeVJ3QCsD5iEI/OZlmWn5WYf7O8LB/1A7scrYv44FD8ck3Z+hxXpkklAsjJMsHZa9mBqh+VR1AicX4uZG8m16x65ZU2uUpBa3rn8CTNmw17ZHOiuSWJtS9+PrZVA8ljgf4QgA1g6NPOEiLG2fn8Gm+r5Ak+9tqv72KDd2FPBJ7Xx4stYj/WjNPtEUhW4rcLK3ktLfcy6ea7Rocw5y5AgMBAAGjUTBPMAsGA1UdDwQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR6jArOL0hiF+KU0a5VwVLscXSkVjAQBgkrBgEEAYI3FQEEAwIBADANBgkqhkiG9w0BAQsFAAOCAgEAW4ioo1+J9VWC0UntSBXcXRm1ePTVamtsxVy/GpP4EmJd3Ub53JzNBfYdgfUL51CppS3ZY6BoagB+DqoA2GbSL+7sFGHBl5ka6FNelrwsH6VVw4xV/8klIjmqOyfatPYsz0sUdZev+reeiGpKVoXrK6BDnUU27/mgPtem5YKWvHB/soofUrLKzZV3WfGdx9zBr8V0xW6vO3CKaqkqU9y6EsQw34n7eJCbEVVQ8VdFd9iV1pmXwaBAfBwkviPTKEP9Cm+zbFIOLr3V3CL9hJj+gkTUuXWlJJ6wVXEG5i4rIbLAV59UrW4LonP+seqvWMJYUFxu/niF0R3fSGM+NU11DtBVkhRZt1u0kFhZqjDz1dWyfT/N7Hke3WsDqUFsBi+8SEw90rWx2aUkLvKo83oU4Mx4na+2I3l9F2a2VNGk4K7l3a00g51miPiq0Da0jqw30PaLluTMTGY5+RnZVh50JD6nk+Ea3wRkU8aiYFnpIxfKBZ72whmYYa/egj9IKeqpR0vuLebbU0fJBf880K1jWD3Z5SFyJXo057Mv0OPw5mttytE585ZIy5JsaRXlsOoWGRXE3kUT/MKR1UoAgR54c8Bsh+9Dq2wqIK9mRn15zvBDeyHG6+czurLopziOUeWokxZN1syrEdKlhFoPYavm6t+PzIcpdxZwHA+V3jLJPfI=") + ), + ), + ) + } From 5e75688c152ee9c90dc06c39c4719a7c9a2c9ba1 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 13 Sep 2022 19:49:22 +0200 Subject: [PATCH 24/58] Use try-with-resources in FidoMetadataDownloader --- .../yubico/fido/metadata/FidoMetadataDownloader.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/FidoMetadataDownloader.java b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/FidoMetadataDownloader.java index 517936f97..2f12c8d94 100644 --- a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/FidoMetadataDownloader.java +++ b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/FidoMetadataDownloader.java @@ -822,7 +822,9 @@ private Optional refreshBlobInternal( log.debug("Writing new BLOB to cache..."); if (blobCacheFile != null) { - new FileOutputStream(blobCacheFile).write(downloadedBytes.getBytes()); + try (FileOutputStream f = new FileOutputStream(blobCacheFile)) { + f.write(downloadedBytes.getBytes()); + } } if (blobCacheConsumer != null) { @@ -886,7 +888,9 @@ private X509Certificate retrieveTrustRootCert() cert.checkValidity(Date.from(clock.instant())); if (trustRootCacheFile != null) { - new FileOutputStream(trustRootCacheFile).write(downloaded.getBytes()); + try (FileOutputStream f = new FileOutputStream(trustRootCacheFile)) { + f.write(downloaded.getBytes()); + } } if (trustRootCacheConsumer != null) { @@ -955,8 +959,8 @@ private Optional loadCachedBlobOnly(X509Certificate trustRootCerti private Optional readCacheFile(File cacheFile) throws IOException { if (cacheFile.exists() && cacheFile.canRead() && cacheFile.isFile()) { - try { - return Optional.of(readAll(new FileInputStream(cacheFile))); + try (FileInputStream f = new FileInputStream(cacheFile)) { + return Optional.of(readAll(f)); } catch (FileNotFoundException e) { throw new RuntimeException( "This exception should be impossible, please file a bug report.", e); From 45f4badf7aa4be6c4176cca8b04fa01d56762ece Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 13 Sep 2022 19:49:43 +0200 Subject: [PATCH 25/58] Test that FidoMetadataDownloader does not write cache file if not necessary --- .../yubico/fido/metadata/FidoMetadataDownloaderSpec.scala | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/webauthn-server-attestation/src/test/scala/com/yubico/fido/metadata/FidoMetadataDownloaderSpec.scala b/webauthn-server-attestation/src/test/scala/com/yubico/fido/metadata/FidoMetadataDownloaderSpec.scala index 531a3c8bc..8d2734604 100644 --- a/webauthn-server-attestation/src/test/scala/com/yubico/fido/metadata/FidoMetadataDownloaderSpec.scala +++ b/webauthn-server-attestation/src/test/scala/com/yubico/fido/metadata/FidoMetadataDownloaderSpec.scala @@ -410,7 +410,7 @@ class FidoMetadataDownloaderSpec } it( - "The trust root is not downloaded if there's a valid one in file cache." + "The trust root is not downloaded and not written to cache if there's a valid one in file cache." ) { val random = new SecureRandom() val trustRootDistinguishedName = @@ -438,6 +438,10 @@ class FidoMetadataDownloaderSpec f.write(trustRootCert.getEncoded) f.close() cacheFile.deleteOnExit() + cacheFile.setLastModified( + cacheFile.lastModified() - 1000 + ) // Set mtime in the past to ensure any write will change it + val initialModTime = cacheFile.lastModified val blob = load( FidoMetadataDownloader @@ -456,6 +460,7 @@ class FidoMetadataDownloaderSpec blob.getHeader.getX5c.get.asScala.last.getIssuerDN.getName should equal( trustRootDistinguishedName ) + cacheFile.lastModified should equal(initialModTime) } it( From bf6858c3890021e05fb1a20eb95b3e8d37715b37 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 13 Sep 2022 19:49:58 +0200 Subject: [PATCH 26/58] Fix argument order in attestation trust failure log message --- .../java/com/yubico/webauthn/FinishRegistrationSteps.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishRegistrationSteps.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishRegistrationSteps.java index fdc305bb4..b4f9fbb16 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishRegistrationSteps.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishRegistrationSteps.java @@ -561,10 +561,10 @@ public boolean attestationTrusted() { } catch (CertPathValidatorException e) { log.info( "Failed to derive trust in attestation statement: {} at cert index {}: {}. Attestation object: {}", - response.getResponse().getAttestationObject(), e.getReason(), e.getIndex(), - e.getMessage()); + e.getMessage(), + response.getResponse().getAttestationObject()); return false; } catch (CertificateException e) { From cde7099fe9d645fed45505b284c4368bce7efee8 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 13 Sep 2022 19:50:30 +0200 Subject: [PATCH 27/58] Move TestWithEachProvider to yubico-util-scala module --- yubico-util-scala/build.gradle.kts | 4 ++-- .../scala/com/yubico/webauthn/TestWithEachProvider.scala | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename {webauthn-server-core/src/test => yubico-util-scala/src/main}/scala/com/yubico/webauthn/TestWithEachProvider.scala (100%) diff --git a/yubico-util-scala/build.gradle.kts b/yubico-util-scala/build.gradle.kts index 7ffbc14c7..8e5ee2718 100644 --- a/yubico-util-scala/build.gradle.kts +++ b/yubico-util-scala/build.gradle.kts @@ -13,8 +13,8 @@ dependencies { implementation(platform(rootProject)) implementation(platform(project(":test-platform"))) + implementation("org.bouncycastle:bcprov-jdk18on") implementation("org.scala-lang:scala-library") implementation("org.scalacheck:scalacheck_2.13") - - testImplementation( "org.scalatest:scalatest_2.13") + implementation("org.scalatest:scalatest_2.13") } diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/TestWithEachProvider.scala b/yubico-util-scala/src/main/scala/com/yubico/webauthn/TestWithEachProvider.scala similarity index 100% rename from webauthn-server-core/src/test/scala/com/yubico/webauthn/TestWithEachProvider.scala rename to yubico-util-scala/src/main/scala/com/yubico/webauthn/TestWithEachProvider.scala From 59fc44f67c1c7cc4cd5c7d0472d902a57e20ca0f Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 13 Sep 2022 19:51:04 +0200 Subject: [PATCH 28/58] Add log hint about policyTreeValidator setting --- .../java/com/yubico/webauthn/FinishRegistrationSteps.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishRegistrationSteps.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishRegistrationSteps.java index b4f9fbb16..3aa0a2906 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishRegistrationSteps.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishRegistrationSteps.java @@ -30,6 +30,7 @@ import COSE.CoseException; import com.upokecenter.cbor.CBORObject; import com.yubico.webauthn.attestation.AttestationTrustSource; +import com.yubico.webauthn.attestation.AttestationTrustSource.TrustRootsResult; import com.yubico.webauthn.data.AttestationObject; import com.yubico.webauthn.data.AttestationType; import com.yubico.webauthn.data.AuthenticatorAttestationResponse; @@ -52,6 +53,7 @@ import java.security.cert.CertificateFactory; import java.security.cert.PKIXCertPathValidatorResult; import java.security.cert.PKIXParameters; +import java.security.cert.PKIXReason; import java.security.cert.TrustAnchor; import java.security.cert.X509Certificate; import java.security.spec.InvalidKeySpecException; @@ -565,6 +567,12 @@ public boolean attestationTrusted() { e.getIndex(), e.getMessage(), response.getResponse().getAttestationObject()); + if (PKIXReason.INVALID_POLICY.equals(e.getReason())) { + log.info( + "You may need to set the policyTreeValidator property on the {} returned by your {}.", + TrustRootsResult.class.getSimpleName(), + AttestationTrustSource.class.getSimpleName()); + } return false; } catch (CertificateException e) { From 823cd019a66c65e40695499d0120b0109d910571 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 13 Sep 2022 19:51:37 +0200 Subject: [PATCH 29/58] Test that RelyingParty trusts MDS results in integration test --- .../FidoMetadataServiceIntegrationTest.scala | 67 ++++++++++++++----- .../webauthn/TestWithEachProvider.scala | 31 +++++---- 2 files changed, 68 insertions(+), 30 deletions(-) diff --git a/webauthn-server-attestation/src/integrationTest/scala/com/yubico/fido/metadata/FidoMetadataServiceIntegrationTest.scala b/webauthn-server-attestation/src/integrationTest/scala/com/yubico/fido/metadata/FidoMetadataServiceIntegrationTest.scala index 4a1e4dfbb..ef1b22bab 100644 --- a/webauthn-server-attestation/src/integrationTest/scala/com/yubico/fido/metadata/FidoMetadataServiceIntegrationTest.scala +++ b/webauthn-server-attestation/src/integrationTest/scala/com/yubico/fido/metadata/FidoMetadataServiceIntegrationTest.scala @@ -6,8 +6,13 @@ import com.yubico.fido.metadata.AttachmentHint.ATTACHMENT_HINT_NFC import com.yubico.fido.metadata.AttachmentHint.ATTACHMENT_HINT_WIRED import com.yubico.fido.metadata.AttachmentHint.ATTACHMENT_HINT_WIRELESS import com.yubico.internal.util.CertificateParser +import com.yubico.webauthn.FinishRegistrationOptions +import com.yubico.webauthn.RelyingParty +import com.yubico.webauthn.TestWithEachProvider import com.yubico.webauthn.data.AttestationObject +import com.yubico.webauthn.test.Helpers import com.yubico.webauthn.test.RealExamples +import org.bouncycastle.jce.provider.BouncyCastleProvider import org.junit.runner.RunWith import org.scalatest.BeforeAndAfter import org.scalatest.funspec.AnyFunSpec @@ -18,11 +23,13 @@ import org.scalatestplus.junit.JUnitRunner import java.io.IOException import java.security.cert.X509Certificate +import java.time.Clock +import java.time.ZoneOffset import java.util import java.util.Optional import scala.jdk.CollectionConverters.IteratorHasAsScala +import scala.jdk.CollectionConverters.SetHasAsJava import scala.jdk.CollectionConverters.SetHasAsScala -import scala.jdk.OptionConverters.RichOption import scala.jdk.OptionConverters.RichOptional import scala.util.Try @@ -32,7 +39,8 @@ import scala.util.Try class FidoMetadataServiceIntegrationTest extends AnyFunSpec with Matchers - with BeforeAndAfter { + with BeforeAndAfter + with TestWithEachProvider { describe("FidoMetadataService") { @@ -60,7 +68,7 @@ class FidoMetadataServiceIntegrationTest val attachmentHintsNfc = attachmentHintsUsb ++ Set(ATTACHMENT_HINT_WIRELESS, ATTACHMENT_HINT_NFC) - describe("correctly identifies") { + describe("correctly identifies and trusts") { def check( expectedDescriptionRegex: String, testData: RealExamples.Example, @@ -101,17 +109,38 @@ class FidoMetadataServiceIntegrationTest def getX5cArray(attestationObject: AttestationObject): JsonNode = attestationObject.getAttestationStatement.get("x5c") - val entries = fidoMds.get - .findEntries( - getAttestationTrustPath( - testData.attestation.attestationObject - ).get, - Some( - new AAGUID( - testData.attestation.attestationObject.getAuthenticatorData.getAttestedCredentialData.get.getAaguid - ) - ).toJava, + val rp = RelyingParty + .builder() + .identity(testData.rp) + .credentialRepository(Helpers.CredentialRepository.empty) + .origins( + Set(testData.attestation.collectedClientData.getOrigin).asJava ) + .allowUntrustedAttestation(false) + .attestationTrustSource(fidoMds.get) + .clock( + Clock.fixed( + CertificateParser + .parseDer(testData.attestationCert.getBytes) + .getNotBefore + .toInstant, + ZoneOffset.UTC, + ) + ) + .build() + + val registrationResult = rp.finishRegistration( + FinishRegistrationOptions + .builder() + .request(testData.asRegistrationTestData.request) + .response(testData.attestation.credential) + .build() + ) + + registrationResult.isAttestationTrusted should be(true) + + val entries = fidoMds.get + .findEntries(registrationResult) .asScala entries should not be empty val metadataStatements = @@ -214,11 +243,13 @@ class FidoMetadataServiceIntegrationTest } it("a YubiKey 5.4 NFC FIPS.") { - check( - "YubiKey 5 FIPS Series with NFC", - RealExamples.YubikeyFips5Nfc, - attachmentHintsNfc, - ) + withProviderContext(List(new BouncyCastleProvider)) { // Needed for JDK<14 because this example uses EdDSA + check( + "YubiKey 5 FIPS Series with NFC", + RealExamples.YubikeyFips5Nfc, + attachmentHintsNfc, + ) + } } it("a YubiKey 5.4 Ci FIPS.") { diff --git a/yubico-util-scala/src/main/scala/com/yubico/webauthn/TestWithEachProvider.scala b/yubico-util-scala/src/main/scala/com/yubico/webauthn/TestWithEachProvider.scala index 908717f0b..7d9d53d0a 100644 --- a/yubico-util-scala/src/main/scala/com/yubico/webauthn/TestWithEachProvider.scala +++ b/yubico-util-scala/src/main/scala/com/yubico/webauthn/TestWithEachProvider.scala @@ -10,6 +10,24 @@ import java.security.Security trait TestWithEachProvider extends Matchers { this: AnyFunSpec => + /** Run the `body` in a context with the given JCA [[Security]] providers, + * then reset the providers to their state before. + */ + def withProviderContext( + providers: List[Provider] + )( + body: => Any + ): Unit = { + val originalProviders = Security.getProviders.toList + Security.getProviders.foreach(prov => Security.removeProvider(prov.getName)) + providers.foreach(Security.addProvider) + + body + + Security.getProviders.foreach(prov => Security.removeProvider(prov.getName)) + originalProviders.foreach(Security.addProvider) + } + def wrapItFunctionWithProviderContext( providerSetName: String, providers: List[Provider], @@ -29,18 +47,7 @@ trait TestWithEachProvider extends Matchers { */ def it(testName: String)(testFun: => Any): Unit = { this.it.apply(testName) { - val originalProviders = Security.getProviders.toList - Security.getProviders.foreach(prov => - Security.removeProvider(prov.getName) - ) - providers.foreach(Security.addProvider) - - testFun - - Security.getProviders.foreach(prov => - Security.removeProvider(prov.getName) - ) - originalProviders.foreach(Security.addProvider) + withProviderContext(providers)(testFun) } } From a61bc6cc7fa783a6977542371efd970cb797d767 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 13 Sep 2022 19:52:13 +0200 Subject: [PATCH 30/58] Accept any policy tree in FidoMetadataService --- .../metadata/FidoMetadataServiceIntegrationTest.scala | 9 +++++++++ .../com/yubico/fido/metadata/FidoMetadataService.java | 1 + 2 files changed, 10 insertions(+) diff --git a/webauthn-server-attestation/src/integrationTest/scala/com/yubico/fido/metadata/FidoMetadataServiceIntegrationTest.scala b/webauthn-server-attestation/src/integrationTest/scala/com/yubico/fido/metadata/FidoMetadataServiceIntegrationTest.scala index ef1b22bab..6a2782ded 100644 --- a/webauthn-server-attestation/src/integrationTest/scala/com/yubico/fido/metadata/FidoMetadataServiceIntegrationTest.scala +++ b/webauthn-server-attestation/src/integrationTest/scala/com/yubico/fido/metadata/FidoMetadataServiceIntegrationTest.scala @@ -2,6 +2,7 @@ package com.yubico.fido.metadata import com.fasterxml.jackson.databind.JsonNode import com.yubico.fido.metadata.AttachmentHint.ATTACHMENT_HINT_EXTERNAL +import com.yubico.fido.metadata.AttachmentHint.ATTACHMENT_HINT_INTERNAL import com.yubico.fido.metadata.AttachmentHint.ATTACHMENT_HINT_NFC import com.yubico.fido.metadata.AttachmentHint.ATTACHMENT_HINT_WIRED import com.yubico.fido.metadata.AttachmentHint.ATTACHMENT_HINT_WIRELESS @@ -267,6 +268,14 @@ class FidoMetadataServiceIntegrationTest attachmentHintsUsb, ) } + + it("a Windows Hello attestation.") { + check( + "Windows Hello.*", + RealExamples.WindowsHelloTpm, + Set(ATTACHMENT_HINT_INTERNAL), + ) + } } } } diff --git a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/FidoMetadataService.java b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/FidoMetadataService.java index 7f627ac85..1895122ef 100644 --- a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/FidoMetadataService.java +++ b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/FidoMetadataService.java @@ -616,6 +616,7 @@ public TrustRootsResult findTrustRoots( .collect(Collectors.toSet())) .certStore(certStore) .enableRevocationChecking(false) + .policyTreeValidator(policyNode -> true) .build(); } } From bd522d7289507d002528bb5da35833136a7353ef Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 13 Sep 2022 21:08:16 +0200 Subject: [PATCH 31/58] Explain policyTreeValidator setting better in NEWS and README --- NEWS | 3 ++- README | 15 +++++++++++++++ .../yubico/fido/metadata/FidoMetadataService.java | 8 +++++++- 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/NEWS b/NEWS index f987eeeaa..90b5b6f7c 100644 --- a/NEWS +++ b/NEWS @@ -23,7 +23,8 @@ New features: predicate function will be used to validate the certificate policy tree after successful attestation certificate path validation. This may be required for some JCA providers to accept attestation certificates with critical - certificate policy extensions. + certificate policy extensions. See the JavaDoc for + `TrustRootsResultBuilder.policyTreeValidator(Predicate)` for more information. Fixes: diff --git a/README b/README index 0e2be4146..56d9937ea 100644 --- a/README +++ b/README @@ -624,6 +624,21 @@ The link:webauthn-server-attestation[`webauthn-server-attestation` module] provides optional additional features for working with attestation. See the module documentation for more details. +Alternatively, you can use the +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.0.0/com/yubico/webauthn/attestation/AttestationTrustSource.html[`AttestationTrustSource`] +interface to implement your own source of attestation root certificates +and set it as the +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.0.0/com/yubico/webauthn/RelyingParty.RelyingPartyBuilder.html#attestationTrustSource(com.yubico.webauthn.attestation.AttestationTrustSource)[`attestationTrustSource`] +for your +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.0.0/com/yubico/webauthn/RelyingParty.html[`RelyingParty`] +instance. +Note that depending on your JCA provider configuration, you may need to set the +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.0.0/com/yubico/webauthn/attestation/AttestationTrustSource.TrustRootsResult.TrustRootsResultBuilder.html#enableRevocationChecking(boolean)[`enableRevocationChecking`] +and/or +link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.0.0/com/yubico/webauthn/attestation/AttestationTrustSource.TrustRootsResult.TrustRootsResultBuilder.html#policyTreeValidator(java.util.function.Predicate)[`policyTreeValidator`] +settings for compatibility with some authenticators' attestation certificates. +See the JavaDoc for these settings for more information. + == Building diff --git a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/FidoMetadataService.java b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/FidoMetadataService.java index 1895122ef..2776e4383 100644 --- a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/FidoMetadataService.java +++ b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/FidoMetadataService.java @@ -69,7 +69,13 @@ * *

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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