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