1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29 package dk.sosi.seal.pki;
30
31 import org.apache.commons.logging.Log;
32 import org.apache.commons.logging.LogFactory;
33 import org.bouncycastle.asn1.ASN1InputStream;
34 import org.bouncycastle.asn1.ASN1Sequence;
35 import org.bouncycastle.asn1.DERIA5String;
36 import org.bouncycastle.asn1.DEROctetString;
37 import org.bouncycastle.asn1.x509.*;
38 import org.bouncycastle.asn1.x509.X509Extension;
39
40 import java.io.ByteArrayInputStream;
41 import java.io.IOException;
42 import java.io.InputStream;
43 import java.net.HttpURLConnection;
44 import java.net.URL;
45 import java.net.URLConnection;
46 import java.security.cert.*;
47 import java.util.Date;
48 import java.util.Map;
49 import java.util.Set;
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68 public class CRLCertificateStatusChecker implements CertificateStatusChecker {
69
70
71
72
73 public static final int NEVER = -1;
74
75
76
77 public static final int ALWAYS = 0;
78
79
80
81
82 public static final Date INVALID_TIMESTAMP = new Date(0);
83
84
85
86
87 protected final boolean strict;
88
89
90
91
92 protected final long interval;
93
94
95
96
97 private final CRLCache cache;
98
99
100
101
102 private final CertificateResolver certificateResolver;
103
104
105
106
107 private final static int DEFAULT_CONNECT_TIMEOUT = 3000;
108
109
110
111
112 private final static int DEFAULT_READ_TIMEOUT = 3000;
113
114
115
116
117 private int connectTimeout = DEFAULT_CONNECT_TIMEOUT;
118
119
120
121
122 private int readTimeout = DEFAULT_READ_TIMEOUT;
123
124
125
126
127
128
129
130
131 private final int ttl;
132
133 private static Log log = LogFactory.getLog(CRLCertificateStatusChecker.class);
134
135
136
137
138
139
140
141
142
143
144
145 public CRLCertificateStatusChecker(final CRLCache cache,
146 final int interval,
147 final boolean strict,
148 final int ttl,
149 final CertificateResolver certificateResolver) {
150
151 if (cache == null) throw new IllegalArgumentException("'cache' must not be null");
152 if (certificateResolver == null) throw new IllegalArgumentException("'certificateResolver' must not be null");
153 if (interval < -1) throw new IllegalArgumentException("Illegal interval");
154
155 this.cache = cache;
156 this.strict = strict;
157 this.interval = calcInterval(interval);
158 this.certificateResolver = certificateResolver;
159 this.ttl = ttl;
160 }
161
162 private int calcInterval(int interval) {
163 if (interval == ALWAYS || interval == NEVER) {
164 return interval;
165 } else {
166 return interval * 1000;
167 }
168 }
169
170 public void setConnectTimeout(int connectTimeout) {
171 if (connectTimeout <= 0) {
172 throw new IllegalArgumentException("'connectTimeout' must be positive");
173 }
174 this.connectTimeout = connectTimeout;
175 }
176
177 public void setReadTimeout(int readTimeout) {
178 if (readTimeout <= 0) {
179 throw new IllegalArgumentException("'readTimeout' must be positive");
180 }
181 this.readTimeout = readTimeout;
182 }
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197 public CertificateStatus getRevocationStatus(final X509Certificate cert) {
198 if (cert == null) {
199 throw new IllegalArgumentException("'cert' must not be null");
200 }
201
202 final String url = getCRLUrlFromCertificate(cert);
203 if (url == null) {
204 return check(null, cert);
205 }
206 final CRLCache.CRLInfo crlInfo = cache.get(url);
207
208
209 if (crlInfo == null) {
210 return check(checkCRL(url, createNew(url), cert), cert);
211 }
212
213 return check(checkCRL(url, checkAndUpdate(url, crlInfo), cert), cert);
214 }
215
216
217
218
219
220
221
222
223
224 private CRLCache.CRLInfo checkAndUpdate(String url, final CRLCache.CRLInfo crlInfo) {
225 final boolean update;
226 if (interval == NEVER) {
227 update = false;
228 } else if (crlInfo == null) {
229 log.debug("CRL download triggered by having no existing CRL.");
230 update = true;
231 } else if (interval == ALWAYS) {
232 log.debug("CRL download triggered by ALWAYS.");
233 update = true;
234 } else if (!hasTTL(crlInfo.getCrl())) {
235 update = true;
236 log.debug("CRL download triggered by ttl.");
237 } else {
238 update = System.currentTimeMillis() - crlInfo.getCreated() > interval;
239 if (update) log.debug("CRL download triggered interval.");
240 }
241
242 if (update) {
243 try {
244 return cache.update(url, load(url, crlInfo));
245 } catch (Throwable t) {
246 log.error("While trying to download " + url + " <" + t.toString() + "> occurred.");
247 return cache.update(url, (CRLCache.CRLInfo) null);
248 }
249 }
250 return crlInfo;
251 }
252
253 private CRLCache.CRLInfo checkCRL(String url, CRLCache.CRLInfo crlInfo, X509Certificate cert) {
254 if (crlInfo instanceof UncheckedCRLInfo) {
255 if (isValidCRL(crlInfo.getCrl(), cert)) {
256 return cache.update(url, new CRLCache.CRLInfo(crlInfo));
257 } else {
258
259
260 cache.update(url, (CRLCache.CRLInfo) null);
261
262
263 return crlInfo;
264 }
265 } else {
266
267 return crlInfo;
268 }
269 }
270
271
272
273
274 protected static class UncheckedCRLInfo extends CRLCache.CRLInfo {
275 public UncheckedCRLInfo(final X509CRL crl, final long lastModified) {
276 super(crl, lastModified);
277 }
278 }
279
280
281
282
283
284
285
286
287
288
289
290 protected CRLCache.CRLInfo load(final String url, final CRLCache.CRLInfo crlInfo) throws IOException {
291 return downloadCRL(url, crlInfo);
292 }
293
294 private CRLCache.CRLInfo createNew(final String url) {
295 return checkAndUpdate(url, null);
296 }
297
298
299
300
301
302
303 protected void updateAll() {
304 final Set<Map.Entry<String, CRLCache.CRLInfo>> entries = cache.entries();
305 for (Map.Entry<String, CRLCache.CRLInfo> entry : entries) {
306 createNew(entry.getKey());
307 }
308 }
309
310
311
312
313
314
315
316
317
318
319
320
321 protected CertificateStatus check(CRLCache.CRLInfo info, X509Certificate cert) {
322 if (info == null || info instanceof UncheckedCRLInfo) {
323 if (strict) {
324 throw new IllegalStateException("Unable to check certificate revocation.");
325 } else {
326 return nonStrictCaseWithoutCRL();
327 }
328 }
329 return new CertificateStatus(! info.getCrl().isRevoked(cert), info.getCrl().getThisUpdate());
330 }
331
332 private CertificateStatus nonStrictCaseWithoutCRL() {
333 return new CertificateStatus(true, INVALID_TIMESTAMP);
334 }
335
336
337
338
339
340
341
342 private static String getCRLUrlFromCertificate(final X509Certificate cert) {
343 final byte[] extensionValue = cert.getExtensionValue(X509Extension.cRLDistributionPoints.getId());
344
345
346 if (extensionValue == null) {
347 return null;
348 }
349
350 final DEROctetString oct;
351 final ASN1Sequence seq;
352 try {
353 oct = (DEROctetString) new ASN1InputStream(new ByteArrayInputStream(extensionValue)).readObject();
354 seq = (ASN1Sequence) new ASN1InputStream(oct.getOctets()).readObject();
355 } catch (IOException e) {
356 return null;
357 }
358
359
360 final CRLDistPoint distPoint = CRLDistPoint.getInstance(seq);
361 for (final DistributionPoint distributionPoint : distPoint.getDistributionPoints()) {
362
363 if (distributionPoint.getDistributionPoint().getType() == DistributionPointName.FULL_NAME) {
364
365 for (final GeneralName name : ((GeneralNames) distributionPoint.getDistributionPoint().getName()).getNames()) {
366 if (name.getTagNo() == GeneralName.uniformResourceIdentifier) {
367 return DERIA5String.getInstance(name.getName().getDERObject()).getString();
368 }
369 }
370
371 }
372 }
373
374 return null;
375 }
376
377
378 CRLCache.CRLInfo downloadCRL(String url, CRLCache.CRLInfo old) throws IOException {
379 final CRLCache.CRLInfo crlInfo = downloadCRL(new URL(url), old);
380
381 if (crlInfo == null && old == null) {
382 throw new IllegalStateException("CRL could not be downloaded");
383 }
384
385 if (crlInfo == null) {
386
387 if (old instanceof UncheckedCRLInfo)
388 return new UncheckedCRLInfo(old.getCrl(), old.getLastModified());
389 else
390 return new CRLCache.CRLInfo(old.getCrl(), old.getLastModified());
391 } else {
392 return crlInfo;
393 }
394
395 }
396
397
398
399
400
401
402
403
404
405
406
407
408 private CRLCache.CRLInfo downloadCRL(URL url, CRLCache.CRLInfo crlInfo) throws IOException {
409 X509CRL x509CRL = crlInfo == null ? null : crlInfo.getCrl();
410 long lastModified = crlInfo == null ? -1 : crlInfo.getLastModified();
411 final URLConnection conn = url.openConnection();
412 conn.setConnectTimeout(connectTimeout);
413 conn.setReadTimeout(readTimeout);
414 if (lastModified != 0) {
415 conn.setIfModifiedSince(lastModified);
416 }
417 conn.connect();
418 if (conn instanceof HttpURLConnection) {
419
420 if (HttpURLConnection.HTTP_NOT_MODIFIED != ((HttpURLConnection) conn).getResponseCode()) {
421 lastModified = conn.getLastModified();
422 x509CRL = generateCrl(conn.getInputStream());
423 }
424 } else if (lastModified != conn.getLastModified()) {
425 lastModified = conn.getLastModified();
426 x509CRL = generateCrl(conn.getInputStream());
427 }
428 if (x509CRL == null) {
429 return null;
430 }
431
432
433 if (crlInfo == null || lastModified != crlInfo.getLastModified())
434 return new UncheckedCRLInfo(x509CRL, lastModified);
435 else
436 return new CRLCache.CRLInfo(x509CRL, lastModified);
437 }
438
439 private static X509CRL generateCrl(InputStream in) {
440 try {
441 final CertificateFactory certificatefactory = CertificateFactory.getInstance("X.509");
442 return (X509CRL) certificatefactory.generateCRL(in);
443 } catch (CertificateException e) {
444 throw new PKIException(e);
445 } catch (CRLException e) {
446 throw new PKIException(e);
447 } finally {
448 closeStream(in);
449 }
450 }
451
452
453 private static void closeStream(InputStream in) {
454 if (in != null) {
455 try {
456 in.close();
457 } catch (IOException e) {
458
459 }
460 }
461 }
462
463
464 protected boolean verify(X509CRL crl, X509Certificate cert) {
465 try {
466 crl.verify(certificateResolver.getIssuingCertificate(cert).getPublicKey());
467 } catch(Exception e) {
468 log.error("CRL verification failed.", e);
469 return false;
470 }
471 return true;
472 }
473
474 private boolean isValidCRL(X509CRL crl, X509Certificate cert) {
475 return verify(crl, cert) && notPartitioned(crl) && hasTTL(crl);
476 }
477
478 protected boolean notPartitioned(X509CRL crl) {
479 boolean notPartitioned = !crl.getCriticalExtensionOIDs().contains(X509Extension.issuingDistributionPoint.getId());
480 if (!notPartitioned) log.error("CRL is partitioned, which is not supported.");
481 return notPartitioned;
482 }
483
484 protected boolean hasTTL(X509CRL crl) {
485 if (ttl == NEVER) return true;
486
487 long now = System.currentTimeMillis();
488 boolean result = crl.getNextUpdate().getTime() + ttl > now;
489 if (!result) log.error("The CRL is not live, the next update timestamp was: " + crl.getNextUpdate());
490 return result;
491 }
492
493 }