You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

X509Utils.java 35KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991
  1. /*
  2. * Copyright 2012 gitblit.com.
  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. package com.gitblit.utils;
  17. import java.io.BufferedWriter;
  18. import java.io.File;
  19. import java.io.FileInputStream;
  20. import java.io.FileOutputStream;
  21. import java.io.FileWriter;
  22. import java.io.IOException;
  23. import java.lang.reflect.Field;
  24. import java.math.BigInteger;
  25. import java.security.InvalidKeyException;
  26. import java.security.KeyPair;
  27. import java.security.KeyPairGenerator;
  28. import java.security.KeyStore;
  29. import java.security.NoSuchAlgorithmException;
  30. import java.security.PrivateKey;
  31. import java.security.SecureRandom;
  32. import java.security.Security;
  33. import java.security.SignatureException;
  34. import java.security.cert.CertPathBuilder;
  35. import java.security.cert.CertPathBuilderException;
  36. import java.security.cert.CertStore;
  37. import java.security.cert.Certificate;
  38. import java.security.cert.CollectionCertStoreParameters;
  39. import java.security.cert.PKIXBuilderParameters;
  40. import java.security.cert.PKIXCertPathBuilderResult;
  41. import java.security.cert.TrustAnchor;
  42. import java.security.cert.X509CRL;
  43. import java.security.cert.X509CertSelector;
  44. import java.security.cert.X509Certificate;
  45. import java.text.MessageFormat;
  46. import java.text.SimpleDateFormat;
  47. import java.util.Arrays;
  48. import java.util.Calendar;
  49. import java.util.Date;
  50. import java.util.HashMap;
  51. import java.util.HashSet;
  52. import java.util.Map;
  53. import java.util.Set;
  54. import java.util.TimeZone;
  55. import java.util.zip.ZipEntry;
  56. import java.util.zip.ZipOutputStream;
  57. import javax.crypto.Cipher;
  58. import org.bouncycastle.asn1.ASN1ObjectIdentifier;
  59. import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
  60. import org.bouncycastle.asn1.x500.X500Name;
  61. import org.bouncycastle.asn1.x500.X500NameBuilder;
  62. import org.bouncycastle.asn1.x500.style.BCStyle;
  63. import org.bouncycastle.asn1.x509.BasicConstraints;
  64. import org.bouncycastle.asn1.x509.GeneralName;
  65. import org.bouncycastle.asn1.x509.GeneralNames;
  66. import org.bouncycastle.asn1.x509.KeyUsage;
  67. import org.bouncycastle.asn1.x509.X509Extension;
  68. import org.bouncycastle.cert.X509CRLHolder;
  69. import org.bouncycastle.cert.X509v2CRLBuilder;
  70. import org.bouncycastle.cert.X509v3CertificateBuilder;
  71. import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
  72. import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils;
  73. import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
  74. import org.bouncycastle.jce.PrincipalUtil;
  75. import org.bouncycastle.jce.interfaces.PKCS12BagAttributeCarrier;
  76. import org.bouncycastle.openssl.PEMWriter;
  77. import org.bouncycastle.operator.ContentSigner;
  78. import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
  79. import org.slf4j.Logger;
  80. import org.slf4j.LoggerFactory;
  81. import sun.security.x509.X509CRLImpl;
  82. import com.gitblit.Constants;
  83. /**
  84. * Utility class to generate X509 certificates, keystores, and truststores.
  85. *
  86. * @author James Moger
  87. *
  88. */
  89. public class X509Utils {
  90. public static final String SERVER_KEY_STORE = "serverKeyStore.jks";
  91. public static final String SERVER_TRUST_STORE = "serverTrustStore.jks";
  92. public static final String CERTS = "certs";
  93. public static final String CA_KEY_STORE = "certs/caKeyStore.p12";
  94. public static final String CA_REVOCATION_LIST = "certs/caRevocationList.crl";
  95. public static final String CA_CONFIG = "certs/authority.conf";
  96. public static final String CA_CN = "Gitblit Certificate Authority";
  97. public static final String CA_FN = CA_CN;
  98. private static final String BC = org.bouncycastle.jce.provider.BouncyCastleProvider.PROVIDER_NAME;
  99. public static final boolean unlimitedStrength;
  100. private static final Logger logger = LoggerFactory.getLogger(X509Utils.class);
  101. static {
  102. Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
  103. // check for JCE Unlimited Strength
  104. int maxKeyLen = 0;
  105. try {
  106. maxKeyLen = Cipher.getMaxAllowedKeyLength("AES");
  107. } catch (NoSuchAlgorithmException e) {
  108. }
  109. unlimitedStrength = maxKeyLen > 128;
  110. if (unlimitedStrength) {
  111. logger.info("Using JCE Unlimited Strength Jurisdiction Policy files");
  112. } else {
  113. logger.info("Using JCE Standard Encryption Policy files, encryption key lengths will be limited");
  114. }
  115. }
  116. public static enum RevocationReason {
  117. // https://en.wikipedia.org/wiki/Revocation_list
  118. unspecified, keyCompromise, caCompromise, affiliationChanged, superseded,
  119. cessationOfOperation, certificateHold, unused, removeFromCRL, privilegeWithdrawn,
  120. ACompromise;
  121. public static RevocationReason [] reasons = {
  122. unspecified, keyCompromise, caCompromise,
  123. affiliationChanged, superseded, cessationOfOperation,
  124. privilegeWithdrawn };
  125. @Override
  126. public String toString() {
  127. return name() + " (" + ordinal() + ")";
  128. }
  129. }
  130. public static class X509Metadata {
  131. // map for distinguished name OIDs
  132. public final Map<String, String> oids;
  133. // CN in distingiushed name
  134. public final String commonName;
  135. // password for store
  136. public final String password;
  137. // password hint for README in bundle
  138. public String passwordHint;
  139. // E or EMAILADDRESS in distinguished name
  140. public String emailAddress;
  141. // start date of generated certificate
  142. public Date notBefore;
  143. // expiraiton date of generated certificate
  144. public Date notAfter;
  145. // hostname of server for which certificate is generated
  146. public String serverHostname;
  147. // displayname of user for README in bundle
  148. public String userDisplayname;
  149. public X509Metadata(String cn, String pwd) {
  150. if (StringUtils.isEmpty(cn)) {
  151. throw new RuntimeException("Common name required!");
  152. }
  153. if (StringUtils.isEmpty(pwd)) {
  154. throw new RuntimeException("Password required!");
  155. }
  156. commonName = cn;
  157. password = pwd;
  158. Calendar c = Calendar.getInstance(TimeZone.getDefault());
  159. c.set(Calendar.SECOND, 0);
  160. c.set(Calendar.MILLISECOND, 0);
  161. notBefore = c.getTime();
  162. c.add(Calendar.YEAR, 1);
  163. c.add(Calendar.DATE, 1);
  164. notAfter = c.getTime();
  165. oids = new HashMap<String, String>();
  166. }
  167. public X509Metadata clone(String commonName, String password) {
  168. X509Metadata clone = new X509Metadata(commonName, password);
  169. clone.emailAddress = emailAddress;
  170. clone.notBefore = notBefore;
  171. clone.notAfter = notAfter;
  172. clone.oids.putAll(oids);
  173. clone.passwordHint = passwordHint;
  174. clone.serverHostname = serverHostname;
  175. clone.userDisplayname = userDisplayname;
  176. return clone;
  177. }
  178. }
  179. /**
  180. * Prepare all the certificates and stores necessary for a Gitblit GO server.
  181. *
  182. * @param metadata
  183. * @param folder
  184. * @param logger
  185. */
  186. public static void prepareX509Infrastructure(X509Metadata metadata, File folder) {
  187. // make the specified folder, if necessary
  188. folder.mkdirs();
  189. // Gitblit CA certificate
  190. File caKeyStore = new File(folder, CA_KEY_STORE);
  191. if (!caKeyStore.exists()) {
  192. logger.info(MessageFormat.format("Generating {0} ({1})", CA_CN, caKeyStore.getAbsolutePath()));
  193. X509Certificate caCert = newCertificateAuthority(metadata, caKeyStore);
  194. saveCertificate(caCert, new File(caKeyStore.getParentFile(), "ca.cer"));
  195. }
  196. // rename the old keystore to the new name
  197. File oldKeyStore = new File(folder, "keystore");
  198. if (oldKeyStore.exists()) {
  199. oldKeyStore.renameTo(new File(folder, SERVER_KEY_STORE));
  200. logger.info(MessageFormat.format("Renaming {0} to {1}", oldKeyStore.getName(), SERVER_KEY_STORE));
  201. }
  202. // create web SSL certificate signed by CA
  203. File serverKeyStore = new File(folder, SERVER_KEY_STORE);
  204. if (!serverKeyStore.exists()) {
  205. logger.info(MessageFormat.format("Generating SSL certificate for {0} signed by {1} ({2})", metadata.commonName, CA_CN, serverKeyStore.getAbsolutePath()));
  206. PrivateKey caPrivateKey = getPrivateKey(CA_FN, caKeyStore, metadata.password);
  207. X509Certificate caCert = getCertificate(CA_FN, caKeyStore, metadata.password);
  208. newSSLCertificate(metadata, caPrivateKey, caCert, serverKeyStore);
  209. }
  210. // server certificate trust store holds trusted public certificates
  211. File serverTrustStore = new File(folder, X509Utils.SERVER_TRUST_STORE);
  212. if (!serverTrustStore.exists()) {
  213. logger.info(MessageFormat.format("Importing {0} into trust store ({1})", CA_FN, serverTrustStore.getAbsolutePath()));
  214. X509Certificate caCert = getCertificate(CA_FN, caKeyStore, metadata.password);
  215. addTrustedCertificate(CA_FN, caCert, serverTrustStore, metadata.password);
  216. }
  217. }
  218. /**
  219. * Open a keystore. Store type is determined by file extension of name. If
  220. * undetermined, JKS is assumed. The keystore does not need to exist.
  221. *
  222. * @param storeFile
  223. * @param storePassword
  224. * @return a KeyStore
  225. */
  226. public static KeyStore openKeyStore(File storeFile, String storePassword) {
  227. String lc = storeFile.getName().toLowerCase();
  228. String type = "JKS";
  229. String provider = null;
  230. if (lc.endsWith(".p12") || lc.endsWith(".pfx")) {
  231. type = "PKCS12";
  232. provider = BC;
  233. }
  234. try {
  235. KeyStore store;
  236. if (provider == null) {
  237. store = KeyStore.getInstance(type);
  238. } else {
  239. store = KeyStore.getInstance(type, provider);
  240. }
  241. if (storeFile.exists()) {
  242. FileInputStream fis = null;
  243. try {
  244. fis = new FileInputStream(storeFile);
  245. store.load(fis, storePassword.toCharArray());
  246. } finally {
  247. if (fis != null) {
  248. fis.close();
  249. }
  250. }
  251. } else {
  252. store.load(null);
  253. }
  254. return store;
  255. } catch (Exception e) {
  256. throw new RuntimeException("Could not open keystore " + storeFile, e);
  257. }
  258. }
  259. /**
  260. * Saves the keystore to the specified file.
  261. *
  262. * @param targetStoreFile
  263. * @param store
  264. * @param password
  265. */
  266. public static void saveKeyStore(File targetStoreFile, KeyStore store, String password) {
  267. File folder = targetStoreFile.getAbsoluteFile().getParentFile();
  268. if (!folder.exists()) {
  269. folder.mkdirs();
  270. }
  271. File tmpFile = new File(folder, Long.toHexString(System.currentTimeMillis()) + ".tmp");
  272. FileOutputStream fos = null;
  273. try {
  274. fos = new FileOutputStream(tmpFile);
  275. store.store(fos, password.toCharArray());
  276. fos.flush();
  277. fos.close();
  278. if (targetStoreFile.exists()) {
  279. targetStoreFile.delete();
  280. }
  281. tmpFile.renameTo(targetStoreFile);
  282. } catch (IOException e) {
  283. String message = e.getMessage().toLowerCase();
  284. if (message.contains("illegal key size")) {
  285. throw new RuntimeException("Illegal Key Size! You might consider installing the JCE Unlimited Strength Jurisdiction Policy files for your JVM.");
  286. } else {
  287. throw new RuntimeException("Could not save keystore " + targetStoreFile, e);
  288. }
  289. } catch (Exception e) {
  290. throw new RuntimeException("Could not save keystore " + targetStoreFile, e);
  291. } finally {
  292. if (fos != null) {
  293. try {
  294. fos.close();
  295. } catch (IOException e) {
  296. }
  297. }
  298. if (tmpFile.exists()) {
  299. tmpFile.delete();
  300. }
  301. }
  302. }
  303. /**
  304. * Retrieves the X509 certificate with the specified alias from the certificate
  305. * store.
  306. *
  307. * @param alias
  308. * @param storeFile
  309. * @param storePassword
  310. * @return the certificate
  311. */
  312. public static X509Certificate getCertificate(String alias, File storeFile, String storePassword) {
  313. try {
  314. KeyStore store = openKeyStore(storeFile, storePassword);
  315. X509Certificate caCert = (X509Certificate) store.getCertificate(alias);
  316. return caCert;
  317. } catch (Exception e) {
  318. throw new RuntimeException(e);
  319. }
  320. }
  321. /**
  322. * Retrieves the private key for the specified alias from the certificate
  323. * store.
  324. *
  325. * @param alias
  326. * @param storeFile
  327. * @param storePassword
  328. * @return the private key
  329. */
  330. public static PrivateKey getPrivateKey(String alias, File storeFile, String storePassword) {
  331. try {
  332. KeyStore store = openKeyStore(storeFile, storePassword);
  333. PrivateKey key = (PrivateKey) store.getKey(alias, storePassword.toCharArray());
  334. return key;
  335. } catch (Exception e) {
  336. throw new RuntimeException(e);
  337. }
  338. }
  339. /**
  340. * Saves the certificate to the file system. If the destination filename
  341. * ends with the pem extension, the certificate is written in the PEM format,
  342. * otherwise the certificate is written in the DER format.
  343. *
  344. * @param cert
  345. * @param targetFile
  346. */
  347. public static void saveCertificate(X509Certificate cert, File targetFile) {
  348. File folder = targetFile.getAbsoluteFile().getParentFile();
  349. if (!folder.exists()) {
  350. folder.mkdirs();
  351. }
  352. File tmpFile = new File(folder, Long.toHexString(System.currentTimeMillis()) + ".tmp");
  353. try {
  354. boolean asPem = targetFile.getName().toLowerCase().endsWith(".pem");
  355. if (asPem) {
  356. // PEM encoded X509
  357. PEMWriter pemWriter = null;
  358. try {
  359. pemWriter = new PEMWriter(new FileWriter(tmpFile));
  360. pemWriter.writeObject(cert);
  361. pemWriter.flush();
  362. } finally {
  363. if (pemWriter != null) {
  364. pemWriter.close();
  365. }
  366. }
  367. } else {
  368. // DER encoded X509
  369. FileOutputStream fos = null;
  370. try {
  371. fos = new FileOutputStream(tmpFile);
  372. fos.write(cert.getEncoded());
  373. fos.flush();
  374. } finally {
  375. if (fos != null) {
  376. fos.close();
  377. }
  378. }
  379. }
  380. // rename tmp file to target
  381. if (targetFile.exists()) {
  382. targetFile.delete();
  383. }
  384. tmpFile.renameTo(targetFile);
  385. } catch (Exception e) {
  386. if (tmpFile.exists()) {
  387. tmpFile.delete();
  388. }
  389. throw new RuntimeException("Failed to save certificate " + cert.getSubjectX500Principal().getName(), e);
  390. }
  391. }
  392. /**
  393. * Generate a new keypair.
  394. *
  395. * @return a keypair
  396. * @throws Exception
  397. */
  398. private static KeyPair newKeyPair() throws Exception {
  399. KeyPairGenerator kpGen = KeyPairGenerator.getInstance("RSA", BC);
  400. kpGen.initialize(2048, new SecureRandom());
  401. return kpGen.generateKeyPair();
  402. }
  403. /**
  404. * Builds a distinguished name from the X509Metadata.
  405. *
  406. * @return a DN
  407. */
  408. private static X500Name buildDistinguishedName(X509Metadata metadata) {
  409. X500NameBuilder dnBuilder = new X500NameBuilder(BCStyle.INSTANCE);
  410. setOID(dnBuilder, metadata, "C", null);
  411. setOID(dnBuilder, metadata, "ST", null);
  412. setOID(dnBuilder, metadata, "L", null);
  413. setOID(dnBuilder, metadata, "O", Constants.NAME);
  414. setOID(dnBuilder, metadata, "OU", Constants.NAME);
  415. setOID(dnBuilder, metadata, "E", metadata.emailAddress);
  416. setOID(dnBuilder, metadata, "CN", metadata.commonName);
  417. X500Name dn = dnBuilder.build();
  418. return dn;
  419. }
  420. private static void setOID(X500NameBuilder dnBuilder, X509Metadata metadata,
  421. String oid, String defaultValue) {
  422. String value = null;
  423. if (metadata.oids != null && metadata.oids.containsKey(oid)) {
  424. value = metadata.oids.get(oid);
  425. }
  426. if (StringUtils.isEmpty(value)) {
  427. value = defaultValue;
  428. }
  429. if (!StringUtils.isEmpty(value)) {
  430. try {
  431. Field field = BCStyle.class.getField(oid);
  432. ASN1ObjectIdentifier objectId = (ASN1ObjectIdentifier) field.get(null);
  433. dnBuilder.addRDN(objectId, value);
  434. } catch (Exception e) {
  435. logger.error(MessageFormat.format("Failed to set OID \"{0}\"!", oid) ,e);
  436. }
  437. }
  438. }
  439. /**
  440. * Creates a new SSL certificate signed by the CA private key and stored in
  441. * keyStore.
  442. *
  443. * @param sslMetadata
  444. * @param caPrivateKey
  445. * @param caCert
  446. * @param targetStoreFile
  447. */
  448. public static X509Certificate newSSLCertificate(X509Metadata sslMetadata, PrivateKey caPrivateKey, X509Certificate caCert, File targetStoreFile) {
  449. try {
  450. KeyPair pair = newKeyPair();
  451. X500Name webDN = buildDistinguishedName(sslMetadata);
  452. X500Name issuerDN = new X500Name(PrincipalUtil.getIssuerX509Principal(caCert).getName());
  453. X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(
  454. issuerDN,
  455. BigInteger.valueOf(System.currentTimeMillis()),
  456. sslMetadata.notBefore,
  457. sslMetadata.notAfter,
  458. webDN,
  459. pair.getPublic());
  460. JcaX509ExtensionUtils extUtils = new JcaX509ExtensionUtils();
  461. certBuilder.addExtension(X509Extension.subjectKeyIdentifier, false, extUtils.createSubjectKeyIdentifier(pair.getPublic()));
  462. certBuilder.addExtension(X509Extension.basicConstraints, false, new BasicConstraints(false));
  463. certBuilder.addExtension(X509Extension.authorityKeyIdentifier, false, extUtils.createAuthorityKeyIdentifier(caCert.getPublicKey()));
  464. ContentSigner caSigner = new JcaContentSignerBuilder("SHA256WithRSAEncryption")
  465. .setProvider(BC).build(caPrivateKey);
  466. X509Certificate cert = new JcaX509CertificateConverter().setProvider(BC)
  467. .getCertificate(certBuilder.build(caSigner));
  468. cert.checkValidity(new Date());
  469. cert.verify(caCert.getPublicKey());
  470. // Save to keystore
  471. KeyStore serverStore = openKeyStore(targetStoreFile, sslMetadata.password);
  472. serverStore.setKeyEntry(sslMetadata.commonName, pair.getPrivate(), sslMetadata.password.toCharArray(),
  473. new Certificate[] { cert, caCert });
  474. saveKeyStore(targetStoreFile, serverStore, sslMetadata.password);
  475. log(targetStoreFile.getParentFile(), MessageFormat.format("New web certificate {0,number,0} [{1}]", cert.getSerialNumber(), webDN.toString()));
  476. return cert;
  477. } catch (Throwable t) {
  478. throw new RuntimeException("Failed to generate SSL certificate!", t);
  479. }
  480. }
  481. /**
  482. * Creates a new certificate authority PKCS#12 store. This function will
  483. * destroy any existing CA store.
  484. *
  485. * @param metadata
  486. * @param storeFile
  487. * @param keystorePassword
  488. * @return
  489. */
  490. public static X509Certificate newCertificateAuthority(X509Metadata metadata, File storeFile) {
  491. try {
  492. KeyPair caPair = newKeyPair();
  493. ContentSigner caSigner = new JcaContentSignerBuilder("SHA1WithRSA").setProvider(BC).build(caPair.getPrivate());
  494. // clone metadata
  495. X509Metadata caMetadata = metadata.clone(CA_CN, metadata.password);
  496. X500Name issuerDN = buildDistinguishedName(caMetadata);
  497. // Generate self-signed certificate
  498. X509v3CertificateBuilder caBuilder = new JcaX509v3CertificateBuilder(
  499. issuerDN,
  500. BigInteger.valueOf(System.currentTimeMillis()),
  501. caMetadata.notBefore,
  502. caMetadata.notAfter,
  503. issuerDN,
  504. caPair.getPublic());
  505. JcaX509ExtensionUtils extUtils = new JcaX509ExtensionUtils();
  506. caBuilder.addExtension(X509Extension.subjectKeyIdentifier, false, extUtils.createSubjectKeyIdentifier(caPair.getPublic()));
  507. caBuilder.addExtension(X509Extension.authorityKeyIdentifier, false, extUtils.createAuthorityKeyIdentifier(caPair.getPublic()));
  508. caBuilder.addExtension(X509Extension.basicConstraints, false, new BasicConstraints(true));
  509. caBuilder.addExtension(X509Extension.keyUsage, true, new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyCertSign | KeyUsage.cRLSign));
  510. JcaX509CertificateConverter converter = new JcaX509CertificateConverter().setProvider(BC);
  511. X509Certificate cert = converter.getCertificate(caBuilder.build(caSigner));
  512. // confirm the validity of the CA certificate
  513. cert.checkValidity(new Date());
  514. cert.verify(cert.getPublicKey());
  515. // Delete existing keystore
  516. if (storeFile.exists()) {
  517. storeFile.delete();
  518. }
  519. // Save private key and certificate to new keystore
  520. KeyStore store = openKeyStore(storeFile, caMetadata.password);
  521. store.setKeyEntry(CA_FN, caPair.getPrivate(), caMetadata.password.toCharArray(),
  522. new Certificate[] { cert });
  523. saveKeyStore(storeFile, store, caMetadata.password);
  524. log(storeFile.getParentFile(), MessageFormat.format("New CA certificate {0,number,0} [{1}]", cert.getSerialNumber(), issuerDN.toString()));
  525. return cert;
  526. } catch (Throwable t) {
  527. throw new RuntimeException("Failed to generate Gitblit CA certificate!", t);
  528. }
  529. }
  530. /**
  531. * Imports a certificate into the trust store.
  532. *
  533. * @param alias
  534. * @param cert
  535. * @param storeFile
  536. * @param storePassword
  537. */
  538. public static void addTrustedCertificate(String alias, X509Certificate cert, File storeFile, String storePassword) {
  539. try {
  540. KeyStore store = openKeyStore(storeFile, storePassword);
  541. store.setCertificateEntry(alias, cert);
  542. saveKeyStore(storeFile, store, storePassword);
  543. } catch (Exception e) {
  544. throw new RuntimeException("Failed to import certificate into trust store " + storeFile, e);
  545. }
  546. }
  547. /**
  548. * Creates a new client certificate PKCS#12 and PEM store. Any existing
  549. * stores are destroyed. After generation, the certificates are bundled
  550. * into a zip file with a personalized README file.
  551. *
  552. * The zip file reference is returned.
  553. *
  554. * @param clientMetadata a container for dynamic parameters needed for generation
  555. * @param caKeystoreFile
  556. * @param caKeystorePassword
  557. * @return a zip file containing the P12, PEM, and personalized README
  558. */
  559. public static File newClientBundle(X509Metadata clientMetadata, File caKeystoreFile, String caKeystorePassword) {
  560. try {
  561. // read the Gitblit CA key and certificate
  562. KeyStore store = openKeyStore(caKeystoreFile, caKeystorePassword);
  563. PrivateKey caPrivateKey = (PrivateKey) store.getKey(CA_FN, caKeystorePassword.toCharArray());
  564. X509Certificate caCert = (X509Certificate) store.getCertificate(CA_FN);
  565. // generate the P12 and PEM files
  566. File targetFolder = new File(caKeystoreFile.getParentFile(), clientMetadata.commonName);
  567. newClientCertificate(clientMetadata, caPrivateKey, caCert, targetFolder);
  568. // process template message
  569. String readme = processTemplate(new File(caKeystoreFile.getParentFile(), "instructions.tmpl"), clientMetadata);
  570. // Create a zip bundle with the p12, pem, and a personalized readme
  571. File zipFile = new File(targetFolder, clientMetadata.commonName + ".zip");
  572. if (zipFile.exists()) {
  573. zipFile.delete();
  574. }
  575. ZipOutputStream zos = null;
  576. try {
  577. zos = new ZipOutputStream(new FileOutputStream(zipFile));
  578. File p12File = new File(targetFolder, clientMetadata.commonName + ".p12");
  579. if (p12File.exists()) {
  580. zos.putNextEntry(new ZipEntry(p12File.getName()));
  581. zos.write(FileUtils.readContent(p12File));
  582. zos.closeEntry();
  583. }
  584. File pemFile = new File(targetFolder, clientMetadata.commonName + ".pem");
  585. if (pemFile.exists()) {
  586. zos.putNextEntry(new ZipEntry(pemFile.getName()));
  587. zos.write(FileUtils.readContent(pemFile));
  588. zos.closeEntry();
  589. }
  590. if (readme != null) {
  591. zos.putNextEntry(new ZipEntry("README.TXT"));
  592. zos.write(readme.getBytes("UTF-8"));
  593. zos.closeEntry();
  594. }
  595. zos.flush();
  596. } finally {
  597. if (zos != null) {
  598. zos.close();
  599. }
  600. }
  601. return zipFile;
  602. } catch (Throwable t) {
  603. throw new RuntimeException("Failed to generate client bundle!", t);
  604. }
  605. }
  606. /**
  607. * Creates a new client certificate PKCS#12 and PEM store. Any existing
  608. * stores are destroyed.
  609. *
  610. * @param clientMetadata a container for dynamic parameters needed for generation
  611. * @param caKeystoreFile
  612. * @param caKeystorePassword
  613. * @param targetFolder
  614. * @return
  615. */
  616. public static X509Certificate newClientCertificate(X509Metadata clientMetadata,
  617. PrivateKey caPrivateKey, X509Certificate caCert, File targetFolder) {
  618. try {
  619. KeyPair pair = newKeyPair();
  620. X500Name userDN = buildDistinguishedName(clientMetadata);
  621. X500Name issuerDN = new X500Name(PrincipalUtil.getIssuerX509Principal(caCert).getName());
  622. // create a new certificate signed by the Gitblit CA certificate
  623. X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(
  624. issuerDN,
  625. BigInteger.valueOf(System.currentTimeMillis()),
  626. clientMetadata.notBefore,
  627. clientMetadata.notAfter,
  628. userDN,
  629. pair.getPublic());
  630. JcaX509ExtensionUtils extUtils = new JcaX509ExtensionUtils();
  631. certBuilder.addExtension(X509Extension.subjectKeyIdentifier, false, extUtils.createSubjectKeyIdentifier(pair.getPublic()));
  632. certBuilder.addExtension(X509Extension.basicConstraints, false, new BasicConstraints(false));
  633. certBuilder.addExtension(X509Extension.authorityKeyIdentifier, false, extUtils.createAuthorityKeyIdentifier(caCert.getPublicKey()));
  634. certBuilder.addExtension(X509Extension.keyUsage, true, new KeyUsage(KeyUsage.keyEncipherment | KeyUsage.digitalSignature));
  635. if (!StringUtils.isEmpty(clientMetadata.emailAddress)) {
  636. GeneralNames subjectAltName = new GeneralNames(
  637. new GeneralName(GeneralName.rfc822Name, clientMetadata.emailAddress));
  638. certBuilder.addExtension(X509Extension.subjectAlternativeName, false, subjectAltName);
  639. }
  640. ContentSigner signer = new JcaContentSignerBuilder("SHA1WithRSA").setProvider(BC).build(caPrivateKey);
  641. X509Certificate userCert = new JcaX509CertificateConverter().setProvider(BC).getCertificate(certBuilder.build(signer));
  642. PKCS12BagAttributeCarrier bagAttr = (PKCS12BagAttributeCarrier)pair.getPrivate();
  643. bagAttr.setBagAttribute(PKCSObjectIdentifiers.pkcs_9_at_localKeyId,
  644. extUtils.createSubjectKeyIdentifier(pair.getPublic()));
  645. // confirm the validity of the user certificate
  646. userCert.checkValidity();
  647. userCert.verify(caCert.getPublicKey());
  648. userCert.getIssuerDN().equals(caCert.getSubjectDN());
  649. // verify user certificate chain
  650. verifyChain(userCert, caCert);
  651. targetFolder.mkdirs();
  652. // save certificate, stamped with unique name
  653. String date = new SimpleDateFormat("yyyyMMdd").format(new Date());
  654. String id = date;
  655. File certFile = new File(targetFolder, id + ".cer");
  656. int count = 0;
  657. while (certFile.exists()) {
  658. id = date + "_" + Character.toString((char) (0x61 + count));
  659. certFile = new File(targetFolder, id + ".cer");
  660. count++;
  661. }
  662. // save user private key, user certificate and CA certificate to a PKCS#12 store
  663. File p12File = new File(targetFolder, clientMetadata.commonName + ".p12");
  664. if (p12File.exists()) {
  665. p12File.delete();
  666. }
  667. KeyStore userStore = openKeyStore(p12File, clientMetadata.password);
  668. userStore.setKeyEntry(MessageFormat.format("Gitblit ({0}) {1} {2}", clientMetadata.serverHostname, clientMetadata.userDisplayname, id), pair.getPrivate(), null, new Certificate [] { userCert });
  669. userStore.setCertificateEntry(MessageFormat.format("Gitblit ({0}) Certificate Authority", clientMetadata.serverHostname), caCert);
  670. saveKeyStore(p12File, userStore, clientMetadata.password);
  671. // save user private key, user certificate, and CA certificate to a PEM store
  672. File pemFile = new File(targetFolder, clientMetadata.commonName + ".pem");
  673. if (pemFile.exists()) {
  674. pemFile.delete();
  675. }
  676. PEMWriter pemWriter = new PEMWriter(new FileWriter(pemFile));
  677. pemWriter.writeObject(pair.getPrivate(), "DES-EDE3-CBC", clientMetadata.password.toCharArray(), new SecureRandom());
  678. pemWriter.writeObject(userCert);
  679. pemWriter.writeObject(caCert);
  680. pemWriter.flush();
  681. pemWriter.close();
  682. // save certificate after successfully creating the key stores
  683. saveCertificate(userCert, certFile);
  684. log(targetFolder.getParentFile(), MessageFormat.format("New client certificate {0,number,0} [{1}]", userCert.getSerialNumber(), userDN.toString()));
  685. return userCert;
  686. } catch (Throwable t) {
  687. throw new RuntimeException("Failed to generate client certificate!", t);
  688. }
  689. }
  690. /**
  691. * Verifies a certificate's chain to ensure that it will function properly.
  692. *
  693. * @param testCert
  694. * @param additionalCerts
  695. * @return
  696. */
  697. public static PKIXCertPathBuilderResult verifyChain(X509Certificate testCert, X509Certificate... additionalCerts) {
  698. try {
  699. // Check for self-signed certificate
  700. if (isSelfSigned(testCert)) {
  701. throw new RuntimeException("The certificate is self-signed. Nothing to verify.");
  702. }
  703. // Prepare a set of all certificates
  704. // chain builder must have all certs, including cert to validate
  705. // http://stackoverflow.com/a/10788392
  706. Set<X509Certificate> certs = new HashSet<X509Certificate>();
  707. certs.add(testCert);
  708. certs.addAll(Arrays.asList(additionalCerts));
  709. // Attempt to build the certification chain and verify it
  710. // Create the selector that specifies the starting certificate
  711. X509CertSelector selector = new X509CertSelector();
  712. selector.setCertificate(testCert);
  713. // Create the trust anchors (set of root CA certificates)
  714. Set<TrustAnchor> trustAnchors = new HashSet<TrustAnchor>();
  715. for (X509Certificate cert : additionalCerts) {
  716. if (isSelfSigned(cert)) {
  717. trustAnchors.add(new TrustAnchor(cert, null));
  718. }
  719. }
  720. // Configure the PKIX certificate builder
  721. PKIXBuilderParameters pkixParams = new PKIXBuilderParameters(trustAnchors, selector);
  722. pkixParams.setRevocationEnabled(false);
  723. pkixParams.addCertStore(CertStore.getInstance("Collection", new CollectionCertStoreParameters(certs), BC));
  724. // Build and verify the certification chain
  725. CertPathBuilder builder = CertPathBuilder.getInstance("PKIX", BC);
  726. PKIXCertPathBuilderResult verifiedCertChain = (PKIXCertPathBuilderResult) builder.build(pkixParams);
  727. // The chain is built and verified
  728. return verifiedCertChain;
  729. } catch (CertPathBuilderException e) {
  730. throw new RuntimeException("Error building certification path: " + testCert.getSubjectX500Principal(), e);
  731. } catch (Exception e) {
  732. throw new RuntimeException("Error verifying the certificate: " + testCert.getSubjectX500Principal(), e);
  733. }
  734. }
  735. /**
  736. * Checks whether given X.509 certificate is self-signed.
  737. *
  738. * @param cert
  739. * @return true if the certificate is self-signed
  740. */
  741. public static boolean isSelfSigned(X509Certificate cert) {
  742. try {
  743. cert.verify(cert.getPublicKey());
  744. return true;
  745. } catch (SignatureException e) {
  746. return false;
  747. } catch (InvalidKeyException e) {
  748. return false;
  749. } catch (Exception e) {
  750. throw new RuntimeException(e);
  751. }
  752. }
  753. public static String processTemplate(File template, X509Metadata metadata) {
  754. String content = null;
  755. if (template.exists()) {
  756. String message = FileUtils.readContent(template, "\n");
  757. if (!StringUtils.isEmpty(message)) {
  758. content = message;
  759. content = content.replace("$serverHostname", metadata.serverHostname);
  760. content = content.replace("$username", metadata.commonName);
  761. content = content.replace("$userDisplayname", metadata.userDisplayname);
  762. content = content.replace("$storePasswordHint", metadata.passwordHint);
  763. }
  764. }
  765. return content;
  766. }
  767. private static void log(File folder, String message) {
  768. BufferedWriter writer = null;
  769. try {
  770. writer = new BufferedWriter(new FileWriter(new File(folder, "log.txt"), true));
  771. writer.write(MessageFormat.format("{0,date,yyyy-MM-dd HH:mm}: {1}", new Date(), message));
  772. writer.newLine();
  773. writer.flush();
  774. } catch (Exception e) {
  775. logger.error("Failed to append log entry!", e);
  776. } finally {
  777. if (writer != null) {
  778. try {
  779. writer.close();
  780. } catch (IOException e) {
  781. }
  782. }
  783. }
  784. }
  785. /**
  786. * Revoke a certificate.
  787. *
  788. * @param cert
  789. * @param reason
  790. * @param caRevocationList
  791. * @param caKeystoreFile
  792. * @param caKeystorePassword
  793. * @return true if the certificate has been revoked
  794. */
  795. public static boolean revoke(X509Certificate cert, RevocationReason reason,
  796. File caRevocationList, File caKeystoreFile, String caKeystorePassword) {
  797. try {
  798. // read the Gitblit CA key and certificate
  799. KeyStore store = openKeyStore(caKeystoreFile, caKeystorePassword);
  800. PrivateKey caPrivateKey = (PrivateKey) store.getKey(CA_FN, caKeystorePassword.toCharArray());
  801. return revoke(cert, reason, caRevocationList, caPrivateKey);
  802. } catch (Exception e) {
  803. logger.error(MessageFormat.format("Failed to revoke certificate {0,number,0} [{1}] in {2}",
  804. cert.getSerialNumber(), cert.getSubjectDN().getName(), caRevocationList));
  805. }
  806. return false;
  807. }
  808. /**
  809. * Revoke a certificate.
  810. *
  811. * @param cert
  812. * @param reason
  813. * @param caRevocationList
  814. * @param caPrivateKey
  815. * @return true if the certificate has been revoked
  816. */
  817. public static boolean revoke(X509Certificate cert, RevocationReason reason,
  818. File caRevocationList, PrivateKey caPrivateKey) {
  819. try {
  820. X500Name subjectDN = new X500Name(PrincipalUtil.getSubjectX509Principal(cert).getName());
  821. X500Name issuerDN = new X500Name(PrincipalUtil.getIssuerX509Principal(cert).getName());
  822. X509v2CRLBuilder crlBuilder = new X509v2CRLBuilder(issuerDN, new Date());
  823. if (caRevocationList.exists()) {
  824. byte [] data = FileUtils.readContent(caRevocationList);
  825. X509CRLHolder crl = new X509CRLHolder(data);
  826. crlBuilder.addCRL(crl);
  827. }
  828. crlBuilder.addCRLEntry(cert.getSerialNumber(), new Date(), reason.ordinal());
  829. // build and sign CRL with CA private key
  830. ContentSigner signer = new JcaContentSignerBuilder("SHA1WithRSA").setProvider(BC).build(caPrivateKey);
  831. X509CRLHolder crl = crlBuilder.build(signer);
  832. File tmpFile = new File(caRevocationList.getParentFile(), Long.toHexString(System.currentTimeMillis()) + ".tmp");
  833. FileOutputStream fos = null;
  834. try {
  835. fos = new FileOutputStream(tmpFile);
  836. fos.write(crl.getEncoded());
  837. fos.flush();
  838. fos.close();
  839. if (caRevocationList.exists()) {
  840. caRevocationList.delete();
  841. }
  842. tmpFile.renameTo(caRevocationList);
  843. log(caRevocationList.getParentFile(), MessageFormat.format("Revoked certificate {0,number,0} reason: {1} [{2}]",
  844. cert.getSerialNumber(), reason.toString(), subjectDN.toString()));
  845. } finally {
  846. if (fos != null) {
  847. fos.close();
  848. }
  849. if (tmpFile.exists()) {
  850. tmpFile.delete();
  851. }
  852. }
  853. return true;
  854. } catch (Exception e) {
  855. logger.error(MessageFormat.format("Failed to revoke certificate {0,number,0} [{1}] in {2}",
  856. cert.getSerialNumber(), cert.getSubjectDN().getName(), caRevocationList));
  857. }
  858. return false;
  859. }
  860. /**
  861. * Returns true if the certificate has been revoked.
  862. *
  863. * @param cert
  864. * @param caRevocationList
  865. * @return true if the certificate is revoked
  866. */
  867. public static boolean isRevoked(X509Certificate cert, File caRevocationList) {
  868. if (!caRevocationList.exists()) {
  869. return false;
  870. }
  871. try {
  872. byte [] data = FileUtils.readContent(caRevocationList);
  873. X509CRL crl = new X509CRLImpl(data);
  874. return crl.isRevoked(cert);
  875. } catch (Exception e) {
  876. logger.error(MessageFormat.format("Failed to check revocation status for certificate {0,number,0} [{1}] in {2}",
  877. cert.getSerialNumber(), cert.getSubjectDN().getName(), caRevocationList));
  878. }
  879. return false;
  880. }
  881. }