HttpsServerCertificateManager.java 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418
  1. /*
  2. * Password Management Servlets (PWM)
  3. * http://www.pwm-project.org
  4. *
  5. * Copyright (c) 2006-2009 Novell, Inc.
  6. * Copyright (c) 2009-2018 The PWM Project
  7. *
  8. * This program is free software; you can redistribute it and/or modify
  9. * it under the terms of the GNU General Public License as published by
  10. * the Free Software Foundation; either version 2 of the License, or
  11. * (at your option) any later version.
  12. *
  13. * This program is distributed in the hope that it will be useful,
  14. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. * GNU General Public License for more details.
  17. *
  18. * You should have received a copy of the GNU General Public License
  19. * along with this program; if not, write to the Free Software
  20. * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
  21. */
  22. package password.pwm.util.secure;
  23. import org.bouncycastle.asn1.x500.X500NameBuilder;
  24. import org.bouncycastle.asn1.x500.style.BCStyle;
  25. import org.bouncycastle.asn1.x509.BasicConstraints;
  26. import org.bouncycastle.asn1.x509.ExtendedKeyUsage;
  27. import org.bouncycastle.asn1.x509.Extension;
  28. import org.bouncycastle.asn1.x509.KeyPurposeId;
  29. import org.bouncycastle.asn1.x509.KeyUsage;
  30. import org.bouncycastle.cert.X509v3CertificateBuilder;
  31. import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
  32. import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
  33. import org.bouncycastle.jce.provider.BouncyCastleProvider;
  34. import org.bouncycastle.operator.ContentSigner;
  35. import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
  36. import password.pwm.AppProperty;
  37. import password.pwm.PwmApplication;
  38. import password.pwm.PwmConstants;
  39. import password.pwm.bean.PrivateKeyCertificate;
  40. import password.pwm.config.Configuration;
  41. import password.pwm.config.PwmSetting;
  42. import password.pwm.config.StoredValue;
  43. import password.pwm.config.stored.StoredConfiguration;
  44. import password.pwm.config.value.PrivateKeyValue;
  45. import password.pwm.error.ErrorInformation;
  46. import password.pwm.error.PwmError;
  47. import password.pwm.error.PwmUnrecoverableException;
  48. import password.pwm.util.PasswordData;
  49. import password.pwm.util.java.JsonUtil;
  50. import password.pwm.util.java.StringUtil;
  51. import password.pwm.util.logging.PwmLogger;
  52. import java.io.ByteArrayInputStream;
  53. import java.io.ByteArrayOutputStream;
  54. import java.io.IOException;
  55. import java.io.InputStream;
  56. import java.io.ObjectInputStream;
  57. import java.io.ObjectOutputStream;
  58. import java.io.Serializable;
  59. import java.math.BigInteger;
  60. import java.net.URI;
  61. import java.net.URISyntaxException;
  62. import java.security.KeyPair;
  63. import java.security.KeyPairGenerator;
  64. import java.security.KeyStore;
  65. import java.security.PrivateKey;
  66. import java.security.SecureRandom;
  67. import java.security.Security;
  68. import java.security.cert.X509Certificate;
  69. import java.text.SimpleDateFormat;
  70. import java.util.ArrayList;
  71. import java.util.Arrays;
  72. import java.util.Date;
  73. import java.util.Enumeration;
  74. import java.util.List;
  75. import java.util.concurrent.TimeUnit;
  76. public class HttpsServerCertificateManager
  77. {
  78. private static final PwmLogger LOGGER = PwmLogger.forClass( HttpsServerCertificateManager.class );
  79. private static boolean bouncyCastleInitialized;
  80. private static void initBouncyCastleProvider( )
  81. {
  82. if ( !bouncyCastleInitialized )
  83. {
  84. Security.addProvider( new BouncyCastleProvider() );
  85. bouncyCastleInitialized = true;
  86. }
  87. }
  88. public static KeyStore keyStoreForApplication(
  89. final PwmApplication pwmApplication,
  90. final PasswordData passwordData,
  91. final String alias
  92. )
  93. throws PwmUnrecoverableException
  94. {
  95. KeyStore keyStore = null;
  96. keyStore = exportKey( pwmApplication.getConfig(), KeyStoreFormat.JKS, passwordData, alias );
  97. if ( keyStore == null )
  98. {
  99. keyStore = makeSelfSignedCert( pwmApplication, passwordData, alias );
  100. }
  101. return keyStore;
  102. }
  103. private static KeyStore exportKey(
  104. final Configuration configuration,
  105. final KeyStoreFormat format,
  106. final PasswordData passwordData,
  107. final String alias
  108. )
  109. throws PwmUnrecoverableException
  110. {
  111. final PrivateKeyCertificate privateKeyCertificate = configuration.readSettingAsPrivateKey( PwmSetting.HTTPS_CERT );
  112. if ( privateKeyCertificate == null )
  113. {
  114. return null;
  115. }
  116. final KeyStore.PasswordProtection passwordProtection = new KeyStore.PasswordProtection( passwordData.getStringValue().toCharArray() );
  117. try
  118. {
  119. final KeyStore keyStore = KeyStore.getInstance( format.toString() );
  120. //load of null is required to init keystore.
  121. keyStore.load( null, passwordData.getStringValue().toCharArray() );
  122. keyStore.setEntry(
  123. alias,
  124. new KeyStore.PrivateKeyEntry(
  125. privateKeyCertificate.getKey(),
  126. privateKeyCertificate.getCertificates().toArray( new X509Certificate[ privateKeyCertificate.getCertificates().size() ] )
  127. ),
  128. passwordProtection
  129. );
  130. return keyStore;
  131. }
  132. catch ( Exception e )
  133. {
  134. throw new PwmUnrecoverableException( new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, "error generating keystore file;: " + e.getMessage() ) );
  135. }
  136. }
  137. private static KeyStore makeSelfSignedCert( final PwmApplication pwmApplication, final PasswordData password, final String alias )
  138. throws PwmUnrecoverableException
  139. {
  140. final Configuration configuration = pwmApplication.getConfig();
  141. try
  142. {
  143. final SelfCertGenerator selfCertGenerator = new SelfCertGenerator( configuration );
  144. return selfCertGenerator.makeSelfSignedCert( pwmApplication, password, alias );
  145. }
  146. catch ( Exception e )
  147. {
  148. throw new PwmUnrecoverableException( new ErrorInformation( PwmError.ERROR_CERTIFICATE_ERROR, "unable to generate self signed certificate: " + e.getMessage() ) );
  149. }
  150. }
  151. public static class StoredCertData implements Serializable
  152. {
  153. private final X509Certificate x509Certificate;
  154. private String keypairb64;
  155. public StoredCertData( final X509Certificate x509Certificate, final KeyPair keypair )
  156. throws IOException
  157. {
  158. this.x509Certificate = x509Certificate;
  159. final ByteArrayOutputStream baos = new ByteArrayOutputStream();
  160. final ObjectOutputStream oos = new ObjectOutputStream( baos );
  161. oos.writeObject( keypair );
  162. final byte[] ba = baos.toByteArray();
  163. keypairb64 = StringUtil.base64Encode( ba );
  164. }
  165. public X509Certificate getX509Certificate( )
  166. {
  167. return x509Certificate;
  168. }
  169. public KeyPair getKeypair( )
  170. throws IOException, ClassNotFoundException
  171. {
  172. final byte[] ba = StringUtil.base64Decode( keypairb64 );
  173. final ByteArrayInputStream bais = new ByteArrayInputStream( ba );
  174. final ObjectInputStream ois = new ObjectInputStream( bais );
  175. return ( KeyPair ) ois.readObject();
  176. }
  177. }
  178. public static class SelfCertGenerator
  179. {
  180. private final Configuration config;
  181. public SelfCertGenerator( final Configuration config )
  182. {
  183. this.config = config;
  184. }
  185. public KeyStore makeSelfSignedCert( final PwmApplication pwmApplication, final PasswordData password, final String alias )
  186. throws Exception
  187. {
  188. final String cnName = makeSubjectName();
  189. final KeyStore keyStore = KeyStore.getInstance( "jks" );
  190. keyStore.load( null, password.getStringValue().toCharArray() );
  191. StoredCertData storedCertData = pwmApplication.readAppAttribute( PwmApplication.AppAttribute.HTTPS_SELF_CERT, StoredCertData.class );
  192. if ( storedCertData != null )
  193. {
  194. if ( !cnName.equals( storedCertData.getX509Certificate().getSubjectDN().getName() ) )
  195. {
  196. LOGGER.info( "replacing stored self cert, subject name does not match configured site url" );
  197. storedCertData = null;
  198. }
  199. else if ( storedCertData.getX509Certificate().getNotBefore().after( new Date() ) )
  200. {
  201. LOGGER.info( "replacing stored self cert, not-before date is in the future" );
  202. storedCertData = null;
  203. }
  204. else if ( storedCertData.getX509Certificate().getNotAfter().before( new Date() ) )
  205. {
  206. LOGGER.info( "replacing stored self cert, not-after date is in the past" );
  207. storedCertData = null;
  208. }
  209. }
  210. if ( storedCertData == null )
  211. {
  212. storedCertData = makeSelfSignedCert( cnName );
  213. pwmApplication.writeAppAttribute( PwmApplication.AppAttribute.HTTPS_SELF_CERT, storedCertData );
  214. }
  215. keyStore.setKeyEntry(
  216. alias,
  217. storedCertData.getKeypair().getPrivate(),
  218. password.getStringValue().toCharArray(),
  219. new X509Certificate[]
  220. {
  221. storedCertData.getX509Certificate(),
  222. }
  223. );
  224. return keyStore;
  225. }
  226. public String makeSubjectName( )
  227. throws Exception
  228. {
  229. String cnName = PwmConstants.PWM_APP_NAME.toLowerCase() + ".example.com";
  230. {
  231. final String siteURL = config.readSettingAsString( PwmSetting.PWM_SITE_URL );
  232. if ( siteURL != null && !siteURL.isEmpty() )
  233. {
  234. try
  235. {
  236. final URI uri = new URI( siteURL );
  237. if ( uri.getHost() != null && !uri.getHost().isEmpty() )
  238. {
  239. cnName = uri.getHost();
  240. }
  241. }
  242. catch ( URISyntaxException e )
  243. {
  244. // disregard
  245. }
  246. }
  247. }
  248. return cnName;
  249. }
  250. public StoredCertData makeSelfSignedCert( final String cnName )
  251. throws Exception
  252. {
  253. initBouncyCastleProvider();
  254. LOGGER.debug( "creating self-signed certificate with cn of " + cnName );
  255. final KeyPair keyPair = generateRSAKeyPair( config );
  256. final long futureSeconds = Long.parseLong( config.readAppProperty( AppProperty.SECURITY_HTTPSSERVER_SELF_FUTURESECONDS ) );
  257. final X509Certificate certificate = generateV3Certificate( keyPair, cnName, futureSeconds );
  258. return new StoredCertData( certificate, keyPair );
  259. }
  260. public static X509Certificate generateV3Certificate( final KeyPair pair, final String cnValue, final long futureSeconds )
  261. throws Exception
  262. {
  263. final X500NameBuilder subjectName = new X500NameBuilder( BCStyle.INSTANCE );
  264. subjectName.addRDN( BCStyle.CN, cnValue );
  265. final SimpleDateFormat formatter = new SimpleDateFormat( "yyyyMMddhhmmss" );
  266. final String serNumStr = formatter.format( new Date( System.currentTimeMillis() ) );
  267. final BigInteger serialNumber = new BigInteger( serNumStr );
  268. // 2 days in the past
  269. final Date notBefore = new Date( System.currentTimeMillis() - TimeUnit.DAYS.toMillis( 2 ) );
  270. final Date notAfter = new Date( System.currentTimeMillis() + ( futureSeconds * 1000 ) );
  271. final X509v3CertificateBuilder certGen = new JcaX509v3CertificateBuilder(
  272. subjectName.build(),
  273. serialNumber,
  274. notBefore,
  275. notAfter,
  276. subjectName.build(),
  277. pair.getPublic()
  278. );
  279. // false == not a CA
  280. final BasicConstraints basic = new BasicConstraints( false );
  281. // OID, critical, ASN.1 encoded value
  282. certGen.addExtension( Extension.basicConstraints, true, basic.getEncoded() );
  283. // sign and key encipher
  284. final KeyUsage keyUsage = new KeyUsage( KeyUsage.digitalSignature | KeyUsage.keyEncipherment );
  285. // OID, critical, ASN.1 encoded value
  286. certGen.addExtension( Extension.keyUsage, true, keyUsage.getEncoded() );
  287. // server authentication
  288. final ExtendedKeyUsage extKeyUsage = new ExtendedKeyUsage( KeyPurposeId.id_kp_serverAuth );
  289. // OID, critical, ASN.1 encoded value
  290. certGen.addExtension( Extension.extendedKeyUsage, true, extKeyUsage.getEncoded() );
  291. final ContentSigner sigGen = new JcaContentSignerBuilder( "SHA256WithRSAEncryption" ).setProvider( "BC" ).build( pair.getPrivate() );
  292. return new JcaX509CertificateConverter().setProvider( "BC" ).getCertificate( certGen.build( sigGen ) );
  293. }
  294. static KeyPair generateRSAKeyPair( final Configuration config )
  295. throws Exception
  296. {
  297. final int keySize = Integer.parseInt( config.readAppProperty( AppProperty.SECURITY_HTTPSSERVER_SELF_KEY_SIZE ) );
  298. final String keyAlg = config.readAppProperty( AppProperty.SECURITY_HTTPSSERVER_SELF_ALG );
  299. final KeyPairGenerator kpGen = KeyPairGenerator.getInstance( keyAlg, "BC" );
  300. kpGen.initialize( keySize, new SecureRandom() );
  301. return kpGen.generateKeyPair();
  302. }
  303. }
  304. public enum KeyStoreFormat
  305. {
  306. PKCS12,
  307. JKS,
  308. }
  309. public static void importKey(
  310. final StoredConfiguration storedConfiguration,
  311. final KeyStoreFormat keyStoreFormat,
  312. final InputStream inputStream,
  313. final PasswordData password,
  314. final String alias
  315. ) throws PwmUnrecoverableException
  316. {
  317. final char[] charPassword = password == null ? new char[ 0 ] : password.getStringValue().toCharArray();
  318. final PrivateKeyCertificate privateKeyCertificate;
  319. try
  320. {
  321. final KeyStore keyStore = KeyStore.getInstance( keyStoreFormat.toString() );
  322. keyStore.load( inputStream, charPassword );
  323. final String effectiveAlias;
  324. {
  325. final List<String> allAliases = new ArrayList<>();
  326. for ( final Enumeration enu = keyStore.aliases(); enu.hasMoreElements(); )
  327. {
  328. final String value = ( String ) enu.nextElement();
  329. allAliases.add( value );
  330. }
  331. effectiveAlias = allAliases.size() == 1 ? allAliases.iterator().next() : alias;
  332. }
  333. final KeyStore.PasswordProtection passwordProtection = new KeyStore.PasswordProtection( charPassword );
  334. final KeyStore.PrivateKeyEntry entry = ( KeyStore.PrivateKeyEntry ) keyStore.getEntry( effectiveAlias, passwordProtection );
  335. if ( entry == null )
  336. {
  337. final String errorMsg = "unable to import https key entry with alias '" + alias + "'";
  338. throw new PwmUnrecoverableException( new ErrorInformation(
  339. PwmError.ERROR_CERTIFICATE_ERROR,
  340. errorMsg,
  341. new String[]
  342. {
  343. "no key entry alias '" + alias + "' in keystore",
  344. }
  345. ) );
  346. }
  347. final PrivateKey key = entry.getPrivateKey();
  348. final List<X509Certificate> certificates = Arrays.asList( ( X509Certificate[] ) entry.getCertificateChain() );
  349. LOGGER.debug( "importing certificate chain: " + JsonUtil.serializeCollection( X509Utils.makeDebugInfoMap( certificates ) ) );
  350. privateKeyCertificate = new PrivateKeyCertificate( certificates, key );
  351. }
  352. catch ( Exception e )
  353. {
  354. final String errorMsg = "unable to load configured https certificate: " + e.getMessage();
  355. final String[] errorDetail = new String[]
  356. {
  357. e.getMessage(),
  358. };
  359. throw new PwmUnrecoverableException( new ErrorInformation( PwmError.ERROR_CERTIFICATE_ERROR, errorMsg, errorDetail ) );
  360. }
  361. final StoredValue storedValue = new PrivateKeyValue( privateKeyCertificate );
  362. storedConfiguration.writeSetting( PwmSetting.HTTPS_CERT, storedValue, null );
  363. }
  364. }