FidoMetadataDownloader.java

1
// Copyright (c) 2015-2021, Yubico AB
2
// All rights reserved.
3
//
4
// Redistribution and use in source and binary forms, with or without
5
// modification, are permitted provided that the following conditions are met:
6
//
7
// 1. Redistributions of source code must retain the above copyright notice, this
8
//    list of conditions and the following disclaimer.
9
//
10
// 2. Redistributions in binary form must reproduce the above copyright notice,
11
//    this list of conditions and the following disclaimer in the documentation
12
//    and/or other materials provided with the distribution.
13
//
14
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
15
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
17
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
18
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
19
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
20
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
21
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
22
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24
25
package com.yubico.fido.metadata;
26
27
import com.fasterxml.jackson.core.Base64Variants;
28
import com.fasterxml.jackson.databind.ObjectMapper;
29
import com.yubico.fido.metadata.FidoMetadataDownloaderException.Reason;
30
import com.yubico.internal.util.BinaryUtil;
31
import com.yubico.internal.util.CertificateParser;
32
import com.yubico.internal.util.CollectionUtil;
33
import com.yubico.internal.util.OptionalUtil;
34
import com.yubico.webauthn.data.ByteArray;
35
import com.yubico.webauthn.data.exception.Base64UrlException;
36
import com.yubico.webauthn.data.exception.HexException;
37
import java.io.ByteArrayInputStream;
38
import java.io.File;
39
import java.io.FileInputStream;
40
import java.io.FileNotFoundException;
41
import java.io.FileOutputStream;
42
import java.io.IOException;
43
import java.io.InputStream;
44
import java.net.MalformedURLException;
45
import java.net.URL;
46
import java.net.URLConnection;
47
import java.nio.charset.StandardCharsets;
48
import java.security.DigestException;
49
import java.security.InvalidAlgorithmParameterException;
50
import java.security.InvalidKeyException;
51
import java.security.KeyManagementException;
52
import java.security.KeyStore;
53
import java.security.KeyStoreException;
54
import java.security.MessageDigest;
55
import java.security.NoSuchAlgorithmException;
56
import java.security.Signature;
57
import java.security.SignatureException;
58
import java.security.cert.CRL;
59
import java.security.cert.CRLException;
60
import java.security.cert.CertPath;
61
import java.security.cert.CertPathValidator;
62
import java.security.cert.CertPathValidatorException;
63
import java.security.cert.CertStore;
64
import java.security.cert.CertStoreParameters;
65
import java.security.cert.CertificateException;
66
import java.security.cert.CertificateFactory;
67
import java.security.cert.CollectionCertStoreParameters;
68
import java.security.cert.PKIXParameters;
69
import java.security.cert.TrustAnchor;
70
import java.security.cert.X509Certificate;
71
import java.time.Clock;
72
import java.util.ArrayList;
73
import java.util.Collection;
74
import java.util.Collections;
75
import java.util.Date;
76
import java.util.HashSet;
77
import java.util.List;
78
import java.util.Optional;
79
import java.util.Scanner;
80
import java.util.Set;
81
import java.util.UUID;
82
import java.util.function.Consumer;
83
import java.util.function.Function;
84
import java.util.function.Supplier;
85
import java.util.stream.Collectors;
86
import java.util.stream.Stream;
87
import javax.net.ssl.HttpsURLConnection;
88
import javax.net.ssl.SSLContext;
89
import javax.net.ssl.TrustManagerFactory;
90
import lombok.AccessLevel;
91
import lombok.AllArgsConstructor;
92
import lombok.Builder;
93
import lombok.NonNull;
94
import lombok.RequiredArgsConstructor;
95
import lombok.Value;
96
import lombok.extern.jackson.Jacksonized;
97
import lombok.extern.slf4j.Slf4j;
98
99
/**
100
 * Utility for downloading, caching and verifying Fido Metadata Service BLOBs and associated
101
 * certificates.
102
 *
103
 * <p>This class is NOT THREAD SAFE since it reads and writes caches. However, it has no internal
104
 * mutable state, so instances MAY be reused in single-threaded or externally synchronized contexts.
105
 * See also the {@link #loadCachedBlob()} and {@link #refreshBlob()} methods.
106
 *
107
 * <p>Use the {@link #builder() builder} to configure settings, then use the {@link
108
 * #loadCachedBlob()} and {@link #refreshBlob()} methods to load the metadata BLOB.
109
 */
