/* * Password Management Servlets (PWM) * http://www.pwm-project.org * * Copyright (c) 2006-2009 Novell, Inc. * Copyright (c) 2009-2017 The PWM Project * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package password.pwm.util.secure; import org.bouncycastle.asn1.x500.X500NameBuilder; import org.bouncycastle.asn1.x500.style.BCStyle; import org.bouncycastle.asn1.x509.BasicConstraints; import org.bouncycastle.asn1.x509.ExtendedKeyUsage; import org.bouncycastle.asn1.x509.Extension; import org.bouncycastle.asn1.x509.KeyPurposeId; import org.bouncycastle.asn1.x509.KeyUsage; import org.bouncycastle.cert.X509v3CertificateBuilder; import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.operator.ContentSigner; import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; import password.pwm.AppProperty; import password.pwm.PwmApplication; import password.pwm.PwmConstants; import password.pwm.bean.PrivateKeyCertificate; import password.pwm.config.Configuration; import password.pwm.config.PwmSetting; import password.pwm.config.StoredValue; import password.pwm.config.stored.StoredConfiguration; import password.pwm.config.value.PrivateKeyValue; import password.pwm.error.ErrorInformation; import password.pwm.error.PwmError; import password.pwm.error.PwmUnrecoverableException; import password.pwm.util.java.JsonUtil; import password.pwm.util.PasswordData; import password.pwm.util.java.StringUtil; import password.pwm.util.logging.PwmLogger; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; import java.math.BigInteger; import java.net.URI; import java.net.URISyntaxException; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.KeyStore; import java.security.PrivateKey; import java.security.SecureRandom; import java.security.Security; import java.security.cert.X509Certificate; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.Enumeration; import java.util.List; import java.util.concurrent.TimeUnit; /** * {@link } */ public class HttpsServerCertificateManager { private static final PwmLogger LOGGER = PwmLogger.forClass(HttpsServerCertificateManager.class); private static boolean bouncyCastleInitialized; private static void initBouncyCastleProvider() { if (!bouncyCastleInitialized) { Security.addProvider(new BouncyCastleProvider()); bouncyCastleInitialized = true; } } public static KeyStore keyStoreForApplication(final PwmApplication pwmApplication, final PasswordData passwordData, final String alias) throws PwmUnrecoverableException { KeyStore keyStore = null; keyStore = exportKey(pwmApplication.getConfig(), KeyStoreFormat.JKS, passwordData, alias); if (keyStore == null) { keyStore = makeSelfSignedCert(pwmApplication, passwordData, alias); } return keyStore; } private static KeyStore exportKey( final Configuration configuration, final KeyStoreFormat format, final PasswordData passwordData, final String alias ) throws PwmUnrecoverableException { final PrivateKeyCertificate privateKeyCertificate = configuration.readSettingAsPrivateKey(PwmSetting.HTTPS_CERT); if (privateKeyCertificate == null) { return null; } final KeyStore.PasswordProtection passwordProtection = new KeyStore.PasswordProtection(passwordData.getStringValue().toCharArray()); try { final KeyStore keyStore = KeyStore.getInstance(format.toString()); keyStore.load(null, passwordData.getStringValue().toCharArray()); //load of null is required to init keystore. keyStore.setEntry( alias, new KeyStore.PrivateKeyEntry( privateKeyCertificate.getKey(), privateKeyCertificate.getCertificates() ), passwordProtection ); return keyStore; } catch (Exception e) { throw new PwmUnrecoverableException(new ErrorInformation(PwmError.CONFIG_FORMAT_ERROR, "error generating keystore file;: " + e.getMessage())); } } private static KeyStore makeSelfSignedCert(final PwmApplication pwmApplication, final PasswordData password, final String alias) throws PwmUnrecoverableException { final Configuration configuration = pwmApplication.getConfig(); try { final SelfCertGenerator selfCertGenerator = new SelfCertGenerator(configuration); return selfCertGenerator.makeSelfSignedCert(pwmApplication, password, alias); } catch (Exception e) { throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_CERTIFICATE_ERROR, "unable to generate self signed certificate: " + e.getMessage())); } } public static class StoredCertData implements Serializable { private final X509Certificate x509Certificate; private String keypairb64; public StoredCertData(final X509Certificate x509Certificate, final KeyPair keypair) throws IOException { this.x509Certificate = x509Certificate; final ByteArrayOutputStream baos = new ByteArrayOutputStream(); final ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(keypair); final byte[] ba = baos.toByteArray(); keypairb64 = StringUtil.base64Encode(ba); } public X509Certificate getX509Certificate() { return x509Certificate; } public KeyPair getKeypair() throws IOException, ClassNotFoundException { final byte[] ba = StringUtil.base64Decode(keypairb64); final ByteArrayInputStream bais = new ByteArrayInputStream(ba); final ObjectInputStream ois = new ObjectInputStream(bais); return (KeyPair) ois.readObject(); } } public static class SelfCertGenerator { private final Configuration config; public SelfCertGenerator(final Configuration config) { this.config = config; } public KeyStore makeSelfSignedCert(final PwmApplication pwmApplication, final PasswordData password, final String alias) throws Exception { final String cnName = makeSubjectName(); final KeyStore keyStore = KeyStore.getInstance("jks"); keyStore.load(null, password.getStringValue().toCharArray()); StoredCertData storedCertData = pwmApplication.readAppAttribute(PwmApplication.AppAttribute.HTTPS_SELF_CERT, StoredCertData.class); if (storedCertData != null) { if (!cnName.equals(storedCertData.getX509Certificate().getSubjectDN().getName())) { LOGGER.info("replacing stored self cert, subject name does not match configured site url"); storedCertData = null; } else if (storedCertData.getX509Certificate().getNotBefore().after(new Date())) { LOGGER.info("replacing stored self cert, not-before date is in the future"); storedCertData = null; } else if (storedCertData.getX509Certificate().getNotAfter().before(new Date())) { LOGGER.info("replacing stored self cert, not-after date is in the past"); storedCertData = null; } } if (storedCertData == null) { storedCertData = makeSelfSignedCert(cnName); pwmApplication.writeAppAttribute(PwmApplication.AppAttribute.HTTPS_SELF_CERT, storedCertData); } keyStore.setKeyEntry( alias, storedCertData.getKeypair().getPrivate(), password.getStringValue().toCharArray(), new X509Certificate[]{storedCertData.getX509Certificate()} ); return keyStore; } public String makeSubjectName() throws Exception { String cnName = PwmConstants.PWM_APP_NAME.toLowerCase() + ".example.com"; { final String siteURL = config.readSettingAsString(PwmSetting.PWM_SITE_URL); if (siteURL != null && !siteURL.isEmpty()) { try { final URI uri = new URI(siteURL); if (uri.getHost() != null && !uri.getHost().isEmpty()) { cnName = uri.getHost(); } } catch (URISyntaxException e) { // disregard } } } return cnName; } public StoredCertData makeSelfSignedCert(final String cnName) throws Exception { initBouncyCastleProvider(); LOGGER.debug("creating self-signed certificate with cn of " + cnName); final KeyPair keyPair = generateRSAKeyPair(config); final long futureSeconds = Long.parseLong(config.readAppProperty(AppProperty.SECURITY_HTTPSSERVER_SELF_FUTURESECONDS)); final X509Certificate certificate = generateV3Certificate(keyPair, cnName, futureSeconds); return new StoredCertData(certificate, keyPair); } public static X509Certificate generateV3Certificate(final KeyPair pair, final String cnValue, final long futureSeconds) throws Exception { final X500NameBuilder subjectName = new X500NameBuilder(BCStyle.INSTANCE); subjectName.addRDN(BCStyle.CN, cnValue); final SimpleDateFormat formatter = new SimpleDateFormat("yyyyMMddhhmmss"); final String serNumStr = formatter.format(new Date(System.currentTimeMillis())); final BigInteger serialNumber = new BigInteger(serNumStr); final Date notBefore = new Date(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(2)); // 2 days in the past final Date notAfter = new Date(System.currentTimeMillis() + (futureSeconds * 1000)); final X509v3CertificateBuilder certGen = new JcaX509v3CertificateBuilder(subjectName.build(), serialNumber, notBefore, notAfter, subjectName.build(), pair.getPublic()); final BasicConstraints basic = new BasicConstraints(false); // not a CA certGen.addExtension(Extension.basicConstraints, true, basic.getEncoded()); // OID, critical, ASN.1 encoded value final KeyUsage keyUsage = new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyEncipherment); // sign and key encipher certGen.addExtension(Extension.keyUsage, true, keyUsage.getEncoded()); // OID, critical, ASN.1 encoded value final ExtendedKeyUsage extKeyUsage = new ExtendedKeyUsage(KeyPurposeId.id_kp_serverAuth); // server authentication certGen.addExtension(Extension.extendedKeyUsage, true, extKeyUsage.getEncoded()); // OID, critical, ASN.1 encoded value final ContentSigner sigGen = new JcaContentSignerBuilder("SHA256WithRSAEncryption").setProvider("BC").build(pair.getPrivate()); return new JcaX509CertificateConverter().setProvider("BC").getCertificate(certGen.build(sigGen)); } static KeyPair generateRSAKeyPair(final Configuration config) throws Exception { final int keySize = Integer.parseInt(config.readAppProperty(AppProperty.SECURITY_HTTPSSERVER_SELF_KEY_SIZE)); final String keyAlg = config.readAppProperty(AppProperty.SECURITY_HTTPSSERVER_SELF_ALG); final KeyPairGenerator kpGen = KeyPairGenerator.getInstance(keyAlg, "BC"); kpGen.initialize(keySize, new SecureRandom()); return kpGen.generateKeyPair(); } } public enum KeyStoreFormat { PKCS12, JKS, } public static void importKey( final StoredConfiguration storedConfiguration, final KeyStoreFormat keyStoreFormat, final InputStream inputStream, final PasswordData password, final String alias ) throws PwmUnrecoverableException { final char[] charPassword = password == null ? new char[0] : password.getStringValue().toCharArray(); final PrivateKeyCertificate privateKeyCertificate; try { final KeyStore keyStore = KeyStore.getInstance(keyStoreFormat.toString()); keyStore.load(inputStream, charPassword); final String effectiveAlias; { final List<String> allAliases = new ArrayList<>(); for (final Enumeration enu = keyStore.aliases(); enu.hasMoreElements(); ) { final String value = (String) enu.nextElement(); allAliases.add(value); } effectiveAlias = allAliases.size() == 1 ? allAliases.iterator().next() : alias; } final KeyStore.PasswordProtection passwordProtection = new KeyStore.PasswordProtection(charPassword); final KeyStore.PrivateKeyEntry entry = (KeyStore.PrivateKeyEntry)keyStore.getEntry(effectiveAlias, passwordProtection); if (entry == null) { final String errorMsg = "unable to import https key entry with alias '" + alias + "'"; throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_CERTIFICATE_ERROR, errorMsg, new String[]{"no key entry alias '" + alias + "' in keystore"})); } final PrivateKey key = entry.getPrivateKey(); final X509Certificate[] certificates = (X509Certificate[])entry.getCertificateChain(); LOGGER.debug("importing certificate chain: " + JsonUtil.serializeCollection(X509Utils.makeDebugInfoMap(certificates))); privateKeyCertificate = new PrivateKeyCertificate(certificates, key); } catch (Exception e) { final String errorMsg = "unable to load configured https certificate: " + e.getMessage(); throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_CERTIFICATE_ERROR, errorMsg, new String[]{e.getMessage()})); } final StoredValue storedValue = new PrivateKeyValue(privateKeyCertificate); storedConfiguration.writeSetting(PwmSetting.HTTPS_CERT,storedValue,null); } }