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

Mutations

141

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

164

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

172

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

239

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

240

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

285

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

314

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

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

315

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

318

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

327

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

328

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

361

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

362

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

384

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

385

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

386

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

424

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

446

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

447

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

450

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

471

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

472

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

505

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

506

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

529

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

530

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

531

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

540

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

565

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

567

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

583

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

585

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

599

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

618

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

622

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

633

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

642

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

662

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

696

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

703

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

710

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

792

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

794

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

798

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

806

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

808

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

817

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

895

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

897

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

901

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

907

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

911

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

932

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

933

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

935

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

942

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

944

1.1
Location : refreshBlobInternal
Killed by : none
changed conditional boundary → SURVIVED

2.2
Location : refreshBlobInternal
Killed by : none
negated conditional → MEMORY_ERROR

946

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

952

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

957

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

959

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

963

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

964

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

967

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

970

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

974

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

982

1.1
Location : refreshBlobInternal
Killed by : none
negated conditional → MEMORY_ERROR

986

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

1008

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

1009

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

1013

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

1020

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

1022

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

1026

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

1034

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

1036

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

1042

1.1
Location : retrieveTrustRootCert
Killed by : none
removed call to java/security/cert/X509Certificate::checkValidity → MEMORY_ERROR

1044

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

1046

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

1050

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

1051

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

1055

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

1084

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

1085

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

1097

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

1110

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

1113

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

1122

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

1136

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

1137

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

1149

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

1151

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

1155

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

1157

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

1159

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

1166

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

1167

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

1169

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

1172

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

1176

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

1187

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

1189

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

1194

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

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

3.3
Location : lambda$download$3
Killed by : none
negated conditional → NO_COVERAGE

1197

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

1203

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

1216

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

1229

1.1
Location : parseAndMaybeVerifyBlob
Killed by : none
negated conditional → MEMORY_ERROR

1230

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

1232

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

1264

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

1265

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

1268

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

1277

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

1278

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

1283

1.1
Location : verifyBlob
Killed by : none
removed call to java/util/Optional::ifPresent → MEMORY_ERROR

1285

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

1288

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

1300

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

1312

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

1316

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

1320

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

1331

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

1332

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

1350

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

1352

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

1353

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

1354

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

1355

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

1369

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

1370

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

1371

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

1373

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

1399

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

1412

1.1
Location : fetchCrlDistributionPoints
Killed by : none
negated conditional → MEMORY_ERROR

1422

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

1436

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

1448

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

1451

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

1452

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

1456

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

1460

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

Active mutators

Tests examined


Report generated by PIT 1.15.0