110
@Slf4j
111
@AllArgsConstructor(access = AccessLevel.PRIVATE)
112
public final class FidoMetadataDownloader {
113
114
  @NonNull private final Set<String> expectedLegalHeaders;
115
  private final Set<TrustAnchor> trustAnchors;
116
  private final List<URL> trustRootUrls;
117
  private final Set<ByteArray> trustRootSha256;
118
  private final File trustRootCacheFile;
119
  private final Supplier<Optional<ByteArray>> trustRootCacheSupplier;
120
  private final Consumer<ByteArray> trustRootCacheConsumer;
121
  private final String blobJwt;
122
  private final URL blobUrl;
123
  private final File blobCacheFile;
124
  private final Supplier<Optional<ByteArray>> blobCacheSupplier;
125
  private final Consumer<ByteArray> blobCacheConsumer;
126
  private final CertStore certStore;
127
  @NonNull private final Clock clock;
128
  private final KeyStore httpsTrustStore;
129
  private final boolean verifyDownloadsOnly;
130
  private final Function<Exception, CachePolicyDecision> cachePolicy;
131
132
  /** For overriding JSON mapper settings in tests. */
133
  private final Supplier<ObjectMapper> makeHeaderJsonMapper;
134
135
  /** For overriding JSON mapper settings in tests. */
136
  private final Supplier<ObjectMapper> makePayloadJsonMapper;
137
138
  /**
139
   * Begin configuring a {@link FidoMetadataDownloader} instance. See the {@link
140
   * FidoMetadataDownloaderBuilder.Step1 Step1} type.
141
   *
142
   * @see FidoMetadataDownloaderBuilder.Step1
143
   */
144
  public static FidoMetadataDownloaderBuilder.Step1 builder() {
145 1 1. builder : replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader::builder → KILLED
    return new FidoMetadataDownloaderBuilder.Step1();
146
  }
147
148
  @RequiredArgsConstructor(access = AccessLevel.PRIVATE)
149
  public static class FidoMetadataDownloaderBuilder {
150
    @NonNull private final Set<String> expectedLegalHeaders;
151
    private final Set<TrustAnchor> trustAnchors;
152
    private final List<URL> trustRootUrls;
153
    private final Set<ByteArray> trustRootSha256;
154
    private final File trustRootCacheFile;
155
    private final Supplier<Optional<ByteArray>> trustRootCacheSupplier;
156
    private final Consumer<ByteArray> trustRootCacheConsumer;
157
    private final String blobJwt;
158
    private final URL blobUrl;
159
    private final File blobCacheFile;
160
    private final Supplier<Optional<ByteArray>> blobCacheSupplier;
161
    private final Consumer<ByteArray> blobCacheConsumer;
162
163
    private CertStore certStore = null;
164
    @NonNull private Clock clock = Clock.systemUTC();
165
    private KeyStore httpsTrustStore = null;
166
    private boolean verifyDownloadsOnly = false;
167
    private Function<Exception, CachePolicyDecision> cachePolicy =
168
        (e) -> CachePolicyDecision.USE_CACHED;
169
170
    private Supplier<ObjectMapper> makeHeaderJsonMapper =
171
        FidoMetadataDownloader::defaultHeaderJsonMapper;
172
    private Supplier<ObjectMapper> makePayloadJsonMapper =
173
        FidoMetadataDownloader::defaultPayloadJsonMapper;
174
175
    public FidoMetadataDownloader build() {
176 1 1. build : replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader$FidoMetadataDownloaderBuilder::build → KILLED
      return new FidoMetadataDownloader(
177
          expectedLegalHeaders,
178
          trustAnchors,
179
          trustRootUrls,
180
          trustRootSha256,
181
          trustRootCacheFile,
182
          trustRootCacheSupplier,
183
          trustRootCacheConsumer,
184
          blobJwt,
185
          blobUrl,
186
          blobCacheFile,
187
          blobCacheSupplier,
188
          blobCacheConsumer,
189
          certStore,
190
          clock,
191
          httpsTrustStore,
192
          verifyDownloadsOnly,
193
          cachePolicy,
194
          makeHeaderJsonMapper,
195
          makePayloadJsonMapper);
196
    }
197
198
    /**
199
     * Step 1: Set the legal header to expect from the FIDO Metadata Service.
200
     *
201
     * <p>By using the FIDO Metadata Service, you will be subject to its terms of service. This step
202
     * serves two purposes:
203
     *
204
     * <ol>
205
     *   <li>To remind you and any code reviewers that you need to read those terms of service
206
     *       before using this feature.
207
     *   <li>To help you detect if the legal header changes, so you can take appropriate action.
208
     * </ol>
209
     *
210
     * <p>See {@link Step1#expectLegalHeader(String...)}.
211
     *
212
     * @see Step1#expectLegalHeader(String...)
213
     */
214
    @AllArgsConstructor(access = AccessLevel.PRIVATE)
215
    public static class Step1 {
216
217
      /**
218
       * Set legal headers expected in the metadata BLOB.
219
       *
220
       * <p>By using the FIDO Metadata Service, you will be subject to its terms of service. This
221
       * builder step serves two purposes:
222
       *
223
       * <ol>
224
       *   <li>To remind you and any code reviewers that you need to read those terms of service
225
       *       before using this feature.
226
       *   <li>To help you detect if the legal header changes, so you can take appropriate action.
227
       * </ol>
228
       *
229
       * <p>If the legal header in the downloaded BLOB does not equal any of the <code>
230
       * expectedLegalHeaders</code>, an {@link UnexpectedLegalHeader} exception will be thrown in
231
       * the finalizing builder step.
232
       *
233
       * <p>Note that this library makes no guarantee that a change to the FIDO Metadata Service
234
       * terms of service will also cause a change to the legal header in the BLOB.
235
       *
236
       * <p>At the time of this library release, the current legal header is <code>
237
       * "Retrieval and use of this BLOB indicates acceptance of the appropriate agreement located at https://fidoalliance.org/metadata/metadata-legal-terms/"
238
       * </code>.
239
       *
240
       * @param expectedLegalHeaders the set of BLOB legal headers you expect in the metadata BLOB
241
       *     payload.
242
       */
243
      public Step2 expectLegalHeader(@NonNull String... expectedLegalHeaders) {
244 1 1. expectLegalHeader : replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader$FidoMetadataDownloaderBuilder$Step1::expectLegalHeader → KILLED
        return new Step2(Stream.of(expectedLegalHeaders).collect(Collectors.toSet()));
245
      }
246
    }
247
248
    /**
249
     * Step 2: Configure how to retrieve the FIDO Metadata Service trust root certificate when
250
     * necessary.
251
     *
252
     * <p>This step offers three mutually exclusive options:
253
     *
254
     * <ol>
255
     *   <li>Use the default download URL and certificate hash. This is the main intended use case.
256
     *       See {@link #useDefaultTrustRoot()}.
257
     *   <li>Use custom download URLs and certificate hashes. This is for future-proofing in case
258
     *       the upstream trust roots change and there is no new release of this library. See {@link
259
     *       #downloadTrustRoot(URL, Set)} and {@link #downloadTrustRoots(List, Set)}.
260
     *   <li>Use a pre-retrieved trust root certificate or set of trust anchors. It is up to you to
261
     *       perform any integrity checks and caching as desired. See {@link
262
     *       #useTrustRoot(X509Certificate)} and {@link #useTrustRoots(Set)}.
263
     * </ol>
264
     */
265
    @AllArgsConstructor(access = AccessLevel.PRIVATE)
266
    public static class Step2 {
267
268
      @NonNull private final Set<String> expectedLegalHeaders;
269
270
      /**
271
       * Download the trust root certificate from a hard-coded URL and verify it against a
272
       * hard-coded SHA-256 hash.
273
       *
274
       * <p>This is an alias of:
275
       *
276
       * <pre>
277
       * downloadTrustRoot(
278
       *   new URL("https://secure.globalsign.com/cacert/rootr46.crt"),
279
       *   Collections.singletonList(ByteArray.fromHex("4fa3126d8d3a11d1c4855a4f807cbad6cf919d3a5a88b03bea2c6372d93c40c9"))
280
       * )
281
       * </pre>
282
       *
283
       * This is the current FIDO Metadata Service trust root certificate at the time of this
284
       * library release.
285
       *
286
       * @see #downloadTrustRoot(URL, Set)
287
       * @see #downloadTrustRoots(List, Set)
288
       */
289
      public Step3 useDefaultTrustRoot() {
290
        try {
291 1 1. useDefaultTrustRoot : replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader$FidoMetadataDownloaderBuilder$Step2::useDefaultTrustRoot → NO_COVERAGE
          return downloadTrustRoot(
292
              new URL("https://secure.globalsign.com/cacert/rootr46.crt"),
293
              Collections.singleton(
294
                  ByteArray.fromHex(
295
                      "4fa3126d8d3a11d1c4855a4f807cbad6cf919d3a5a88b03bea2c6372d93c40c9")));
296
        } catch (MalformedURLException e) {
297
          throw new RuntimeException(
298
              "Bad hard-coded trust root certificate URL. Please file a bug report.", e);
299
        } catch (HexException e) {
300
          throw new RuntimeException(
301
              "Bad hard-coded trust root certificate hash. Please file a bug report.", e);
302
        }
303
      }
304
305
      /**
306
       * Download the trust root certificate from the given HTTPS <code>url</code> and verify its
307
       * SHA-256 hash against <code>acceptedCertSha256</code>.
308
       *
309
       * <p>The certificate will be downloaded if it does not exist in the cache, or if the cached
310
       * certificate is not currently valid.
311
       *
312
       * <p>If the cert is downloaded, it is also written to the cache {@link File} or {@link
313
       * Consumer} configured in the {@link Step3 next step}.
314
       *
315
       * <p>This is an alias of <code>
316
       * downloadTrustRoots(Collections.singletonList(url), acceptedCertSha256)</code>. See {@link
317
       * #downloadTrustRoots(List, Set)}.
318
       *
319
       * @param url the HTTP URL to download. It MUST use the <code>https:</code> scheme.
320
       * @param acceptedCertSha256 a set of SHA-256 hashes to verify the downloaded certificate
321
       *     against. The downloaded certificate MUST match at least one of these hashes.
322
       * @throws IllegalArgumentException if <code>url</code> is not a HTTPS URL.
323
       * @see #downloadTrustRoots(List, Set)
324
       */
325
      public Step3 downloadTrustRoot(@NonNull URL url, @NonNull Set<ByteArray> acceptedCertSha256) {
326 1 1. downloadTrustRoot : replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader$FidoMetadataDownloaderBuilder$Step2::downloadTrustRoot → KILLED
        return downloadTrustRoots(Collections.singletonList(url), acceptedCertSha256);
327
      }
328
329
      /**
330
       * Download the trust root certificate from the given HTTPS <code>url</code> and verify its
331
       * SHA-256 hash against <code>acceptedCertSha256</code>.
332
       *
333
       * <p>The certificate will be downloaded if it does not exist in the cache, or if the cached
334
       * certificate is not currently valid.
335
       *
336
       * <p>If the cert is downloaded, it is also written to the cache {@link File} or {@link
337
       * Consumer} configured in the {@link Step3 next step}.
338
       *
339
       * @param urls a non-empty list of HTTPS URLs to download. Each URL MUST use the <code>https:
340
       *     </code> scheme.
341
       * @param acceptedCertSha256 a set of SHA-256 hashes to verify downloaded certificates
342
       *     against. Each downloaded certificate MUST match at least one of these hashes.
343
       * @throws IllegalArgumentException if <code>urls</code> is empty or if any element of <code>
344
       *     urls</code> is not a HTTPS URL.
345
       * @see #downloadTrustRoot(URL, Set)
346
       */
347
      public Step3 downloadTrustRoots(
348
          @NonNull List<URL> urls, @NonNull Set<ByteArray> acceptedCertSha256) {
349 1 1. downloadTrustRoots : negated conditional → KILLED
        if (urls.isEmpty()) {
350
          throw new IllegalArgumentException(
351
              "List of trust certificate download URLs must not be empty.");
352
        }
353 3 1. lambda$downloadTrustRoots$0 : replaced boolean return with true for com/yubico/fido/metadata/FidoMetadataDownloader$FidoMetadataDownloaderBuilder$Step2::lambda$downloadTrustRoots$0 → SURVIVED
2. downloadTrustRoots : negated conditional → KILLED
3. lambda$downloadTrustRoots$0 : replaced boolean return with false for com/yubico/fido/metadata/FidoMetadataDownloader$FidoMetadataDownloaderBuilder$Step2::lambda$downloadTrustRoots$0 → KILLED
        if (!urls.stream().allMatch(u -> "https".equals(u.getProtocol()))) {
354
          throw new IllegalArgumentException("Trust certificate download URL must be a HTTPS URL.");
355
        }
356 1 1. downloadTrustRoots : replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader$FidoMetadataDownloaderBuilder$Step2::downloadTrustRoots → KILLED
        return new Step3(this, null, CollectionUtil.immutableList(urls), acceptedCertSha256);
357
      }
358
359
      /**
360
       * Use the given trust root certificate. It is the caller's responsibility to perform any
361
       * integrity checks and/or caching logic.
362
       *
363
       * <p>This is a shortcut for {@link #useTrustRoots(Set)} with <code>trustRootCertificate
364
       * </code> imported into a singleton set.
365
       *
366
       * @param trustRootCertificate the certificate to use as the FIDO Metadata Service trust root.
367
       * @see #useTrustRoots(Set)
368
       */
369
      public Step4 useTrustRoot(@NonNull X509Certificate trustRootCertificate) {
370 1 1. useTrustRoot : replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader$FidoMetadataDownloaderBuilder$Step2::useTrustRoot → KILLED
        return useTrustRoots(Collections.singleton(importTrustAnchor(trustRootCertificate)));
371
      }
372
373
      /**
374
       * Use the given set of trust anchors. It is the caller's responsibility to perform any
375
       * integrity checks and/or caching logic.
376
       *
377
       * @param trustAnchors the trust anchors to use as the FIDO Metadata Service trust root. The
378
       *     set will be copied, so subsequent modifications to <code>trustAnchors</code> will not
379
       *     affect the <code>FidoMetadataDownloader</code> instance.
380
       * @see #useTrustRoot(X509Certificate)
381
       */
382
      public Step4 useTrustRoots(@NonNull Set<TrustAnchor> trustAnchors) {
383 1 1. useTrustRoots : replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader$FidoMetadataDownloaderBuilder$Step2::useTrustRoots → KILLED
        return new Step4(
384
            new Step3(this, CollectionUtil.immutableSet(trustAnchors), null, null),
385
            null,
386
            null,
387
            null);
388
      }
389
    }
390
391
    /**
392
     * Step 3: Configure how to cache the trust root certificate.
393
     *
394
     * <p>This step offers two mutually exclusive options:
395
     *
396
     * <ol>
397
     *   <li>Cache trust root certificates in a {@link File}. See {@link
398
     *       Step3#useTrustRootCacheFile(File)}.
399
     *   <li>Cache trust root certificates using a {@link Supplier} to read the cache and a {@link
400
     *       Consumer} to write the cache. See {@link Step3#useTrustRootCache(Supplier, Consumer)}.
401
     * </ol>
402
     */
403
    @AllArgsConstructor(access = AccessLevel.PRIVATE)
404
    public static class Step3 {
405
      @NonNull private final Step2 step2;
406
      private final Set<TrustAnchor> trustAnchors;
407
      private final List<URL> trustRootUrls;
408
      private final Set<ByteArray> trustRootSha256;
409
410
      /**
411
       * Cache trust root certificates in the file <code>cacheFile</code>.
412
       *
413
       * <p>If <code>cacheFile</code> exists, is a normal file and is readable, then trust root
414
       * certificates will be attempted to be read from this file. The internal format of the file
415
       * is opaque and subject to change without a major version release of the library.
416
       *
417
       * <p>If reading from the cache fails, then trust root certificates will instead be downloaded
418
       * and written to this file.
419
       *
420
       * <p>The cache is invalidated whenever the configured list of trust root download URLs
421
       * changes or differs in length from the number of cached certificates, or whenever any cached
422
       * certificate matches none of the configured SHA-256 hashes.
423
       */
424
      public Step4 useTrustRootCacheFile(@NonNull File cacheFile) {
425 1 1. useTrustRootCacheFile : replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader$FidoMetadataDownloaderBuilder$Step3::useTrustRootCacheFile → KILLED
        return new Step4(this, cacheFile, null, null);
426
      }
427
428
      /**
429
       * Cache the trust root certificate using a {@link Supplier} to read the cache, and using a
430
       * {@link Consumer} to write the cache.
431
       *
432
       * <p>If <code>getCachedTrustRootCerts</code> returns non-empty, then trust root certificates
433
       * will be attempted to be read from the contained {@link ByteArray}. The internal format of
434
       * the byte array is opaque and subject to change without a major version release of the
435
       * library.
436
       *
437
       * <p>If the supplier returns empty or reading from the contained byte array fails, then trust
438
       * root certificates will be downloaded and written to <code>
439
       * writeCachedTrustRootCerts</code>.
440
       *
441
       * <p>The cache is invalidated whenever the configured list of trust root download URLs
442
       * changes or differs in length from the number of cached certificates, or whenever any cached
443
       * certificate matches none of the configured SHA-256 hashes.
444
       *
445
       * @param getCachedTrustRootCerts a {@link Supplier} that fetches cached trust root
446
       *     certificates if they exist. MUST NOT return <code>null</code>. The format of the
447
       *     returned value, if present, is opaque to the supplier.
448
       * @param writeCachedTrustRootCerts a {@link Consumer} that accepts trust root certificates in
449
       *     an unspecified opaque format and writes it to the cache. Its argument will never be
450
       *     <code>null</code>.
451
       */
452
      public Step4 useTrustRootCache(
453
          @NonNull Supplier<Optional<ByteArray>> getCachedTrustRootCerts,
454
          @NonNull Consumer<ByteArray> writeCachedTrustRootCerts) {
455 1 1. useTrustRootCache : replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader$FidoMetadataDownloaderBuilder$Step3::useTrustRootCache → KILLED
        return new Step4(this, null, getCachedTrustRootCerts, writeCachedTrustRootCerts);
456
      }
457
    }
458
459
    /**
460
     * Step 4: Configure how to fetch the FIDO Metadata Service metadata BLOB.
461
     *
462
     * <p>This step offers three mutually exclusive options:
463
     *
464
     * <ol>
465
     *   <li>Use the default download URL. This is the main intended use case. See {@link
466
     *       #useDefaultBlob()}.
467
     *   <li>Use a custom download URL. This is for future-proofing in case the BLOB download URL
468
     *       changes and there is no new release of this library. See {@link #downloadBlob(URL)}.
469
     *   <li>Use a pre-retrieved BLOB. The signature will still be verified, but it is up to you to
470
     *       renew it when appropriate and perform any caching as desired. See {@link
471
     *       #useBlob(String)}.
472
     * </ol>
473
     */
474
    @AllArgsConstructor(access = AccessLevel.PRIVATE)
475
    public static class Step4 {
476
      @NonNull private final Step3 step3;
477
      private final File trustRootCacheFile;
478
      private final Supplier<Optional<ByteArray>> trustRootCacheSupplier;
479
      private final Consumer<ByteArray> trustRootCacheConsumer;
480
481
      /**
482
       * Download the metadata BLOB from a hard-coded URL.
483
       *
484
       * <p>This is an alias of <code>downloadBlob(new URL("https://mds.fidoalliance.org/"))</code>.
485
       *
486
       * <p>This is the current FIDO Metadata Service BLOB download URL at the time of this library
487
       * release.
488
       *
489
       * @see #downloadBlob(URL)
490
       */
491
      public Step5 useDefaultBlob() {
492
        try {
493 1 1. useDefaultBlob : replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader$FidoMetadataDownloaderBuilder$Step4::useDefaultBlob → NO_COVERAGE
          return downloadBlob(new URL("https://mds.fidoalliance.org/"));
494
        } catch (MalformedURLException e) {
495
          throw new RuntimeException(
496
              "Bad hard-coded trust root certificate URL. Please file a bug report.", e);
497
        }
498
      }
499
500
      /**
501
       * Download the metadata BLOB from the given HTTP or HTTPS <code>url</code>.
502
       *
503
       * <p>The BLOB will be downloaded if it does not exist in the cache, or if the <code>
504
       * nextUpdate</code> property of the cached BLOB is the current date or earlier.
505
       *
506
       * <p>If the BLOB is downloaded, it is also written to the cache {@link File} or {@link
507
       * Consumer} configured in the next step.
508
       *
509
       * <p>It is RECOMMENDED to use a HTTPS URL for improved transport security. Most notably this
510
       * helps prevent attacks that could force the application to continue using a stale cached
511
       * BLOB even after the real MDS has a newer BLOB available.
512
       *
513
       * @param url the HTTP or HTTPS URL to download.
514
       */
515
      public Step5 downloadBlob(@NonNull URL url) {
516 1 1. downloadBlob : negated conditional → SURVIVED
        if (!"https".equals(url.getProtocol())) {
517
          log.warn("FIDO MDS BLOB download URL is not a HTTPS URL: {}", url);
518
        }
519 1 1. downloadBlob : replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader$FidoMetadataDownloaderBuilder$Step4::downloadBlob → KILLED
        return new Step5(this, null, url);
520
      }
521
522
      /**
523
       * Use the given metadata BLOB; never download it.
524
       *
525
       * <p>The blob signature and trust chain will still be verified, but it is the caller's
526
       * responsibility to renew the metadata BLOB according to the <a
527
       * href="https://fidoalliance.org/specs/mds/fido-metadata-service-v3.0-ps-20210518.html#metadata-blob-object-processing-rules">FIDO
528
       * Metadata Service specification</a>.
529
       *
530
       * @param blobJwt the Metadata BLOB in JWT format as defined in <a
531
       *     href="https://fidoalliance.org/specs/mds/fido-metadata-service-v3.0-ps-20210518.html#metadata-blob">FIDO
532
       *     Metadata Service §3.1.7. Metadata BLOB</a>. The byte array MUST NOT be Base64-decoded.
533
       * @see <a
534
       *     href="https://fidoalliance.org/specs/mds/fido-metadata-service-v3.0-ps-20210518.html#metadata-blob">FIDO
535
       *     Metadata Service §3.1.7. Metadata BLOB</a>
536
       * @see <a
537
       *     href="https://fidoalliance.org/specs/mds/fido-metadata-service-v3.0-ps-20210518.html#metadata-blob-object-processing-rules">FIDO
538
       *     Metadata Service §3.2. Metadata BLOB object processing rules</a>
539
       */
540
      public FidoMetadataDownloaderBuilder useBlob(@NonNull String blobJwt) {
541 1 1. useBlob : replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader$FidoMetadataDownloaderBuilder$Step4::useBlob → KILLED
        return finishRequiredSteps(new Step5(this, blobJwt, null), null, null, null);
542
      }
543
    }
544
545
    /**
546
     * Step 5: Configure how to cache the metadata BLOB.
547
     *
548
     * <p>This step offers two mutually exclusive options:
549
     *
550
     * <ol>
551
     *   <li>Cache the metadata BLOB in a {@link File}. See {@link Step5#useBlobCacheFile(File)}.
552
     *   <li>Cache the metadata BLOB using a {@link Supplier} to read the cache and a {@link
553
     *       Consumer} to write the cache. See {@link Step5#useBlobCache(Supplier, Consumer)}.
554
     * </ol>
555
     */
556
    @AllArgsConstructor(access = AccessLevel.PRIVATE)
557
    public static class Step5 {
558
      @NonNull private final Step4 step4;
559
      private final String blobJwt;
560
      private final URL blobUrl;
561
562
      /**
563
       * Cache metadata BLOB in the file <code>cacheFile</code>.
564
       *
565
       * <p>If <code>cacheFile</code> exists, is a normal file, is readable, and is not out of date,
566
       * then it will be used as the FIDO Metadata Service BLOB.
567
       *
568
       * <p>Otherwise, the metadata BLOB will be downloaded and written to this file.
569
       *
570
       * @param cacheFile a {@link File} which may or may not exist. If it exists, it MUST contain
571
       *     the metadata BLOB in JWS compact serialization format <a
572
       *     href="https://datatracker.ietf.org/doc/html/rfc7515#section-3.1">[RFC7515]</a>.
573
       */
574
      public FidoMetadataDownloaderBuilder useBlobCacheFile(@NonNull File cacheFile) {
575 1 1. useBlobCacheFile : replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader$FidoMetadataDownloaderBuilder$Step5::useBlobCacheFile → KILLED
        return finishRequiredSteps(this, cacheFile, null, null);
576
      }
577
578
      /**
579
       * Cache the metadata BLOB using a {@link Supplier} to read the cache, and using a {@link
580
       * Consumer} to write the cache.
581
       *
582
       * <p>If <code>getCachedBlob</code> returns non-empty and the content is not out of date, then
583
       * it will be used as the FIDO Metadata Service BLOB.
584
       *
585
       * <p>Otherwise, the metadata BLOB will be downloaded and written to <code>writeCachedBlob
586
       * </code>.
587
       *
588
       * @param getCachedBlob a {@link Supplier} that fetches the cached metadata BLOB if it exists.
589
       *     MUST NOT return <code>null</code>. The returned value, if present, MUST be in JWS
590
       *     compact serialization format <a
591
       *     href="https://datatracker.ietf.org/doc/html/rfc7515#section-3.1">[RFC7515]</a>.
592
       * @param writeCachedBlob a {@link Consumer} that accepts the metadata BLOB in JWS compact
593
       *     serialization format <a
594
       *     href="https://datatracker.ietf.org/doc/html/rfc7515#section-3.1">[RFC7515]</a> and
595
       *     writes it to the cache. Its argument will never be <code>null</code>.
596
       */
597
      public FidoMetadataDownloaderBuilder useBlobCache(
598
          @NonNull Supplier<Optional<ByteArray>> getCachedBlob,
599
          @NonNull Consumer<ByteArray> writeCachedBlob) {
600 1 1. useBlobCache : replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader$FidoMetadataDownloaderBuilder$Step5::useBlobCache → KILLED
        return finishRequiredSteps(this, null, getCachedBlob, writeCachedBlob);
601
      }
602
    }
603
604
    private static FidoMetadataDownloaderBuilder finishRequiredSteps(
605
        FidoMetadataDownloaderBuilder.Step5 step5,
606
        File blobCacheFile,
607
        Supplier<Optional<ByteArray>> blobCacheSupplier,
608
        Consumer<ByteArray> blobCacheConsumer) {
609 1 1. finishRequiredSteps : replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader$FidoMetadataDownloaderBuilder::finishRequiredSteps → KILLED
      return new FidoMetadataDownloaderBuilder(
610
          step5.step4.step3.step2.expectedLegalHeaders,
611
          step5.step4.step3.trustAnchors,
612
          step5.step4.step3.trustRootUrls,
613
          step5.step4.step3.trustRootSha256,
614
          step5.step4.trustRootCacheFile,
615
          step5.step4.trustRootCacheSupplier,
616
          step5.step4.trustRootCacheConsumer,
617
          step5.blobJwt,
618
          step5.blobUrl,
619
          blobCacheFile,
620
          blobCacheSupplier,
621
          blobCacheConsumer);
622
    }
623
624
    /**
625
     * Use <code>clock</code> as the source of the current time for some application-level logic.
626
     *
627
     * <p>This is primarily intended for testing.
628
     *
629
     * <p>The default is {@link Clock#systemUTC()}.
630
     *
631
     * @param clock a {@link Clock} which the finished {@link FidoMetadataDownloader} will use to
632
     *     tell the time.
633
     */
634
    public FidoMetadataDownloaderBuilder clock(@NonNull Clock clock) {
635
      this.clock = clock;
636 1 1. clock : replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader$FidoMetadataDownloaderBuilder::clock → KILLED
      return this;
637
    }
638
639
    /**
640
     * Use the provided CRLs.
641
     *
642
     * <p>CRLs will also be downloaded from distribution points for any certificates with a
643
     * CRLDistributionPoints extension, if the extension can be successfully interpreted. A warning
644
     * message will be logged CRLDistributionPoints parsing fails.
645
     *
646
     * @throws InvalidAlgorithmParameterException if {@link CertStore#getInstance(String,
647
     *     CertStoreParameters)} does.
648
     * @throws NoSuchAlgorithmException if a <code>"Collection"</code> type {@link CertStore}
649
     *     provider is not available.
650
     * @see #useCrls(CertStore)
651
     */
652
    public FidoMetadataDownloaderBuilder useCrls(@NonNull Collection<CRL> crls)
653
        throws InvalidAlgorithmParameterException, NoSuchAlgorithmException {
654 1 1. useCrls : replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader$FidoMetadataDownloaderBuilder::useCrls → KILLED
      return useCrls(CertStore.getInstance("Collection", new CollectionCertStoreParameters(crls)));
655
    }
656
657
    /**
658
     * Use CRLs in the provided {@link CertStore}.
659
     *
660
     * <p>CRLs will also be downloaded from distribution points for any certificates with a
661
     * CRLDistributionPoints extension, if the extension can be successfully interpreted. A warning
662
     * message will be logged CRLDistributionPoints parsing fails.
663
     *
664
     * @see #useCrls(Collection)
665
     */
666
    public FidoMetadataDownloaderBuilder useCrls(CertStore certStore) {
667
      this.certStore = certStore;
668 1 1. useCrls : replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader$FidoMetadataDownloaderBuilder::useCrls → KILLED
      return this;
669
    }
670
671
    /**
672
     * Use the provided {@link X509Certificate}s as trust roots for HTTPS downloads.
673
     *
674
     * <p>This is primarily useful when setting {@link Step2#downloadTrustRoot(URL, Set)
675
     * downloadTrustRoot} or {@link Step2#downloadTrustRoots(List, Set) downloadTrustRoots} and/or
676
     * {@link Step4#downloadBlob(URL) downloadBlob} to download from custom servers instead of the
677
     * defaults.
678
     *
679
     * <p>If provided, these will be used for downloading
680
     *
681
     * <ul>
682
     *   <li>the trust root certificate for the BLOB signature chain, and
683
     *   <li>the metadata BLOB.
684
     * </ul>
685
     *
686
     * If not set, the system default certificate store will be used.
687
     */
688
    public FidoMetadataDownloaderBuilder trustHttpsCerts(@NonNull X509Certificate... certificates) {
689
      final KeyStore trustStore;
690
      try {
691
        trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
692 1 1. trustHttpsCerts : removed call to java/security/KeyStore::load → KILLED
        trustStore.load(null);
693
      } catch (KeyStoreException
694
          | IOException
695
          | NoSuchAlgorithmException
696
          | CertificateException e) {
697
        throw new RuntimeException(
698
            "Failed to instantiate or initialize KeyStore. This should not be possible, please file a bug report.",
699
            e);
700
      }
701
      for (X509Certificate cert : certificates) {
702
        try {
703 1 1. trustHttpsCerts : removed call to java/security/KeyStore::setCertificateEntry → KILLED
          trustStore.setCertificateEntry(UUID.randomUUID().toString(), cert);
704
        } catch (KeyStoreException e) {
705
          throw new RuntimeException(
706
              "Failed to import HTTPS cert into KeyStore. This should not be possible, please file a bug report.",
707
              e);
708
        }
709
      }
710
      this.httpsTrustStore = trustStore;
711
712 1 1. trustHttpsCerts : replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader$FidoMetadataDownloaderBuilder::trustHttpsCerts → KILLED
      return this;
713
    }
714
715
    /**
716
     * If set to <code>true</code>, the BLOB signature will not be verified when loading the BLOB
717
     * from cache or when explicitly set via {@link Step4#useBlob(String)}. This means that if a
718
     * BLOB was successfully verified once and written to cache, that cached value will be
719
     * implicitly trusted when loaded in the future.
720
     *
721
     * <p>If set to <code>false</code>, the BLOB signature will always be verified no matter where
722
     * the BLOB came from. This means that a cached BLOB may become invalid if the BLOB certificate
723
     * expires, even if the BLOB was successfully verified at the time it was downloaded.
724
     *
725
     * <p>The default setting is <code>false</code>.
726
     *
727
     * @param verifyDownloadsOnly <code>true</code> if the BLOB signature should be ignored when
728
     *     loading the BLOB from cache or when explicitly set via {@link Step4#useBlob(String)}.
729
     */
730
    public FidoMetadataDownloaderBuilder verifyDownloadsOnly(final boolean verifyDownloadsOnly) {
731
      this.verifyDownloadsOnly = verifyDownloadsOnly;
732 1 1. verifyDownloadsOnly : replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader$FidoMetadataDownloaderBuilder::verifyDownloadsOnly → KILLED
      return this;
733
    }
734
735
    /**
736
     * Define a policy for how {@link #refreshBlob()} and {@link #loadCachedBlob()} should behave
737
     * when a BLOB download fails.
738
     *
739
     * <p><code>cachePolicy</code> will be invoked when a cached BLOB is available and any attempt
740
     * to download, parse and verify a new BLOB fails. Its argument will be the {@link Exception}
741
     * that caused the failure. If <code>cachePolicy</code> returns {@link
742
     * CachePolicyDecision#USE_CACHED}, then the {@link #refreshBlob()} or {@link #loadCachedBlob()}
743
     * invocation will log a warning and return the cached BLOB as a successful result. If <code>
744
     * cachePolicy</code> returns {@link CachePolicyDecision#THROW}, then the exception will be
745
     * re-thrown and the {@link #refreshBlob()} or {@link #loadCachedBlob()} invocation will fail.
746
     *
747
     * <p><code>cachePolicy</code> MUST NOT return <code>null</code>.
748
     *
749
     * <p>When no cached BLOB is available, the exception is automatically re-thrown and <code>
750
     * cachePolicy</code> is not invoked.
751
     *
752
     * <p>See the documentation of {@link #refreshBlob()} and {@link #loadCachedBlob()} for what
753
     * kinds of exceptions may be thrown.
754
     *
755
     * <p>The default policy always returns {@link CachePolicyDecision#USE_CACHED}.
756
     *
757
     * @param cachePolicy the policy used to decide whether to throw or fall back to cache when a
758
     *     BLOB download fails. MUST NOT return <code>null</code>.
759
     * @see CachePolicyDecision
760
     * @see #refreshBlob()
761
     * @see #loadCachedBlob() ()
762
     */
763
    public FidoMetadataDownloaderBuilder cachePolicy(
764
        final Function<Exception, CachePolicyDecision> cachePolicy) {
765
      this.cachePolicy = cachePolicy;
766 1 1. cachePolicy : replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader$FidoMetadataDownloaderBuilder::cachePolicy → SURVIVED
      return this;
767
    }
768
769
    /** For internal testing use only. */
770
    FidoMetadataDownloaderBuilder headerJsonMapper(
771
        final Supplier<ObjectMapper> makeHeaderJsonMapper) {
772
      this.makeHeaderJsonMapper = makeHeaderJsonMapper;
773 1 1. headerJsonMapper : replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader$FidoMetadataDownloaderBuilder::headerJsonMapper → NO_COVERAGE
      return this;
774
    }
775
776
    /** For internal testing use only. */
777
    FidoMetadataDownloaderBuilder payloadJsonMapper(
778
        final Supplier<ObjectMapper> makePayloadJsonMapper) {
779
      this.makePayloadJsonMapper = makePayloadJsonMapper;
780 1 1. payloadJsonMapper : replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader$FidoMetadataDownloaderBuilder::payloadJsonMapper → NO_COVERAGE
      return this;
781
    }
782
  }
783
784
  /**
785
   * Load the metadata BLOB from cache, or download a fresh one if necessary.
786
   *
787
   * <p>This method is NOT THREAD SAFE since it reads and writes caches.
788
   *
789
   * <p>On each execution this will, in order:
790
   *
791
   * <ol>
792
   *   <li>Download the trust root certificate, if necessary: if the cache is empty, the cache fails
793
   *       to load, or the cached cert is not valid at the current time (as determined by the {@link
794
   *       FidoMetadataDownloaderBuilder#clock(Clock) clock} setting).
795
   *   <li>If downloaded, cache the trust root certificate using the configured {@link File} or
796
   *       {@link Consumer} (see {@link FidoMetadataDownloaderBuilder.Step3})
797
   *   <li>Download the metadata BLOB, if necessary: if the cache is empty, the cache fails to load,
798
   *       or the <code>"nextUpdate"</code> property in the cached BLOB is the current date (as
799
   *       determined by the {@link FidoMetadataDownloaderBuilder#clock(Clock) clock} setting) or
800
   *       earlier.
801
   *   <li>Check the <code>"no"</code> property of the downloaded BLOB, if any, and compare it with
802
   *       the <code>"no"</code> of the cached BLOB, if any. The one with a greater <code>"no"
803
   *       </code> overrides the other, even if its <code>"nextUpdate"</code> is in the past.
804
   *   <li>If a BLOB with a newer <code>"no"</code> was downloaded, verify that the value of its
805
   *       <code>"legalHeader"</code> appears in the configured {@link
806
   *       FidoMetadataDownloaderBuilder.Step1#expectLegalHeader(String...) expectLegalHeader}
807
   *       setting. If not, throw an {@link UnexpectedLegalHeader} exception containing the cached
808
   *       BLOB, if any, and the downloaded BLOB.
809
   *   <li>If a BLOB with a newer <code>"no"</code> was downloaded and had an expected <code>
810
   *       "legalHeader"</code>, cache the new BLOB using the configured {@link File} or {@link
811
   *       Consumer} (see {@link FidoMetadataDownloaderBuilder.Step5}).
812
   * </ol>
813
   *
814
   * No internal mutable state is maintained between invocations of this method; each invocation
815
   * will reload/rewrite caches, perform downloads and check the <code>"legalHeader"
816
   * </code> as necessary. You may therefore reuse a {@link FidoMetadataDownloader} instance and,
817
   * for example, call this method periodically to refresh the BLOB when appropriate. Each call will
818
   * return a new {@link MetadataBLOB} instance; ones already returned will not be updated by
819
   * subsequent calls.
820
   *
821
   * @return the successfully retrieved and validated metadata BLOB.
822
   * @throws Base64UrlException if the explicitly configured or newly downloaded BLOB is not a
823
   *     well-formed JWT in compact serialization.
824
   * @throws CertPathValidatorException if the explicitly configured or newly downloaded BLOB fails
825
   *     certificate path validation.
826
   * @throws CertificateException if the trust root certificate was downloaded and passed the
827
   *     SHA-256 integrity check, but does not contain a currently valid X.509 DER certificate; or
828
   *     if the BLOB signing certificate chain fails to parse.
829
   * @throws DigestException if the trust root certificate was downloaded but failed the SHA-256
830
   *     integrity check.
831
   * @throws FidoMetadataDownloaderException if the explicitly configured or newly downloaded BLOB
832
   *     (if any) has a bad signature and there is no cached BLOB to fall back to.
833
   * @throws IOException if any of the following fails: downloading the trust root certificate,
834
   *     downloading the BLOB, reading or writing any cache file (if any), or parsing the BLOB
835
   *     contents.
836
   * @throws InvalidAlgorithmParameterException if certificate path validation fails.
837
   * @throws InvalidKeyException if signature verification fails.
838
   * @throws NoSuchAlgorithmException if signature verification fails, or if the SHA-256 algorithm
839
   *     or the <code>"Collection"</code> type {@link CertStore} is not available.
840
   * @throws SignatureException if signature verification fails.
841
   * @throws UnexpectedLegalHeader if the downloaded BLOB (if any) contains a <code>"legalHeader"
842
   *     </code> value not configured in {@link
843
   *     FidoMetadataDownloaderBuilder.Step1#expectLegalHeader(String...)
844
   *     expectLegalHeader(String...)} but is otherwise valid. The downloaded BLOB will not be
845
   *     written to cache in this case.
846
   */
847
  public MetadataBLOB loadCachedBlob()
848
      throws CertPathValidatorException,
849
          InvalidAlgorithmParameterException,
850
          Base64UrlException,
851
          CertificateException,
852
          IOException,
853
          NoSuchAlgorithmException,
854
          SignatureException,
855
          InvalidKeyException,
856
          UnexpectedLegalHeader,
857
          DigestException,
858
          FidoMetadataDownloaderException {
859
    final Set<TrustAnchor> trustAnchors = retrieveTrustAnchors();
860
861
    final Optional<MetadataBLOB> explicit = loadExplicitBlobOnly(trustAnchors);
862 1 1. loadCachedBlob : negated conditional → KILLED
    if (explicit.isPresent()) {
863
      log.debug("Explicit BLOB is set - disregarding cache and download.");
864 1 1. loadCachedBlob : replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader::loadCachedBlob → KILLED
      return explicit.get();
865
    }
866
867
    final Optional<MetadataBLOB> cached = loadCachedBlobOnly(trustAnchors);
868 1 1. loadCachedBlob : negated conditional → KILLED
    if (cached.isPresent()) {
869
      log.debug("Cached BLOB exists, checking expiry date...");
870
      if (cached
871
          .get()
872
          .getPayload()
873
          .getNextUpdate()
874
          .atStartOfDay()
875
          .atZone(clock.getZone())
876 1 1. loadCachedBlob : negated conditional → KILLED
          .isAfter(clock.instant().atZone(clock.getZone()))) {
877
        log.debug("Cached BLOB has not yet expired - using cached BLOB.");
878 1 1. loadCachedBlob : replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader::loadCachedBlob → KILLED
        return cached.get();
879
      } else {
880
        log.debug("Cached BLOB has expired.");
881
      }
882
883
    } else {
884
      log.debug("Cached BLOB does not exist or is invalid.");
885
    }
886
887 1 1. loadCachedBlob : replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader::loadCachedBlob → KILLED
    return refreshBlobInternal(trustAnchors, cached).get();
888
  }
889
890
  /**
891
   * Download and cache a fresh metadata BLOB, or read it from cache if the downloaded BLOB is not
892
   * up to date.
893
   *
894
   * <p>This method is NOT THREAD SAFE since it reads and writes caches.
895
   *
896
   * <p>On each execution this will, in order:
897
   *
898
   * <ol>
899
   *   <li>Download the trust root certificate, if necessary: if the cache is empty, the cache fails
900
   *       to load, or the cached cert is not valid at the current time (as determined by the {@link
901
   *       FidoMetadataDownloaderBuilder#clock(Clock) clock} setting).
902
   *   <li>If downloaded, cache the trust root certificate using the configured {@link File} or
903
   *       {@link Consumer} (see {@link FidoMetadataDownloaderBuilder.Step3})
904
   *   <li>Download the metadata BLOB.
905
   *   <li>Check the <code>"no"</code> property of the downloaded BLOB and compare it with the
906
   *       <code>"no"</code> of the cached BLOB, if any. The one with a greater <code>"no"
907
   *       </code> overrides the other, even if its <code>"nextUpdate"</code> is in the past.
908
   *   <li>If the downloaded BLOB has a newer <code>"no"</code>, or if no BLOB was cached, verify
909
   *       that the value of the downloaded BLOB's <code>"legalHeader"</code> appears in the
910
   *       configured {@link FidoMetadataDownloaderBuilder.Step1#expectLegalHeader(String...)
911
   *       expectLegalHeader} setting. If not, throw an {@link UnexpectedLegalHeader} exception
912
   *       containing the cached BLOB, if any, and the downloaded BLOB.
913
   *   <li>If the downloaded BLOB has an expected <code>
914
   *       "legalHeader"</code>, cache it using the configured {@link File} or {@link Consumer} (see
915
   *       {@link FidoMetadataDownloaderBuilder.Step5}).
916
   * </ol>
917
   *
918
   * No internal mutable state is maintained between invocations of this method; each invocation
919
   * will reload/rewrite caches, perform downloads and check the <code>"legalHeader"
920
   * </code> as necessary. You may therefore reuse a {@link FidoMetadataDownloader} instance and,
921
   * for example, call this method periodically to refresh the BLOB. Each call will return a new
922
   * {@link MetadataBLOB} instance; ones already returned will not be updated by subsequent calls.
923
   *
924
   * @return the successfully retrieved and validated metadata BLOB.
925
   * @throws Base64UrlException if the explicitly configured or newly downloaded BLOB is not a
926
   *     well-formed JWT in compact serialization.
927
   * @throws CertPathValidatorException if the explicitly configured or newly downloaded BLOB fails
928
   *     certificate path validation.
929
   * @throws CertificateException if the trust root certificate was downloaded and passed the
930
   *     SHA-256 integrity check, but does not contain a currently valid X.509 DER certificate; or
931
   *     if the BLOB signing certificate chain fails to parse.
932
   * @throws DigestException if the trust root certificate was downloaded but failed the SHA-256
933
   *     integrity check.
934
   * @throws FidoMetadataDownloaderException if the explicitly configured or newly downloaded BLOB
935
   *     (if any) has a bad signature and there is no cached BLOB to fall back to.
936
   * @throws IOException if any of the following fails: downloading the trust root certificate,
937
   *     downloading the BLOB, reading or writing any cache file (if any), or parsing the BLOB
938
   *     contents.
939
   * @throws InvalidAlgorithmParameterException if certificate path validation fails.
940
   * @throws InvalidKeyException if signature verification fails.
941
   * @throws NoSuchAlgorithmException if signature verification fails, or if the SHA-256 algorithm
942
   *     or the <code>"Collection"</code> type {@link CertStore} is not available.
943
   * @throws SignatureException if signature verification fails.
944
   * @throws UnexpectedLegalHeader if the downloaded BLOB (if any) contains a <code>"legalHeader"
945
   *     </code> value not configured in {@link
946
   *     FidoMetadataDownloaderBuilder.Step1#expectLegalHeader(String...)
947
   *     expectLegalHeader(String...)} but is otherwise valid. The downloaded BLOB will not be
948
   *     written to cache in this case.
949
   */
950
  public MetadataBLOB refreshBlob()
951
      throws CertPathValidatorException,
952
          InvalidAlgorithmParameterException,
953
          Base64UrlException,
954
          CertificateException,
955
          IOException,
956
          NoSuchAlgorithmException,
957
          SignatureException,
958
          InvalidKeyException,
959
          UnexpectedLegalHeader,
960
          DigestException,
961
          FidoMetadataDownloaderException {
962
    final Set<TrustAnchor> trustAnchors = retrieveTrustAnchors();
963
964
    final Optional<MetadataBLOB> explicit = loadExplicitBlobOnly(trustAnchors);
965 1 1. refreshBlob : negated conditional → KILLED
    if (explicit.isPresent()) {
966
      log.debug("Explicit BLOB is set - disregarding cache and download.");
967 1 1. refreshBlob : replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader::refreshBlob → KILLED
      return explicit.get();
968
    }
969
970
    final Optional<MetadataBLOB> cached = loadCachedBlobOnly(trustAnchors);
971 1 1. refreshBlob : negated conditional → SURVIVED
    if (cached.isPresent()) {
972
      log.debug("Cached BLOB exists, proceeding to compare against fresh BLOB...");
973
    } else {
974
      log.debug("Cached BLOB does not exist or is invalid.");
975
    }
976
977 1 1. refreshBlob : replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader::refreshBlob → KILLED
    return refreshBlobInternal(trustAnchors, cached).get();
978
  }
979
980
  private Optional<MetadataBLOB> refreshBlobInternal(
981
      @NonNull Set<TrustAnchor> trustAnchors, @NonNull Optional<MetadataBLOB> cached)
982
      throws CertPathValidatorException,
983
          InvalidAlgorithmParameterException,
984
          Base64UrlException,
985
          CertificateException,
986
          IOException,
987
          NoSuchAlgorithmException,
988
          SignatureException,
989
          InvalidKeyException,
990
          UnexpectedLegalHeader,
991
          FidoMetadataDownloaderException {
992
993
    try {
994
      log.debug("Attempting to download new BLOB...");
995
      final DownloadResult downloadResult =
996
          download(
997
              blobUrl,
998
              // This should ideally use the value of the ETag response header from when the cached
999
              // BLOB was downloaded, but we don't have anywhere to store that without changing the
1000
              // format of the cache serialization. This is good enough as the MDS explicitly
1001
              // specifies that the ETag is set to the "no" of the BLOB.
1002 1 1. lambda$refreshBlobInternal$0 : replaced return value with "" for com/yubico/fido/metadata/FidoMetadataDownloader::lambda$refreshBlobInternal$0 → KILLED
              cached.map(cachedBlob -> String.format("%d", cachedBlob.getPayload().getNo())));
1003 1 1. refreshBlobInternal : negated conditional → KILLED
      if (downloadResult.isNotModified()) {
1004
        log.debug("Remote BLOB not modified - using cached BLOB.");
1005 1 1. refreshBlobInternal : replaced return value with Optional.empty for com/yubico/fido/metadata/FidoMetadataDownloader::refreshBlobInternal → NO_COVERAGE
        return cached;
1006
1007
      } else {
1008
        byte[] downloadedBytes = downloadResult.getContent();
1009
        final MetadataBLOB downloadedBlob = parseAndVerifyBlob(downloadedBytes, trustAnchors);
1010
        log.debug("New BLOB downloaded.");
1011
1012 1 1. refreshBlobInternal : negated conditional → KILLED
        if (cached.isPresent()) {
1013
          log.debug("Cached BLOB exists - checking if new BLOB has a higher \"no\"...");
1014 2 1. refreshBlobInternal : changed conditional boundary → SURVIVED
2. refreshBlobInternal : negated conditional → KILLED
          if (downloadedBlob.getPayload().getNo() <= cached.get().getPayload().getNo()) {
1015
            log.debug("New BLOB does not have a higher \"no\" - using cached BLOB instead.");
1016 1 1. refreshBlobInternal : replaced return value with Optional.empty for com/yubico/fido/metadata/FidoMetadataDownloader::refreshBlobInternal → KILLED
            return cached;
1017
          }
1018
          log.debug("New BLOB has a higher \"no\" - proceeding with new BLOB.");
1019
        }
1020
1021
        log.debug("Checking legalHeader in new BLOB...");
1022 1 1. refreshBlobInternal : negated conditional → KILLED
        if (!expectedLegalHeaders.contains(downloadedBlob.getPayload().getLegalHeader())) {
1023
          throw new UnexpectedLegalHeader(cached.orElse(null), downloadedBlob);
1024
        }
1025
1026
        log.debug("Writing new BLOB to cache...");
1027 1 1. refreshBlobInternal : negated conditional → KILLED
        if (blobCacheFile != null) {
1028
          try (FileOutputStream f = new FileOutputStream(blobCacheFile)) {
1029 1 1. refreshBlobInternal : removed call to java/io/FileOutputStream::write → KILLED
            f.write(downloadedBytes);
1030
          }
1031
        }
1032
1033 1 1. refreshBlobInternal : negated conditional → KILLED
        if (blobCacheConsumer != null) {
1034 1 1. refreshBlobInternal : removed call to java/util/function/Consumer::accept → KILLED
          blobCacheConsumer.accept(new ByteArray(downloadedBytes));
1035
        }
1036
1037 1 1. refreshBlobInternal : replaced return value with Optional.empty for com/yubico/fido/metadata/FidoMetadataDownloader::refreshBlobInternal → KILLED
        return Optional.of(downloadedBlob);
1038
      }
1039
    } catch (FidoMetadataDownloaderException e) {
1040 2 1. refreshBlobInternal : negated conditional → KILLED
2. refreshBlobInternal : negated conditional → KILLED
      if (e.getReason() == Reason.BAD_SIGNATURE && cached.isPresent()) {
1041
        switch (cachePolicy.apply(e)) {
1042
          case USE_CACHED:
1043
            log.warn("New BLOB has bad signature - falling back to cached BLOB.");
1044 1 1. refreshBlobInternal : replaced return value with Optional.empty for com/yubico/fido/metadata/FidoMetadataDownloader::refreshBlobInternal → KILLED
            return cached;
1045
          default:
1046
            throw e;
1047
        }
1048
      } else {
1049
        throw e;
1050
      }
1051
    } catch (Exception e) {
1052 1 1. refreshBlobInternal : negated conditional → KILLED
      if (cached.isPresent()) {
1053
        switch (cachePolicy.apply(e)) {
1054
          case USE_CACHED:
1055
            log.warn("Failed to download new BLOB - falling back to cached BLOB.", e);
1056 1 1. refreshBlobInternal : replaced return value with Optional.empty for com/yubico/fido/metadata/FidoMetadataDownloader::refreshBlobInternal → KILLED
            return cached;
1057
          default:
1058
            throw e;
1059
        }
1060
      } else {
1061
        throw e;
1062
      }
1063
    }
1064
  }
1065
1066
  /**
1067
   * @throws CertificateException if the trust root certificate was downloaded and passed the
1068
   *     SHA-256 integrity check, but does not contain a currently valid X.509 DER certificate.
1069
   * @throws DigestException if the trust root certificate was downloaded but failed the SHA-256
1070
   *     integrity check.
1071
   * @throws IOException if the trust root certificate download failed, or if reading or writing the
1072
   *     cache file (if any) failed.
1073
   * @throws NoSuchAlgorithmException if the SHA-256 algorithm is not available.
1074
   */
1075
  private Set<TrustAnchor> retrieveTrustAnchors()
1076
      throws CertificateException, DigestException, IOException, NoSuchAlgorithmException {
1077
1078 1 1. retrieveTrustAnchors : negated conditional → KILLED
    if (trustAnchors != null) {
1079 1 1. retrieveTrustAnchors : replaced return value with Collections.emptySet for com/yubico/fido/metadata/FidoMetadataDownloader::retrieveTrustAnchors → KILLED
      return trustAnchors;
1080
1081
    } else {
1082
      final Optional<ByteArray> cachedContents;
1083 1 1. retrieveTrustAnchors : negated conditional → KILLED
      if (trustRootCacheFile != null) {
1084
        cachedContents = readCacheFile(trustRootCacheFile);
1085
      } else {
1086
        cachedContents = trustRootCacheSupplier.get();
1087
      }
1088
1089
      Set<X509Certificate> certs =
1090
          cachedContents
1091 1 1. lambda$retrieveTrustAnchors$1 : replaced return value with Optional.empty for com/yubico/fido/metadata/FidoMetadataDownloader::lambda$retrieveTrustAnchors$1 → KILLED
              .flatMap(cc -> readTrustAnchorsCache(new ByteArrayInputStream(cc.getBytes())))
1092
              .orElseGet(HashSet::new);
1093
1094 1 1. retrieveTrustAnchors : negated conditional → KILLED
      if (certs.isEmpty()) {
1095
        List<byte[]> downloadedChunks = new ArrayList<>();
1096
        for (URL trustRootUrl : trustRootUrls) {
1097
          final byte[] downloaded = verifyHash(download(trustRootUrl), trustRootSha256);
1098 1 1. retrieveTrustAnchors : negated conditional → KILLED
          if (downloaded == null) {
1099
            throw new DigestException(
1100
                "Downloaded trust root certificate matches none of the acceptable hashes.");
1101
          }
1102
1103
          final X509Certificate cert = CertificateParser.parseDer(downloaded);
1104 1 1. retrieveTrustAnchors : removed call to java/security/cert/X509Certificate::checkValidity → SURVIVED
          cert.checkValidity(Date.from(clock.instant()));
1105
          certs.add(cert);
1106
          downloadedChunks.add(downloaded);
1107
        }
1108
1109
        final TrustRootsCacheValue cacheValue =
1110
            new TrustRootsCacheValue(
1111
                trustRootUrls.stream().map(URL::toString).collect(Collectors.toList()),
1112
                downloadedChunks);
1113 1 1. retrieveTrustAnchors : negated conditional → KILLED
        if (trustRootCacheFile != null) {
1114
          try (FileOutputStream f = new FileOutputStream(trustRootCacheFile)) {
1115 1 1. retrieveTrustAnchors : removed call to com/fasterxml/jackson/databind/ObjectMapper::writeValue → KILLED
            com.yubico.internal.util.JacksonCodecs.cbor().writeValue(f, cacheValue);
1116
          }
1117
        }
1118
1119 1 1. retrieveTrustAnchors : negated conditional → KILLED
        if (trustRootCacheConsumer != null) {
1120 1 1. retrieveTrustAnchors : removed call to java/util/function/Consumer::accept → KILLED
          trustRootCacheConsumer.accept(
1121
              new ByteArray(
1122
                  com.yubico.internal.util.JacksonCodecs.cbor().writeValueAsBytes(cacheValue)));
1123
        }
1124
      }
1125
1126 1 1. retrieveTrustAnchors : replaced return value with Collections.emptySet for com/yubico/fido/metadata/FidoMetadataDownloader::retrieveTrustAnchors → KILLED
      return certs.stream()
1127
          .map(FidoMetadataDownloader::importTrustAnchor)
1128
          .collect(Collectors.toSet());
1129
    }
1130
  }
1131
1132
  static TrustAnchor importTrustAnchor(X509Certificate trustRootCertificate) {
1133 1 1. importTrustAnchor : replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader::importTrustAnchor → KILLED
    return new TrustAnchor(trustRootCertificate, null);
1134
  }
1135
1136
  /**
1137
   * @throws Base64UrlException if the metadata BLOB is not a well-formed JWT in compact
1138
   *     serialization.
1139
   * @throws CertPathValidatorException if the explicitly configured BLOB fails certificate path
1140
   *     validation.
1141
   * @throws CertificateException if the BLOB signing certificate chain fails to parse.
1142
   * @throws IOException on failure to parse the BLOB contents.
1143
   * @throws InvalidAlgorithmParameterException if certificate path validation fails.
1144
   * @throws InvalidKeyException if signature verification fails.
1145
   * @throws NoSuchAlgorithmException if signature verification fails, or if the SHA-256 algorithm
1146
   *     or the <code>"Collection"</code> type {@link CertStore} is not available.
1147
   * @throws SignatureException if signature verification fails.
1148
   * @throws FidoMetadataDownloaderException if the explicitly configured BLOB (if any) has a bad
1149
   *     signature.
1150
   */
1151
  private Optional<MetadataBLOB> loadExplicitBlobOnly(Set<TrustAnchor> trustAnchors)
1152
      throws Base64UrlException,
1153
          CertPathValidatorException,
1154
          CertificateException,
1155
          IOException,
1156
          InvalidAlgorithmParameterException,
1157
          InvalidKeyException,
1158
          NoSuchAlgorithmException,
1159
          SignatureException,
1160
          FidoMetadataDownloaderException {
1161 1 1. loadExplicitBlobOnly : negated conditional → KILLED
    if (blobJwt != null) {
1162 1 1. loadExplicitBlobOnly : replaced return value with Optional.empty for com/yubico/fido/metadata/FidoMetadataDownloader::loadExplicitBlobOnly → KILLED
      return Optional.of(
1163
          parseAndMaybeVerifyBlob(blobJwt.getBytes(StandardCharsets.UTF_8), trustAnchors));
1164
1165
    } else {
1166
      return Optional.empty();
1167
    }
1168
  }
1169
1170
  private Optional<MetadataBLOB> loadCachedBlobOnly(Set<TrustAnchor> trustAnchors) {
1171
1172
    final Optional<ByteArray> cachedContents;
1173 1 1. loadCachedBlobOnly : negated conditional → KILLED
    if (blobCacheFile != null) {
1174
      log.debug("Attempting to read BLOB from cache file...");
1175
1176
      try {
1177
        cachedContents = readCacheFile(blobCacheFile);
1178
      } catch (IOException e) {
1179
        return Optional.empty();
1180
      }
1181
    } else {
1182
      log.debug("Attempting to read BLOB from cache Supplier...");
1183
      cachedContents = blobCacheSupplier.get();
1184
    }
1185
1186 1 1. loadCachedBlobOnly : replaced return value with Optional.empty for com/yubico/fido/metadata/FidoMetadataDownloader::loadCachedBlobOnly → KILLED
    return cachedContents.map(
1187
        cached -> {
1188
          try {
1189 1 1. lambda$loadCachedBlobOnly$2 : replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader::lambda$loadCachedBlobOnly$2 → KILLED
            return parseAndMaybeVerifyBlob(cached.getBytes(), trustAnchors);
1190
          } catch (Exception e) {
1191
            log.warn("Failed to read or parse cached BLOB.", e);
1192
            return null;
1193
          }
1194
        });
1195
  }
1196
1197
  Optional<ByteArray> readCacheFile(File cacheFile) throws IOException {
1198 3 1. readCacheFile : negated conditional → KILLED
2. readCacheFile : negated conditional → KILLED
3. readCacheFile : negated conditional → KILLED
    if (cacheFile.exists() && cacheFile.canRead() && cacheFile.isFile()) {
1199
      try (FileInputStream f = new FileInputStream(cacheFile)) {
1200 1 1. readCacheFile : replaced return value with Optional.empty for com/yubico/fido/metadata/FidoMetadataDownloader::readCacheFile → KILLED
        return Optional.of(new ByteArray(readAll(f)));
1201
      } catch (FileNotFoundException e) {
1202
        throw new RuntimeException(
1203
            "This exception should be impossible, please file a bug report.", e);
1204
      }
1205
    } else {
1206
      return Optional.empty();
1207
    }
1208
  }
1209
1210
  Optional<Set<X509Certificate>> readTrustAnchorsCache(InputStream is) {
1211
    try {
1212
      TrustRootsCacheValue cache =
1213
          com.yubico.internal.util.JacksonCodecs.cbor().readValue(is, TrustRootsCacheValue.class);
1214 1 1. readTrustAnchorsCache : negated conditional → KILLED
      if (cache.urls.equals(trustRootUrls.stream().map(URL::toString).collect(Collectors.toList()))
1215 1 1. readTrustAnchorsCache : negated conditional → KILLED
          && cache.urls.size() == cache.certsDer.size()) {
1216
        Set<X509Certificate> cachedCerts = new HashSet<>();
1217
        for (byte[] der : cache.certsDer) {
1218
          X509Certificate cachedCert = CertificateParser.parseDer(der);
1219
          final byte[] verifiedCachedContents =
1220
              verifyHash(cachedCert.getEncoded(), trustRootSha256);
1221 1 1. readTrustAnchorsCache : negated conditional → KILLED
          if (verifiedCachedContents != null) {
1222 1 1. readTrustAnchorsCache : removed call to java/security/cert/X509Certificate::checkValidity → SURVIVED
            cachedCert.checkValidity(Date.from(clock.instant()));
1223
          } else {
1224
            log.debug(
1225
                "Cached trust root certificate does not match any acceptable trust root SHA-256 hash.");
1226
            return Optional.empty();
1227
          }
1228
          cachedCerts.add(cachedCert);
1229
        }
1230 1 1. readTrustAnchorsCache : replaced return value with Optional.empty for com/yubico/fido/metadata/FidoMetadataDownloader::readTrustAnchorsCache → KILLED
        return Optional.of(cachedCerts);
1231
      } else {
1232
        log.debug(
1233
            "Cached trust root certificate URLs differ from current configuration, or number of URLs does not equal number of cached certificates - ignoring cache.");
1234
        return Optional.empty();
1235
      }
1236
    } catch (IOException | CertificateException | NoSuchAlgorithmException e) {
1237
      log.debug("Failed to read trust root certificates from cache", e);
1238
      return Optional.empty();
1239
    }
1240
  }
1241
1242
  private byte[] download(URL url) throws IOException {
1243
    final DownloadResult downloadResult = download(url, Optional.empty());
1244 1 1. download : negated conditional → KILLED
    if (downloadResult.isOk()) {
1245 1 1. download : replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader::download → KILLED
      return downloadResult.getContent();
1246
    } else {
1247
      final String msg =
1248
          "download(URL, Optional.empty()) returned non-OK success response. This should be impossible, please file a bug report.";
1249
      log.error(msg);
1250
      throw new RuntimeException(msg);
1251
    }
1252
  }
1253
1254
  private DownloadResult download(URL url, Optional<String> etag) throws IOException {
1255
    URLConnection conn = url.openConnection();
1256
1257 1 1. download : negated conditional → KILLED
    if (conn instanceof HttpsURLConnection) {
1258
      HttpsURLConnection httpsConn = (HttpsURLConnection) conn;
1259 1 1. download : negated conditional → KILLED
      if (httpsTrustStore != null) {
1260
        try {
1261
          TrustManagerFactory trustMan =
1262
              TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
1263 1 1. download : removed call to javax/net/ssl/TrustManagerFactory::init → KILLED
          trustMan.init(httpsTrustStore);
1264
          SSLContext sslContext = SSLContext.getInstance("TLS");
1265 1 1. download : removed call to javax/net/ssl/SSLContext::init → KILLED
          sslContext.init(null, trustMan.getTrustManagers(), null);
1266
1267 1 1. download : removed call to javax/net/ssl/HttpsURLConnection::setSSLSocketFactory → KILLED
          httpsConn.setSSLSocketFactory(sslContext.getSocketFactory());
1268
        } catch (NoSuchAlgorithmException | KeyStoreException | KeyManagementException e) {
1269
          throw new RuntimeException(
1270
              "Failed to initialize HTTPS trust store. This should be impossible, please file a bug report.",
1271
              e);
1272
        }
1273
      }
1274 1 1. download : removed call to javax/net/ssl/HttpsURLConnection::setRequestMethod → SURVIVED
      httpsConn.setRequestMethod("GET");
1275 1 1. download : removed call to java/util/Optional::ifPresent → KILLED
      etag.ifPresent(
1276
          et -> {
1277 1 1. lambda$download$3 : removed call to javax/net/ssl/HttpsURLConnection::addRequestProperty → KILLED
            httpsConn.addRequestProperty("If-None-Match", String.format("\"%s\"", et));
1278
          });
1279
1280 1 1. download : negated conditional → SURVIVED
      if (httpsConn.getResponseCode() != HttpsURLConnection.HTTP_OK) {
1281
        switch (httpsConn.getResponseCode()) {
1282
          case 304: // Not Modified
1283
            log.debug("Received 304 Not Modified response to download request: {}", url);
1284 1 1. download : replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader::download → NO_COVERAGE
            return DownloadResult.notModified();
1285
        }
1286
1287
        log.warn(
1288
            "Received non-200 status: {} to download request: {}",
1289
            httpsConn.getResponseCode(),
1290
            url);
1291
1292
        // As of 2026-05-05, FIDO MDS returns an ETag header even with 429 Too Many Requests status.
1293
        // We might as well use it when present, even when the status is technically a failure.
1294
        final String responseEtag = httpsConn.getHeaderField("ETag");
1295 1 1. download : negated conditional → SURVIVED
        if (responseEtag != null) {
1296
          log.debug("Response ETag: {}", responseEtag);
1297 1 1. download : negated conditional → NO_COVERAGE
          if (etag.map(
1298
                  et ->
1299
                      // ETag header value should be wrapped with double quotes (`etag: "243"`), but
1300
                      // FIDO MDS returns it like: `etag: 243`. Try both in case that changes in the
1301
                      // future.
1302 3 1. lambda$download$4 : negated conditional → NO_COVERAGE
2. lambda$download$4 : negated conditional → NO_COVERAGE
3. lambda$download$4 : replaced Boolean return with True for com/yubico/fido/metadata/FidoMetadataDownloader::lambda$download$4 → NO_COVERAGE
                      et.equals(responseEtag) || String.format("\"%s\"", et).equals(responseEtag))
1303
              .orElse(false)) {
1304
            log.debug("Response ETag matches local ETag - interpreting as not modified.");
1305 1 1. download : replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader::download → NO_COVERAGE
            return DownloadResult.notModified();
1306
          }
1307
        }
1308
      }
1309
    }
1310
1311 1 1. download : replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader::download → KILLED
    return DownloadResult.ok(readAll(conn.getInputStream()));
1312
  }
1313
1314
  private MetadataBLOB parseAndVerifyBlob(byte[] jwt, Set<TrustAnchor> trustAnchors)
1315
      throws CertPathValidatorException,
1316
          InvalidAlgorithmParameterException,
1317
          CertificateException,
1318
          IOException,
1319
          NoSuchAlgorithmException,
1320
          SignatureException,
1321
          InvalidKeyException,
1322
          Base64UrlException,
1323
          FidoMetadataDownloaderException {
1324 1 1. parseAndVerifyBlob : replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader::parseAndVerifyBlob → KILLED
    return verifyBlob(parseBlob(jwt), trustAnchors);
1325
  }
1326
1327
  private MetadataBLOB parseAndMaybeVerifyBlob(byte[] jwt, Set<TrustAnchor> trustAnchors)
1328
      throws CertPathValidatorException,
1329
          InvalidAlgorithmParameterException,
1330
          CertificateException,
1331
          IOException,
1332
          NoSuchAlgorithmException,
1333
          SignatureException,
1334
          InvalidKeyException,
1335
          Base64UrlException,
1336
          FidoMetadataDownloaderException {
1337 1 1. parseAndMaybeVerifyBlob : negated conditional → KILLED
    if (verifyDownloadsOnly) {
1338 1 1. parseAndMaybeVerifyBlob : replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader::parseAndMaybeVerifyBlob → KILLED
      return parseBlob(jwt).blob;
1339
    } else {
1340 1 1. parseAndMaybeVerifyBlob : replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader::parseAndMaybeVerifyBlob → KILLED
      return verifyBlob(parseBlob(jwt), trustAnchors);
1341
    }
1342
  }
1343
1344
  private MetadataBLOB verifyBlob(ParseResult parseResult, Set<TrustAnchor> trustAnchors)
1345
      throws IOException,
1346
          CertificateException,
1347
          NoSuchAlgorithmException,
1348
          InvalidKeyException,
1349
          SignatureException,
1350
          CertPathValidatorException,
1351
          InvalidAlgorithmParameterException,
1352
          FidoMetadataDownloaderException {
1353
    final MetadataBLOBHeader header = parseResult.blob.getHeader();
1354
    final Optional<List<X509Certificate>> certChain = fetchHeaderCertChain(header);
1355 1 1. verifyBlob : negated conditional → KILLED
    if (certChain.isPresent()) {
1356 1 1. verifyBlob : replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader::verifyBlob → KILLED
      return tryVerifyBlob(parseResult, trustAnchors, certChain.get());
1357
    } else {
1358
      log.debug(
1359
          "x5u and x5c both missing from BLOB header. Falling back to using trust anchors as BLOB signer.");
1360
      for (TrustAnchor ta : trustAnchors) {
1361
        final X509Certificate cert = ta.getTrustedCert();
1362 1 1. verifyBlob : negated conditional → KILLED
        if (cert != null) {
1363
          try {
1364 1 1. verifyBlob : replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader::verifyBlob → KILLED
            return tryVerifyBlob(parseResult, trustAnchors, Collections.singletonList(cert));
1365
          } catch (FidoMetadataDownloaderException e) {
1366 1 1. verifyBlob : negated conditional → SURVIVED
            if (e.getReason() == Reason.BAD_SIGNATURE) {
1367
              log.debug("Failed to verify BLOB with trust anchor: {}", ta);
1368
            } else {
1369
              throw e;
1370
            }
1371
          } catch (SignatureException | CertPathValidatorException e) {
1372
            log.debug("Failed to verify BLOB with trust anchor: {}", ta);
1373
          }
1374
        }
1375
      }
1376
      throw new IllegalArgumentException("Failed to verify BLOB with any trust anchor.");
1377
    }
1378
  }
1379
1380
  private MetadataBLOB tryVerifyBlob(
1381
      ParseResult parseResult, Set<TrustAnchor> trustAnchors, List<X509Certificate> certChain)
1382
      throws CertificateException,
1383
          NoSuchAlgorithmException,
1384
          InvalidKeyException,
1385
          SignatureException,
1386
          CertPathValidatorException,
1387
          InvalidAlgorithmParameterException,
1388
          FidoMetadataDownloaderException {
1389
    final MetadataBLOBHeader header = parseResult.blob.getHeader();
1390
    final X509Certificate leafCert = certChain.get(0);
1391
1392
    final Signature signature;
1393
    switch (header.getAlg()) {
1394
      case "RS256":
1395
        signature = Signature.getInstance("SHA256withRSA");
1396
        break;
1397
1398
      case "ES256":
1399
        signature = Signature.getInstance("SHA256withECDSA");
1400
        break;
1401
1402
      default:
1403
        throw new UnsupportedOperationException(
1404
            "Unimplemented JWT verification algorithm: " + header.getAlg());
1405
    }
1406
1407 1 1. tryVerifyBlob : removed call to java/security/Signature::initVerify → KILLED
    signature.initVerify(leafCert.getPublicKey());
1408 1 1. tryVerifyBlob : removed call to java/security/Signature::update → KILLED
    signature.update(
1409
        (parseResult.jwtHeader.getBase64Url() + "." + parseResult.jwtPayload.getBase64Url())
1410
            .getBytes(StandardCharsets.UTF_8));
1411 1 1. tryVerifyBlob : negated conditional → KILLED
    if (!signature.verify(parseResult.jwtSignature.getBytes())) {
1412
      throw new FidoMetadataDownloaderException(Reason.BAD_SIGNATURE);
1413
    }
1414
1415
    final CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
1416
    final CertPathValidator cpv = CertPathValidator.getInstance("PKIX");
1417
    final PKIXParameters pathParams = new PKIXParameters(trustAnchors);
1418 1 1. tryVerifyBlob : negated conditional → KILLED
    if (certStore != null) {
1419 1 1. tryVerifyBlob : removed call to java/security/cert/PKIXParameters::addCertStore → KILLED
      pathParams.addCertStore(certStore);
1420
    }
1421
1422
    // Parse CRLDistributionPoints ourselves so users don't have to set the
1423
    // `com.sun.security.enableCRLDP=true` system property
1424 1 1. tryVerifyBlob : removed call to java/util/Optional::ifPresent → SURVIVED
    fetchCrlDistributionPoints(certChain, certFactory).ifPresent(pathParams::addCertStore);
1425
1426 1 1. tryVerifyBlob : removed call to java/security/cert/PKIXParameters::setDate → KILLED
    pathParams.setDate(Date.from(clock.instant()));
1427
1428
    // Try validating first the full cert path, and if that fails retry by omitting one cert at a
1429
    // time from the end.
1430
    // This enables "short-circuiting" the cert path if the trust anchor appears in the cert path,
1431
    // as was the case in August 2026 when the new trust anchor "R46" appeared last in the cert path
1432
    // signed by the previous trust anchor "R3".
1433
    CertPathValidatorException firstError = null;
1434 2 1. tryVerifyBlob : negated conditional → KILLED
2. tryVerifyBlob : changed conditional boundary → KILLED
    for (int pathLen = certChain.size(); pathLen >= 1; --pathLen) {
1435
      final CertPath blobCertPath = certFactory.generateCertPath(certChain.subList(0, pathLen));
1436
      try {
1437
        cpv.validate(blobCertPath, pathParams);
1438 1 1. tryVerifyBlob : replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader::tryVerifyBlob → KILLED
        return parseResult.blob;
1439
      } catch (CertPathValidatorException e) {
1440 1 1. tryVerifyBlob : negated conditional → KILLED
        if (firstError == null) {
1441
          firstError = e;
1442
        }
1443 1 1. tryVerifyBlob : negated conditional → KILLED
        if (pathLen == 1) {
1444
          throw firstError;
1445
        }
1446
      }
1447
    }
1448
    throw new IllegalStateException(
1449
        "Exited without finding a certification path or failing to validate any certification path. This should be impossible, please file a bug report.");
1450
  }
1451
1452
  ParseResult parseBlob(byte[] jwt) throws IOException, Base64UrlException {
1453
    Scanner s = new Scanner(new ByteArrayInputStream(jwt)).useDelimiter("\\.");
1454
    final ByteArray jwtHeader = ByteArray.fromBase64Url(s.next());
1455
    final ByteArray jwtPayload = ByteArray.fromBase64Url(s.next());
1456
    final ByteArray jwtSignature = ByteArray.fromBase64Url(s.next());
1457
1458
    final ObjectMapper headerJsonMapper =
1459
        makeHeaderJsonMapper.get().setBase64Variant(Base64Variants.MIME_NO_LINEFEEDS);
1460
1461 1 1. parseBlob : replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader::parseBlob → KILLED
    return new ParseResult(
1462
        new MetadataBLOB(
1463
            headerJsonMapper.readValue(jwtHeader.getBytes(), MetadataBLOBHeader.class),
1464
            makePayloadJsonMapper
1465
                .get()
1466
                .readValue(jwtPayload.getBytes(), MetadataBLOBPayload.class)),
1467
        jwtHeader,
1468
        jwtPayload,
1469
        jwtSignature);
1470
  }
1471
1472
  static ObjectMapper defaultHeaderJsonMapper() {
1473 1 1. defaultHeaderJsonMapper : replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader::defaultHeaderJsonMapper → KILLED
    return JacksonCodecs.json();
1474
  }
1475
1476
  static ObjectMapper defaultPayloadJsonMapper() {
1477 1 1. defaultPayloadJsonMapper : replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader::defaultPayloadJsonMapper → KILLED
    return JacksonCodecs.jsonWithDefaultEnums();
1478
  }
1479
1480
  private static byte[] readAll(InputStream is) throws IOException {
1481 1 1. readAll : replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader::readAll → KILLED
    return BinaryUtil.readAll(is);
1482
  }
1483
1484
  /**
1485
   * @return <code>contents</code> if its SHA-256 hash matches any element of <code>
1486
   *     acceptedCertSha256</code>, otherwise <code>null</code>.
1487
   */
1488
  private static byte[] verifyHash(byte[] contents, Set<ByteArray> acceptedCertSha256)
1489
      throws NoSuchAlgorithmException {
1490
    MessageDigest digest = MessageDigest.getInstance("SHA-256");
1491
    final ByteArray hash = new ByteArray(digest.digest(contents));
1492 1 1. verifyHash : negated conditional → KILLED
    if (acceptedCertSha256.stream().anyMatch(hash::equals)) {
1493 1 1. verifyHash : replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader::verifyHash → KILLED
      return contents;
1494
    } else {
1495
      return null;
1496
    }
1497
  }
1498
1499
  @Value
1500
  static class ParseResult {
1501
    private MetadataBLOB blob;
1502
    private ByteArray jwtHeader;
1503
    private ByteArray jwtPayload;
1504
    private ByteArray jwtSignature;
1505
  }
1506
1507
  /** Parse the header cert chain and download any certificates as necessary. */
1508
  Optional<List<X509Certificate>> fetchHeaderCertChain(MetadataBLOBHeader header)
1509
      throws IOException, CertificateException {
1510 1 1. fetchHeaderCertChain : negated conditional → KILLED
    if (header.getX5u().isPresent()) {
1511
      final URL x5u = header.getX5u().get();
1512 1 1. fetchHeaderCertChain : negated conditional → KILLED
      if (blobUrl != null
1513 1 1. fetchHeaderCertChain : negated conditional → SURVIVED
          && (!(x5u.getHost().equals(blobUrl.getHost())
1514 1 1. fetchHeaderCertChain : negated conditional → SURVIVED
              && x5u.getProtocol().equals(blobUrl.getProtocol())
1515 1 1. fetchHeaderCertChain : negated conditional → KILLED
              && x5u.getPort() == blobUrl.getPort()))) {
1516
        throw new IllegalArgumentException(
1517
            String.format(
1518
                "x5u in BLOB header must have same origin as the URL the BLOB was downloaded from. Expected origin of: %s ; found: %s",
1519
                blobUrl, x5u));
1520
      }
1521
      List<X509Certificate> certs = new ArrayList<>();
1522
      for (String pem :
1523
          new String(download(x5u), StandardCharsets.UTF_8)
1524
              .trim()
1525
              .split("\\n+-----END CERTIFICATE-----\\n+-----BEGIN CERTIFICATE-----\\n+")) {
1526
        X509Certificate x509Certificate = CertificateParser.parsePem(pem);
1527
        certs.add(x509Certificate);
1528
      }
1529 1 1. fetchHeaderCertChain : replaced return value with Optional.empty for com/yubico/fido/metadata/FidoMetadataDownloader::fetchHeaderCertChain → KILLED
      return Optional.of(certs);
1530 1 1. fetchHeaderCertChain : negated conditional → KILLED
    } else if (header.getX5c().isPresent()) {
1531 1 1. fetchHeaderCertChain : replaced return value with Optional.empty for com/yubico/fido/metadata/FidoMetadataDownloader::fetchHeaderCertChain → KILLED
      return Optional.of(header.getX5c().get());
1532
    } else {
1533
      return Optional.empty();
1534
    }
1535
  }
1536
1537
  /**
1538
   * Parse the CRLDistributionPoints extension of each certificate, fetch each distribution point
1539
   * and assemble them into a {@link CertStore} ready to be injected into {@link
1540
   * PKIXParameters#addCertStore(CertStore)} to provide CRLs for the verification procedure.
1541
   *
1542
   * <p>We do this ourselves so that users don't have to set the <code>
1543
   * com.sun.security.enableCRLDP=true</code> system property. This is required by the default SUN
1544
   * provider in order to enable CRLDistributionPoints resolution.
1545
   *
1546
   * <p>Any CRLDistributionPoints entries in unknown format are ignored and log a warning.
1547
   */
1548
  private Optional<CertStore> fetchCrlDistributionPoints(
1549
      List<X509Certificate> certChain, CertificateFactory certFactory)
1550
      throws InvalidAlgorithmParameterException, NoSuchAlgorithmException {
1551
    final List<URL> crlDistributionPointUrls =
1552
        certChain.stream()
1553
            .flatMap(
1554
                cert -> {
1555
                  log.debug(
1556
                      "Attempting to parse CRLDistributionPoints extension of cert: {}",
1557
                      cert.getSubjectX500Principal());
1558
                  try {
1559 1 1. lambda$fetchCrlDistributionPoints$5 : replaced return value with Stream.empty for com/yubico/fido/metadata/FidoMetadataDownloader::lambda$fetchCrlDistributionPoints$5 → SURVIVED
                    return CertificateParser.parseCrlDistributionPointsExtension(cert)
1560
                        .getDistributionPoints()
1561
                        .stream();
1562
                  } catch (Exception e) {
1563
                    log.warn(
1564
                        "Failed to parse CRLDistributionPoints extension of cert: {}",
1565
                        cert.getSubjectX500Principal(),
1566
                        e);
1567
                    return Stream.empty();
1568
                  }
1569
                })
1570
            .collect(Collectors.toList());
1571
1572 1 1. fetchCrlDistributionPoints : negated conditional → SURVIVED
    if (crlDistributionPointUrls.isEmpty()) {
1573
      return Optional.empty();
1574
1575
    } else {
1576
      final List<CRL> crldpCrls =
1577
          crlDistributionPointUrls.stream()
1578
              .map(
1579
                  crldpUrl -> {
1580
                    log.debug("Attempting to download CRL distribution point: {}", crldpUrl);
1581
                    try {
1582 1 1. lambda$fetchCrlDistributionPoints$6 : replaced return value with Optional.empty for com/yubico/fido/metadata/FidoMetadataDownloader::lambda$fetchCrlDistributionPoints$6 → NO_COVERAGE
                      return Optional.of(
1583
                          certFactory.generateCRL(new ByteArrayInputStream(download(crldpUrl))));
1584
                    } catch (CRLException e) {
1585
                      log.warn("Failed to import CRL from distribution point: {}", crldpUrl, e);
1586
                      return Optional.<CRL>empty();
1587
                    } catch (Exception e) {
1588
                      log.warn("Failed to download CRL distribution point: {}", crldpUrl, e);
1589
                      return Optional.<CRL>empty();
1590
                    }
1591
                  })
1592
              .flatMap(OptionalUtil::stream)
1593
              .collect(Collectors.toList());
1594
1595 1 1. fetchCrlDistributionPoints : replaced return value with Optional.empty for com/yubico/fido/metadata/FidoMetadataDownloader::fetchCrlDistributionPoints → NO_COVERAGE
      return Optional.of(
1596
          CertStore.getInstance("Collection", new CollectionCertStoreParameters(crldpCrls)));
1597
    }
1598
  }
1599
1600
  @Value
1601
  @AllArgsConstructor(access = AccessLevel.PRIVATE)
1602
  private static class DownloadResult {
1603
    private boolean notModified;
1604
    private Optional<byte[]> content;
1605
1606
    static DownloadResult notModified() {
1607 1 1. notModified : replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader$DownloadResult::notModified → NO_COVERAGE
      return new DownloadResult(true, Optional.empty());
1608
    }
1609
1610
    static DownloadResult ok(@NonNull byte[] content) {
1611 1 1. ok : replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader$DownloadResult::ok → KILLED
      return new DownloadResult(false, Optional.of(content));
1612
    }
1613
1614
    byte[] getContent() {
1615 1 1. getContent : replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader$DownloadResult::getContent → KILLED
      return content.get();
1616
    }
1617
1618
    boolean isOk() {
1619 2 1. isOk : replaced boolean return with true for com/yubico/fido/metadata/FidoMetadataDownloader$DownloadResult::isOk → SURVIVED
2. isOk : replaced boolean return with false for com/yubico/fido/metadata/FidoMetadataDownloader$DownloadResult::isOk → KILLED
      return content.isPresent();
1620
    }
1621
  }
1622
1623
  /**
1624
   * Values for the {@link FidoMetadataDownloaderBuilder#cachePolicy(Function)} argument function to
1625
   * return to express how {@link #refreshBlob()} and {@link #loadCachedBlob()} should behave when a
1626
   * BLOB download fails.
1627
   */
1628
  public enum CachePolicyDecision {
1629
    /** Recover by returning the cached BLOB as a successful result. */
1630
    USE_CACHED,
1631
1632
    /** Propagate the failure by re-throwing the exception. */
1633
    THROW;
1634
  }
1635
1636
  @Value
1637
  @Builder
1638
  @Jacksonized
1639
  static class TrustRootsCacheValue {
1640
    List<String> urls;
1641
    List<byte[]> certsDer;
1642
  }
1643
}

