001: /**
002: * $RCSfile$
003: * $Revision: $
004: * $Date: $
005: *
006: * Copyright (C) 2006 Jive Software. All rights reserved.
007: *
008: * This software is published under the terms of the GNU Public License (GPL),
009: * a copy of which is included in this distribution.
010: */package org.jivesoftware.util;
011:
012: import org.bouncycastle.asn1.*;
013: import org.bouncycastle.asn1.x509.GeneralName;
014: import org.bouncycastle.asn1.x509.GeneralNames;
015: import org.bouncycastle.asn1.x509.X509Extensions;
016: import org.bouncycastle.asn1.x509.X509Name;
017: import org.bouncycastle.jce.PKCS10CertificationRequest;
018: import org.bouncycastle.jce.provider.BouncyCastleProvider;
019: import org.bouncycastle.openssl.PEMReader;
020: import org.bouncycastle.openssl.PasswordFinder;
021: import org.bouncycastle.x509.X509V3CertificateGenerator;
022:
023: import java.io.*;
024: import java.math.BigInteger;
025: import java.security.*;
026: import java.security.cert.Certificate;
027: import java.security.cert.CertificateFactory;
028: import java.security.cert.CertificateParsingException;
029: import java.security.cert.X509Certificate;
030: import java.util.*;
031: import java.util.LinkedList;
032: import java.util.concurrent.CopyOnWriteArrayList;
033: import java.util.regex.Matcher;
034: import java.util.regex.Pattern;
035:
036: /**
037: * Utility class that provides similar functionality to the keytool tool. Generated certificates
038: * conform to the XMPP spec where domains are kept in the subject alternative names extension.
039: *
040: * @author Gaston Dombiak
041: */
042: public class CertificateManager {
043:
044: private static Pattern cnPattern = Pattern
045: .compile("(?i)(cn=)([^,]*)");
046: private static Pattern valuesPattern = Pattern
047: .compile("(?i)(=)([^,]*)");
048:
049: private static Provider provider = new BouncyCastleProvider();
050:
051: /**
052: * The maximum length of lines in certification requests
053: */
054: private static final int CERT_REQ_LINE_LENGTH = 76;
055:
056: private static List<CertificateEventListener> listeners = new CopyOnWriteArrayList<CertificateEventListener>();
057:
058: static {
059: // Add the BC provider to the list of security providers
060: Security.addProvider(provider);
061: }
062:
063: /**
064: * Creates a new X509 certificate using the DSA algorithm. The new certificate together with its private
065: * key are stored in the specified key store. However, the key store is not saved to the disk. This means
066: * that it is up to the "caller" to save the key store to disk after new certificates have been added
067: * to the store.
068: *
069: * @param ksKeys key store where the new certificate and private key are going to be stored.
070: * @param keyPassword password of the keystore.
071: * @param alias name to use when storing the certificate in the key store.
072: * @param issuerDN Issuer string e.g "O=Grid,OU=OGSA,CN=ACME"
073: * @param subjectDN Subject string e.g "O=Grid,OU=OGSA,CN=John Doe"
074: * @param domain domain of the server to store in the subject alternative name extension.
075: * @return the new X509 V3 Certificate.
076: * @throws GeneralSecurityException
077: * @throws IOException
078: */
079: public static X509Certificate createDSACert(KeyStore ksKeys,
080: String keyPassword, String alias, String issuerDN,
081: String subjectDN, String domain)
082: throws GeneralSecurityException, IOException {
083: // Generate public and private keys
084: KeyPair keyPair = generateKeyPair("DSA", 1024);
085: // Create X509 certificate with keys and specified domain
086: X509Certificate cert = createX509V3Certificate(keyPair, 60,
087: issuerDN, subjectDN, domain, "SHA1withDSA");
088: // Store new certificate and private key in the keystore
089: ksKeys.setKeyEntry(alias, keyPair.getPrivate(), keyPassword
090: .toCharArray(), new X509Certificate[] { cert });
091: // Notify listeners that a new certificate has been created
092: for (CertificateEventListener listener : listeners) {
093: try {
094: listener.certificateCreated(ksKeys, alias, cert);
095: } catch (Exception e) {
096: Log.error(e);
097: }
098: }
099: // Return new certificate
100: return cert;
101: }
102:
103: /**
104: * Creates a new X509 certificate using the RSA algorithm. The new certificate together with its private
105: * key are stored in the specified key store. However, the key store is not saved to the disk. This means
106: * that it is up to the "caller" to save the key store to disk after new certificates have been added
107: * to the store.
108: *
109: * @param ksKeys key store where the new certificate and private key are going to be stored.
110: * @param keyPassword password of the keystore.
111: * @param alias name to use when storing the certificate in the key store.
112: * @param issuerDN Issuer string e.g "O=Grid,OU=OGSA,CN=ACME"
113: * @param subjectDN Subject string e.g "O=Grid,OU=OGSA,CN=John Doe"
114: * @param domain domain of the server to store in the subject alternative name extension.
115: * @return the new X509 V3 Certificate.
116: * @throws GeneralSecurityException
117: * @throws IOException
118: */
119: public static X509Certificate createRSACert(KeyStore ksKeys,
120: String keyPassword, String alias, String issuerDN,
121: String subjectDN, String domain)
122: throws GeneralSecurityException, IOException {
123: // Generate public and private keys
124: KeyPair keyPair = generateKeyPair("RSA", 1024);
125: // Create X509 certificate with keys and specified domain
126: X509Certificate cert = createX509V3Certificate(keyPair, 60,
127: issuerDN, subjectDN, domain, "MD5withRSA");
128: // Store new certificate and private key in the keystore
129: ksKeys.setKeyEntry(alias, keyPair.getPrivate(), keyPassword
130: .toCharArray(), new X509Certificate[] { cert });
131: // Notify listeners that a new certificate has been created
132: for (CertificateEventListener listener : listeners) {
133: try {
134: listener.certificateCreated(ksKeys, alias, cert);
135: } catch (Exception e) {
136: Log.error(e);
137: }
138: }
139: // Return new certificate
140: return cert;
141: }
142:
143: /**
144: * Deletes the specified certificate from the
145: *
146: * @param ksKeys key store where the certificate is stored.
147: * @param alias alias of the certificate to delete.
148: * @throws GeneralSecurityException
149: * @throws IOException
150: */
151: public static void deleteCertificate(KeyStore ksKeys, String alias)
152: throws GeneralSecurityException, IOException {
153: ksKeys.deleteEntry(alias);
154: // Notify listeners that a new certificate has been created
155: for (CertificateEventListener listener : listeners) {
156: try {
157: listener.certificateDeleted(ksKeys, alias);
158: } catch (Exception e) {
159: Log.error(e);
160: }
161: }
162: }
163:
164: /**
165: * Returns the identities of the remote server as defined in the specified certificate. The
166: * identities are defined in the subjectDN of the certificate and it can also be defined in
167: * the subjectAltName extensions of type "xmpp". When the extension is being used then the
168: * identities defined in the extension are going to be returned. Otherwise, the value stored in
169: * the subjectDN is returned.
170: *
171: * @param x509Certificate the certificate the holds the identities of the remote server.
172: * @return the identities of the remote server as defined in the specified certificate.
173: */
174: public static List<String> getPeerIdentities(
175: X509Certificate x509Certificate) {
176: // Look the identity in the subjectAltName extension if available
177: List<String> names = getSubjectAlternativeNames(x509Certificate);
178: if (names.isEmpty()) {
179: String name = x509Certificate.getSubjectDN().getName();
180: Matcher matcher = cnPattern.matcher(name);
181: if (matcher.find()) {
182: name = matcher.group(2);
183: }
184: // Create an array with the unique identity
185: names = new ArrayList<String>();
186: names.add(name);
187: }
188: return names;
189: }
190:
191: /**
192: * Returns the JID representation of an XMPP entity contained as a SubjectAltName extension
193: * in the certificate. If none was found then return <tt>null</tt>.
194: *
195: * @param certificate the certificate presented by the remote entity.
196: * @return the JID representation of an XMPP entity contained as a SubjectAltName extension
197: * in the certificate. If none was found then return <tt>null</tt>.
198: */
199: private static List<String> getSubjectAlternativeNames(
200: X509Certificate certificate) {
201: List<String> identities = new ArrayList<String>();
202: try {
203: Collection<List<?>> altNames = certificate
204: .getSubjectAlternativeNames();
205: // Check that the certificate includes the SubjectAltName extension
206: if (altNames == null) {
207: return Collections.emptyList();
208: }
209: // Use the type OtherName to search for the certified server name
210: for (List item : altNames) {
211: Integer type = (Integer) item.get(0);
212: if (type == 0) {
213: // Type OtherName found so return the associated value
214: try {
215: // Value is encoded using ASN.1 so decode it to get the server's identity
216: ASN1InputStream decoder = new ASN1InputStream(
217: (byte[]) item.toArray()[1]);
218: DEREncodable encoded = decoder.readObject();
219: encoded = ((DERSequence) encoded)
220: .getObjectAt(1);
221: encoded = ((DERTaggedObject) encoded)
222: .getObject();
223: encoded = ((DERTaggedObject) encoded)
224: .getObject();
225: String identity = ((DERUTF8String) encoded)
226: .getString();
227: if (!"".equals(identity)) {
228: // Add the decoded server name to the list of identities
229: identities.add(identity);
230: }
231: } catch (UnsupportedEncodingException e) {
232: // Ignore
233: } catch (IOException e) {
234: // Ignore
235: } catch (Exception e) {
236: Log
237: .error(
238: "CertificateManager: Error decoding subjectAltName",
239: e);
240: }
241: }
242: // Other types are not good for XMPP so ignore them
243: else if (Log.isDebugEnabled()) {
244: Log
245: .debug("CertificateManager: SubjectAltName of invalid type found: "
246: + certificate.getSubjectDN());
247: }
248: }
249: } catch (CertificateParsingException e) {
250: Log.error(
251: "CertificateManager: Error parsing SubjectAltName in certificate: "
252: + certificate.getSubjectDN(), e);
253: }
254: return identities;
255: }
256:
257: /**
258: * Returns true if an RSA certificate was found in the specified keystore for the specified domain.
259: *
260: * @param ksKeys the keystore that contains the certificates.
261: * @param domain domain of the server signed by the certificate.
262: * @return true if an RSA certificate was found in the specified keystore for the specified domain.
263: * @throws KeyStoreException
264: */
265: public static boolean isRSACertificate(KeyStore ksKeys,
266: String domain) throws KeyStoreException {
267: return isCertificate(ksKeys, domain, "RSA");
268: }
269:
270: /**
271: * Returns true if an DSA certificate was found in the specified keystore for the specified domain.
272: *
273: * @param ksKeys the keystore that contains the certificates.
274: * @param domain domain of the server signed by the certificate.
275: * @return true if an DSA certificate was found in the specified keystore for the specified domain.
276: * @throws KeyStoreException
277: */
278: public static boolean isDSACertificate(KeyStore ksKeys,
279: String domain) throws KeyStoreException {
280: return isCertificate(ksKeys, domain, "DSA");
281: }
282:
283: /**
284: * Returns true if the specified certificate is using the DSA algorithm. The DSA algorithm is not
285: * good for encryption but only for authentication. On the other hand, the RSA algorithm is good
286: * for encryption and authentication.
287: *
288: * @param certificate the certificate to analyze.
289: * @return true if the specified certificate is using the DSA algorithm.
290: * @throws KeyStoreException
291: */
292: public static boolean isDSACertificate(X509Certificate certificate)
293: throws KeyStoreException {
294: return certificate.getPublicKey().getAlgorithm().equals("DSA");
295: }
296:
297: /**
298: * Returns true if a certificate with the specifed configuration was found in the key store.
299: *
300: * @param ksKeys the keystore to use for searching the certificate.
301: * @param domain the domain present in the subjectAltName or "*" if anything is accepted.
302: * @param algorithm the DSA or RSA algorithm used by the certificate.
303: * @return true if a certificate with the specifed configuration was found in the key store.
304: * @throws KeyStoreException
305: */
306: private static boolean isCertificate(KeyStore ksKeys,
307: String domain, String algorithm) throws KeyStoreException {
308: for (Enumeration<String> aliases = ksKeys.aliases(); aliases
309: .hasMoreElements();) {
310: X509Certificate certificate = (X509Certificate) ksKeys
311: .getCertificate(aliases.nextElement());
312: if ("*".equals(domain)) {
313: // Any domain certified by the certificate is accepted
314: if (certificate.getPublicKey().getAlgorithm().equals(
315: algorithm)) {
316: return true;
317: }
318: } else {
319: // Only accept certified domains that match the specified domain
320: for (String identity : getPeerIdentities(certificate)) {
321: if (identity.endsWith(domain)
322: && certificate.getPublicKey()
323: .getAlgorithm().equals(algorithm)) {
324: return true;
325: }
326: }
327: }
328: }
329: return false;
330: }
331:
332: /**
333: * Returns true if the specified certificate is a self-signed certificate.
334: *
335: * @param keyStore key store that holds the certificate to verify.
336: * @param alias alias of the certificate in the key store.
337: * @return true if the specified certificate is a self-signed certificate.
338: * @throws KeyStoreException if an error happens while usign the keystore
339: */
340: public static boolean isSelfSignedCertificate(KeyStore keyStore,
341: String alias) throws KeyStoreException {
342: // Get certificate chain
343: java.security.cert.Certificate[] certificateChain = keyStore
344: .getCertificateChain(alias);
345: // Verify that the chain is empty or was signed by himself
346: return certificateChain == null || certificateChain.length == 1;
347: }
348:
349: /**
350: * Returns true if the specified certificate is ready to be signed by a Certificate Authority. Self-signed
351: * certificates need to get their issuer information entered to be able to generate a Certificate
352: * Signing Request (CSR).
353: *
354: * @param keyStore key store that holds the certificate to verify.
355: * @param alias alias of the certificate in the key store.
356: * @return true if the specified certificate is ready to be signed by a Certificate Authority.
357: * @throws KeyStoreException if an error happens while usign the keystore
358: */
359: public static boolean isSigningRequestPending(KeyStore keyStore,
360: String alias) throws KeyStoreException {
361: // Verify that this is a self-signed certificate
362: if (!isSelfSignedCertificate(keyStore, alias)) {
363: return false;
364: }
365: // Verify that the issuer information has been entered
366: X509Certificate certificate = (X509Certificate) keyStore
367: .getCertificate(alias);
368: Matcher matcher = valuesPattern.matcher(certificate
369: .getIssuerDN().toString());
370: return matcher.find() && matcher.find();
371: }
372:
373: /**
374: * Creates and returns the content of a new singing request for the specified certificate. Signing
375: * requests are required by Certificate Authorities as part of their signing process. The signing request
376: * contains information about the certificate issuer, subject DN, subject alternative names and public key.
377: * Private keys are not included. After the Certificate Authority verified and signed the certificate a new
378: * certificate is going to be returned. Use {@link #installReply(java.security.KeyStore, java.security.KeyStore, String, String, java.io.InputStream, boolean, boolean)}
379: * to import the CA reply.
380: *
381: * @param cert the certificate to create a signing request.
382: * @param privKey the private key of the certificate.
383: * @return the content of a new singing request for the specified certificate.
384: * @throws Exception
385: */
386: public static String createSigningRequest(X509Certificate cert,
387: PrivateKey privKey) throws Exception {
388: StringBuilder sb = new StringBuilder();
389:
390: String subject = cert.getSubjectDN().getName();
391: X509Name xname = new X509Name(subject);
392:
393: PublicKey pubKey = cert.getPublicKey();
394:
395: String signatureAlgorithm = "DSA".equals(pubKey.getAlgorithm()) ? "SHA1withDSA"
396: : "MD5withRSA";
397:
398: PKCS10CertificationRequest csr = new PKCS10CertificationRequest(
399: signatureAlgorithm, xname, pubKey, null, privKey);
400:
401: ByteArrayOutputStream baos = new ByteArrayOutputStream();
402: DEROutputStream deros = new DEROutputStream(baos);
403: deros.writeObject(csr.getDERObject());
404: String sTmp = new String(org.bouncycastle.util.encoders.Base64
405: .encode(baos.toByteArray()));
406:
407: // Header
408: sb.append("-----BEGIN NEW CERTIFICATE REQUEST-----\n");
409:
410: // Add signing request content (base 64 encoded)
411: for (int iCnt = 0; iCnt < sTmp.length(); iCnt += CERT_REQ_LINE_LENGTH) {
412: int iLineLength;
413:
414: if ((iCnt + CERT_REQ_LINE_LENGTH) > sTmp.length()) {
415: iLineLength = sTmp.length() - iCnt;
416: } else {
417: iLineLength = CERT_REQ_LINE_LENGTH;
418: }
419:
420: sb.append(sTmp.substring(iCnt, iCnt + iLineLength)).append(
421: "\n");
422: }
423:
424: // Footer
425: sb.append("-----END NEW CERTIFICATE REQUEST-----\n");
426: return sb.toString();
427: }
428:
429: /**
430: * Installs the Certificate Authority reply returned as part of the signing request. The certificate
431: * being signed will get its certificate chain updated with the imported certificate(s). An exception
432: * will be thrown if the replied certificate does not match a local certificate or if the signing
433: * authority is not known by the server (i.e. keystore and truststore files). When <tt>trustCACerts</tt>
434: * is set to <tt>true</tt> then certificates present in the truststore file will be used to verify the
435: * identity of the entity signing the certificate. In case the reply is composed of more than one
436: * certificate then you can also specify if you want to verify that the root certificate in the chain
437: * can be trusted.
438: *
439: * @param keyStore key store where the certificate is stored.
440: * @param trustStore key store where ca certificates are stored.
441: * @param keyPassword password of the keystore.
442: * @param alias the alias of the existing certificate being signed.
443: * @param inputStream the stream containing the CA reply.
444: * @param trustCACerts true if certificates present in the truststore file will be used to verify the
445: * identity of the entity signing the certificate.
446: * @param validateRoot true if you want to verify that the root certificate in the chain can be trusted
447: * based on the truststore.
448: * @return true if the CA reply was successfully processed.
449: * @throws Exception
450: */
451: public static boolean installReply(KeyStore keyStore,
452: KeyStore trustStore, String keyPassword, String alias,
453: InputStream inputStream, boolean trustCACerts,
454: boolean validateRoot) throws Exception {
455:
456: // Check that there is a certificate for the specified alias
457: X509Certificate certificate = (X509Certificate) keyStore
458: .getCertificate(alias);
459: if (certificate == null) {
460: Log.warn("Certificate not found for alias: " + alias);
461: return false;
462: }
463: // Retrieve the private key of the stored certificate
464: PrivateKey privKey = (PrivateKey) keyStore.getKey(alias,
465: keyPassword.toCharArray());
466: // Load certificates found in the PEM input stream
467: List<X509Certificate> certs = new ArrayList<X509Certificate>();
468: for (Certificate cert : CertificateFactory.getInstance("X509")
469: .generateCertificates(inputStream)) {
470: certs.add((X509Certificate) cert);
471: }
472: if (certs.isEmpty()) {
473: throw new Exception("Reply has no certificates");
474: }
475: List<X509Certificate> newCerts;
476: if (certs.size() == 1) {
477: // Reply has only one certificate
478: newCerts = establishCertChain(keyStore, trustStore, null,
479: certs.get(0), trustCACerts);
480: } else {
481: // Reply has a chain of certificates
482: newCerts = validateReply(keyStore, trustStore, alias, null,
483: certs, trustCACerts, validateRoot);
484: }
485: if (newCerts != null) {
486: keyStore.setKeyEntry(alias, privKey, keyPassword
487: .toCharArray(), newCerts
488: .toArray(new X509Certificate[newCerts.size()]));
489:
490: // Notify listeners that a new certificate has been created
491: for (CertificateEventListener listener : listeners) {
492: try {
493: listener.certificateSigned(keyStore, alias,
494: newCerts);
495: } catch (Exception e) {
496: Log.error(e);
497: }
498: }
499:
500: return true;
501: } else {
502: return false;
503: }
504: }
505:
506: /**
507: * Imports a new signed certificate and its private key into the keystore. The certificate input
508: * stream may contain the signed certificate as well as its CA chain.
509: *
510: * @param keyStore key store where the certificate will be stored.
511: * @param trustStore key store where ca certificates are stored.
512: * @param keyPassword password of the keystore.
513: * @param alias the alias of the the new signed certificate.
514: * @param pkInputStream the stream containing the private key.
515: * @param passPhrase is the password phrased used when creating the private key.
516: * @param inputStream the stream containing the signed certificate.
517: * @param trustCACerts true if certificates present in the truststore file will be used to verify the
518: * identity of the entity signing the certificate.
519: * @param validateRoot true if you want to verify that the root certificate in the chain can be trusted
520: * based on the truststore.
521: * @return true if the certificate was successfully imported.
522: * @throws Exception if no certificates were found in the inputStream.
523: */
524: public static boolean installCert(KeyStore keyStore,
525: KeyStore trustStore, String keyPassword, String alias,
526: InputStream pkInputStream, final String passPhrase,
527: InputStream inputStream, boolean trustCACerts,
528: boolean validateRoot) throws Exception {
529: // Check that there is a certificate for the specified alias
530: X509Certificate certificate = (X509Certificate) keyStore
531: .getCertificate(alias);
532: if (certificate != null) {
533: Log.warn("Certificate already exists for alias: " + alias);
534: return false;
535: }
536: // Retrieve the private key of the stored certificate
537: PasswordFinder passwordFinder = new PasswordFinder() {
538: public char[] getPassword() {
539: return passPhrase != null ? passPhrase.toCharArray()
540: : new char[] {};
541: }
542: };
543: PEMReader pemReader = new PEMReader(new InputStreamReader(
544: pkInputStream), passwordFinder);
545: KeyPair kp = (KeyPair) pemReader.readObject();
546: PrivateKey privKey = kp.getPrivate();
547:
548: // Load certificates found in the PEM input stream
549: List<X509Certificate> certs = new ArrayList<X509Certificate>();
550: for (Certificate cert : CertificateFactory.getInstance("X509")
551: .generateCertificates(inputStream)) {
552: certs.add((X509Certificate) cert);
553: }
554: if (certs.isEmpty()) {
555: throw new Exception("No certificates were found");
556: }
557: List<X509Certificate> newCerts;
558: if (certs.size() == 1) {
559: // Reply has only one certificate
560: newCerts = establishCertChain(keyStore, trustStore,
561: certificate, certs.get(0), trustCACerts);
562: } else {
563: // Reply has a chain of certificates
564: newCerts = validateReply(keyStore, trustStore, alias,
565: certificate, certs, trustCACerts, validateRoot);
566: }
567: if (newCerts != null) {
568: keyStore.setKeyEntry(alias, privKey, keyPassword
569: .toCharArray(), newCerts
570: .toArray(new X509Certificate[newCerts.size()]));
571:
572: // Notify listeners that a new certificate has been created (and signed)
573: for (CertificateEventListener listener : listeners) {
574: try {
575: listener.certificateCreated(keyStore, alias, certs
576: .get(0));
577: if (newCerts.size() > 1) {
578: listener.certificateSigned(keyStore, alias,
579: newCerts);
580: }
581: } catch (Exception e) {
582: Log.error(e);
583: }
584: }
585:
586: return true;
587: } else {
588: return false;
589: }
590: }
591:
592: /**
593: * Registers a listener to receive events.
594: *
595: * @param listener the listener.
596: */
597: public static void addListener(CertificateEventListener listener) {
598: if (listener == null) {
599: throw new NullPointerException();
600: }
601: listeners.add(listener);
602: }
603:
604: /**
605: * Unregisters a listener to receive events.
606: *
607: * @param listener the listener.
608: */
609: public static void removeListener(CertificateEventListener listener) {
610: listeners.remove(listener);
611: }
612:
613: private static List<X509Certificate> establishCertChain(
614: KeyStore keyStore, KeyStore trustStore,
615: X509Certificate certificate, X509Certificate certReply,
616: boolean trustCACerts) throws Exception {
617: if (certificate != null) {
618: PublicKey publickey = certificate.getPublicKey();
619: PublicKey publickey1 = certReply.getPublicKey();
620: if (!publickey.equals(publickey1)) {
621: throw new Exception(
622: "Public keys in reply and keystore don't match");
623: }
624: if (certReply.equals(certificate)) {
625: throw new Exception(
626: "Certificate reply and certificate in keystore are identical");
627: }
628: }
629: Map<Principal, List<X509Certificate>> knownCerts = new Hashtable<Principal, List<X509Certificate>>();
630: if (keyStore.size() > 0) {
631: knownCerts.putAll(getCertsByIssuer(keyStore));
632: }
633: if (trustCACerts && trustStore.size() > 0) {
634: knownCerts.putAll(getCertsByIssuer(trustStore));
635: }
636: LinkedList<X509Certificate> answer = new LinkedList<X509Certificate>();
637: if (buildChain(certReply, answer, knownCerts)) {
638: return answer;
639: } else {
640: throw new Exception("Failed to establish chain from reply");
641: }
642: }
643:
644: /**
645: * Builds the certificate chain of the specified certificate based on the known list of certificates
646: * that were issued by their respective Principals. Returns true if the entire chain of all certificates
647: * was successfully built.
648: *
649: * @param certificate certificate to build its chain.
650: * @param answer the certificate chain for the corresponding certificate.
651: * @param knownCerts list of known certificates grouped by their issues (i.e. Principals).
652: * @return true if the entire chain of all certificates was successfully built.
653: */
654: private static boolean buildChain(X509Certificate certificate,
655: LinkedList<X509Certificate> answer,
656: Map<Principal, List<X509Certificate>> knownCerts) {
657: Principal subject = certificate.getSubjectDN();
658: Principal issuer = certificate.getIssuerDN();
659: // Check if the certificate is a root certificate (i.e. was issued by the same Principal that
660: // is present in the subject)
661: if (subject.equals(issuer)) {
662: answer.addFirst(certificate);
663: return true;
664: }
665: // Get the list of known certificates of the certificate's issuer
666: List<X509Certificate> issuerCerts = knownCerts.get(issuer);
667: if (issuerCerts == null || issuerCerts.isEmpty()) {
668: // No certificates were found so building of chain failed
669: return false;
670: }
671: for (X509Certificate issuerCert : issuerCerts) {
672: PublicKey publickey = issuerCert.getPublicKey();
673: try {
674: // Verify the certificate with the specified public key
675: certificate.verify(publickey);
676: // Certificate was verified successfully so build chain of issuer's certificate
677: if (!buildChain(issuerCert, answer, knownCerts)) {
678: return false;
679: }
680: } catch (Exception exception) {
681: // Failed to verify certificate
682: return false;
683: }
684: }
685: answer.addFirst(certificate);
686: return true;
687: }
688:
689: /**
690: * Returns a Map where the key holds the certificate issuers and values the certificates of each issuer.
691: *
692: * @param ks the keystore to get its certs per issuer.
693: * @return a map with the certificates per issuer.
694: * @throws Exception
695: */
696: private static Map<Principal, List<X509Certificate>> getCertsByIssuer(
697: KeyStore ks) throws Exception {
698: Map<Principal, List<X509Certificate>> answer = new HashMap<Principal, List<X509Certificate>>();
699: Enumeration<String> aliases = ks.aliases();
700: while (aliases.hasMoreElements()) {
701: String alias = aliases.nextElement();
702: X509Certificate cert = (X509Certificate) ks
703: .getCertificate(alias);
704: if (cert != null) {
705: Principal subjectDN = cert.getSubjectDN();
706: List<X509Certificate> vec = answer.get(subjectDN);
707: if (vec == null) {
708: vec = new ArrayList<X509Certificate>();
709: vec.add(cert);
710: } else {
711: if (!vec.contains(cert)) {
712: vec.add(cert);
713: }
714: }
715: answer.put(subjectDN, vec);
716: }
717: }
718: return answer;
719: }
720:
721: /**
722: * Validates chain in certification reply, and returns the ordered
723: * elements of the chain (with user certificate first, and root
724: * certificate last in the array).
725: *
726: * @param alias the alias name
727: * @param userCert the user certificate of the alias
728: * @param replyCerts the chain provided in the reply
729: */
730: private static List<X509Certificate> validateReply(
731: KeyStore keyStore, KeyStore trustStore, String alias,
732: X509Certificate userCert, List<X509Certificate> replyCerts,
733: boolean trustCACerts, boolean verifyRoot) throws Exception {
734: // order the certs in the reply (bottom-up).
735: int i;
736: X509Certificate tmpCert;
737: if (userCert != null) {
738: PublicKey userPubKey = userCert.getPublicKey();
739: for (i = 0; i < replyCerts.size(); i++) {
740: if (userPubKey.equals(replyCerts.get(i).getPublicKey())) {
741: break;
742: }
743: }
744: if (i == replyCerts.size()) {
745: throw new Exception(
746: "Certificate reply does not contain public key for <alias>: "
747: + alias);
748: }
749:
750: tmpCert = replyCerts.get(0);
751: replyCerts.set(0, replyCerts.get(i));
752: replyCerts.set(i, tmpCert);
753: }
754:
755: Principal issuer = replyCerts.get(0).getIssuerDN();
756:
757: for (i = 1; i < replyCerts.size() - 1; i++) {
758: // find a cert in the reply whose "subject" is the same as the
759: // given "issuer"
760: int j;
761: for (j = i; j < replyCerts.size(); j++) {
762: Principal subject = replyCerts.get(j).getSubjectDN();
763: if (subject.equals(issuer)) {
764: tmpCert = replyCerts.get(i);
765: replyCerts.set(i, replyCerts.get(j));
766: replyCerts.set(j, tmpCert);
767: issuer = replyCerts.get(i).getIssuerDN();
768: break;
769: }
770: }
771: if (j == replyCerts.size()) {
772: throw new Exception(
773: "Incomplete certificate chain in reply");
774: }
775: }
776:
777: // now verify each cert in the ordered chain
778: for (i = 0; i < replyCerts.size() - 1; i++) {
779: PublicKey pubKey = replyCerts.get(i + 1).getPublicKey();
780: try {
781: replyCerts.get(i).verify(pubKey);
782: } catch (Exception e) {
783: throw new Exception(
784: "Certificate chain in reply does not verify: "
785: + e.getMessage());
786: }
787: }
788:
789: if (!verifyRoot) {
790: return replyCerts;
791: }
792:
793: // do we trust the (root) cert at the top?
794: X509Certificate topCert = replyCerts.get(replyCerts.size() - 1);
795: boolean foundInKeyStore = keyStore.getCertificateAlias(topCert) != null;
796: boolean foundInCAStore = trustCACerts
797: && (trustStore.getCertificateAlias(topCert) != null);
798: if (!foundInKeyStore && !foundInCAStore) {
799: boolean verified = false;
800: X509Certificate rootCert = null;
801: if (trustCACerts) {
802: for (Enumeration<String> aliases = trustStore.aliases(); aliases
803: .hasMoreElements();) {
804: String name = aliases.nextElement();
805: rootCert = (X509Certificate) trustStore
806: .getCertificate(name);
807: if (rootCert != null) {
808: try {
809: topCert.verify(rootCert.getPublicKey());
810: verified = true;
811: break;
812: } catch (Exception e) {
813: // Ignore
814: }
815: }
816: }
817: }
818: if (!verified) {
819: return null;
820: } else {
821: // Check if the cert is a self-signed cert
822: if (!topCert.getSubjectDN().equals(
823: topCert.getIssuerDN())) {
824: // append the (self-signed) root CA cert to the chain
825: replyCerts.add(rootCert);
826: }
827: }
828: }
829:
830: return replyCerts;
831: }
832:
833: /**
834: * Creates an X509 version3 certificate.
835: *
836: * @param kp KeyPair that keeps the public and private keys for the new certificate.
837: * @param months time to live
838: * @param issuerDN Issuer string e.g "O=Grid,OU=OGSA,CN=ACME"
839: * @param subjectDN Subject string e.g "O=Grid,OU=OGSA,CN=John Doe"
840: * @param domain Domain of the server.
841: * @param signAlgoritm Signature algorithm. This can be either a name or an OID.
842: * @return X509 V3 Certificate
843: * @throws GeneralSecurityException
844: * @throws IOException
845: */
846: private static synchronized X509Certificate createX509V3Certificate(
847: KeyPair kp, int months, String issuerDN, String subjectDN,
848: String domain, String signAlgoritm)
849: throws GeneralSecurityException, IOException {
850: PublicKey pubKey = kp.getPublic();
851: PrivateKey privKey = kp.getPrivate();
852:
853: byte[] serno = new byte[8];
854: SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
855: random.setSeed((new Date().getTime()));
856: random.nextBytes(serno);
857: BigInteger serial = (new java.math.BigInteger(serno)).abs();
858:
859: X509V3CertificateGenerator certGenerator = new X509V3CertificateGenerator();
860: certGenerator.reset();
861:
862: certGenerator.setSerialNumber(serial);
863: certGenerator.setIssuerDN(new X509Name(issuerDN));
864: certGenerator
865: .setNotBefore(new Date(System.currentTimeMillis()));
866: certGenerator.setNotAfter(new Date(System.currentTimeMillis()
867: + months * (1000L * 60 * 60 * 24 * 30)));
868: certGenerator.setSubjectDN(new X509Name(subjectDN));
869: certGenerator.setPublicKey(pubKey);
870: certGenerator.setSignatureAlgorithm(signAlgoritm);
871:
872: // Generate the subject alternative name
873: boolean critical = subjectDN == null
874: || "".equals(subjectDN.trim());
875: DERSequence othernameSequence = new DERSequence(
876: new ASN1Encodable[] {
877: new DERObjectIdentifier("1.3.6.1.5.5.7.8.5"),
878: new DERTaggedObject(true, 0, new DERUTF8String(
879: domain)) });
880: GeneralName othernameGN = new GeneralName(
881: GeneralName.otherName, othernameSequence);
882: GeneralNames subjectAltNames = new GeneralNames(
883: new DERSequence(new ASN1Encodable[] { othernameGN }));
884: // Add subject alternative name extension
885: certGenerator.addExtension(
886: X509Extensions.SubjectAlternativeName, critical,
887: subjectAltNames);
888:
889: X509Certificate cert = certGenerator.generateX509Certificate(
890: privKey, "BC", new SecureRandom());
891: cert.checkValidity(new Date());
892: cert.verify(pubKey);
893:
894: return cert;
895: }
896:
897: /**
898: * Returns a new public & private key with the specified algorithm (e.g. DSA, RSA, etc.).
899: *
900: * @param algorithm DSA, RSA, etc.
901: * @param keysize the keysize. This is an algorithm-specific metric, such as modulus
902: * length, specified in number of bits.
903: * @return a new public & private key with the specified algorithm (e.g. DSA, RSA, etc.).
904: * @throws GeneralSecurityException
905: */
906: private static KeyPair generateKeyPair(String algorithm, int keysize)
907: throws GeneralSecurityException {
908: KeyPairGenerator generator;
909: if (provider == null) {
910: generator = KeyPairGenerator.getInstance(algorithm);
911: } else {
912: generator = KeyPairGenerator.getInstance(algorithm,
913: provider);
914: }
915: generator.initialize(keysize, new SecureRandom());
916: return generator.generateKeyPair();
917: }
918: }
|