Skip to content

Commit c54b4b7

Browse files
SNOW-211714: Add building verified certificate paths logic (#2261)
Co-authored-by: Piotr Fus <piotr.fus@snowflake.com>
1 parent 6ab27e6 commit c54b4b7

3 files changed

Lines changed: 795 additions & 0 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,6 @@ whitesource/
7474

7575
#created in some tests
7676
placeholder
77+
78+
#vs code
79+
.vscode/
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
package net.snowflake.client;
2+
3+
import java.security.InvalidAlgorithmParameterException;
4+
import java.security.NoSuchAlgorithmException;
5+
import java.security.cert.CertPath;
6+
import java.security.cert.CertPathBuilder;
7+
import java.security.cert.CertPathBuilderException;
8+
import java.security.cert.CertPathBuilderResult;
9+
import java.security.cert.CertStore;
10+
import java.security.cert.Certificate;
11+
import java.security.cert.CertificateException;
12+
import java.security.cert.CollectionCertStoreParameters;
13+
import java.security.cert.PKIXBuilderParameters;
14+
import java.security.cert.PKIXCertPathBuilderResult;
15+
import java.security.cert.TrustAnchor;
16+
import java.security.cert.X509CertSelector;
17+
import java.security.cert.X509Certificate;
18+
import java.util.ArrayList;
19+
import java.util.Arrays;
20+
import java.util.Collections;
21+
import java.util.HashSet;
22+
import java.util.List;
23+
import java.util.Set;
24+
import java.util.stream.Collectors;
25+
import javax.net.ssl.X509TrustManager;
26+
import net.snowflake.client.log.SFLogger;
27+
import net.snowflake.client.log.SFLoggerFactory;
28+
29+
/**
30+
* Builds and verifies certificate paths using a truststore and CertPathBuilder. This class takes a
31+
* certificate chain presented by a server and returns verified paths that include trust anchors for
32+
* CRL validation support.
33+
*/
34+
class VerifiedCertPathBuilder {
35+
36+
private static final SFLogger logger = SFLoggerFactory.getLogger(VerifiedCertPathBuilder.class);
37+
private final X509TrustManager trustManager;
38+
private final Set<TrustAnchor> trustAnchors;
39+
40+
/**
41+
* Constructor that initializes the VerifiedCertPathBuilder with the provided trust manager.
42+
*
43+
* @param trustManager the X509TrustManager to use for certificate validation
44+
* @throws IllegalArgumentException if trustManager is null
45+
*/
46+
public VerifiedCertPathBuilder(X509TrustManager trustManager) throws CertificateException {
47+
if (trustManager == null) {
48+
throw new IllegalArgumentException("Trust manager cannot be null");
49+
}
50+
this.trustManager = trustManager;
51+
this.trustAnchors = createTrustAnchors(trustManager);
52+
}
53+
54+
/**
55+
* Builds and verifies all possible certificate paths from leaf certificates to trust anchors.
56+
* Unlike standard PKIX path building, this method includes trust anchor certificates at the end
57+
* of each path for CRL validation support.
58+
*
59+
* @param certificateChain the certificate chain presented by the server
60+
* @param authType the authentication type used for the connection
61+
* @return a list of all verified certificate paths with trust anchors included
62+
* @throws CertificateException if certificate validation fails
63+
* @throws CertPathBuilderException if no valid certificate paths could be built
64+
*/
65+
public List<X509Certificate[]> buildAllVerifiedPaths(
66+
X509Certificate[] certificateChain, String authType)
67+
throws CertificateException, CertPathBuilderException {
68+
69+
if (certificateChain == null || certificateChain.length == 0) {
70+
throw new IllegalArgumentException("Certificate chain cannot be null or empty");
71+
}
72+
if (authType == null || authType.trim().isEmpty()) {
73+
throw new IllegalArgumentException("Authentication type cannot be null or empty");
74+
}
75+
76+
logger.debug(
77+
"Building verified paths for chain length: {} with authType: {}",
78+
certificateChain.length,
79+
authType);
80+
81+
List<X509Certificate[]> allVerifiedPaths = new ArrayList<>();
82+
83+
try {
84+
List<Certificate> certCollection = Arrays.asList(certificateChain);
85+
CertStore certStore =
86+
CertStore.getInstance("Collection", new CollectionCertStoreParameters(certCollection));
87+
88+
X509Certificate leafCertificate = identifyLeafCertificate(certificateChain);
89+
logger.debug("Identified leaf certificate: {}", leafCertificate.getSubjectX500Principal());
90+
91+
allVerifiedPaths.addAll(
92+
findAllPathsForTarget(leafCertificate, trustAnchors, certStore, authType));
93+
94+
} catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException e) {
95+
throw new CertificateException("Failed to build certificate paths", e);
96+
}
97+
98+
if (allVerifiedPaths.isEmpty()) {
99+
throw new CertPathBuilderException("No valid certificate paths could be built");
100+
}
101+
102+
logger.debug("Successfully built {} verified certificate paths", allVerifiedPaths.size());
103+
return allVerifiedPaths;
104+
}
105+
106+
/** Finds all possible valid paths from a leaf certificate to trust anchors. */
107+
private List<X509Certificate[]> findAllPathsForTarget(
108+
X509Certificate targetCert,
109+
Set<TrustAnchor> trustAnchors,
110+
CertStore certStore,
111+
String authType) {
112+
113+
List<X509Certificate[]> pathsForTarget = new ArrayList<>();
114+
115+
for (TrustAnchor trustAnchor : trustAnchors) {
116+
try {
117+
Set<TrustAnchor> singleTrustAnchor = Collections.singleton(trustAnchor);
118+
PKIXBuilderParameters singleAnchorParams =
119+
new PKIXBuilderParameters(singleTrustAnchor, null);
120+
singleAnchorParams.addCertStore(certStore);
121+
singleAnchorParams.setRevocationEnabled(false);
122+
X509CertSelector selector = new X509CertSelector();
123+
selector.setCertificate(targetCert);
124+
singleAnchorParams.setTargetCertConstraints(selector);
125+
126+
CertPathBuilder builder = CertPathBuilder.getInstance("PKIX");
127+
CertPathBuilderResult result = builder.build(singleAnchorParams);
128+
129+
if (result instanceof PKIXCertPathBuilderResult) {
130+
PKIXCertPathBuilderResult pkixResult = (PKIXCertPathBuilderResult) result;
131+
CertPath certPath = pkixResult.getCertPath();
132+
133+
try {
134+
X509Certificate[] certArray = convertCertPathToArray(certPath);
135+
trustManager.checkServerTrusted(certArray, authType);
136+
137+
// Create path with trust anchor included for CRL validation
138+
X509Certificate[] pathWithTrustAnchor = new X509Certificate[certArray.length + 1];
139+
System.arraycopy(certArray, 0, pathWithTrustAnchor, 0, certArray.length);
140+
pathWithTrustAnchor[certArray.length] = trustAnchor.getTrustedCert();
141+
142+
pathsForTarget.add(pathWithTrustAnchor);
143+
logger.trace(
144+
"Found valid path via trust anchor {}: length {}",
145+
trustAnchor.getTrustedCert().getSubjectX500Principal(),
146+
pathWithTrustAnchor.length);
147+
148+
} catch (CertificateException e) {
149+
logger.trace(
150+
"Path validation failed via trust anchor {}: {}",
151+
trustAnchor.getTrustedCert().getSubjectX500Principal(),
152+
e.getMessage());
153+
}
154+
}
155+
} catch (CertPathBuilderException
156+
| NoSuchAlgorithmException
157+
| InvalidAlgorithmParameterException e) {
158+
logger.trace(
159+
"Failed to build path via trust anchor {}: {}",
160+
trustAnchor.getTrustedCert().getSubjectX500Principal(),
161+
e.getMessage());
162+
}
163+
}
164+
165+
return pathsForTarget;
166+
}
167+
168+
/**
169+
* Identifies the leaf certificate (end-entity certificate) in the certificate chain.
170+
*
171+
* @param certificateChain the certificate chain to analyze
172+
* @return the leaf certificate found in the chain
173+
* @throws CertificateException if no leaf certificate is found in the chain
174+
*/
175+
private X509Certificate identifyLeafCertificate(X509Certificate[] certificateChain)
176+
throws CertificateException {
177+
Set<X509Certificate> leafCerts =
178+
Arrays.stream(certificateChain)
179+
.filter(
180+
cert ->
181+
cert != null
182+
&& cert.getBasicConstraints()
183+
== -1) // Basic constraints -1 indicates a leaf certificate
184+
.collect(Collectors.toSet());
185+
if (leafCerts.isEmpty()) {
186+
throw new CertificateException("No leaf certificate found in the chain");
187+
}
188+
if (leafCerts.size() > 1) {
189+
throw new CertificateException("Multiple leaf certificates found");
190+
}
191+
return leafCerts.iterator().next();
192+
}
193+
194+
/** Creates trust anchors from the truststore. */
195+
private Set<TrustAnchor> createTrustAnchors(X509TrustManager trustManager)
196+
throws CertificateException {
197+
Set<TrustAnchor> trustAnchors = new HashSet<>();
198+
199+
try {
200+
X509Certificate[] trustedCerts = trustManager.getAcceptedIssuers();
201+
for (X509Certificate cert : trustedCerts) {
202+
trustAnchors.add(new TrustAnchor(cert, null));
203+
}
204+
logger.debug("Created {} trust anchors from truststore", trustAnchors.size());
205+
} catch (Exception e) {
206+
throw new CertificateException("Failed to create trust anchors", e);
207+
}
208+
209+
return trustAnchors;
210+
}
211+
212+
/** Converts a CertPath to an X509Certificate array. */
213+
private X509Certificate[] convertCertPathToArray(CertPath certPath) throws CertificateException {
214+
List<? extends Certificate> certificates = certPath.getCertificates();
215+
216+
if (certificates == null || certificates.isEmpty()) {
217+
throw new CertificateException("Certificate path is empty");
218+
}
219+
220+
X509Certificate[] certArray = new X509Certificate[certificates.size()];
221+
for (int i = 0; i < certificates.size(); i++) {
222+
Certificate cert = certificates.get(i);
223+
if (!(cert instanceof X509Certificate)) {
224+
throw new CertificateException(
225+
"Certificate path contains non-X509 certificate: "
226+
+ cert.getClass().getCanonicalName());
227+
}
228+
certArray[i] = (X509Certificate) cert;
229+
}
230+
231+
return certArray;
232+
}
233+
}

0 commit comments

Comments
 (0)