Mutations

145

1.1
Location : builder
Killed by : com.yubico.fido.metadata.FidoMds3Spec
replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader::builder → KILLED

176

1.1
Location : build
Killed by : com.yubico.fido.metadata.FidoMds3Spec
replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader$FidoMetadataDownloaderBuilder::build → KILLED

244

1.1
Location : expectLegalHeader
Killed by : com.yubico.fido.metadata.FidoMds3Spec
replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader$FidoMetadataDownloaderBuilder$Step1::expectLegalHeader → KILLED

291

1.1
Location : useDefaultTrustRoot
Killed by : none
replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader$FidoMetadataDownloaderBuilder$Step2::useDefaultTrustRoot → NO_COVERAGE

326

1.1
Location : downloadTrustRoot
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader$FidoMetadataDownloaderBuilder$Step2::downloadTrustRoot → KILLED

349

1.1
Location : downloadTrustRoots
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
negated conditional → KILLED

353

1.1
Location : lambda$downloadTrustRoots$0
Killed by : none
replaced boolean return with true for com/yubico/fido/metadata/FidoMetadataDownloader$FidoMetadataDownloaderBuilder$Step2::lambda$downloadTrustRoots$0 → SURVIVED
Covering tests

2.2
Location : downloadTrustRoots
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
negated conditional → KILLED

