View Javadoc
1   /**
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements. See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership. The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License. You may obtain a copy of the License at
9    *
10   * http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied. See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  
20  package org.apache.wss4j.common.crypto;
21  
22  import java.math.BigInteger;
23  import java.security.MessageDigest;
24  import java.security.NoSuchAlgorithmException;
25  import java.security.PrivateKey;
26  import java.security.PublicKey;
27  import java.security.cert.CertPath;
28  import java.security.cert.CertPathValidator;
29  import java.security.cert.CertificateEncodingException;
30  import java.security.cert.PKIXParameters;
31  import java.security.cert.TrustAnchor;
32  import java.security.cert.X509Certificate;
33  import java.util.Arrays;
34  import java.util.Collection;
35  import java.util.HashSet;
36  import java.util.List;
37  import java.util.Set;
38  import java.util.regex.Pattern;
39  
40  import javax.security.auth.callback.CallbackHandler;
41  import javax.security.auth.x500.X500Principal;
42  
43  import org.apache.wss4j.common.ext.WSSecurityException;
44  
45  /**
46   * A Crypto implementation based on a simple array of X509Certificate(s). PrivateKeys are not
47   * supported, so this cannot be used for signature creation, or decryption.
48   */
49  public class CertificateStore extends CryptoBase {
50  
51      private static final org.slf4j.Logger LOG =
52          org.slf4j.LoggerFactory.getLogger(CertificateStore.class);
53  
54      private X509Certificate[] trustedCerts;
55  
56      /**
57       * Constructor
58       */
59      public CertificateStore(X509Certificate[] trustedCerts) {
60          this.trustedCerts = trustedCerts;
61      }
62  
63      /**
64       * Get an X509Certificate (chain) corresponding to the CryptoType argument. The supported
65       * types are as follows:
66       *
67       * TYPE.ISSUER_SERIAL - A certificate (chain) is located by the issuer name and serial number
68       * TYPE.THUMBPRINT_SHA1 - A certificate (chain) is located by the SHA1 of the (root) cert
69       * TYPE.SKI_BYTES - A certificate (chain) is located by the SKI bytes of the (root) cert
70       * TYPE.SUBJECT_DN - A certificate (chain) is located by the Subject DN of the (root) cert
71       * Note that TYPE.ALIAS is not allowed, as it doesn't have any meaning with a CertificateStore
72       */
73      public X509Certificate[] getX509Certificates(CryptoType cryptoType) throws WSSecurityException {
74          if (cryptoType == null) {
75              return new X509Certificate[0];
76          }
77          CryptoType.TYPE type = cryptoType.getType();
78          X509Certificate[] certs = new X509Certificate[0];
79          switch (type) {
80          case ISSUER_SERIAL:
81              certs = getX509Certificates(cryptoType.getIssuer(), cryptoType.getSerial());
82              break;
83          case THUMBPRINT_SHA1:
84              certs = getX509Certificates(cryptoType.getBytes());
85              break;
86          case SKI_BYTES:
87              certs = getX509CertificatesSKI(cryptoType.getBytes());
88              break;
89          case SUBJECT_DN:
90              certs = getX509CertificatesSubjectDN(cryptoType.getSubjectDN());
91              break;
92          case ALIAS:
93              throw new WSSecurityException(
94                      WSSecurityException.ErrorCode.FAILURE, "generic.EmptyMessage",
95                      new Object[] {"The alias CryptoType is not allowed for CertificateStore"}
96              );
97          case ENDPOINT:
98              break;
99          }
100         return certs;
101     }
102 
103     /**
104      * Get the implementation-specific identifier corresponding to the cert parameter. In this
105      * case, the identifier refers to the subject DN.
106      * @param cert The X509Certificate for which to search for an identifier
107      * @return the identifier corresponding to the cert parameter
108      * @throws WSSecurityException
109      */
110     public String getX509Identifier(X509Certificate cert) throws WSSecurityException {
111         return cert.getSubjectDN().toString();
112     }
113 
114     /**
115      * Gets the private key corresponding to the certificate. Not supported.
116      *
117      * @param certificate The X509Certificate corresponding to the private key
118      * @param callbackHandler The callbackHandler needed to get the password
119      * @return The private key
120      */
121     public PrivateKey getPrivateKey(
122         X509Certificate certificate, CallbackHandler callbackHandler
123     ) throws WSSecurityException {
124         return null;
125     }
126 
127     /**
128      * Gets the private key corresponding to the given PublicKey.
129      *
130      * @param publicKey The PublicKey corresponding to the private key
131      * @param callbackHandler The callbackHandler needed to get the password
132      * @return The private key
133      */
134     public PrivateKey getPrivateKey(
135         PublicKey publicKey,
136         CallbackHandler callbackHandler
137     ) throws WSSecurityException {
138         return null;
139     }
140 
141     /**
142      * Gets the private key corresponding to the identifier. Not supported.
143      *
144      * @param identifier The implementation-specific identifier corresponding to the key
145      * @param password The password needed to get the key
146      * @return The private key
147      */
148     public PrivateKey getPrivateKey(
149         String identifier,
150         String password
151     ) throws WSSecurityException {
152         return null;
153     }
154 
155     /**
156      * Evaluate whether a given certificate chain should be trusted.
157      *
158      * @param certs Certificate chain to validate
159      * @param enableRevocation whether to enable CRL verification or not
160      * @param subjectCertConstraints A set of constraints on the Subject DN of the certificates
161      * @throws WSSecurityException if the certificate chain is invalid
162      */
163     protected void verifyTrust(
164         X509Certificate[] certs,
165         boolean enableRevocation,
166         Collection<Pattern> subjectCertConstraints
167     ) throws WSSecurityException {
168         //
169         // FIRST step - Search the trusted certs for the transmitted certificate
170         //
171         if (certs.length == 1 && !enableRevocation) {
172             String issuerString = certs[0].getIssuerX500Principal().getName();
173             BigInteger issuerSerial = certs[0].getSerialNumber();
174 
175             CryptoType cryptoType = new CryptoType(CryptoType.TYPE.ISSUER_SERIAL);
176             cryptoType.setIssuerSerial(issuerString, issuerSerial);
177             X509Certificate[] foundCerts = getX509Certificates(cryptoType);
178 
179             //
180             // If a certificate has been found, the certificates must be compared
181             // to ensure against phony DNs (compare encoded form including signature)
182             //
183             if (foundCerts != null && foundCerts.length > 0 && foundCerts[0] != null && foundCerts[0].equals(certs[0])) {
184                 LOG.debug(
185                     "Direct trust for certificate with {}", certs[0].getSubjectX500Principal().getName()
186                 );
187                 return;
188             }
189         }
190 
191 
192         //
193         // SECOND step - Search for the issuer cert (chain) of the transmitted certificate in the
194         // keystore or the truststore
195         //
196         String issuerString = certs[0].getIssuerX500Principal().getName();
197         X509Certificate[] foundCerts = new X509Certificate[0];
198         if (certs.length == 1) {
199             CryptoType cryptoType = new CryptoType(CryptoType.TYPE.SUBJECT_DN);
200             cryptoType.setSubjectDN(issuerString);
201             foundCerts = getX509Certificates(cryptoType);
202 
203             // If the certs have not been found, the issuer is not in the keystore/truststore
204             // As a direct result, do not trust the transmitted certificate
205             if (foundCerts == null || foundCerts.length < 1) {
206                 String subjectString = certs[0].getSubjectX500Principal().getName();
207                 LOG.debug(
208                     "No certs found in keystore for issuer {} of certificate for {}", issuerString, subjectString
209                 );
210                 throw new WSSecurityException(
211                     WSSecurityException.ErrorCode.FAILURE, "certpath",
212                     new Object[] {"No trusted certs found"}
213                 );
214             }
215         }
216 
217         //
218         // THIRD step
219         // Check the certificate trust path for the issuer cert chain
220         //
221         LOG.debug(
222             "Preparing to validate certificate path for issuer {}", issuerString
223         );
224 
225         try {
226             Set<TrustAnchor> set = new HashSet<>();
227             if (trustedCerts != null) {
228                 for (X509Certificate cert : trustedCerts) {
229                     TrustAnchor anchor =
230                         new TrustAnchor(cert, null);
231                     set.add(anchor);
232                 }
233             }
234 
235             // Verify the trust path using the above settings
236             String provider = getCryptoProvider();
237             CertPathValidator validator = null;
238             if (provider == null || provider.length() == 0) {
239                 validator = CertPathValidator.getInstance("PKIX");
240             } else {
241                 validator = CertPathValidator.getInstance("PKIX", provider);
242             }
243 
244             PKIXParameters param = new PKIXParameters(set);
245             param.setRevocationEnabled(enableRevocation);
246 
247             if (foundCerts.length > 0) {
248                 //
249                 // Form a certificate chain from the transmitted certificate
250                 // and the certificate(s) of the issuer from the keystore/truststore
251                 //
252                 X509Certificate[] x509certs = new X509Certificate[foundCerts.length + 1];
253                 x509certs[0] = certs[0];
254                 System.arraycopy(foundCerts, 0, x509certs, 1, foundCerts.length);
255 
256                 // Generate cert path
257                 List<X509Certificate> certList = Arrays.asList(x509certs);
258                 CertPath path = getCertificateFactory().generateCertPath(certList);
259 
260                 validator.validate(path, param);
261             } else {
262                 List<X509Certificate> certList = Arrays.asList(certs);
263                 CertPath path = getCertificateFactory().generateCertPath(certList);
264 
265                 validator.validate(path, param);
266             }
267         } catch (java.security.NoSuchProviderException | NoSuchAlgorithmException
268             | java.security.cert.CertificateException
269             | java.security.InvalidAlgorithmParameterException
270             | java.security.cert.CertPathValidatorException e) {
271                 throw new WSSecurityException(
272                     WSSecurityException.ErrorCode.FAILURE, e, "certpath",
273                     new Object[] {e.getMessage()}
274                 );
275         }
276 
277         // Finally check Cert Constraints
278         if (!matchesSubjectDnPattern(certs[0], subjectCertConstraints)) {
279             throw new WSSecurityException(WSSecurityException.ErrorCode.FAILED_AUTHENTICATION);
280         }
281     }
282 
283     @Override
284     public void verifyTrust(X509Certificate[] certs, boolean enableRevocation, Collection<Pattern> subjectCertConstraints,
285                             Collection<Pattern> issuerCertConstraints) throws WSSecurityException {
286         verifyTrust(certs, enableRevocation, subjectCertConstraints);
287         if (!matchesIssuerDnPattern(certs[0], issuerCertConstraints)) {
288             throw new WSSecurityException(WSSecurityException.ErrorCode.FAILED_AUTHENTICATION);
289         }
290     }
291 
292     /**
293      * Evaluate whether a given public key should be trusted.
294      *
295      * @param publicKey The PublicKey to be evaluated
296      * @throws WSSecurityException if the PublicKey is invalid
297      */
298     public void verifyTrust(PublicKey publicKey) throws WSSecurityException {
299         //
300         // If the public key is null, do not trust the signature
301         //
302         if (publicKey == null) {
303             throw new WSSecurityException(WSSecurityException.ErrorCode.FAILED_AUTHENTICATION);
304         }
305 
306         //
307         // Search the trusted certs for the transmitted public key (direct trust)
308         //
309         for (X509Certificate trustedCert : trustedCerts) {
310             if (publicKey.equals(trustedCert.getPublicKey())) {
311                 return;
312             }
313         }
314         throw new WSSecurityException(WSSecurityException.ErrorCode.FAILED_AUTHENTICATION);
315     }
316 
317     /**
318      * Get an X509 Certificate (chain) according to a given serial number and issuer string.
319      *
320      * @param issuer The Issuer String
321      * @param serialNumber The serial number of the certificate
322      * @return the X509 Certificate (chain) that was found
323      * @throws WSSecurityException
324      */
325     private X509Certificate[] getX509Certificates(
326         String issuer,
327         BigInteger serialNumber
328     ) throws WSSecurityException {
329         //
330         // Convert the subject DN to a java X500Principal object first. This is to ensure
331         // interop with a DN constructed from .NET, where e.g. it uses "S" instead of "ST".
332         // Then convert it to a BouncyCastle X509Name, which will order the attributes of
333         // the DN in a particular way (see WSS-168). If the conversion to an X500Principal
334         // object fails (e.g. if the DN contains "E" instead of "EMAILADDRESS"), then fall
335         // back on a direct conversion to a BC X509Name
336         //
337         Object issuerName = null;
338         try {
339             X500Principal issuerRDN = new X500Principal(issuer);
340             issuerName = createBCX509Name(issuerRDN.getName());
341         } catch (IllegalArgumentException ex) {
342             issuerName = createBCX509Name(issuer);
343         }
344 
345         for (X509Certificate trustedCert : trustedCerts) {
346             if (trustedCert.getSerialNumber().compareTo(serialNumber) == 0) {
347                 Object certName =
348                     createBCX509Name(trustedCert.getIssuerX500Principal().getName());
349                 if (certName.equals(issuerName)) {
350                     return new X509Certificate[]{trustedCert};
351                 }
352             }
353         }
354 
355         return new X509Certificate[0];
356     }
357 
358     /**
359      * Get an X509 Certificate (chain) according to a given Thumbprint.
360      *
361      * @param thumb The SHA1 thumbprint info bytes
362      * @return the X509 certificate (chain) that was found (can be null)
363      * @throws WSSecurityException if problems during keystore handling or wrong certificate
364      */
365     private X509Certificate[] getX509Certificates(byte[] thumb) throws WSSecurityException {
366         MessageDigest sha = null;
367 
368         if (trustedCerts == null) {
369             return new X509Certificate[0];
370         }
371 
372         try {
373             sha = MessageDigest.getInstance("SHA1");
374         } catch (NoSuchAlgorithmException e) {
375             throw new WSSecurityException(
376                 WSSecurityException.ErrorCode.FAILURE, e, "decoding.general"
377             );
378         }
379         for (X509Certificate trustedCert : trustedCerts) {
380             try {
381                 sha.update(trustedCert.getEncoded());
382             } catch (CertificateEncodingException ex) {
383                 throw new WSSecurityException(
384                     WSSecurityException.ErrorCode.SECURITY_TOKEN_UNAVAILABLE, ex, "encodeError"
385                 );
386             }
387             byte[] data = sha.digest();
388 
389             if (Arrays.equals(data, thumb)) {
390                 return new X509Certificate[]{trustedCert};
391             }
392         }
393         return new X509Certificate[0];
394     }
395 
396     /**
397      * Get an X509 Certificate (chain) according to a given SubjectKeyIdentifier.
398      *
399      * @param skiBytes The SKI bytes
400      * @return the X509 certificate (chain) that was found (can be null)
401      */
402     private X509Certificate[] getX509CertificatesSKI(byte[] skiBytes) throws WSSecurityException {
403         if (trustedCerts == null) {
404             return new X509Certificate[0];
405         }
406         for (X509Certificate trustedCert : trustedCerts) {
407             byte[] data = getSKIBytesFromCert(trustedCert);
408             if (data.length == skiBytes.length && Arrays.equals(data, skiBytes)) {
409                 return new X509Certificate[]{trustedCert};
410             }
411         }
412         return new X509Certificate[0];
413     }
414 
415     /**
416      * Get an X509 Certificate (chain) according to a given DN of the subject of the certificate
417      *
418      * @param subjectDN The DN of subject to look for
419      * @return An X509 Certificate (chain) with the same DN as given in the parameters
420      * @throws WSSecurityException
421      */
422     private X509Certificate[] getX509CertificatesSubjectDN(String subjectDN)
423         throws WSSecurityException {
424         //
425         // Convert the subject DN to a java X500Principal object first. This is to ensure
426         // interop with a DN constructed from .NET, where e.g. it uses "S" instead of "ST".
427         // Then convert it to a BouncyCastle X509Name, which will order the attributes of
428         // the DN in a particular way (see WSS-168). If the conversion to an X500Principal
429         // object fails (e.g. if the DN contains "E" instead of "EMAILADDRESS"), then fall
430         // back on a direct conversion to a BC X509Name
431         //
432         Object subject;
433         try {
434             X500Principal subjectRDN = new X500Principal(subjectDN);
435             subject = createBCX509Name(subjectRDN.getName());
436         } catch (IllegalArgumentException ex) {
437             subject = createBCX509Name(subjectDN);
438         }
439 
440         if (trustedCerts != null) {
441             for (X509Certificate trustedCert : trustedCerts) {
442                 X500Principal foundRDN = trustedCert.getSubjectX500Principal();
443                 Object certName = createBCX509Name(foundRDN.getName());
444 
445                 if (subject.equals(certName)) {
446                     return new X509Certificate[]{trustedCert};
447                 }
448             }
449         }
450 
451         return new X509Certificate[0];
452     }
453 
454 }