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:
+ *
+ *
+ * - 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).
+ *
- If downloaded, cache the trust root certificate using the configured {@link File} or
+ * {@link Consumer} (see {@link FidoMetadataDownloaderBuilder.Step3})
+ *
- Download the metadata BLOB.
+ *
- 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.
+ * - 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.
+ * - 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)
}
}