3.3
Location : lambda$downloadTrustRoots$0
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
replaced boolean return with false for com/yubico/fido/metadata/FidoMetadataDownloader$FidoMetadataDownloaderBuilder$Step2::lambda$downloadTrustRoots$0 → KILLED

356

1.1
Location : downloadTrustRoots
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader$FidoMetadataDownloaderBuilder$Step2::downloadTrustRoots → KILLED

370

1.1
Location : useTrustRoot
Killed by : com.yubico.fido.metadata.FidoMds3Spec
replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader$FidoMetadataDownloaderBuilder$Step2::useTrustRoot → KILLED

383

1.1
Location : useTrustRoots
Killed by : com.yubico.fido.metadata.FidoMds3Spec
replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader$FidoMetadataDownloaderBuilder$Step2::useTrustRoots → KILLED

425

1.1
Location : useTrustRootCacheFile
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader$FidoMetadataDownloaderBuilder$Step3::useTrustRootCacheFile → KILLED

455

1.1
Location : useTrustRootCache
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader$FidoMetadataDownloaderBuilder$Step3::useTrustRootCache → KILLED

493

1.1
Location : useDefaultBlob
Killed by : none
replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader$FidoMetadataDownloaderBuilder$Step4::useDefaultBlob → NO_COVERAGE

