Skip to content

Commit ce2d592

Browse files
SNOW-2117143: Add CRL verification (#2287)
1 parent bbc424a commit ce2d592

File tree

12 files changed

+1425
-11
lines changed

12 files changed

+1425
-11
lines changed

TestOnly/pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
<mockito.version>3.5.6</mockito.version>
2424
<netty.version>4.1.118.Final</netty.version>
2525
<apache.httpclient.version>4.5.14</apache.httpclient.version>
26+
<bouncycastle.version>1.78.1</bouncycastle.version>
2627
<shadeBase>net.snowflake.client.jdbc.internal</shadeBase>
2728
</properties>
2829

@@ -257,6 +258,11 @@
257258
<artifactId>jna-platform</artifactId>
258259
<version>${jna.version}</version>
259260
</dependency>
261+
<dependency>
262+
<groupId>org.bouncycastle</groupId>
263+
<artifactId>bcpkix-jdk18on</artifactId>
264+
<version>${bouncycastle.version}</version>
265+
</dependency>
260266
</dependencies>
261267

262268
<build>
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package net.snowflake.client.core.crl;
2+
3+
class CRLValidationConfig {
4+
5+
enum CertRevocationCheckMode {
6+
DISABLED,
7+
ENABLED,
8+
ADVISORY
9+
}
10+
11+
private final CertRevocationCheckMode certRevocationCheckMode;
12+
private final boolean allowCertificatesWithoutCrlUrl;
13+
private final int connectionTimeoutMs;
14+
private final int readTimeoutMs;
15+
16+
private CRLValidationConfig(Builder builder) {
17+
this.certRevocationCheckMode = builder.certRevocationCheckMode;
18+
this.allowCertificatesWithoutCrlUrl = builder.allowCertificatesWithoutCrlUrl;
19+
this.connectionTimeoutMs = builder.connectionTimeoutMs;
20+
this.readTimeoutMs = builder.readTimeoutMs;
21+
}
22+
23+
CertRevocationCheckMode getCertRevocationCheckMode() {
24+
return certRevocationCheckMode;
25+
}
26+
27+
boolean isAllowCertificatesWithoutCrlUrl() {
28+
return allowCertificatesWithoutCrlUrl;
29+
}
30+
31+
int getConnectionTimeoutMs() {
32+
return connectionTimeoutMs;
33+
}
34+
35+
int getReadTimeoutMs() {
36+
return readTimeoutMs;
37+
}
38+
39+
static Builder builder() {
40+
return new Builder();
41+
}
42+
43+
static class Builder {
44+
private CertRevocationCheckMode certRevocationCheckMode = CertRevocationCheckMode.DISABLED;
45+
private boolean allowCertificatesWithoutCrlUrl = false;
46+
private int connectionTimeoutMs = 30000; // 30 seconds
47+
private int readTimeoutMs = 30000; // 30 seconds
48+
49+
Builder certRevocationCheckMode(CertRevocationCheckMode mode) {
50+
this.certRevocationCheckMode = mode;
51+
return this;
52+
}
53+
54+
Builder allowCertificatesWithoutCrlUrl(boolean allow) {
55+
this.allowCertificatesWithoutCrlUrl = allow;
56+
return this;
57+
}
58+
59+
Builder connectionTimeoutMs(int timeoutMs) {
60+
this.connectionTimeoutMs = timeoutMs;
61+
return this;
62+
}
63+
64+
Builder readTimeoutMs(int timeoutMs) {
65+
this.readTimeoutMs = timeoutMs;
66+
return this;
67+
}
68+
69+
CRLValidationConfig build() {
70+
return new CRLValidationConfig(this);
71+
}
72+
}
73+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package net.snowflake.client.core.crl;
2+
3+
enum CRLValidationResult {
4+
CHAIN_UNREVOKED,
5+
CHAIN_REVOKED,
6+
CHAIN_ERROR
7+
}
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
package net.snowflake.client.core.crl;
2+
3+
import java.security.cert.X509CRL;
4+
import java.security.cert.X509Certificate;
5+
import java.time.LocalDate;
6+
import java.time.ZoneId;
7+
import java.util.ArrayList;
8+
import java.util.Arrays;
9+
import java.util.Date;
10+
import java.util.List;
11+
import java.util.stream.Collectors;
12+
import net.snowflake.client.log.SFLogger;
13+
import net.snowflake.client.log.SFLoggerFactory;
14+
import org.bouncycastle.asn1.ASN1OctetString;
15+
import org.bouncycastle.asn1.ASN1Primitive;
16+
import org.bouncycastle.asn1.DERIA5String;
17+
import org.bouncycastle.asn1.x509.CRLDistPoint;
18+
import org.bouncycastle.asn1.x509.DistributionPoint;
19+
import org.bouncycastle.asn1.x509.DistributionPointName;
20+
import org.bouncycastle.asn1.x509.GeneralName;
21+
import org.bouncycastle.asn1.x509.GeneralNames;
22+
import org.bouncycastle.asn1.x509.IssuingDistributionPoint;
23+
24+
class CRLValidationUtils {
25+
26+
private static final SFLogger logger = SFLoggerFactory.getLogger(CRLValidationUtils.class);
27+
28+
// CA/Browser Forum Baseline Requirements date thresholds (using UTC for consistency)
29+
private static final Date MARCH_15_2024 =
30+
Date.from(LocalDate.of(2024, 3, 15).atStartOfDay(ZoneId.of("UTC")).toInstant());
31+
private static final Date MARCH_15_2026 =
32+
Date.from(LocalDate.of(2026, 3, 15).atStartOfDay(ZoneId.of("UTC")).toInstant());
33+
34+
static List<String> extractCRLDistributionPoints(X509Certificate cert) {
35+
List<String> crlUrls = new ArrayList<>();
36+
37+
try {
38+
byte[] extensionBytes = cert.getExtensionValue("2.5.29.31");
39+
if (extensionBytes == null) {
40+
logger.debug(
41+
"No CRL Distribution Points extension found for certificate: {}",
42+
cert.getSubjectX500Principal());
43+
return crlUrls;
44+
}
45+
46+
ASN1OctetString octetString = (ASN1OctetString) ASN1Primitive.fromByteArray(extensionBytes);
47+
CRLDistPoint crlDistPoint =
48+
CRLDistPoint.getInstance(ASN1Primitive.fromByteArray(octetString.getOctets()));
49+
50+
DistributionPoint[] distributionPoints = crlDistPoint.getDistributionPoints();
51+
if (distributionPoints != null) {
52+
for (DistributionPoint dp : distributionPoints) {
53+
DistributionPointName dpName = dp.getDistributionPoint();
54+
if (dpName != null && dpName.getType() == DistributionPointName.FULL_NAME) {
55+
GeneralNames generalNames = (GeneralNames) dpName.getName();
56+
for (GeneralName generalName : generalNames.getNames()) {
57+
if (generalName.getTagNo() == GeneralName.uniformResourceIdentifier) {
58+
String url = ((DERIA5String) generalName.getName()).getString();
59+
if (url.toLowerCase().startsWith("http://")
60+
|| url.toLowerCase().startsWith("https://")) {
61+
logger.debug("Found CRL URL: {}", url);
62+
crlUrls.add(url);
63+
}
64+
}
65+
}
66+
}
67+
}
68+
}
69+
} catch (Exception e) {
70+
logger.debug(
71+
"Failed to extract CRL distribution points from certificate {}: {}",
72+
cert.getSubjectX500Principal(),
73+
e.getMessage());
74+
}
75+
76+
logger.debug(
77+
"Extracted {} CRL URLs for certificate: {}",
78+
crlUrls.size(),
79+
cert.getSubjectX500Principal());
80+
return crlUrls;
81+
}
82+
83+
/**
84+
* Determines if a certificate is short-lived according to CA/Browser Forum Baseline Requirements.
85+
*/
86+
static boolean isShortLived(X509Certificate cert) {
87+
Date notBefore = cert.getNotBefore();
88+
Date notAfter = cert.getNotAfter();
89+
90+
// Certificates issued before March 15, 2024 are not considered short-lived
91+
if (notBefore.before(MARCH_15_2024)) {
92+
return false;
93+
}
94+
95+
// Determine the maximum validity period based on issuance date
96+
long maxValidityPeriodMs;
97+
if (notBefore.before(MARCH_15_2026)) {
98+
maxValidityPeriodMs =
99+
10L * 24 * 60 * 60 * 1000; // 10 days for certificates before March 15, 2026
100+
} else {
101+
maxValidityPeriodMs =
102+
7L * 24 * 60 * 60 * 1000; // 7 days for certificates after March 15, 2026
103+
}
104+
105+
maxValidityPeriodMs += 60 * 1000;
106+
107+
long actualValidityPeriodMs = notAfter.getTime() - notBefore.getTime();
108+
return actualValidityPeriodMs <= maxValidityPeriodMs;
109+
}
110+
111+
static boolean verifyIssuingDistributionPoint(X509CRL crl, X509Certificate cert, String crlUrl) {
112+
try {
113+
byte[] extensionBytes = crl.getExtensionValue("2.5.29.28");
114+
if (extensionBytes == null) {
115+
logger.debug("No IDP extension found - CRL covers all certificates");
116+
return true;
117+
}
118+
119+
ASN1OctetString octetString = (ASN1OctetString) ASN1Primitive.fromByteArray(extensionBytes);
120+
IssuingDistributionPoint idp =
121+
IssuingDistributionPoint.getInstance(
122+
ASN1Primitive.fromByteArray(octetString.getOctets()));
123+
124+
// Check if this CRL only covers user certificates
125+
if (idp.onlyContainsUserCerts() && cert.getBasicConstraints() != -1) {
126+
logger.debug("CRL only covers user certificates, but certificate is a CA certificate");
127+
return false;
128+
}
129+
130+
// Check if this CRL only covers CA certificates
131+
if (idp.onlyContainsCACerts() && cert.getBasicConstraints() == -1) {
132+
logger.debug("CRL only covers CA certificates, but certificate is not a CA certificate");
133+
return false;
134+
}
135+
136+
DistributionPointName dpName = idp.getDistributionPoint();
137+
if (dpName != null) {
138+
if (dpName.getType() == DistributionPointName.FULL_NAME) {
139+
GeneralNames generalNames = (GeneralNames) dpName.getName();
140+
boolean foundMatch = false;
141+
142+
for (GeneralName generalName : generalNames.getNames()) {
143+
if (generalName.getTagNo() == GeneralName.uniformResourceIdentifier) {
144+
String idpUrl = ((DERIA5String) generalName.getName()).getString();
145+
if (idpUrl.equals(crlUrl)) {
146+
foundMatch = true;
147+
break;
148+
}
149+
}
150+
}
151+
152+
if (!foundMatch) {
153+
logger.debug(
154+
"CRL URL {} not found in IDP distribution points - this CRL is not authorized for this certificate",
155+
crlUrl);
156+
return false;
157+
}
158+
}
159+
}
160+
161+
logger.debug("IDP extension verification passed");
162+
return true;
163+
} catch (Exception e) {
164+
logger.debug("Failed to verify IDP extension: {}", e.getMessage());
165+
return false;
166+
}
167+
}
168+
169+
static String getCertChainSubjects(List<X509Certificate[]> certificateChains) {
170+
return certificateChains.stream()
171+
.flatMap(Arrays::stream)
172+
.map(cert -> cert.getSubjectX500Principal().getName())
173+
.collect(Collectors.joining(", "));
174+
}
175+
}

0 commit comments

Comments
 (0)