Coverage Report - org.openpermis.cert.ChainCertificateVerifier
 
Classes in this File Line Coverage Branch Coverage Complexity
ChainCertificateVerifier
51%
44/85
47%
18/38
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  
 }