516

1.1
Location : downloadBlob
Killed by : none
negated conditional → SURVIVED
Covering tests

519

1.1
Location : downloadBlob
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader$FidoMetadataDownloaderBuilder$Step4::downloadBlob → KILLED

541

1.1
Location : useBlob
Killed by : com.yubico.fido.metadata.FidoMds3Spec
replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader$FidoMetadataDownloaderBuilder$Step4::useBlob → KILLED

575

1.1
Location : useBlobCacheFile
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader$FidoMetadataDownloaderBuilder$Step5::useBlobCacheFile → KILLED

600

1.1
Location : useBlobCache
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader$FidoMetadataDownloaderBuilder$Step5::useBlobCache → KILLED

609

1.1
Location : finishRequiredSteps
Killed by : com.yubico.fido.metadata.FidoMds3Spec
replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader$FidoMetadataDownloaderBuilder::finishRequiredSteps → KILLED

636

1.1
Location : clock
Killed by : com.yubico.fido.metadata.FidoMds3Spec
replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader$FidoMetadataDownloaderBuilder::clock → KILLED

654

1.1
Location : useCrls
Killed by : com.yubico.fido.metadata.FidoMds3Spec
replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader$FidoMetadataDownloaderBuilder::useCrls → KILLED

