Classes in this File | Line Coverage | Branch Coverage | Complexity | ||||
ChainCertificateVerifier |
|
| 6.833333333333333;6.833 |
1 | /* | |
2 | * Copyright (c) 2009, Swiss Federal Department of Defence Civil Protection and Sport | |
3 | * (http://www.vbs.admin.ch) | |
4 | * Copyright (c) 2009, Ergon Informatik AG (http://www.ergon.ch) | |
5 | * All rights reserved. | |
6 | * | |
7 | * Licensed under the Open Permis License which accompanies this distribution, | |
8 | * and is available at http://www.openpermis.org/BSDlicenceKent.txt | |
9 | */ | |
10 | package org.openpermis.cert; | |
11 | ||
12 | import java.io.IOException; | |
13 | import java.security.InvalidAlgorithmParameterException; | |
14 | import java.security.InvalidKeyException; | |
15 | import java.security.NoSuchAlgorithmException; | |
16 | import java.security.NoSuchProviderException; | |
17 | import java.security.Principal; | |
18 | import java.security.SignatureException; | |
19 | import java.security.cert.CertStore; | |
20 | import java.security.cert.CertStoreException; | |
21 | import java.security.cert.CertStoreParameters; | |
22 | import java.security.cert.Certificate; | |
23 | import java.security.cert.CertificateException; | |
24 | import java.security.cert.CollectionCertStoreParameters; | |
25 | import java.security.cert.X509CertSelector; | |
26 | import java.security.cert.X509Certificate; | |
27 | import java.util.ArrayList; | |
28 | import java.util.Collections; | |
29 | import java.util.HashMap; | |
30 | import java.util.HashSet; | |
31 | import java.util.List; | |
32 | import java.util.Map; | |
33 | import java.util.Set; | |
34 | ||
35 | ||
36 | /** | |
37 | * This certificate verifier supports mutiple trusted issuers ("trusted anchors") and certificate | |
38 | * chains. | |
39 | * | |
40 | * This verifier maintains a set of trusted root issuer certificates and a maximum chain length. | |
41 | * When verifying a certificate, this class looks for a certificate chain to one of the trusted | |
42 | * root certificates by asking the specified {@link CertificateRepository} for any required | |
43 | * intermediate certificates. | |
44 | * | |
45 | * A {@link CertificateRepository} may for example return certificates from an LDAP directory or a | |
46 | * file directory. | |
47 | * | |
48 | * This class supports certificate chains greater than one. | |
49 | * A chain depth of one means that the trusted root directly signed the certificate to be | |
50 | * verifer (only one signature involved). | |
51 | * | |
52 | * <em>CAUTION:</em> Be extremely careful when using a chain length greater than one! A length | |
53 | * if two, for example, means that you implicitly trust all entities that have been signed by | |
54 | * the trusted roots. This is normally not the case in reality (trust is not transitive!). | |
55 | * | |
56 | * If a specific crypto provider should be used when using JCE functions, you can use method | |
57 | * {@link #setProvider(String)} to specify it. | |
58 | * | |
59 | * @since 0.3.0 | |
60 | */ | |
61 | public class ChainCertificateVerifier implements CertificateVerifier { | |
62 | ||
63 | //---- State | |
64 | /** | |
65 | * The set of trusted issuer (trust anchors, root certificates). They are organized by | |
66 | * subject name so they can be found efficiently. | |
67 | */ | |
68 | private final Map<Principal, X509Certificate> trustAnchorCertsBySubject; | |
69 | ||
70 | /** | |
71 | * The certificate repository to ask for intermediate certificates if necessary. | |
72 | */ | |
73 | private final CertificateRepository certificateRepository; | |
74 | ||
75 | /** | |
76 | * The maximum length of a certificate chain. | |
77 | */ | |
78 | private final int maxChainLength; | |
79 | ||
80 | /** | |
81 | * The crypto provider or null if non is secified. | |
82 | */ | |
83 | String provider; | |
84 | ||
85 | //---- Constructors | |
86 | ||
87 | /** | |
88 | * Creates a certificate verifier using the trusted roots and a certificate repository that | |
89 | * can be asked for intermediate certificates if required and restricting the maximum chain | |
90 | * depth to the indicated value. | |
91 | * | |
92 | * A chain depth of one means that the trusted root directly signed the certificate to be | |
93 | * verifer (only one signature involved). | |
94 | * | |
95 | * <em>CAUTION:</em> Be extremely careful when using a chain length greater than one! A length | |
96 | * if two, for example, means that you implicitly trust all entities that have been signed by | |
97 | * the trusted roots. This is normally not the case in reality (trust is not transitive!). | |
98 | * | |
99 | * @param trustedRoots A set of trusted root certificates. The subjects of the specified | |
100 | * certificates must be trusted and their public keys in the certificates must be | |
101 | * authentic. This parameter must not be <code>null</code>. | |
102 | * @param certificateRepository The certificate repository is used to ask for | |
103 | * intermediate certificates needed to build a certificate chain. Using <code>null</code> as | |
104 | * value tells the class not to use a certificate repository. | |
105 | * @param maxChainLength The maximum allowed chain length. The value must be one or greater. | |
106 | * @since 0.3.0 | |
107 | */ | |
108 | public ChainCertificateVerifier ( | |
109 | Set<X509Certificate> trustedRoots, | |
110 | CertificateRepository certificateRepository, | |
111 | int maxChainLength | |
112 | 6 | ) { |
113 | // setup map of trusted roots | |
114 | 6 | if (trustedRoots == null) { |
115 | 0 | throw new IllegalArgumentException("set of trusted roots is null"); |
116 | } | |
117 | 6 | final Map<Principal, X509Certificate> map = new HashMap<Principal, X509Certificate>(); |
118 | 6 | for (X509Certificate c : trustedRoots) { |
119 | 11 | map.put(c.getSubjectX500Principal(), c); |
120 | } | |
121 | 6 | this.trustAnchorCertsBySubject = Collections.unmodifiableMap(map); |
122 | ||
123 | // cert repository (null (=none) is allowed) | |
124 | 6 | this.certificateRepository = certificateRepository; |
125 | ||
126 | // chain length: must be > 0 | |
127 | 6 | if (maxChainLength < 1) { |
128 | 0 | throw new IllegalArgumentException("maximum chain length is zero or negative"); |
129 | } | |
130 | 6 | this.maxChainLength = maxChainLength; |
131 | ||
132 | // start with no specific provider | |
133 | 6 | this.provider = null; |
134 | 6 | } |
135 | ||
136 | /** | |
137 | * Creates a certificate verifier using the trusted roots, allowing only chains of length one | |
138 | * and therefore needs no certificate repository to get intermediate certificates from. | |
139 | * | |
140 | * @param trustedRoots A set of trusted root certificates. The subjects of the specified | |
141 | * certificates must be trusted and their public keys in the certificates must be | |
142 | * authentic. This parameter must not be <code>null</code>. | |
143 | * @since 0.3.0 | |
144 | */ | |
145 | 0 | public ChainCertificateVerifier (Set<X509Certificate> trustedRoots) { |
146 | // setup map of trusted roots | |
147 | 0 | if (trustedRoots == null) { |
148 | 0 | throw new IllegalArgumentException("set of trusted roots is null"); |
149 | } | |
150 | 0 | final Map<Principal, X509Certificate> map = new HashMap<Principal, X509Certificate>(); |
151 | 0 | for (X509Certificate c : trustedRoots) { |
152 | 0 | this.trustAnchorCertsBySubject.put(c.getSubjectX500Principal(), c); |
153 | } | |
154 | 0 | this.trustAnchorCertsBySubject = Collections.unmodifiableMap(map); |
155 | 0 | this.certificateRepository = null; |
156 | 0 | this.maxChainLength = 1; |
157 | 0 | this.provider = null; |
158 | 0 | } |
159 | ||
160 | /** | |
161 | * This is a convenience constructor doing the same as | |
162 | * {@link #ChainCertificateVerifier(Set, CertificateRepository, int)} but using the | |
163 | * specified set of intermediate certificates as in-memory certificate repository. | |
164 | * | |
165 | * It the trusted roots and restricts the maximum chain depth to the indicated value. | |
166 | * | |
167 | * A chain depth of one means that the trusted root directly signed the certificate to be | |
168 | * verifer (only one signature involved). | |
169 | * | |
170 | * <em>CAUTION:</em> Be extremely careful when using a chain length greater than one! A length | |
171 | * if two, for example, means that you implicitly trust all entities that have been signed by | |
172 | * the trusted roots. This is normally not the case in reality (trust is not transitive!). | |
173 | * | |
174 | * @param trustedRoots A set of trusted root certificates. The subjects of the specified | |
175 | * certificates must be trusted and their public keys in the certificates must be | |
176 | * authentic. This parameter must not be <code>null</code>. | |
177 | * @param intermediateCerts A set of certificates that may serve as intermediate certificates | |
178 | * in certifiate chains. Must not be null. | |
179 | * @param maxChainLength The maximum allowed chain length. The value must be one or greater. | |
180 | * @throws NoSuchAlgorithmException Thrown if no collection based {@link CertStore} | |
181 | * implementation is availabel from the underlying crypto provider. | |
182 | * @throws InvalidAlgorithmParameterException Thrown if the parameters passed to the | |
183 | * collection based {@link CertStore} are invalid. | |
184 | * @since 0.3.0 | |
185 | */ | |
186 | public ChainCertificateVerifier ( | |
187 | Set<X509Certificate> trustedRoots, | |
188 | Set<X509Certificate> intermediateCerts, | |
189 | int maxChainLength | |
190 | 0 | ) throws InvalidAlgorithmParameterException, NoSuchAlgorithmException { |
191 | // setup map of trusted roots | |
192 | 0 | if (trustedRoots == null) { |
193 | 0 | throw new IllegalArgumentException("set of trusted roots is null"); |
194 | } | |
195 | 0 | final Map<Principal, X509Certificate> map = new HashMap<Principal, X509Certificate>(); |
196 | 0 | for (X509Certificate c : trustedRoots) { |
197 | 0 | this.trustAnchorCertsBySubject.put(c.getSubjectX500Principal(), c); |
198 | } | |
199 | 0 | this.trustAnchorCertsBySubject = Collections.unmodifiableMap(map); |
200 | ||
201 | // create a collection based certificate store | |
202 | 0 | if (intermediateCerts == null) { |
203 | 0 | throw new IllegalArgumentException("Set of intermediate certificates is null."); |
204 | } | |
205 | 0 | final CertStoreParameters certStoreParams = |
206 | new CollectionCertStoreParameters(intermediateCerts); | |
207 | 0 | final CertStore certStore = CertStore.getInstance("Collection", certStoreParams); |
208 | 0 | this.certificateRepository = new CertStoreCertificateRepository(certStore); |
209 | ||
210 | // chain length: must be > 0 | |
211 | 0 | if (maxChainLength < 1) { |
212 | 0 | throw new IllegalArgumentException("maximum chain length is zero or negative"); |
213 | } | |
214 | 0 | this.maxChainLength = maxChainLength; |
215 | ||
216 | // start with no specific provider | |
217 | 0 | this.provider = null; |
218 | 0 | } |
219 | ||
220 | //---- CertificateVerifier | |
221 | /** | |
222 | * {@inheritDoc}. | |
223 | * @since 0.3.0 | |
224 | */ | |
225 | public void verifyCertificate (Certificate certificate) | |
226 | throws | |
227 | CertificateException, | |
228 | NoSuchAlgorithmException, | |
229 | InvalidKeyException, | |
230 | NoSuchProviderException, | |
231 | SignatureException | |
232 | { | |
233 | 5 | final List<X509Certificate> chain = new ArrayList<X509Certificate>(); |
234 | 5 | final Set<Certificate> visitedCerts = new HashSet<Certificate>(); |
235 | 5 | computeChainInternal(chain, this.maxChainLength, visitedCerts, certificate); |
236 | 2 | if (chain.size() < 1) { |
237 | 0 | throw new CertificateException("No certificate chain found."); |
238 | } | |
239 | 2 | } |
240 | ||
241 | //---- Methods | |
242 | ||
243 | /** | |
244 | * Allows to set a specific crypto provider. If none is set (or <code>null</code> is set | |
245 | * explicitly with this method), the default crypto provider is used. | |
246 | * @param provider The name of the crypto provider to use or <code>null</code>. | |
247 | */ | |
248 | public void setProvider (String provider) { | |
249 | 0 | this.provider = provider; |
250 | 0 | } |
251 | ||
252 | /** | |
253 | * Internal method that is called recursively to go up the certificate chain. | |
254 | * @param resultChain The result chain that is build during the search. | |
255 | * @param remainingChainLength The remaining maximum allowed chain length in this step. | |
256 | * @param visitedCerts Internally keeps track of visited certificates in order to detect loops. | |
257 | * @param certToVerify The certificate to verify in this step. | |
258 | * @throws SignatureException | |
259 | * @throws NoSuchProviderException | |
260 | * @throws NoSuchAlgorithmException | |
261 | * @throws CertificateException | |
262 | * @throws InvalidKeyException | |
263 | */ | |
264 | private void computeChainInternal ( | |
265 | List<X509Certificate> resultChain, | |
266 | int remainingChainLength, | |
267 | Set<Certificate> visitedCerts, | |
268 | Certificate certToVerify | |
269 | ) throws | |
270 | InvalidKeyException, | |
271 | CertificateException, | |
272 | NoSuchAlgorithmException, | |
273 | NoSuchProviderException, | |
274 | SignatureException | |
275 | { | |
276 | // check max chain length | |
277 | 7 | if (remainingChainLength <= 0) { |
278 | 1 | throw new CertificateException( |
279 | "Certificate chain too long while buidling certificate chain." | |
280 | ); | |
281 | } | |
282 | ||
283 | // avoid loops: see if the cert to check has already been visted | |
284 | 6 | if (visitedCerts.contains(certToVerify)) { |
285 | // loop --> will never find a trusted root | |
286 | 0 | throw new CertificateException( |
287 | "No path to trust anchor found (cicle in cert path)." | |
288 | ); | |
289 | } | |
290 | ||
291 | // get Issuer of cert to check | |
292 | final Principal issuer; | |
293 | final Principal subject; | |
294 | 6 | if (certToVerify instanceof X509Certificate) { |
295 | 4 | issuer = ((X509Certificate) certToVerify).getIssuerX500Principal(); |
296 | 4 | subject = ((X509Certificate) certToVerify).getSubjectX500Principal(); |
297 | 2 | } else if (certToVerify instanceof AttributeCertificate) { |
298 | 2 | issuer = ((AttributeCertificate) certToVerify).getIssuer().getPrincipals()[0]; |
299 | 2 | subject = ((AttributeCertificate) certToVerify).getHolder().getEntityNames()[0]; |
300 | } else { | |
301 | 0 | throw new CertificateException( |
302 | "unsupported certificate type: " + certToVerify.getClass().getName() | |
303 | ); | |
304 | } | |
305 | ||
306 | // see if the cert to check was signed by a trusted ca cert | |
307 | 6 | final X509Certificate trustedCert = this.trustAnchorCertsBySubject.get(issuer); |
308 | 6 | if (trustedCert != null) { |
309 | // trust is ok, now check signature | |
310 | 2 | if (this.provider == null) { |
311 | 2 | certToVerify.verify(trustedCert.getPublicKey()); |
312 | } else { | |
313 | 0 | certToVerify.verify(trustedCert.getPublicKey(), this.provider); |
314 | } | |
315 | // ok we are done --> update the result | |
316 | 2 | resultChain.add(trustedCert); |
317 | } else { | |
318 | // issuer of certificate to check not found among trusted certs | |
319 | try { | |
320 | 4 | final X509CertSelector certSelector = new X509CertSelector(); |
321 | try { | |
322 | 4 | certSelector.setSubject(issuer.getName()); |
323 | 0 | } catch (IOException e) { |
324 | 0 | throw new CertificateException( |
325 | "Cannot setup internal certificate selector to find issuer " + | |
326 | "certificates in certificate store.", | |
327 | e | |
328 | ); | |
329 | 4 | } |
330 | 4 | if (this.certificateRepository != null) { |
331 | for ( | |
332 | Certificate issuerCandidate : | |
333 | 4 | this.certificateRepository.getCertificates(certSelector)) { |
334 | 2 | final X509Certificate potentialIssuer = (X509Certificate) issuerCandidate; |
335 | // check signature | |
336 | 2 | if (this.provider == null) { |
337 | 2 | certToVerify.verify(potentialIssuer.getPublicKey()); |
338 | } else { | |
339 | 0 | certToVerify.verify(potentialIssuer.getPublicKey(), this.provider); |
340 | } | |
341 | // ok this one verifies --> must follow this path | |
342 | 2 | visitedCerts.add(certToVerify); |
343 | 2 | resultChain.add(potentialIssuer); |
344 | 2 | computeChainInternal( |
345 | resultChain, | |
346 | remainingChainLength - 1, | |
347 | visitedCerts, | |
348 | potentialIssuer | |
349 | ); | |
350 | 1 | return; |
351 | } | |
352 | } | |
353 | // no issuer found that matches and signature verifies | |
354 | 2 | throw new CertificateException( |
355 | "Cannot find issuer for certificate (Subject: " + subject.getName() + | |
356 | ", Issuer: " + issuer.getName() + ")" | |
357 | ); | |
358 | 0 | } catch (CertStoreException e) { |
359 | 0 | throw new CertificateException( |
360 | "Cannot find issuer for certificate because of error: " + e.getMessage(), | |
361 | e | |
362 | ); | |
363 | } | |
364 | } | |
365 | 2 | } |
366 | } |