View Javadoc

1   /*
2    * The MIT License
3    *
4    * Original work sponsored and donated by National Board of e-Health (NSI), Denmark (http://www.nsi.dk)
5    *
6    * Copyright (C) 2011 National Board of e-Health (NSI), Denmark (http://www.nsi.dk)
7    *
8    * Permission is hereby granted, free of charge, to any person obtaining a copy of
9    * this software and associated documentation files (the "Software"), to deal in
10   * the Software without restriction, including without limitation the rights to
11   * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
12   * of the Software, and to permit persons to whom the Software is furnished to do
13   * so, subject to the following conditions:
14   *
15   * The above copyright notice and this permission notice shall be included in all
16   * copies or substantial portions of the Software.
17   *
18   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19   * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20   * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21   * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22   * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23   * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24   * SOFTWARE.
25   *
26   * $HeadURL: https://svn.softwareborsen.dk/sosi/trunk/modules/seal/src/main/java/dk/sosi/seal/pki/CRLCertificateStatusChecker.java $
27   * $Id: CRLCertificateStatusChecker.java 8697 2011-09-02 10:33:55Z chg@lakeside.dk $
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   * The semantic for the interval setting in the properties:
53   * <p/>
54   * NEVER: never download any CRL.
55   * ALWAYS: on each check download the corresponding CRL.
56   * <p/>
57   * The STRICT value governs whether the isValid should throw an
58   * exception when no CRL could be downloaded or found.
59   * <p/>
60   * If the policy adheres that the CRL should be downloaded
61   * but it could not, the cache entry will be invalidated
62   * and the check will be performed as if no CRL was found.
63   * This behavior relates to the STRICT setting.
64   *
65   * @author ht@arosii.dk
66   * @since 2.0
67   */
68  public class CRLCertificateStatusChecker implements CertificateStatusChecker {
69  
70      /**
71       * A constant for never checking for CRL updates
72       */
73      public static final int NEVER = -1;
74      /**
75       * A constant for always checking for CRL updates
76       */
77      public static final int ALWAYS = 0;
78  
79      /**
80       * Check results including this timestamp, should be regarded as artificial
81       */
82      public static final Date INVALID_TIMESTAMP = new Date(0);
83  
84      /**
85       * The strictness of the check, see class javadoc
86       */
87      protected final boolean strict;
88  
89      /**
90       * The check interval for the revocation lists
91       */
92      protected final long interval;
93  
94      /**
95       * The used cache
96       */
97      private final CRLCache cache;
98  
99      /**
100      * Used for CRL verification.
101      */
102     private final CertificateResolver certificateResolver;
103 
104     /**
105      * Default http connect timeout in ms for retrieving CRL
106      */
107     private final static int DEFAULT_CONNECT_TIMEOUT = 3000;
108 
109     /**
110      * Default http read timeout in ms for retrieving CRL
111      */
112     private final static int DEFAULT_READ_TIMEOUT = 3000;
113 
114     /**
115      * http connect timeout in ms for retrieving CRL
116      */
117     private int connectTimeout = DEFAULT_CONNECT_TIMEOUT;
118 
119     /**
120      * http read timeout in ms for retrieving CRL
121      */
122     private int readTimeout = DEFAULT_READ_TIMEOUT;
123 
124     /**
125      * Time to live for a CRL. A CRL must satisfy:
126      *   CRL.getNextUpdate() + ttl > now
127      * before it can be used.
128      *
129      * ttl == NEVER means that the check is disregarded.
130      */
131     private final int ttl;
132 
133     private static Log log = LogFactory.getLog(CRLCertificateStatusChecker.class);
134 
135     /**
136      * Creates a new <code>CRLCertificateStatusChecker</code> with the supplied
137      * cache, interval and strictness.
138      *
139      * @param cache    the used cache.
140      * @param interval the interval in seconds, see NEVER and ALWAYS for special values.
141      * @param strict   see class javadoc for semantic.
142      * @param ttl specifies a CRL's extra time to live
143      * @param certificateResolver the resolver for finding the issuer certificate used for CRL verification
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      * Checks if the certificate supplied is revoked. The check is performed
187      * against the CRL downloaded from the URL found in the certificate.
188      * <p/>
189      * The CRL is conditionally downloaded depending on the settings.
190      * <p/>
191      * If the certificate is null, a negative result is returned with invalid
192      * timestamp.
193      *
194      * @param cert the certificate to check.
195      * @return the result of the check including the timestamp for CRL.
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         // If the CRL was not found in the cache
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      * No measures are taken to ensure, that the same CRL is not downloaded by
218      * multiple threads simultaneously. The cache does however ensure consistency.
219      *
220      * @param url     the CRL endpoint
221      * @param crlInfo the old crlInfo if such exists otherwise null
222      * @return the crlInfo, which might be updated.
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)); // now it is no longer unchecked
257             } else {
258                 // invalidate the cache entry for the invalid CRL, subsequent checks will try to
259                 // download a new CRL
260                 cache.update(url, (CRLCache.CRLInfo) null);
261                 // We could return null, but if the behavior for checking against an unchecked CRL
262                 // changes, this will be nice to have.
263                 return crlInfo;
264             }
265         } else {
266             // do nothing if it is already checked
267             return crlInfo;
268         }
269     }
270 
271     /**
272      * Just a subclass of CRLInfo to indicate the checked status.
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      * Performs the conditional download of the CRL. One can supply other means
282      * to retrieve a CRL, but the timestamp in the CRLInfo should be updated
283      * according to the same semantics.
284      *
285      * @param url     the location of the CRL.
286      * @param crlInfo the previous CRL together with associate timestamps
287      * @return the new CRL together the updated timestamps, or the previous CRL with the timestamps updated.
288      * @throws IOException in case the download fails.
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      * Updates all known CRL entries in the cache.
300      * <p/>
301      * Should not be used lightly since all registered CRL are downloaded.
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      * Check the certificate against the revocation list. If the CRLInfo
312      * is unchecked, the CRLinfo is disregarded.
313      *
314      * Having no CRLInfo in the strict case will throw an exception, while
315      * in the non-strict case nothing is revoked. The timestamp will however be
316      * invalid.
317      * @param info the revocation list; this also contains information about the check status. See <link>UncheckedCRLInfo</link>
318      * @param cert the certificate being checked.
319      * @return the status of the check including the boolean answer and the timestamp for the implicated CRL.
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      * Retrieves the CRL URL from the certificate.
338      *
339      * @param cert the certificate
340      * @return the first found CRL URL in the certificate or <code>null</code> if no such URL was found.
341      */
342     private static String getCRLUrlFromCertificate(final X509Certificate cert) {
343         final byte[] extensionValue = cert.getExtensionValue(X509Extension.cRLDistributionPoints.getId());
344 
345         // In case no such extension exists
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         // Special error handling if no full_name was found or if no URI was found
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     /*Visible for testing*/
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             // not modified, but checked
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      * Download the CRL from the url, but do a If-Modified-Since request
399      * based on the CRLInfo
400      *
401      * @param url the location of the crl
402      * @param crlInfo the date used for the request
403      * @return the newly downloaded crl with corresponding timestamp or null if
404      *         nothing was downloaded.
405      *
406      * @throws IOException in case something went wrong
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             // Last-Modified header not (always) set when HTTP status is 304
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         // always new
433         if (crlInfo == null || lastModified != crlInfo.getLastModified())
434             return new UncheckedCRLInfo(x509CRL, lastModified);
435         else
436             return new CRLCache.CRLInfo(x509CRL, lastModified); // if nothing changed
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                 //ignore
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 }