668

1.1
Location : useCrls
Killed by : com.yubico.fido.metadata.FidoMds3Spec
replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader$FidoMetadataDownloaderBuilder::useCrls → KILLED

692

1.1
Location : trustHttpsCerts
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
removed call to java/security/KeyStore::load → KILLED

703

1.1
Location : trustHttpsCerts
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
removed call to java/security/KeyStore::setCertificateEntry → KILLED

712

1.1
Location : trustHttpsCerts
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader$FidoMetadataDownloaderBuilder::trustHttpsCerts → KILLED

732

1.1
Location : verifyDownloadsOnly
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader$FidoMetadataDownloaderBuilder::verifyDownloadsOnly → KILLED

766

1.1
Location : cachePolicy
Killed by : none
replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader$FidoMetadataDownloaderBuilder::cachePolicy → SURVIVED
Covering tests

773

1.1
Location : headerJsonMapper
Killed by : none
replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader$FidoMetadataDownloaderBuilder::headerJsonMapper → NO_COVERAGE

780

1.1
Location : payloadJsonMapper
Killed by : none
replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader$FidoMetadataDownloaderBuilder::payloadJsonMapper → NO_COVERAGE

862

1.1
Location : loadCachedBlob
Killed by : com.yubico.fido.metadata.FidoMds3Spec
negated conditional → KILLED

