From: Mathieu Baudier Date: Sat, 6 Aug 2022 07:15:48 +0000 (+0200) Subject: Remove dependency to BouncyCastle in core CMS X-Git-Tag: v2.3.10~70 X-Git-Url: https://git.argeo.org/?p=lgpl%2Fargeo-commons.git;a=commitdiff_plain;h=40cdc5aa006aac762c2b241fdec2a33436d43840 Remove dependency to BouncyCastle in core CMS --- diff --git a/org.argeo.cms.lib.sshd/src/org/argeo/cms/bc/BcUtils.java b/org.argeo.cms.lib.sshd/src/org/argeo/cms/bc/BcUtils.java new file mode 100644 index 000000000..d2fc89f79 --- /dev/null +++ b/org.argeo.cms.lib.sshd/src/org/argeo/cms/bc/BcUtils.java @@ -0,0 +1,168 @@ +package org.argeo.cms.bc; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Reader; +import java.math.BigInteger; +import java.net.InetAddress; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.GeneralSecurityException; +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.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.Date; + +import javax.security.auth.x500.X500Principal; + +import org.argeo.api.cms.CmsLog; +import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; +import org.bouncycastle.cert.X509CertificateHolder; +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.openssl.PEMParser; +import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; +import org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8DecryptorProviderBuilder; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.InputDecryptorProvider; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo; +import org.bouncycastle.pkcs.PKCSException; + +/** Utilities around the BouncyCastle crypto library. */ +public class BcUtils { + private final static CmsLog log = CmsLog.getLog(BcUtils.class); + + private final static String BC_SECURITY_PROVIDER; + static { + Security.addProvider(new BouncyCastleProvider()); + BC_SECURITY_PROVIDER = "BC"; + } + + public static void createSelfSignedKeyStore(Path keyStorePath, char[] keyStorePassword, String keyStoreType) { + // for (Provider provider : Security.getProviders()) + // System.out.println(provider.getName()); + // File keyStoreFile = keyStorePath.toFile(); + char[] keyPwd = Arrays.copyOf(keyStorePassword, keyStorePassword.length); + if (!Files.exists(keyStorePath)) { + try { + Files.createDirectories(keyStorePath.getParent()); + KeyStore keyStore = getKeyStore(keyStorePath, keyStorePassword, keyStoreType); + generateSelfSignedCertificate(keyStore, + new X500Principal("CN=" + InetAddress.getLocalHost().getHostName() + ",OU=UNSECURE,O=UNSECURE"), + 1024, keyPwd); + saveKeyStore(keyStorePath, keyStorePassword, keyStore); + if (log.isDebugEnabled()) + log.debug("Created self-signed unsecure keystore " + keyStorePath); + } catch (Exception e) { + try { + if (Files.size(keyStorePath) == 0) + Files.delete(keyStorePath); + } catch (IOException e1) { + // silent + } + log.error("Cannot create keystore " + keyStorePath, e); + } + } else { + throw new IllegalStateException("Keystore " + keyStorePath + " already exists"); + } + } + + public static X509Certificate generateSelfSignedCertificate(KeyStore keyStore, X500Principal x500Principal, + int keySize, char[] keyPassword) { + try { + KeyPairGenerator kpGen = KeyPairGenerator.getInstance("RSA", BC_SECURITY_PROVIDER); + kpGen.initialize(keySize, new SecureRandom()); + KeyPair pair = kpGen.generateKeyPair(); + Date notBefore = new Date(System.currentTimeMillis() - 10000); + Date notAfter = new Date(System.currentTimeMillis() + 365 * 24L * 3600 * 1000); + BigInteger serial = BigInteger.valueOf(System.currentTimeMillis()); + X509v3CertificateBuilder certGen = new JcaX509v3CertificateBuilder(x500Principal, serial, notBefore, + notAfter, x500Principal, pair.getPublic()); + ContentSigner sigGen = new JcaContentSignerBuilder("SHA256WithRSAEncryption") + .setProvider(BC_SECURITY_PROVIDER).build(pair.getPrivate()); + X509Certificate cert = new JcaX509CertificateConverter().setProvider(BC_SECURITY_PROVIDER) + .getCertificate(certGen.build(sigGen)); + cert.checkValidity(new Date()); + cert.verify(cert.getPublicKey()); + + keyStore.setKeyEntry(x500Principal.getName(), pair.getPrivate(), keyPassword, new Certificate[] { cert }); + return cert; + } catch (GeneralSecurityException | OperatorCreationException e) { + throw new RuntimeException("Cannot generate self-signed certificate", e); + } + } + + public static PrivateKey loadPemPrivateKey(Reader reader, char[] keyPassword) { + try (PEMParser pemParser = new PEMParser(reader)) { + JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider(BC_SECURITY_PROVIDER); + Object object = pemParser.readObject(); + PrivateKeyInfo privateKeyInfo; + if (object instanceof PKCS8EncryptedPrivateKeyInfo) { + if (keyPassword == null) + throw new IllegalArgumentException("A key password is required"); + InputDecryptorProvider decProv = new JceOpenSSLPKCS8DecryptorProviderBuilder().build(keyPassword); + privateKeyInfo = ((PKCS8EncryptedPrivateKeyInfo) object).decryptPrivateKeyInfo(decProv); + } else if (object instanceof PrivateKeyInfo) { + privateKeyInfo = (PrivateKeyInfo) object; + } else { + throw new IllegalArgumentException("Unsupported format for private key"); + } + return converter.getPrivateKey(privateKeyInfo); + } catch (IOException | OperatorCreationException | PKCSException e) { + throw new RuntimeException("Cannot read private key", e); + } + } + + public static X509Certificate loadPemCertificate(Reader reader) { + try (PEMParser pemParser = new PEMParser(reader)) { + X509CertificateHolder certHolder = (X509CertificateHolder) pemParser.readObject(); + X509Certificate cert = new JcaX509CertificateConverter().setProvider(BC_SECURITY_PROVIDER) + .getCertificate(certHolder); + return cert; + } catch (IOException | CertificateException e) { + throw new RuntimeException("Cannot read private key", e); + } + } + + private static KeyStore getKeyStore(Path keyStoreFile, char[] keyStorePassword, String keyStoreType) { + try { + KeyStore store = KeyStore.getInstance(keyStoreType, BC_SECURITY_PROVIDER); + if (Files.exists(keyStoreFile)) { + try (InputStream fis = Files.newInputStream(keyStoreFile)) { + store.load(fis, keyStorePassword); + } + } else { + store.load(null); + } + return store; + } catch (GeneralSecurityException | IOException e) { + throw new RuntimeException("Cannot load keystore " + keyStoreFile, e); + } + } + + private static void saveKeyStore(Path keyStoreFile, char[] keyStorePassword, KeyStore keyStore) { + try { + try (OutputStream fis = Files.newOutputStream(keyStoreFile)) { + keyStore.store(fis, keyStorePassword); + } + } catch (GeneralSecurityException | IOException e) { + throw new RuntimeException("Cannot save keystore " + keyStoreFile, e); + } + } + + /** singleton */ + private BcUtils() { + } +} diff --git a/org.argeo.cms/bnd.bnd b/org.argeo.cms/bnd.bnd index 4a5324ef2..ade2f3aa9 100644 --- a/org.argeo.cms/bnd.bnd +++ b/org.argeo.cms/bnd.bnd @@ -1,7 +1,6 @@ Bundle-Activator: org.argeo.cms.internal.osgi.CmsActivator Import-Package: \ -com.sun.security.jgss,\ org.osgi.*;version=0.0.0,\ * diff --git a/org.argeo.cms/src/org/argeo/cms/CmsSshd.java b/org.argeo.cms/src/org/argeo/cms/CmsSshd.java index dd8621f0e..41968be7e 100644 --- a/org.argeo.cms/src/org/argeo/cms/CmsSshd.java +++ b/org.argeo.cms/src/org/argeo/cms/CmsSshd.java @@ -1,10 +1,9 @@ package org.argeo.cms; import org.argeo.api.cms.CmsConstants; -import org.argeo.cms.internal.runtime.KernelConstants; /** Just a marker interface for the time being. */ public interface CmsSshd { final static String NODE_USERNAME_ALIAS = "user.name"; - final static String DEFAULT_SSH_HOST_KEY_PATH = KernelConstants.DIR_PRIVATE + '/' + CmsConstants.NODE + ".ser"; + final static String DEFAULT_SSH_HOST_KEY_PATH = "private/" + CmsConstants.NODE + ".ser"; } diff --git a/org.argeo.cms/src/org/argeo/cms/auth/ConsoleCallbackHandler.java b/org.argeo.cms/src/org/argeo/cms/auth/ConsoleCallbackHandler.java new file mode 100644 index 000000000..874381eb7 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/auth/ConsoleCallbackHandler.java @@ -0,0 +1,73 @@ +package org.argeo.cms.auth; + +import java.io.Console; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Arrays; +import java.util.Scanner; + +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.NameCallback; +import javax.security.auth.callback.PasswordCallback; +import javax.security.auth.callback.TextOutputCallback; +import javax.security.auth.callback.UnsupportedCallbackException; + +/** Callback handler to be used with a command line UI. */ +public class ConsoleCallbackHandler implements CallbackHandler { + + @Override + public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { + Console console = System.console(); +// if (console == null) +// throw new IllegalStateException("No console available"); + + Scanner scanner = null; + PrintWriter writer; + if (console == null) {// IDE + scanner = new Scanner(System.in); + writer = new PrintWriter(System.out, true); + } else { + writer = console.writer(); + + } + for (int i = 0; i < callbacks.length; i++) { + if (callbacks[i] instanceof TextOutputCallback) { + TextOutputCallback callback = (TextOutputCallback) callbacks[i]; + writer.printf(callback.getMessage()); + } else if (callbacks[i] instanceof NameCallback) { + NameCallback callback = (NameCallback) callbacks[i]; + writer.printf(callback.getPrompt()); + if (callback.getDefaultName() != null) + writer.printf(" (" + callback.getDefaultName() + ")"); + String answer = console != null ? console.readLine() : scanner.next(); + if (callback.getDefaultName() != null && answer.trim().equals("")) + callback.setName(callback.getDefaultName()); + else + callback.setName(answer); + } else if (callbacks[i] instanceof PasswordCallback) { + PasswordCallback callback = (PasswordCallback) callbacks[i]; + writer.printf(callback.getPrompt()); + char[] answer = console != null ? console.readPassword() : scanner.next().toCharArray(); + callback.setPassword(answer); + Arrays.fill(answer, ' '); + } +// else if (callbacks[i] instanceof LocaleChoice) { +// LocaleChoice callback = (LocaleChoice) callbacks[i]; +// writer.write("Language"); +// writer.write("\n"); +// for (int j = 0; j < callback.getLocales().size(); j++) { +// Locale locale = callback.getLocales().get(j); +// writer.print(j + " : " + locale.getDisplayName() + "\n"); +// } +// writer.write("(" + callback.getDefaultIndex() + ") : "); +// String answer = console.readLine(); +// if (answer.trim().equals("")) +// callback.setSelectedIndex(callback.getDefaultIndex()); +// else +// callback.setSelectedIndex(new Integer(answer.trim())); +// } + } + } + +} diff --git a/org.argeo.cms/src/org/argeo/cms/client/SpnegoHttpClient.java b/org.argeo.cms/src/org/argeo/cms/client/SpnegoHttpClient.java index e5beb69da..fcbd7a73d 100644 --- a/org.argeo.cms/src/org/argeo/cms/client/SpnegoHttpClient.java +++ b/org.argeo.cms/src/org/argeo/cms/client/SpnegoHttpClient.java @@ -1,5 +1,7 @@ package org.argeo.cms.client; +import java.io.BufferedInputStream; +import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.net.http.HttpClient; @@ -7,9 +9,15 @@ import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandler; import java.net.http.HttpResponse.BodyHandlers; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; +import java.util.Collection; import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManager; @@ -17,10 +25,13 @@ import javax.net.ssl.X509TrustManager; import javax.security.auth.Subject; import javax.security.auth.login.LoginContext; +import org.argeo.cms.auth.ConsoleCallbackHandler; import org.argeo.cms.auth.RemoteAuthUtils; import org.argeo.util.http.HttpHeader; public class SpnegoHttpClient { + public final static String CLIENT_LOGIN_CONTEXT = "CLIENT"; + public static void main(String[] args) throws MalformedURLException { // String principal = System.getProperty("javax.security.auth.login.name"); if (args.length == 0) { @@ -33,10 +44,10 @@ public class SpnegoHttpClient { URL u = new URL(url); String server = u.getHost(); - URL jaasUrl = SpnegoHttpClient.class.getResource("jaas.cfg"); + URL jaasUrl = SpnegoHttpClient.class.getResource("jaas-client-ipa.cfg"); System.setProperty("java.security.auth.login.config", jaasUrl.toExternalForm()); try { - LoginContext lc = new LoginContext("SINGLE_USER"); + LoginContext lc = new LoginContext(CLIENT_LOGIN_CONTEXT, new ConsoleCallbackHandler()); lc.login(); HttpClient httpClient = openHttpClient(lc.getSubject()); @@ -57,31 +68,45 @@ public class SpnegoHttpClient { private static HttpClient openHttpClient(Subject subject) { HttpClient client = HttpClient.newBuilder() // -// .sslContext(insecureContext()) // + .sslContext(ipaSslContext()) // .version(HttpClient.Version.HTTP_1_1) // .build(); return client; } - static SSLContext insecureContext() { - TrustManager[] noopTrustManager = new TrustManager[] { new X509TrustManager() { - public void checkClientTrusted(X509Certificate[] xcs, String string) { + @SuppressWarnings("unchecked") + static SSLContext ipaSslContext() { + try { + final Collection certificates; + Path caCertificatePath = Paths.get("/etc/ipa/ca.crt"); + if (Files.exists(caCertificatePath)) { + CertificateFactory certificateFactory = CertificateFactory.getInstance("X509"); + try (BufferedInputStream in = new BufferedInputStream(Files.newInputStream(caCertificatePath))) { + certificates = (Collection) certificateFactory.generateCertificates(in); + } + } else { + certificates = null; } + TrustManager[] noopTrustManager = new TrustManager[] { new X509TrustManager() { + public void checkClientTrusted(X509Certificate[] xcs, String string) { + } - public void checkServerTrusted(X509Certificate[] xcs, String string) { - } + public void checkServerTrusted(X509Certificate[] xcs, String string) { + } + + public X509Certificate[] getAcceptedIssuers() { + if (certificates == null) + return null; + return certificates.toArray(new X509Certificate[certificates.size()]); + } + } }; - public X509Certificate[] getAcceptedIssuers() { - return null; - } - } }; - try { SSLContext sc = SSLContext.getInstance("ssl"); sc.init(null, noopTrustManager, null); return sc; - } catch (KeyManagementException | NoSuchAlgorithmException e) { - throw new IllegalStateException("Cannot create insecure SSL context ", e); + } catch (KeyManagementException | NoSuchAlgorithmException | CertificateException | IOException e) { + throw new IllegalStateException("Cannot create SSL context ", e); } } diff --git a/org.argeo.cms/src/org/argeo/cms/client/jaas-client-ipa.cfg b/org.argeo.cms/src/org/argeo/cms/client/jaas-client-ipa.cfg new file mode 100644 index 000000000..2921a3397 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/client/jaas-client-ipa.cfg @@ -0,0 +1,4 @@ +CLIENT { + com.sun.security.auth.module.Krb5LoginModule required + useTicketCache=true; +}; diff --git a/org.argeo.cms/src/org/argeo/cms/client/jaas.cfg b/org.argeo.cms/src/org/argeo/cms/client/jaas.cfg deleted file mode 100644 index dc540ddb2..000000000 --- a/org.argeo.cms/src/org/argeo/cms/client/jaas.cfg +++ /dev/null @@ -1,10 +0,0 @@ -SINGLE_USER { - com.sun.security.auth.module.Krb5LoginModule required - useTicketCache=true - debug=true; -}; - -com.sun.security.jgss.krb5.initiate { - com.sun.security.auth.module.Krb5LoginModule - required useTicketCache=true; -}; \ No newline at end of file diff --git a/org.argeo.cms/src/org/argeo/cms/internal/auth/ConsoleCallbackHandler.java b/org.argeo.cms/src/org/argeo/cms/internal/auth/ConsoleCallbackHandler.java deleted file mode 100644 index 0979a2157..000000000 --- a/org.argeo.cms/src/org/argeo/cms/internal/auth/ConsoleCallbackHandler.java +++ /dev/null @@ -1,65 +0,0 @@ -package org.argeo.cms.internal.auth; - -import java.io.Console; -import java.io.IOException; -import java.io.PrintWriter; -import java.util.Arrays; - -import javax.security.auth.callback.Callback; -import javax.security.auth.callback.CallbackHandler; -import javax.security.auth.callback.NameCallback; -import javax.security.auth.callback.PasswordCallback; -import javax.security.auth.callback.TextOutputCallback; -import javax.security.auth.callback.UnsupportedCallbackException; - -/** Callback handler to be used with a command line UI. */ -public class ConsoleCallbackHandler implements CallbackHandler { - - @Override - public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { - Console console = System.console(); - if (console == null) - throw new IllegalStateException("No console available"); - - PrintWriter writer = console.writer(); - for (int i = 0; i < callbacks.length; i++) { - if (callbacks[i] instanceof TextOutputCallback) { - TextOutputCallback callback = (TextOutputCallback) callbacks[i]; - writer.write(callback.getMessage()); - } else if (callbacks[i] instanceof NameCallback) { - NameCallback callback = (NameCallback) callbacks[i]; - writer.write(callback.getPrompt()); - if (callback.getDefaultName() != null) - writer.write(" (" + callback.getDefaultName() + ")"); - writer.write(" : "); - String answer = console.readLine(); - if (callback.getDefaultName() != null && answer.trim().equals("")) - callback.setName(callback.getDefaultName()); - else - callback.setName(answer); - } else if (callbacks[i] instanceof PasswordCallback) { - PasswordCallback callback = (PasswordCallback) callbacks[i]; - writer.write(callback.getPrompt()); - char[] answer = console.readPassword(); - callback.setPassword(answer); - Arrays.fill(answer, ' '); - } -// else if (callbacks[i] instanceof LocaleChoice) { -// LocaleChoice callback = (LocaleChoice) callbacks[i]; -// writer.write("Language"); -// writer.write("\n"); -// for (int j = 0; j < callback.getLocales().size(); j++) { -// Locale locale = callback.getLocales().get(j); -// writer.print(j + " : " + locale.getDisplayName() + "\n"); -// } -// writer.write("(" + callback.getDefaultIndex() + ") : "); -// String answer = console.readLine(); -// if (answer.trim().equals("")) -// callback.setSelectedIndex(callback.getDefaultIndex()); -// else -// callback.setSelectedIndex(new Integer(answer.trim())); -// } - } - } - -} diff --git a/org.argeo.cms/src/org/argeo/cms/internal/runtime/CmsStateImpl.java b/org.argeo.cms/src/org/argeo/cms/internal/runtime/CmsStateImpl.java index c9109c856..b76ca5792 100644 --- a/org.argeo.cms/src/org/argeo/cms/internal/runtime/CmsStateImpl.java +++ b/org.argeo.cms/src/org/argeo/cms/internal/runtime/CmsStateImpl.java @@ -1,5 +1,6 @@ package org.argeo.cms.internal.runtime; +import java.io.BufferedInputStream; import java.io.IOException; import java.io.Reader; import java.net.InetAddress; @@ -66,19 +67,19 @@ public class CmsStateImpl implements CmsState { deployPropertyDefaults.put(CmsDeployProperty.LOCALE, Locale.getDefault().toString()); // certificates - deployPropertyDefaults.put(CmsDeployProperty.SSL_KEYSTORETYPE, PkiUtils.PKCS12); - deployPropertyDefaults.put(CmsDeployProperty.SSL_PASSWORD, PkiUtils.DEFAULT_KEYSTORE_PASSWORD); - Path keyStorePath = getDataPath(PkiUtils.DEFAULT_KEYSTORE_PATH); + deployPropertyDefaults.put(CmsDeployProperty.SSL_KEYSTORETYPE, KernelConstants.PKCS12); + deployPropertyDefaults.put(CmsDeployProperty.SSL_PASSWORD, KernelConstants.DEFAULT_KEYSTORE_PASSWORD); + Path keyStorePath = getDataPath(KernelConstants.DEFAULT_KEYSTORE_PATH); if (keyStorePath != null) { deployPropertyDefaults.put(CmsDeployProperty.SSL_KEYSTORE, keyStorePath.toAbsolutePath().toString()); } - Path trustStorePath = getDataPath(PkiUtils.DEFAULT_TRUSTSTORE_PATH); + Path trustStorePath = getDataPath(KernelConstants.DEFAULT_TRUSTSTORE_PATH); if (trustStorePath != null) { deployPropertyDefaults.put(CmsDeployProperty.SSL_TRUSTSTORE, trustStorePath.toAbsolutePath().toString()); } - deployPropertyDefaults.put(CmsDeployProperty.SSL_TRUSTSTORETYPE, PkiUtils.PKCS12); - deployPropertyDefaults.put(CmsDeployProperty.SSL_TRUSTSTOREPASSWORD, PkiUtils.DEFAULT_KEYSTORE_PASSWORD); + deployPropertyDefaults.put(CmsDeployProperty.SSL_TRUSTSTORETYPE, KernelConstants.PKCS12); + deployPropertyDefaults.put(CmsDeployProperty.SSL_TRUSTSTOREPASSWORD, KernelConstants.DEFAULT_KEYSTORE_PASSWORD); // SSH Path authorizedKeysPath = getDataPath(KernelConstants.NODE_SSHD_AUTHORIZED_KEYS_PATH); @@ -193,8 +194,8 @@ public class CmsStateImpl implements CmsState { private void initCertificates() { // server certificate Path keyStorePath = Paths.get(getDeployProperty(CmsDeployProperty.SSL_KEYSTORE)); - Path pemKeyPath = getDataPath(PkiUtils.DEFAULT_PEM_KEY_PATH); - Path pemCertPath = getDataPath(PkiUtils.DEFAULT_PEM_CERT_PATH); + Path pemKeyPath = getDataPath(KernelConstants.DEFAULT_PEM_KEY_PATH); + Path pemCertPath = getDataPath(KernelConstants.DEFAULT_PEM_CERT_PATH); char[] keyStorePassword = getDeployProperty(CmsDeployProperty.SSL_PASSWORD).toCharArray(); // Keystore @@ -204,7 +205,7 @@ public class CmsStateImpl implements CmsState { KeyStore keyStore = PkiUtils.getKeyStore(keyStorePath, keyStorePassword, getDeployProperty(CmsDeployProperty.SSL_KEYSTORETYPE)); try (Reader key = Files.newBufferedReader(pemKeyPath, StandardCharsets.US_ASCII); - Reader cert = Files.newBufferedReader(pemCertPath, StandardCharsets.US_ASCII);) { + BufferedInputStream cert = new BufferedInputStream(Files.newInputStream(pemCertPath));) { PkiUtils.loadPrivateCertificatePem(keyStore, CmsConstants.NODE, key, keyStorePassword, cert); Files.createDirectories(keyStorePath.getParent()); PkiUtils.saveKeyStore(keyStorePath, keyStorePassword, keyStore); @@ -220,11 +221,11 @@ public class CmsStateImpl implements CmsState { char[] trustStorePassword = getDeployProperty(CmsDeployProperty.SSL_TRUSTSTOREPASSWORD).toCharArray(); // IPA CA - Path ipaCaCertPath = Paths.get(PkiUtils.IPA_PEM_CA_CERT_PATH); + Path ipaCaCertPath = Paths.get(KernelConstants.IPA_PEM_CA_CERT_PATH); if (Files.exists(ipaCaCertPath)) { KeyStore trustStore = PkiUtils.getKeyStore(trustStorePath, trustStorePassword, getDeployProperty(CmsDeployProperty.SSL_TRUSTSTORETYPE)); - try (Reader cert = Files.newBufferedReader(ipaCaCertPath, StandardCharsets.US_ASCII);) { + try (BufferedInputStream cert = new BufferedInputStream(Files.newInputStream(ipaCaCertPath));) { PkiUtils.loadTrustedCertificatePem(trustStore, trustStorePassword, cert); Files.createDirectories(keyStorePath.getParent()); PkiUtils.saveKeyStore(trustStorePath, trustStorePassword, trustStore); @@ -235,16 +236,8 @@ public class CmsStateImpl implements CmsState { } } - if (!Files.exists(keyStorePath)) - PkiUtils.createSelfSignedKeyStore(keyStorePath, keyStorePassword, PkiUtils.PKCS12); -// props.put(JettyHttpConstants.SSL_KEYSTORETYPE, PkiUtils.PKCS12); -// props.put(JettyHttpConstants.SSL_KEYSTORE, keyStorePath.toString()); -// props.put(JettyHttpConstants.SSL_PASSWORD, new String(keyStorePassword)); - -// props.put(InternalHttpConstants.SSL_KEYSTORETYPE, "PKCS11"); -// props.put(InternalHttpConstants.SSL_KEYSTORE, "../../nssdb"); -// props.put(InternalHttpConstants.SSL_PASSWORD, keyStorePassword); - +// if (!Files.exists(keyStorePath)) +// PkiUtils.createSelfSignedKeyStore(keyStorePath, keyStorePassword, PkiUtils.PKCS12); } public void stop() { diff --git a/org.argeo.cms/src/org/argeo/cms/internal/runtime/KernelConstants.java b/org.argeo.cms/src/org/argeo/cms/internal/runtime/KernelConstants.java index c80e86aea..e6ca1ba60 100644 --- a/org.argeo.cms/src/org/argeo/cms/internal/runtime/KernelConstants.java +++ b/org.argeo.cms/src/org/argeo/cms/internal/runtime/KernelConstants.java @@ -1,7 +1,9 @@ package org.argeo.cms.internal.runtime; +import org.argeo.api.cms.CmsConstants; + /** Internal CMS constants. */ -public interface KernelConstants { +interface KernelConstants { // Directories String DIR_PRIVATE = "private"; @@ -16,6 +18,20 @@ public interface KernelConstants { // KERBEROS String DEFAULT_KERBEROS_SERVICE = "HTTP"; + String DEFAULT_KEYSTORE_PATH = DIR_PRIVATE + '/' + CmsConstants.NODE + ".p12"; + + String DEFAULT_TRUSTSTORE_PATH = DIR_PRIVATE + "/trusted.p12"; + + String DEFAULT_PEM_KEY_PATH = DIR_PRIVATE + '/' + CmsConstants.NODE + ".key"; + + String DEFAULT_PEM_CERT_PATH = DIR_PRIVATE + '/' + CmsConstants.NODE + ".crt"; + + String IPA_PEM_CA_CERT_PATH = "/etc/ipa/ca.crt"; + + String DEFAULT_KEYSTORE_PASSWORD = "changeit"; + + String PKCS12 = "PKCS12"; + // HTTP client // String COOKIE_POLICY_BROWSER_COMPATIBILITY = "compatibility"; diff --git a/org.argeo.cms/src/org/argeo/cms/internal/runtime/PkiUtils.java b/org.argeo.cms/src/org/argeo/cms/internal/runtime/PkiUtils.java index f47e54421..378146ea1 100644 --- a/org.argeo.cms/src/org/argeo/cms/internal/runtime/PkiUtils.java +++ b/org.argeo.cms/src/org/argeo/cms/internal/runtime/PkiUtils.java @@ -1,111 +1,38 @@ package org.argeo.cms.internal.runtime; +import java.io.BufferedInputStream; +import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.Reader; -import java.math.BigInteger; -import java.net.InetAddress; import java.nio.file.Files; import java.nio.file.Path; import java.security.GeneralSecurityException; -import java.security.KeyPair; -import java.security.KeyPairGenerator; +import java.security.KeyFactory; import java.security.KeyStore; import java.security.KeyStore.TrustedCertificateEntry; import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; -import java.security.SecureRandom; -import java.security.Security; -import java.security.cert.Certificate; import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; -import java.util.Arrays; -import java.util.Date; +import java.security.interfaces.RSAPrivateKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Base64; +import java.util.Collection; import java.util.Objects; -import javax.security.auth.x500.X500Principal; - -import org.argeo.api.cms.CmsConstants; -import org.argeo.api.cms.CmsLog; -import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; -import org.bouncycastle.cert.X509CertificateHolder; -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.openssl.PEMParser; -import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; -import org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8DecryptorProviderBuilder; -import org.bouncycastle.operator.ContentSigner; -import org.bouncycastle.operator.InputDecryptorProvider; -import org.bouncycastle.operator.OperatorCreationException; -import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; -import org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo; -import org.bouncycastle.pkcs.PKCSException; - /** * Utilities around private keys and certificate, mostly wrapping BouncyCastle * implementations. */ class PkiUtils { - private final static CmsLog log = CmsLog.getLog(PkiUtils.class); - - final static String PKCS12 = "PKCS12"; - final static String JKS = "JKS"; - - static final String DEFAULT_KEYSTORE_PATH = KernelConstants.DIR_PRIVATE + '/' + CmsConstants.NODE + ".p12"; - - static final String DEFAULT_TRUSTSTORE_PATH = KernelConstants.DIR_PRIVATE + "/trusted.p12"; - - static final String DEFAULT_PEM_KEY_PATH = KernelConstants.DIR_PRIVATE + '/' + CmsConstants.NODE + ".key"; - - static final String DEFAULT_PEM_CERT_PATH = KernelConstants.DIR_PRIVATE + '/' + CmsConstants.NODE + ".crt"; - - static final String IPA_PEM_CA_CERT_PATH = "/etc/ipa/ca.crt"; - - static final String DEFAULT_KEYSTORE_PASSWORD = "changeit"; - - private final static String SUN_SECURITY_PROVIDER; - private final static String SUN_JSSE_SECURITY_PROVIDER; - private final static String BC_SECURITY_PROVIDER; - static { - Security.addProvider(new BouncyCastleProvider()); - // BouncyCastle does not store trusted certificates properly - // TODO report it - BC_SECURITY_PROVIDER = "BC"; - SUN_SECURITY_PROVIDER = "SUN"; - SUN_JSSE_SECURITY_PROVIDER = "SunJSSE"; - } - - public static X509Certificate generateSelfSignedCertificate(KeyStore keyStore, X500Principal x500Principal, - int keySize, char[] keyPassword) { - try { - KeyPairGenerator kpGen = KeyPairGenerator.getInstance("RSA", BC_SECURITY_PROVIDER); - kpGen.initialize(keySize, new SecureRandom()); - KeyPair pair = kpGen.generateKeyPair(); - Date notBefore = new Date(System.currentTimeMillis() - 10000); - Date notAfter = new Date(System.currentTimeMillis() + 365 * 24L * 3600 * 1000); - BigInteger serial = BigInteger.valueOf(System.currentTimeMillis()); - X509v3CertificateBuilder certGen = new JcaX509v3CertificateBuilder(x500Principal, serial, notBefore, - notAfter, x500Principal, pair.getPublic()); - ContentSigner sigGen = new JcaContentSignerBuilder("SHA256WithRSAEncryption") - .setProvider(BC_SECURITY_PROVIDER).build(pair.getPrivate()); - X509Certificate cert = new JcaX509CertificateConverter().setProvider(BC_SECURITY_PROVIDER) - .getCertificate(certGen.build(sigGen)); - cert.checkValidity(new Date()); - cert.verify(cert.getPublicKey()); - - keyStore.setKeyEntry(x500Principal.getName(), pair.getPrivate(), keyPassword, new Certificate[] { cert }); - return cert; - } catch (GeneralSecurityException | OperatorCreationException e) { - throw new RuntimeException("Cannot generate self-signed certificate", e); - } - } - public static KeyStore getKeyStore(Path keyStoreFile, char[] keyStorePassword, String keyStoreType) { try { - KeyStore store = KeyStore.getInstance(keyStoreType, SUN_JSSE_SECURITY_PROVIDER); + KeyStore store = KeyStore.getInstance(keyStoreType); if (Files.exists(keyStoreFile)) { try (InputStream fis = Files.newInputStream(keyStoreFile)) { store.load(fis, keyStorePassword); @@ -129,44 +56,8 @@ class PkiUtils { } } -// public static byte[] pemToPKCS12(final String keyFile, final String cerFile, final String password) -// throws Exception { -// // Get the private key -// FileReader reader = new FileReader(keyFile); -// -// PEMReader pem = new PemReader(reader, new PasswordFinder() { -// @Override -// public char[] getPassword() { -// return password.toCharArray(); -// } -// }); -// -// PrivateKey key = ((KeyPair) pem.readObject()).getPrivate(); -// -// pem.close(); -// reader.close(); -// -// // Get the certificate -// reader = new FileReader(cerFile); -// pem = new PEMReader(reader); -// -// X509Certificate cert = (X509Certificate) pem.readObject(); -// -// pem.close(); -// reader.close(); -// -// // Put them into a PKCS12 keystore and write it to a byte[] -// ByteArrayOutputStream bos = new ByteArrayOutputStream(); -// KeyStore ks = KeyStore.getInstance("PKCS12"); -// ks.load(null); -// ks.setKeyEntry("alias", (Key) key, password.toCharArray(), new java.security.cert.Certificate[] { cert }); -// ks.store(bos, password.toCharArray()); -// bos.close(); -// return bos.toByteArray(); -// } - public static void loadPrivateCertificatePem(KeyStore keyStore, String alias, Reader key, char[] keyPassword, - Reader cert) { + BufferedInputStream cert) { Objects.requireNonNull(keyStore); Objects.requireNonNull(key); try { @@ -178,7 +69,7 @@ class PkiUtils { } } - public static void loadTrustedCertificatePem(KeyStore keyStore,char[] keyStorePassword, Reader cert) { + public static void loadTrustedCertificatePem(KeyStore keyStore, char[] keyStorePassword, BufferedInputStream cert) { try { X509Certificate certificate = loadPemCertificate(cert); TrustedCertificateEntry trustedCertificateEntry = new TrustedCertificateEntry(certificate); @@ -189,139 +80,43 @@ class PkiUtils { } public static PrivateKey loadPemPrivateKey(Reader reader, char[] keyPassword) { - try (PEMParser pemParser = new PEMParser(reader)) { - JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider(BC_SECURITY_PROVIDER); - Object object = pemParser.readObject(); - PrivateKeyInfo privateKeyInfo; - if (object instanceof PKCS8EncryptedPrivateKeyInfo) { - if (keyPassword == null) - throw new IllegalArgumentException("A key password is required"); - InputDecryptorProvider decProv = new JceOpenSSLPKCS8DecryptorProviderBuilder().build(keyPassword); - privateKeyInfo = ((PKCS8EncryptedPrivateKeyInfo) object).decryptPrivateKeyInfo(decProv); - } else if (object instanceof PrivateKeyInfo) { - privateKeyInfo = (PrivateKeyInfo) object; - } else { - throw new IllegalArgumentException("Unsupported format for private key"); + try { + StringBuilder key = new StringBuilder(); + try (BufferedReader in = new BufferedReader(reader)) { + String line = in.readLine(); + if (!"-----BEGIN PRIVATE KEY-----".equals(line)) + throw new IllegalArgumentException("Not a PEM private key"); + lines: while ((line = in.readLine()) != null) { + if ("-----END PRIVATE KEY-----".equals(line)) + break lines; + key.append(line); + } } - return converter.getPrivateKey(privateKeyInfo); - } catch (IOException | OperatorCreationException | PKCSException e) { - throw new RuntimeException("Cannot read private key", e); - } - } - public static X509Certificate loadPemCertificate(Reader reader) { - try (PEMParser pemParser = new PEMParser(reader)) { - X509CertificateHolder certHolder = (X509CertificateHolder) pemParser.readObject(); - X509Certificate cert = new JcaX509CertificateConverter().setProvider(SUN_SECURITY_PROVIDER) - .getCertificate(certHolder); - return cert; - } catch (IOException | CertificateException e) { - throw new RuntimeException("Cannot read private key", e); - } - } + byte[] encoded = Base64.getDecoder().decode(key.toString()); - public static void main(String[] args) throws Exception { - final String ALGORITHM = "RSA"; - final String provider = "BC"; - SecureRandom secureRandom = new SecureRandom(); - long begin = System.currentTimeMillis(); - for (int i = 512; i < 1024; i = i + 2) { - try { - KeyPairGenerator keyGen = KeyPairGenerator.getInstance(ALGORITHM, provider); - keyGen.initialize(i, secureRandom); - keyGen.generateKeyPair(); - } catch (Exception e) { - System.err.println(i + " : " + e.getMessage()); - } + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(encoded); + return (RSAPrivateKey) keyFactory.generatePrivate(keySpec); + } catch (NoSuchAlgorithmException | InvalidKeySpecException | IOException e) { + throw new RuntimeException("Cannot load PEM key", e); } - System.out.println((System.currentTimeMillis() - begin) + " ms"); - - // // String text = "a"; - // String text = - // "testtesttesttesttesttesttesttesttesttesttesttesttesttesttest"; - // try { - // System.out.println(text); - // PrivateKey privateKey; - // PublicKey publicKey; - // char[] password = "changeit".toCharArray(); - // String alias = "CN=test"; - // KeyStore keyStore = KeyStore.getInstance("pkcs12"); - // File p12file = new File("test.p12"); - // p12file.delete(); - // if (!p12file.exists()) { - // keyStore.load(null); - // generateSelfSignedCertificate(keyStore, new X500Principal(alias), - // 513, password); - // try (OutputStream out = new FileOutputStream(p12file)) { - // keyStore.store(out, password); - // } - // } - // try (InputStream in = new FileInputStream(p12file)) { - // keyStore.load(in, password); - // privateKey = (PrivateKey) keyStore.getKey(alias, password); - // publicKey = keyStore.getCertificateChain(alias)[0].getPublicKey(); - // } - // // KeyPair key; - // // final KeyPairGenerator keyGen = - // // KeyPairGenerator.getInstance(ALGORITHM); - // // keyGen.initialize(4096, new SecureRandom()); - // // long begin = System.currentTimeMillis(); - // // key = keyGen.generateKeyPair(); - // // System.out.println((System.currentTimeMillis() - begin) + " ms"); - // // keyStore.load(null); - // // keyStore.setKeyEntry("test", key.getPrivate(), password, null); - // // try(OutputStream out=new FileOutputStream(p12file)) { - // // keyStore.store(out, password); - // // } - // // privateKey = key.getPrivate(); - // // publicKey = key.getPublic(); - // - // Cipher encrypt = Cipher.getInstance(ALGORITHM); - // encrypt.init(Cipher.ENCRYPT_MODE, publicKey); - // byte[] encrypted = encrypt.doFinal(text.getBytes()); - // String encryptedBase64 = - // Base64.getEncoder().encodeToString(encrypted); - // System.out.println(encryptedBase64); - // byte[] encryptedFromBase64 = - // Base64.getDecoder().decode(encryptedBase64); - // - // Cipher decrypt = Cipher.getInstance(ALGORITHM); - // decrypt.init(Cipher.DECRYPT_MODE, privateKey); - // byte[] decrypted = decrypt.doFinal(encryptedFromBase64); - // System.out.println(new String(decrypted)); - // } catch (Exception e) { - // e.printStackTrace(); - // } } - public static void createSelfSignedKeyStore(Path keyStorePath, char[] keyStorePassword, String keyStoreType) { - // for (Provider provider : Security.getProviders()) - // System.out.println(provider.getName()); - // File keyStoreFile = keyStorePath.toFile(); - char[] keyPwd = Arrays.copyOf(keyStorePassword, keyStorePassword.length); - if (!Files.exists(keyStorePath)) { - try { - Files.createDirectories(keyStorePath.getParent()); - KeyStore keyStore = getKeyStore(keyStorePath, keyStorePassword, keyStoreType); - generateSelfSignedCertificate(keyStore, - new X500Principal("CN=" + InetAddress.getLocalHost().getHostName() + ",OU=UNSECURE,O=UNSECURE"), - 1024, keyPwd); - saveKeyStore(keyStorePath, keyStorePassword, keyStore); - if (log.isDebugEnabled()) - log.debug("Created self-signed unsecure keystore " + keyStorePath); - } catch (Exception e) { - try { - if (Files.size(keyStorePath) == 0) - Files.delete(keyStorePath); - } catch (IOException e1) { - // silent - } - log.error("Cannot create keystore " + keyStorePath, e); - } - } else { - throw new IllegalStateException("Keystore " + keyStorePath + " already exists"); + public static X509Certificate loadPemCertificate(BufferedInputStream in) { + try { + CertificateFactory certificateFactory = CertificateFactory.getInstance("X509"); + @SuppressWarnings("unchecked") + Collection certificates = (Collection) certificateFactory + .generateCertificates(in); + if (certificates.isEmpty()) + throw new IllegalArgumentException("No certificate found"); + if (certificates.size() != 1) + throw new IllegalArgumentException(certificates.size() + " certificates found"); + return certificates.iterator().next(); + } catch (CertificateException e) { + throw new IllegalStateException("cannot load certifciate", e); } } - }