Skip to content

Commit efa4b00

Browse files
committed
Fetch CRLDistributionPoints in FidoMetadataDownloader
1 parent 5b8c758 commit efa4b00

File tree

3 files changed

+118
-10
lines changed

3 files changed

+118
-10
lines changed

webauthn-server-attestation/build.gradle.kts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,12 @@ dependencies {
4848
testImplementation("org.scalatestplus:junit-4-13_2.13")
4949
testImplementation("org.scalatestplus:scalacheck-1-16_2.13")
5050

51-
testImplementation("org.slf4j:slf4j-api")
51+
testImplementation("org.slf4j:slf4j-api") {
52+
version {
53+
strictly("[1.7.25,1.8-a)") // Pre-1.8 version required by slf4j-test
54+
}
55+
}
56+
testRuntimeOnly("uk.org.lidalia:slf4j-test")
5257
}
5358

5459
val integrationTest = task<Test>("integrationTest") {
@@ -58,9 +63,6 @@ val integrationTest = task<Test>("integrationTest") {
5863
testClassesDirs = sourceSets["integrationTest"].output.classesDirs
5964
classpath = sourceSets["integrationTest"].runtimeClasspath
6065
shouldRunAfter(tasks.test)
61-
62-
// Required for processing CRL distribution points extension
63-
systemProperty("com.sun.security.enableCRLDP", "true")
6466
}
6567
tasks["check"].dependsOn(integrationTest)
6668

webauthn-server-attestation/src/integrationTest/scala/com/yubico/fido/metadata/FidoMetadataDownloaderIntegrationTest.scala

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.yubico.fido.metadata
22

3+
import com.yubico.internal.util.CertificateParser
4+
import com.yubico.webauthn.data.ByteArray
35
import org.junit.runner.RunWith
46
import org.scalatest.BeforeAndAfter
57
import org.scalatest.funspec.AnyFunSpec
@@ -8,7 +10,8 @@ import org.scalatest.tags.Network
810
import org.scalatest.tags.Slow
911
import org.scalatestplus.junit.JUnitRunner
1012

11-
import java.util.Optional
13+
import scala.jdk.CollectionConverters.ListHasAsScala
14+
import scala.jdk.OptionConverters.RichOption
1215
import scala.util.Success
1316
import scala.util.Try
1417

@@ -21,24 +24,56 @@ class FidoMetadataDownloaderIntegrationTest
2124
with BeforeAndAfter {
2225

2326
describe("FidoMetadataDownloader with default settings") {
27+
// Cache downloaded items to avoid cause unnecessary load on remote servers
28+
var trustRootCache: Option[ByteArray] = None
29+
var blobCache: Option[ByteArray] = None
2430
val downloader =
2531
FidoMetadataDownloader
2632
.builder()
2733
.expectLegalHeader(
2834
"Retrieval and use of this BLOB indicates acceptance of the appropriate agreement located at https://fidoalliance.org/metadata/metadata-legal-terms/"
2935
)
3036
.useDefaultTrustRoot()
31-
.useTrustRootCache(() => Optional.empty(), _ => {})
37+
.useTrustRootCache(
38+
() => trustRootCache.toJava,
39+
trustRoot => { trustRootCache = Some(trustRoot) },
40+
)
3241
.useDefaultBlob()
33-
.useBlobCache(() => Optional.empty(), _ => {})
42+
.useBlobCache(
43+
() => blobCache.toJava,
44+
blob => { blobCache = Some(blob) },
45+
)
3446
.build()
3547

3648
it("downloads and verifies the root cert and BLOB successfully.") {
37-
// This test requires the system property com.sun.security.enableCRLDP=true
3849
val blob = Try(downloader.loadCachedBlob)
3950
blob shouldBe a[Success[_]]
4051
blob.get should not be null
4152
}
53+
54+
it(
55+
"does not encounter any CRLDistributionPoints entries in unknown format."
56+
) {
57+
val blob = Try(downloader.loadCachedBlob)
58+
blob shouldBe a[Success[_]]
59+
val trustRootCert =
60+
CertificateParser.parseDer(trustRootCache.get.getBytes)
61+
val certChain = downloader
62+
.fetchHeaderCertChain(
63+
trustRootCert,
64+
FidoMetadataDownloader.parseBlob(blobCache.get).getBlob.getHeader,
65+
)
66+
.asScala :+ trustRootCert
67+
for { cert <- certChain } {
68+
withClue(
69+
s"Unknown CRLDistributionPoints structure in cert [${cert.getSubjectX500Principal}] : ${new ByteArray(cert.getEncoded)}"
70+
) {
71+
CertificateParser
72+
.parseCrlDistributionPointsExtension(cert)
73+
.isAnyDistributionPointUnsupported should be(false)
74+
}
75+
}
76+
}
4277
}
4378

4479
}

webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/FidoMetadataDownloader.java

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import com.yubico.fido.metadata.FidoMetadataDownloaderException.Reason;
3030
import com.yubico.internal.util.BinaryUtil;
3131
import com.yubico.internal.util.CertificateParser;
32+
import com.yubico.internal.util.OptionalUtil;
3233
import com.yubico.webauthn.data.ByteArray;
3334
import com.yubico.webauthn.data.exception.Base64UrlException;
3435
import com.yubico.webauthn.data.exception.HexException;
@@ -54,6 +55,7 @@
5455
import java.security.Signature;
5556
import java.security.SignatureException;
5657
import java.security.cert.CRL;
58+
import java.security.cert.CRLException;
5759
import java.security.cert.CertPath;
5860
import java.security.cert.CertPathValidator;
5961
import java.security.cert.CertPathValidatorException;
@@ -1131,13 +1133,18 @@ private MetadataBLOB verifyBlob(ParseResult parseResult, X509Certificate trustRo
11311133
if (certStore != null) {
11321134
pathParams.addCertStore(certStore);
11331135
}
1136+
1137+
// Parse CRLDistributionPoints ourselves so users don't have to set the
1138+
// `com.sun.security.enableCRLDP=true` system property
1139+
fetchCrlDistributionPoints(certChain, certFactory).ifPresent(pathParams::addCertStore);
1140+
11341141
pathParams.setDate(Date.from(clock.instant()));
11351142
cpv.validate(blobCertPath, pathParams);
11361143

11371144
return parseResult.blob;
11381145
}
11391146

1140-
private static ParseResult parseBlob(ByteArray jwt) throws IOException, Base64UrlException {
1147+
static ParseResult parseBlob(ByteArray jwt) throws IOException, Base64UrlException {
11411148
Scanner s = new Scanner(new ByteArrayInputStream(jwt.getBytes())).useDelimiter("\\.");
11421149
final ByteArray jwtHeader = ByteArray.fromBase64Url(s.next());
11431150
final ByteArray jwtPayload = ByteArray.fromBase64Url(s.next());
@@ -1176,7 +1183,7 @@ private static ByteArray verifyHash(ByteArray contents, Set<ByteArray> acceptedC
11761183
}
11771184

11781185
@Value
1179-
private static class ParseResult {
1186+
static class ParseResult {
11801187
private MetadataBLOB blob;
11811188
private ByteArray jwtHeader;
11821189
private ByteArray jwtPayload;
@@ -1213,4 +1220,68 @@ List<X509Certificate> fetchHeaderCertChain(
12131220
return Collections.singletonList(trustRootCertificate);
12141221
}
12151222
}
1223+
1224+
/**
1225+
* Parse the CRLDistributionPoints extension of each certificate, fetch each distribution point
1226+
* and assemble them into a {@link CertStore} ready to be injected into {@link
1227+
* PKIXParameters#addCertStore(CertStore)} to provide CRLs for the verification procedure.
1228+
*
1229+
* <p>We do this ourselves so that users don't have to set the `com.sun.security.enableCRLDP=true`
1230+
* system property. This is required by the default SUN provider in order to enable
1231+
* CRLDistributionPoints resolution.
1232+
*
1233+
* <p>Any CRLDistributionPoints entries in unknown format are ignored and log a warning.
1234+
*/
1235+
private Optional<CertStore> fetchCrlDistributionPoints(
1236+
List<X509Certificate> certChain, CertificateFactory certFactory)
1237+
throws InvalidAlgorithmParameterException, NoSuchAlgorithmException {
1238+
final List<URL> crlDistributionPointUrls =
1239+
certChain.stream()
1240+
.flatMap(
1241+
cert -> {
1242+
log.debug(
1243+
"Attempting to parse CRLDistributionPoints extension of cert: {}",
1244+
cert.getSubjectX500Principal());
1245+
try {
1246+
return CertificateParser.parseCrlDistributionPointsExtension(cert)
1247+
.getDistributionPoints()
1248+
.stream();
1249+
} catch (Exception e) {
1250+
log.warn(
1251+
"Failed to parse CRLDistributionPoints extension of cert: {}",
1252+
cert.getSubjectX500Principal(),
1253+
e);
1254+
return Stream.empty();
1255+
}
1256+
})
1257+
.collect(Collectors.toList());
1258+
1259+
if (crlDistributionPointUrls.isEmpty()) {
1260+
return Optional.empty();
1261+
1262+
} else {
1263+
final List<CRL> crldpCrls =
1264+
crlDistributionPointUrls.stream()
1265+
.map(
1266+
crldpUrl -> {
1267+
log.debug("Attempting to download CRL distribution point: {}", crldpUrl);
1268+
try {
1269+
return Optional.of(
1270+
certFactory.generateCRL(
1271+
new ByteArrayInputStream(download(crldpUrl).getBytes())));
1272+
} catch (CRLException e) {
1273+
log.warn("Failed to import CRL from distribution point: {}", crldpUrl, e);
1274+
return Optional.<CRL>empty();
1275+
} catch (Exception e) {
1276+
log.warn("Failed to download CRL distribution point: {}", crldpUrl, e);
1277+
return Optional.<CRL>empty();
1278+
}
1279+
})
1280+
.flatMap(OptionalUtil::stream)
1281+
.collect(Collectors.toList());
1282+
1283+
return Optional.of(
1284+
CertStore.getInstance("Collection", new CollectionCertStoreParameters(crldpCrls)));
1285+
}
1286+
}
12161287
}

0 commit comments

Comments
 (0)