864

1.1
Location : loadCachedBlob
Killed by : com.yubico.fido.metadata.FidoMds3Spec
replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader::loadCachedBlob → KILLED

868

1.1
Location : loadCachedBlob
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
negated conditional → KILLED

876

1.1
Location : loadCachedBlob
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
negated conditional → KILLED

878

1.1
Location : loadCachedBlob
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader::loadCachedBlob → KILLED

887

1.1
Location : loadCachedBlob
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader::loadCachedBlob → KILLED

965

1.1
Location : refreshBlob
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
negated conditional → KILLED

967

1.1
Location : refreshBlob
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader::refreshBlob → KILLED

971

1.1
Location : refreshBlob
Killed by : none
negated conditional → SURVIVED
Covering tests

977

1.1
Location : refreshBlob
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader::refreshBlob → KILLED

1002

1.1
Location : lambda$refreshBlobInternal$0
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
replaced return value with "" for com/yubico/fido/metadata/FidoMetadataDownloader::lambda$refreshBlobInternal$0 → KILLED

1003

1.1
Location : refreshBlobInternal
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
negated conditional → KILLED

1005

1.1
Location : refreshBlobInternal
Killed by : none
replaced return value with Optional.empty for com/yubico/fido/metadata/FidoMetadataDownloader::refreshBlobInternal → NO_COVERAGE

