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.IOException;
21  import java.security.GeneralSecurityException;
22  import java.security.cert.CRLException;
23  import java.security.cert.CertificateException;
24  import java.security.cert.CertificateFactory;
25  import java.security.cert.CertificateParsingException;
26  import java.security.cert.X509CRL;
27  import java.security.cert.X509Certificate;
28  import java.util.Collection;
29  import java.util.LinkedList;
30  import java.util.List;
31  
32  import javax.security.auth.x500.X500Principal;
33  
34  import org.apache.commons.ssl.TrustMaterial;
35  import org.bouncycastle.asn1.ASN1InputStream;
36  import org.bouncycastle.asn1.DERObject;
37  import org.bouncycastle.asn1.DERObjectIdentifier;
38  import org.bouncycastle.asn1.DERSequence;
39  import org.bouncycastle.asn1.DERSet;
40  import org.bouncycastle.asn1.DERString;
41  import org.bouncycastle.asn1.x509.SubjectKeyIdentifier;
42  import org.bouncycastle.asn1.x509.X509Extensions;
43  import org.bouncycastle.x509.extension.SubjectKeyIdentifierStructure;
44  import org.bouncycastle.x509.extension.X509ExtensionUtil;
45  import org.opensaml.xml.util.DatatypeHelper;
46  import org.opensaml.xml.util.IPAddressHelper;
47  import org.slf4j.Logger;
48  import org.slf4j.LoggerFactory;
49  
50  /**
51   * Utility class for working with X509 objects.
52   */
53  public class X509Util {
54  
55      /** Encoding used to store a key or certificate in a file. */
56      public static enum ENCODING_FORMAT {
57          PEM, DER
58      };
59  
60      /** Common Name (CN) OID. */
61      public static final String CN_OID = "2.5.4.3";
62  
63      /** RFC 2459 Other Subject Alt Name type. */
64      public static final Integer OTHER_ALT_NAME = new Integer(0);
65  
66      /** RFC 2459 RFC 822 (email address) Subject Alt Name type. */
67      public static final Integer RFC822_ALT_NAME = new Integer(1);
68  
69      /** RFC 2459 DNS Subject Alt Name type. */
70      public static final Integer DNS_ALT_NAME = new Integer(2);
71  
72      /** RFC 2459 X.400 Address Subject Alt Name type. */
73      public static final Integer X400ADDRESS_ALT_NAME = new Integer(3);
74  
75      /** RFC 2459 Directory Name Subject Alt Name type. */
76      public static final Integer DIRECTORY_ALT_NAME = new Integer(4);
77  
78      /** RFC 2459 EDI Party Name Subject Alt Name type. */
79      public static final Integer EDI_PARTY_ALT_NAME = new Integer(5);
80  
81      /** RFC 2459 URI Subject Alt Name type. */
82      public static final Integer URI_ALT_NAME = new Integer(6);
83  
84      /** RFC 2459 IP Address Subject Alt Name type. */
85      public static final Integer IP_ADDRESS_ALT_NAME = new Integer(7);
86  
87      /** RFC 2459 Registered ID Subject Alt Name type. */
88      public static final Integer REGISTERED_ID_ALT_NAME = new Integer(8);
89  
90      /** Class logger. */
91      private static Logger log = LoggerFactory.getLogger(X509Util.class);
92  
93      /** Constructed. */
94      protected X509Util() {
95  
96      }
97  
98      /**
99       * Gets the commons names that appear within the given distinguished name. The returned list provides the names in
100      * the order they appeared in the DN.
101      * 
102      * @param dn the DN to extract the common names from
103      * 
104      * @return the common names that appear in the DN in the order they appear or null if the given DN is null
105      */
106     public static List<String> getCommonNames(X500Principal dn) {
107         if (dn == null) {
108             return null;
109         }
110 
111         log.debug("Extracting CNs from the following DN: {}", dn.toString());
112         List<String> commonNames = new LinkedList<String>();
113         try {
114             ASN1InputStream asn1Stream = new ASN1InputStream(dn.getEncoded());
115             DERObject parent = asn1Stream.readObject();
116 
117             String cn = null;
118             DERObject dnComponent;
119             DERSequence grandChild;
120             DERObjectIdentifier componentId;
121             for (int i = 0; i < ((DERSequence) parent).size(); i++) {
122                 dnComponent = ((DERSequence) parent).getObjectAt(i).getDERObject();
123                 if (!(dnComponent instanceof DERSet)) {
124                     log.debug("No DN components.");
125                     continue;
126                 }
127 
128                 // Each DN component is a set
129                 for (int j = 0; j < ((DERSet) dnComponent).size(); j++) {
130                     grandChild = (DERSequence) ((DERSet) dnComponent).getObjectAt(j).getDERObject();
131 
132                     if (grandChild.getObjectAt(0) != null
133                             && grandChild.getObjectAt(0).getDERObject() instanceof DERObjectIdentifier) {
134                         componentId = (DERObjectIdentifier) grandChild.getObjectAt(0).getDERObject();
135 
136                         if (CN_OID.equals(componentId.getId())) {
137                             // OK, this dn component is actually a cn attribute
138                             if (grandChild.getObjectAt(1) != null
139                                     && grandChild.getObjectAt(1).getDERObject() instanceof DERString) {
140                                 cn = ((DERString) grandChild.getObjectAt(1).getDERObject()).getString();
141                                 commonNames.add(cn);
142                             }
143                         }
144                     }
145                 }
146             }
147 
148             asn1Stream.close();
149 
150             return commonNames;
151 
152         } catch (IOException e) {
153             log.error("Unable to extract common names from DN: ASN.1 parsing failed: " + e);
154             return null;
155         }
156     }
157 
158     /**
159      * Gets the list of alternative names of a given name type.
160      * 
161      * @param certificate the certificate to extract the alternative names from
162      * @param nameTypes the name types
163      * 
164      * @return the alt names, of the given type, within the cert
165      */
166     public static List getAltNames(X509Certificate certificate, Integer[] nameTypes) {
167         if (certificate == null) {
168             return null;
169         }
170 
171         List<Object> names = new LinkedList<Object>();
172         Collection<List<?>> altNames = null;
173         try {
174             altNames = X509ExtensionUtil.getSubjectAlternativeNames(certificate);
175         } catch (CertificateParsingException e) {
176             log.error("Encountered an problem trying to extract Subject Alternate "
177                     + "Name from supplied certificate: " + e);
178             return names;
179         }
180         
181         if (altNames != null) {
182             // 0th position represents the alt name type
183             // 1st position contains the alt name data
184             for (List altName : altNames) {
185                 for (Integer nameType : nameTypes) {
186                     if (altName.get(0).equals(nameType)) {
187                         names.add( convertAltNameType(nameType, altName.get(1)) );
188                         break;
189                     }
190                 }
191             }
192         }
193 
194         return names;
195     }
196     
197 
198     /**
199      * Gets the common name components of the issuer and all the subject alt names of a given type.
200      * 
201      * @param certificate certificate to extract names from
202      * @param altNameTypes type of alt names to extract
203      * 
204      * @return list of subject names in the certificate
205      */
206     @SuppressWarnings("unchecked")
207     public static List getSubjectNames(X509Certificate certificate, Integer[] altNameTypes) {
208         List issuerNames = new LinkedList();
209 
210         List<String> entityCertCNs = X509Util.getCommonNames(certificate.getSubjectX500Principal());
211         issuerNames.add(entityCertCNs.get(0));
212         issuerNames.addAll(X509Util.getAltNames(certificate, altNameTypes));
213 
214         return issuerNames;
215     }
216 
217     /**
218      * Get the plain (non-DER encoded) value of the Subject Key Identifier extension of an X.509 certificate, if
219      * present.
220      * 
221      * @param certificate an X.509 certificate possibly containing a subject key identifier
222      * @return the plain (non-DER encoded) value of the Subject Key Identifier extension, or null if the certificate
223      *         does not contain the extension
224      * @throws IOException
225      */
226     public static byte[] getSubjectKeyIdentifier(X509Certificate certificate) {
227         byte[] derValue = certificate.getExtensionValue(X509Extensions.SubjectKeyIdentifier.getId());
228         if (derValue == null || derValue.length == 0) {
229             return null;
230         }
231 
232         SubjectKeyIdentifier ski = null;
233         try {
234             ski = new SubjectKeyIdentifierStructure(derValue);
235         } catch (IOException e) {
236             log.error("Unable to extract subject key identifier from certificate: ASN.1 parsing failed: " + e);
237             return null;
238         }
239 
240         if (ski != null) {
241             return ski.getKeyIdentifier();
242         } else {
243             return null;
244         }
245     }
246 
247     /**
248      * Decodes X.509 certificates in DER or PEM format.
249      * 
250      * @param certs encoded certs
251      * 
252      * @return decoded certs
253      * 
254      * @throws CertificateException thrown if the certificates can not be decoded
255      */
256     @SuppressWarnings("unchecked")
257     public static Collection<X509Certificate> decodeCertificate(byte[] certs) throws CertificateException {
258         try {
259             TrustMaterial tm = new TrustMaterial(certs);
260             return tm.getCertificates();
261         } catch (Exception e) {
262             throw new CertificateException("Unable to decode X.509 certificates", e);
263         }
264     }
265 
266     /**
267      * Decodes CRLS in DER or PKCS#7 format. If in PKCS#7 format only the CRLs are decode, the rest of the content is
268      * ignored.
269      * 
270      * @param crls encoded CRLs
271      * 
272      * @return decoded CRLs
273      * 
274      * @throws CRLException thrown if the CRLs can not be decoded
275      */
276     @SuppressWarnings("unchecked")
277     public static Collection<X509CRL> decodeCRLs(byte[] crls) throws CRLException {
278         try {
279             CertificateFactory cf = CertificateFactory.getInstance("X.509");
280             return (Collection<X509CRL>) cf.generateCRLs(new ByteArrayInputStream(crls));
281         } catch (GeneralSecurityException e) {
282             throw new CRLException("Unable to decode X.509 certificates");
283         }
284     }
285 
286     /**
287      * Gets a formatted string representing identifier information from the supplied credential.
288      * 
289      * <p>This could for example be used in logging messages.</p>
290      * 
291      * <p>Often it will be the case that a given credential that is being evaluated will NOT have a value for the
292      * entity ID property. So extract the certificate subject DN, and if present, the credential's entity ID.</p>
293      * 
294      * @param credential the credential for which to produce a token.
295      * @param handler the X.500 DN handler to use.  If null, a new instance of 
296      *          {@link InternalX500DNHandler} will be used.
297      * 
298      * @return a formatted string containing identifier information present in the credential
299      */
300     public static String getIdentifiersToken(X509Credential credential, X500DNHandler handler) {
301         X500DNHandler x500DNHandler;
302         if (handler != null) {
303             x500DNHandler = handler;
304         } else {
305             x500DNHandler = new InternalX500DNHandler();
306         }
307         X500Principal x500Principal = credential.getEntityCertificate().getSubjectX500Principal();
308         StringBuilder builder = new StringBuilder();
309         builder.append('[');
310         builder.append(String.format("subjectName='%s'", x500DNHandler.getName(x500Principal)));
311         if (!DatatypeHelper.isEmpty(credential.getEntityId())) {
312             builder.append(String.format(" |credential entityID='%s'", DatatypeHelper.safeTrimOrNullString(credential
313                     .getEntityId())));
314         }
315         builder.append(']');
316         return builder.toString();
317     }
318 
319     /**
320      * Convert types returned by Bouncy Castle X509ExtensionUtil.getSubjectAlternativeNames(X509Certificate)
321      * to be consistent with what is documented for: 
322      * java.security.cert.X509Certificate#getSubjectAlternativeNames.
323      * 
324      * @param nameType the alt name type 
325      * @param nameValue the alt name value
326      * @return converted representation of name value, based on type
327      */
328     private static Object convertAltNameType(Integer nameType, Object nameValue) {
329         if (DIRECTORY_ALT_NAME.equals(nameType) ||
330             DNS_ALT_NAME.equals(nameType) ||
331             RFC822_ALT_NAME.equals(nameType) ||
332             URI_ALT_NAME.equals(nameType) ||
333             REGISTERED_ID_ALT_NAME.equals(nameType) ) {
334             
335             // these are just strings in the appropriate format already, return as-is
336             return nameValue;
337         } 
338         
339         if (IP_ADDRESS_ALT_NAME.equals(nameType)) {
340             // this is a byte[], IP addr in network byte order
341             return IPAddressHelper.addressToString((byte[]) nameValue);
342         } 
343         
344         if (EDI_PARTY_ALT_NAME.equals(nameType) ||
345             X400ADDRESS_ALT_NAME.equals(nameType) ||
346             OTHER_ALT_NAME.equals(nameType)) {
347             
348             // these have no defined representation, just return a DER-encoded byte[]
349             return ((DERObject)nameValue).getDEREncoded();
350         } 
351         
352         log.warn("Encountered unknown alt name type '{}', adding as-is", nameType);
353         return nameValue;
354     }
355 
356 }