View Javadoc

1   /*
2    * Copyright [2006] [University Corporation for Advanced Internet Development, Inc.]
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package org.opensaml.xml.security.x509;
18  
19  import java.io.ByteArrayInputStream;
20  import java.io.File;
21  import java.io.IOException;
22  import java.security.GeneralSecurityException;
23  import java.security.PrivateKey;
24  import java.security.cert.CRLException;
25  import java.security.cert.CertificateException;
26  import java.security.cert.CertificateFactory;
27  import java.security.cert.CertificateParsingException;
28  import java.security.cert.X509CRL;
29  import java.security.cert.X509Certificate;
30  import java.util.Collection;
31  import java.util.LinkedList;
32  import java.util.List;
33  
34  import javax.security.auth.x500.X500Principal;
35  
36  import org.apache.commons.ssl.TrustMaterial;
37  import org.bouncycastle.asn1.ASN1InputStream;
38  import org.bouncycastle.asn1.DERObject;
39  import org.bouncycastle.asn1.DERObjectIdentifier;
40  import org.bouncycastle.asn1.DERSequence;
41  import org.bouncycastle.asn1.DERSet;
42  import org.bouncycastle.asn1.DERString;
43  import org.bouncycastle.asn1.x509.SubjectKeyIdentifier;
44  import org.bouncycastle.asn1.x509.X509Extensions;
45  import org.bouncycastle.x509.extension.SubjectKeyIdentifierStructure;
46  import org.bouncycastle.x509.extension.X509ExtensionUtil;
47  import org.opensaml.xml.schema.SchemaBuilder;
48  import org.opensaml.xml.security.SecurityException;
49  import org.opensaml.xml.security.SecurityHelper;
50  import org.opensaml.xml.util.DatatypeHelper;
51  import org.opensaml.xml.util.IPAddressHelper;
52  import org.slf4j.Logger;
53  import org.slf4j.LoggerFactory;
54  
55  /**
56   * Utility class for working with X509 objects.
57   */
58  public class X509Util {
59  
60      /** Encoding used to store a key or certificate in a file. */
61      public static enum ENCODING_FORMAT {
62          PEM, DER
63      };
64  
65      /** Common Name (CN) OID. */
66      public static final String CN_OID = "2.5.4.3";
67  
68      /** RFC 2459 Other Subject Alt Name type. */
69      public static final Integer OTHER_ALT_NAME = new Integer(0);
70  
71      /** RFC 2459 RFC 822 (email address) Subject Alt Name type. */
72      public static final Integer RFC822_ALT_NAME = new Integer(1);
73  
74      /** RFC 2459 DNS Subject Alt Name type. */
75      public static final Integer DNS_ALT_NAME = new Integer(2);
76  
77      /** RFC 2459 X.400 Address Subject Alt Name type. */
78      public static final Integer X400ADDRESS_ALT_NAME = new Integer(3);
79  
80      /** RFC 2459 Directory Name Subject Alt Name type. */
81      public static final Integer DIRECTORY_ALT_NAME = new Integer(4);
82  
83      /** RFC 2459 EDI Party Name Subject Alt Name type. */
84      public static final Integer EDI_PARTY_ALT_NAME = new Integer(5);
85  
86      /** RFC 2459 URI Subject Alt Name type. */
87      public static final Integer URI_ALT_NAME = new Integer(6);
88  
89      /** RFC 2459 IP Address Subject Alt Name type. */
90      public static final Integer IP_ADDRESS_ALT_NAME = new Integer(7);
91  
92      /** RFC 2459 Registered ID Subject Alt Name type. */
93      public static final Integer REGISTERED_ID_ALT_NAME = new Integer(8);
94  
95      /** Constructed. */
96      protected X509Util() {
97  
98      }
99      
100     /**
101      * Determines the certificate, from the collection, associated with the private key.
102      * 
103      * @param certs certificates to check
104      * @param privateKey entity's private key
105      * 
106      * @return the certificate associated with entity's private key or null if not certificate in the collection is
107      *         associated with the given private key
108      * 
109      * @throws SecurityException thrown if the public or private keys checked are of an unsupported type
110      * 
111      * @since 1.2
112      */
113     public static X509Certificate determineEntityCertificate(Collection<X509Certificate> certs, PrivateKey privateKey)
114             throws SecurityException {
115         if (certs == null || privateKey == null) {
116             return null;
117         }
118 
119         for (X509Certificate certificate : certs) {
120             if (SecurityHelper.matchKeyPair(certificate.getPublicKey(), privateKey)) {
121                 return certificate;
122             }
123         }
124 
125         return null;
126     }
127 
128     /**
129      * Gets the commons names that appear within the given distinguished name. The returned list provides the names in
130      * the order they appeared in the DN.
131      * 
132      * @param dn the DN to extract the common names from
133      * 
134      * @return the common names that appear in the DN in the order they appear or null if the given DN is null
135      */
136     public static List<String> getCommonNames(X500Principal dn) {
137         Logger log = getLogger();
138         if (dn == null) {
139             return null;
140         }
141 
142         log.debug("Extracting CNs from the following DN: {}", dn.toString());
143         List<String> commonNames = new LinkedList<String>();
144         try {
145             ASN1InputStream asn1Stream = new ASN1InputStream(dn.getEncoded());
146             DERObject parent = asn1Stream.readObject();
147 
148             String cn = null;
149             DERObject dnComponent;
150             DERSequence grandChild;
151             DERObjectIdentifier componentId;
152             for (int i = 0; i < ((DERSequence) parent).size(); i++) {
153                 dnComponent = ((DERSequence) parent).getObjectAt(i).getDERObject();
154                 if (!(dnComponent instanceof DERSet)) {
155                     log.debug("No DN components.");
156                     continue;
157                 }
158 
159                 // Each DN component is a set
160                 for (int j = 0; j < ((DERSet) dnComponent).size(); j++) {
161                     grandChild = (DERSequence) ((DERSet) dnComponent).getObjectAt(j).getDERObject();
162 
163                     if (grandChild.getObjectAt(0) != null
164                             && grandChild.getObjectAt(0).getDERObject() instanceof DERObjectIdentifier) {
165                         componentId = (DERObjectIdentifier) grandChild.getObjectAt(0).getDERObject();
166 
167                         if (CN_OID.equals(componentId.getId())) {
168                             // OK, this dn component is actually a cn attribute
169                             if (grandChild.getObjectAt(1) != null
170                                     && grandChild.getObjectAt(1).getDERObject() instanceof DERString) {
171                                 cn = ((DERString) grandChild.getObjectAt(1).getDERObject()).getString();
172                                 commonNames.add(cn);
173                             }
174                         }
175                     }
176                 }
177             }
178 
179             asn1Stream.close();
180 
181             return commonNames;
182 
183         } catch (IOException e) {
184             log.error("Unable to extract common names from DN: ASN.1 parsing failed: " + e);
185             return null;
186         }
187     }
188 
189     /**
190      * Gets the list of alternative names of a given name type.
191      * 
192      * @param certificate the certificate to extract the alternative names from
193      * @param nameTypes the name types
194      * 
195      * @return the alt names, of the given type, within the cert
196      */
197     public static List getAltNames(X509Certificate certificate, Integer[] nameTypes) {
198         Logger log = getLogger();
199         if (certificate == null) {
200             return null;
201         }
202 
203         List<Object> names = new LinkedList<Object>();
204         Collection<List<?>> altNames = null;
205         try {
206             altNames = X509ExtensionUtil.getSubjectAlternativeNames(certificate);
207         } catch (CertificateParsingException e) {
208             log.error("Encountered an problem trying to extract Subject Alternate "
209                     + "Name from supplied certificate: " + e);
210             return names;
211         }
212 
213         if (altNames != null) {
214             // 0th position represents the alt name type
215             // 1st position contains the alt name data
216             for (List altName : altNames) {
217                 for (Integer nameType : nameTypes) {
218                     if (altName.get(0).equals(nameType)) {
219                         names.add(convertAltNameType(nameType, altName.get(1)));
220                         break;
221                     }
222                 }
223             }
224         }
225 
226         return names;
227     }
228 
229     /**
230      * Gets the common name components of the issuer and all the subject alt names of a given type.
231      * 
232      * @param certificate certificate to extract names from
233      * @param altNameTypes type of alt names to extract
234      * 
235      * @return list of subject names in the certificate
236      */
237     @SuppressWarnings("unchecked")
238     public static List getSubjectNames(X509Certificate certificate, Integer[] altNameTypes) {
239         List issuerNames = new LinkedList();
240 
241         List<String> entityCertCNs = X509Util.getCommonNames(certificate.getSubjectX500Principal());
242         issuerNames.add(entityCertCNs.get(0));
243         issuerNames.addAll(X509Util.getAltNames(certificate, altNameTypes));
244 
245         return issuerNames;
246     }
247 
248     /**
249      * Get the plain (non-DER encoded) value of the Subject Key Identifier extension of an X.509 certificate, if
250      * present.
251      * 
252      * @param certificate an X.509 certificate possibly containing a subject key identifier
253      * @return the plain (non-DER encoded) value of the Subject Key Identifier extension, or null if the certificate
254      *         does not contain the extension
255      * @throws IOException
256      */
257     public static byte[] getSubjectKeyIdentifier(X509Certificate certificate) {
258         Logger log = getLogger();
259         byte[] derValue = certificate.getExtensionValue(X509Extensions.SubjectKeyIdentifier.getId());
260         if (derValue == null || derValue.length == 0) {
261             return null;
262         }
263 
264         SubjectKeyIdentifier ski = null;
265         try {
266             ski = new SubjectKeyIdentifierStructure(derValue);
267         } catch (IOException e) {
268             log.error("Unable to extract subject key identifier from certificate: ASN.1 parsing failed: " + e);
269             return null;
270         }
271 
272         if (ski != null) {
273             return ski.getKeyIdentifier();
274         } else {
275             return null;
276         }
277     }
278     
279     /**
280      * Decodes X.509 certificates in DER or PEM format.
281      * 
282      * @param certs encoded certs
283      * 
284      * @return decoded certs
285      * 
286      * @throws CertificateException thrown if the certificates can not be decoded
287      * 
288      * @since 1.2
289      */
290     public static Collection<X509Certificate> decodeCertificate(File certs) throws CertificateException{
291         if(!certs.exists()){
292             throw new CertificateException("Certificate file " + certs.getAbsolutePath() + " does not exist");
293         }
294         
295         if(!certs.canRead()){
296             throw new CertificateException("Certificate file " + certs.getAbsolutePath() + " is not readable");
297         }
298         
299         try{
300             return decodeCertificate(DatatypeHelper.fileToByteArray(certs));
301         }catch(IOException e){
302             throw new CertificateException("Error reading certificate file " + certs.getAbsolutePath(), e);
303         }
304     }
305 
306     /**
307      * Decodes X.509 certificates in DER or PEM format.
308      * 
309      * @param certs encoded certs
310      * 
311      * @return decoded certs
312      * 
313      * @throws CertificateException thrown if the certificates can not be decoded
314      */
315     @SuppressWarnings("unchecked")
316     public static Collection<X509Certificate> decodeCertificate(byte[] certs) throws CertificateException {
317         try {
318             TrustMaterial tm = new TrustMaterial(certs);
319             return tm.getCertificates();
320         } catch (Exception e) {
321             throw new CertificateException("Unable to decode X.509 certificates", e);
322         }
323     }
324     
325     /**
326      * Decodes CRLS in DER or PKCS#7 format. If in PKCS#7 format only the CRLs are decode, the rest of the content is
327      * ignored.
328      * 
329      * @param crls encoded CRLs
330      * 
331      * @return decoded CRLs
332      * 
333      * @throws CRLException thrown if the CRLs can not be decoded
334      * 
335      * @since 1.2
336      */
337     public static Collection<X509CRL> decodeCRLs(File crls) throws CRLException{
338         if(!crls.exists()){
339             throw new CRLException("CRL file " + crls.getAbsolutePath() + " does not exist");
340         }
341         
342         if(!crls.canRead()){
343             throw new CRLException("CRL file " + crls.getAbsolutePath() + " is not readable");
344         }
345         
346         try{
347             return decodeCRLs(DatatypeHelper.fileToByteArray(crls));
348         }catch(IOException e){
349             throw new CRLException("Error reading CRL file " + crls.getAbsolutePath(), e);
350         }
351     }
352 
353     /**
354      * Decodes CRLS in DER or PKCS#7 format. If in PKCS#7 format only the CRLs are decode, the rest of the content is
355      * ignored.
356      * 
357      * @param crls encoded CRLs
358      * 
359      * @return decoded CRLs
360      * 
361      * @throws CRLException thrown if the CRLs can not be decoded
362      */
363     @SuppressWarnings("unchecked")
364     public static Collection<X509CRL> decodeCRLs(byte[] crls) throws CRLException {
365         try {
366             CertificateFactory cf = CertificateFactory.getInstance("X.509");
367             return (Collection<X509CRL>) cf.generateCRLs(new ByteArrayInputStream(crls));
368         } catch (GeneralSecurityException e) {
369             throw new CRLException("Unable to decode X.509 certificates");
370         }
371     }
372 
373     /**
374      * Gets a formatted string representing identifier information from the supplied credential.
375      * 
376      * <p>
377      * This could for example be used in logging messages.
378      * </p>
379      * 
380      * <p>
381      * Often it will be the case that a given credential that is being evaluated will NOT have a value for the entity ID
382      * property. So extract the certificate subject DN, and if present, the credential's entity ID.
383      * </p>
384      * 
385      * @param credential the credential for which to produce a token.
386      * @param handler the X.500 DN handler to use. If null, a new instance of {@link InternalX500DNHandler} will be
387      *            used.
388      * 
389      * @return a formatted string containing identifier information present in the credential
390      */
391     public static String getIdentifiersToken(X509Credential credential, X500DNHandler handler) {
392         X500DNHandler x500DNHandler;
393         if (handler != null) {
394             x500DNHandler = handler;
395         } else {
396             x500DNHandler = new InternalX500DNHandler();
397         }
398         X500Principal x500Principal = credential.getEntityCertificate().getSubjectX500Principal();
399         StringBuilder builder = new StringBuilder();
400         builder.append('[');
401         builder.append(String.format("subjectName='%s'", x500DNHandler.getName(x500Principal)));
402         if (!DatatypeHelper.isEmpty(credential.getEntityId())) {
403             builder.append(String.format(" |credential entityID='%s'", DatatypeHelper.safeTrimOrNullString(credential
404                     .getEntityId())));
405         }
406         builder.append(']');
407         return builder.toString();
408     }
409 
410     /**
411      * Convert types returned by Bouncy Castle X509ExtensionUtil.getSubjectAlternativeNames(X509Certificate) to be
412      * consistent with what is documented for: java.security.cert.X509Certificate#getSubjectAlternativeNames.
413      * 
414      * @param nameType the alt name type
415      * @param nameValue the alt name value
416      * @return converted representation of name value, based on type
417      */
418     private static Object convertAltNameType(Integer nameType, Object nameValue) {
419         Logger log = getLogger();
420         if (DIRECTORY_ALT_NAME.equals(nameType) || DNS_ALT_NAME.equals(nameType) || RFC822_ALT_NAME.equals(nameType)
421                 || URI_ALT_NAME.equals(nameType) || REGISTERED_ID_ALT_NAME.equals(nameType)) {
422 
423             // these are just strings in the appropriate format already, return as-is
424             return nameValue;
425         }
426 
427         if (IP_ADDRESS_ALT_NAME.equals(nameType)) {
428             // this is a byte[], IP addr in network byte order
429             return IPAddressHelper.addressToString((byte[]) nameValue);
430         }
431 
432         if (EDI_PARTY_ALT_NAME.equals(nameType) || X400ADDRESS_ALT_NAME.equals(nameType)
433                 || OTHER_ALT_NAME.equals(nameType)) {
434 
435             // these have no defined representation, just return a DER-encoded byte[]
436             return ((DERObject) nameValue).getDEREncoded();
437         }
438 
439         log.warn("Encountered unknown alt name type '{}', adding as-is", nameType);
440         return nameValue;
441     }
442     
443     /**
444      * Get an SLF4J Logger.
445      * 
446      * @return a Logger instance
447      */
448     private static Logger getLogger() {
449         return LoggerFactory.getLogger(X509Util.class);
450     }
451 }