1012

1.1
Location : refreshBlobInternal
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
negated conditional → KILLED

1014

1.1
Location : refreshBlobInternal
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
negated conditional → KILLED

2.2
Location : refreshBlobInternal
Killed by : none
changed conditional boundary → SURVIVED
Covering tests

1016

1.1
Location : refreshBlobInternal
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
replaced return value with Optional.empty for com/yubico/fido/metadata/FidoMetadataDownloader::refreshBlobInternal → KILLED

1022

1.1
Location : refreshBlobInternal
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
negated conditional → KILLED

1027

1.1
Location : refreshBlobInternal
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
negated conditional → KILLED

1029

1.1
Location : refreshBlobInternal
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
removed call to java/io/FileOutputStream::write → KILLED

1033

1.1
Location : refreshBlobInternal
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
negated conditional → KILLED

1034

1.1
Location : refreshBlobInternal
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
removed call to java/util/function/Consumer::accept → KILLED

1037

1.1
Location : refreshBlobInternal
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
replaced return value with Optional.empty for com/yubico/fido/metadata/FidoMetadataDownloader::refreshBlobInternal → KILLED

1040

1.1
Location : refreshBlobInternal
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
negated conditional → KILLED

2.2
Location : refreshBlobInternal
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
negated conditional → KILLED

1044

1.1
Location : refreshBlobInternal
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
replaced return value with Optional.empty for com/yubico/fido/metadata/FidoMetadataDownloader::refreshBlobInternal → KILLED

1052

1.1
Location : refreshBlobInternal
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
negated conditional → KILLED

1056

1.1
Location : refreshBlobInternal
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
replaced return value with Optional.empty for com/yubico/fido/metadata/FidoMetadataDownloader::refreshBlobInternal → KILLED

1078

1.1
Location : retrieveTrustAnchors
Killed by : com.yubico.fido.metadata.FidoMds3Spec
negated conditional → KILLED

1079

1.1
Location : retrieveTrustAnchors
Killed by : com.yubico.fido.metadata.FidoMds3Spec
replaced return value with Collections.emptySet for com/yubico/fido/metadata/FidoMetadataDownloader::retrieveTrustAnchors → KILLED

1083

1.1
Location : retrieveTrustAnchors
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
negated conditional → KILLED

1091

1.1
Location : lambda$retrieveTrustAnchors$1
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
replaced return value with Optional.empty for com/yubico/fido/metadata/FidoMetadataDownloader::lambda$retrieveTrustAnchors$1 → KILLED

1094

1.1
Location : retrieveTrustAnchors
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
negated conditional → KILLED

1098

1.1
Location : retrieveTrustAnchors
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
negated conditional → KILLED

1104

1.1
Location : retrieveTrustAnchors
Killed by : none
removed call to java/security/cert/X509Certificate::checkValidity → SURVIVED
Covering tests

1113

1.1
Location : retrieveTrustAnchors
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
negated conditional → KILLED

1115

1.1
Location : retrieveTrustAnchors
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
removed call to com/fasterxml/jackson/databind/ObjectMapper::writeValue → KILLED

1119

1.1
Location : retrieveTrustAnchors
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
negated conditional → KILLED

1120

1.1
Location : retrieveTrustAnchors
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
removed call to java/util/function/Consumer::accept → KILLED

1126

1.1
Location : retrieveTrustAnchors
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
replaced return value with Collections.emptySet for com/yubico/fido/metadata/FidoMetadataDownloader::retrieveTrustAnchors → KILLED

1133

1.1
Location : importTrustAnchor
Killed by : com.yubico.fido.metadata.FidoMds3Spec
replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader::importTrustAnchor → KILLED

1161

1.1
Location : loadExplicitBlobOnly
Killed by : com.yubico.fido.metadata.FidoMds3Spec
negated conditional → KILLED

1162

1.1
Location : loadExplicitBlobOnly
Killed by : com.yubico.fido.metadata.FidoMds3Spec
replaced return value with Optional.empty for com/yubico/fido/metadata/FidoMetadataDownloader::loadExplicitBlobOnly → KILLED

1173

1.1
Location : loadCachedBlobOnly
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
negated conditional → KILLED

1186

1.1
Location : loadCachedBlobOnly
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
replaced return value with Optional.empty for com/yubico/fido/metadata/FidoMetadataDownloader::loadCachedBlobOnly → KILLED

1189

1.1
Location : lambda$loadCachedBlobOnly$2
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader::lambda$loadCachedBlobOnly$2 → KILLED

1198

1.1
Location : readCacheFile
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
negated conditional → KILLED

2.2
Location : readCacheFile
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
negated conditional → KILLED

3.3
Location : readCacheFile
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
negated conditional → KILLED

1200

1.1
Location : readCacheFile
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
replaced return value with Optional.empty for com/yubico/fido/metadata/FidoMetadataDownloader::readCacheFile → KILLED

1214

1.1
Location : readTrustAnchorsCache
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
negated conditional → KILLED

1215

1.1
Location : readTrustAnchorsCache
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
negated conditional → KILLED

1221

1.1
Location : readTrustAnchorsCache
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
negated conditional → KILLED

1222

1.1
Location : readTrustAnchorsCache
Killed by : none
removed call to java/security/cert/X509Certificate::checkValidity → SURVIVED
Covering tests

1230

1.1
Location : readTrustAnchorsCache
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
replaced return value with Optional.empty for com/yubico/fido/metadata/FidoMetadataDownloader::readTrustAnchorsCache → KILLED

1244

1.1
Location : download
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
negated conditional → KILLED

1245

1.1
Location : download
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader::download → KILLED

1257

1.1
Location : download
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
negated conditional → KILLED

1259

1.1
Location : download
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
negated conditional → KILLED

1263

1.1
Location : download
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
removed call to javax/net/ssl/TrustManagerFactory::init → KILLED

1265

1.1
Location : download
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
removed call to javax/net/ssl/SSLContext::init → KILLED

1267

1.1
Location : download
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
removed call to javax/net/ssl/HttpsURLConnection::setSSLSocketFactory → KILLED

1274

1.1
Location : download
Killed by : none
removed call to javax/net/ssl/HttpsURLConnection::setRequestMethod → SURVIVED
Covering tests

1275

1.1
Location : download
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
removed call to java/util/Optional::ifPresent → KILLED

1277

1.1
Location : lambda$download$3
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
removed call to javax/net/ssl/HttpsURLConnection::addRequestProperty → KILLED

1280

1.1
Location : download
Killed by : none
negated conditional → SURVIVED
Covering tests

1284

1.1
Location : download
Killed by : none
replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader::download → NO_COVERAGE

1295

1.1
Location : download
Killed by : none
negated conditional → SURVIVED
Covering tests

1297

1.1
Location : download
Killed by : none
negated conditional → NO_COVERAGE

1302

1.1
Location : lambda$download$4
Killed by : none
negated conditional → NO_COVERAGE

2.2
Location : lambda$download$4
Killed by : none
negated conditional → NO_COVERAGE

3.3
Location : lambda$download$4
Killed by : none
replaced Boolean return with True for com/yubico/fido/metadata/FidoMetadataDownloader::lambda$download$4 → NO_COVERAGE

1305

1.1
Location : download
Killed by : none
replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader::download → NO_COVERAGE

1311

1.1
Location : download
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader::download → KILLED

1324

1.1
Location : parseAndVerifyBlob
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader::parseAndVerifyBlob → KILLED

1337

1.1
Location : parseAndMaybeVerifyBlob
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
negated conditional → KILLED

1338

1.1
Location : parseAndMaybeVerifyBlob
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader::parseAndMaybeVerifyBlob → KILLED

1340

1.1
Location : parseAndMaybeVerifyBlob
Killed by : com.yubico.fido.metadata.FidoMds3Spec
replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader::parseAndMaybeVerifyBlob → KILLED

1355

1.1
Location : verifyBlob
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
negated conditional → KILLED

1356

1.1
Location : verifyBlob
Killed by : com.yubico.fido.metadata.FidoMds3Spec
replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader::verifyBlob → KILLED

1362

1.1
Location : verifyBlob
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
negated conditional → KILLED

1364

1.1
Location : verifyBlob
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader::verifyBlob → KILLED

1366

1.1
Location : verifyBlob
Killed by : none
negated conditional → SURVIVED
Covering tests

1407

1.1
Location : tryVerifyBlob
Killed by : com.yubico.fido.metadata.FidoMds3Spec
removed call to java/security/Signature::initVerify → KILLED

1408

1.1
Location : tryVerifyBlob
Killed by : com.yubico.fido.metadata.FidoMds3Spec
removed call to java/security/Signature::update → KILLED

1411

1.1
Location : tryVerifyBlob
Killed by : com.yubico.fido.metadata.FidoMds3Spec
negated conditional → KILLED

1418

1.1
Location : tryVerifyBlob
Killed by : com.yubico.fido.metadata.FidoMds3Spec
negated conditional → KILLED

1419

1.1
Location : tryVerifyBlob
Killed by : com.yubico.fido.metadata.FidoMds3Spec
removed call to java/security/cert/PKIXParameters::addCertStore → KILLED

1424

1.1
Location : tryVerifyBlob
Killed by : none
removed call to java/util/Optional::ifPresent → SURVIVED
Covering tests

1426

1.1
Location : tryVerifyBlob
Killed by : com.yubico.fido.metadata.FidoMds3Spec
removed call to java/security/cert/PKIXParameters::setDate → KILLED

1434

1.1
Location : tryVerifyBlob
Killed by : com.yubico.fido.metadata.FidoMds3Spec
negated conditional → KILLED

2.2
Location : tryVerifyBlob
Killed by : com.yubico.fido.metadata.FidoMds3Spec
changed conditional boundary → KILLED

1438

1.1
Location : tryVerifyBlob
Killed by : com.yubico.fido.metadata.FidoMds3Spec
replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader::tryVerifyBlob → KILLED

1440

1.1
Location : tryVerifyBlob
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
negated conditional → KILLED

1443

1.1
Location : tryVerifyBlob
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
negated conditional → KILLED

1461

1.1
Location : parseBlob
Killed by : com.yubico.fido.metadata.FidoMds3Spec
replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader::parseBlob → KILLED

1473

1.1
Location : defaultHeaderJsonMapper
Killed by : com.yubico.fido.metadata.FidoMds3Spec
replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader::defaultHeaderJsonMapper → KILLED

1477

1.1
Location : defaultPayloadJsonMapper
Killed by : com.yubico.fido.metadata.FidoMds3Spec
replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader::defaultPayloadJsonMapper → KILLED

1481

1.1
Location : readAll
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader::readAll → KILLED

1492

1.1
Location : verifyHash
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
negated conditional → KILLED

1493

1.1
Location : verifyHash
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader::verifyHash → KILLED

1510

1.1
Location : fetchHeaderCertChain
Killed by : com.yubico.fido.metadata.FidoMds3Spec
negated conditional → KILLED

1512

1.1
Location : fetchHeaderCertChain
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
negated conditional → KILLED

1513

1.1
Location : fetchHeaderCertChain
Killed by : none
negated conditional → SURVIVED
Covering tests

1514

1.1
Location : fetchHeaderCertChain
Killed by : none
negated conditional → SURVIVED
Covering tests

1515

1.1
Location : fetchHeaderCertChain
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
negated conditional → KILLED

1529

1.1
Location : fetchHeaderCertChain
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
replaced return value with Optional.empty for com/yubico/fido/metadata/FidoMetadataDownloader::fetchHeaderCertChain → KILLED

1530

1.1
Location : fetchHeaderCertChain
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
negated conditional → KILLED

1531

1.1
Location : fetchHeaderCertChain
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
replaced return value with Optional.empty for com/yubico/fido/metadata/FidoMetadataDownloader::fetchHeaderCertChain → KILLED

1559

1.1
Location : lambda$fetchCrlDistributionPoints$5
Killed by : none
replaced return value with Stream.empty for com/yubico/fido/metadata/FidoMetadataDownloader::lambda$fetchCrlDistributionPoints$5 → SURVIVED
Covering tests

1572

1.1
Location : fetchCrlDistributionPoints
Killed by : none
negated conditional → SURVIVED
Covering tests

1582

1.1
Location : lambda$fetchCrlDistributionPoints$6
Killed by : none
replaced return value with Optional.empty for com/yubico/fido/metadata/FidoMetadataDownloader::lambda$fetchCrlDistributionPoints$6 → NO_COVERAGE

1595

1.1
Location : fetchCrlDistributionPoints
Killed by : none
replaced return value with Optional.empty for com/yubico/fido/metadata/FidoMetadataDownloader::fetchCrlDistributionPoints → NO_COVERAGE

1607

1.1
Location : notModified
Killed by : none
replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader$DownloadResult::notModified → NO_COVERAGE

1611

1.1
Location : ok
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader$DownloadResult::ok → KILLED

1615

1.1
Location : getContent
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
replaced return value with null for com/yubico/fido/metadata/FidoMetadataDownloader$DownloadResult::getContent → KILLED

1619

1.1
Location : isOk
Killed by : com.yubico.fido.metadata.FidoMetadataDownloaderSpec
replaced boolean return with false for com/yubico/fido/metadata/FidoMetadataDownloader$DownloadResult::isOk → KILLED

2.2
Location : isOk
Killed by : none
replaced boolean return with true for com/yubico/fido/metadata/FidoMetadataDownloader$DownloadResult::isOk → SURVIVED
Covering tests

Active mutators

Tests examined


Report generated by PIT 1.20.3