java - from - Obtener de forma programática KeyStore de PEM
keytool (1)
¿Cómo se puede obtener mediante programación un KeyStore de un archivo PEM que contiene un certificado y una clave privada? Estoy intentando proporcionar un certificado de cliente a un servidor en una conexión HTTPS. He confirmado que el certificado de cliente funciona si utilizo openssl y keytool para obtener un archivo jks, que se carga dinámicamente. Incluso puedo hacer que funcione leyendo dinámicamente en un archivo p12 (PKCS12).
Estoy estudiando el uso de la clase PEMReader de BouncyCastle, pero no puedo superar algunos errores. Estoy ejecutando el cliente Java con la opción -Djavax.net.debug = all y el servidor web Apache con el debug LogLevel. Aunque no estoy seguro de qué buscar. El registro de errores de Apache indica:
...
OpenSSL: Write: SSLv3 read client certificate B
OpenSSL: Exit: error in SSLv3 read client certificate B
Re-negotiation handshake failed: Not accepted by client!?
El programa cliente de Java indica:
...
main, WRITE: TLSv1 Handshake, length = 48
main, waiting for close_notify or alert: state 3
main, Exception while waiting for close java.net.SocketException: Software caused connection abort: recv failed
main, handling exception: java.net.SocketException: Software caused connection abort: recv failed
%% Invalidated: [Session-3, TLS_RSA_WITH_AES_128_CBC_SHA]
main, SEND TLSv1 ALERT: fatal, description = unexpected_message
...
El código del cliente:
public void testClientCertPEM() throws Exception {
String requestURL = "https://mydomain/authtest";
String pemPath = "C:/Users/myusername/Desktop/client.pem";
HttpsURLConnection con;
URL url = new URL(requestURL);
con = (HttpsURLConnection) url.openConnection();
con.setSSLSocketFactory(getSocketFactoryFromPEM(pemPath));
con.setRequestMethod("GET");
con.setDoInput(true);
con.setDoOutput(false);
con.connect();
String line;
BufferedReader reader = new BufferedReader(new InputStreamReader(con.getInputStream()));
while((line = reader.readLine()) != null) {
System.out.println(line);
}
reader.close();
con.disconnect();
}
public SSLSocketFactory getSocketFactoryFromPEM(String pemPath) throws Exception {
Security.addProvider(new BouncyCastleProvider());
SSLContext context = SSLContext.getInstance("TLS");
PEMReader reader = new PEMReader(new FileReader(pemPath));
X509Certificate cert = (X509Certificate) reader.readObject();
KeyStore keystore = KeyStore.getInstance("JKS");
keystore.load(null);
keystore.setCertificateEntry("alias", cert);
KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
kmf.init(keystore, null);
KeyManager[] km = kmf.getKeyManagers();
context.init(km, null, null);
return context.getSocketFactory();
}
Noté que el servidor está emitiendo SSLv3 en el registro mientras que el cliente es TLSv1. Si agrego la propiedad del sistema -Dhttps.protocols = SSLv3, el cliente también usará SSLv3, pero aparece el mismo mensaje de error. También he intentado agregar -Dsun.security.ssl.allowUnsafeRenegotiation = true sin ningún cambio en el resultado.
He buscado en Google y la respuesta habitual para esta pregunta es usar openssl y keytool primero. En mi caso necesito leer el PEM directamente sobre la marcha. De hecho, estoy portando un programa en C ++ que ya lo hace, y francamente, estoy muy sorprendido de lo difícil que es hacerlo en Java. El código C ++:
curlpp::Easy request;
...
request.setOpt(new Options::Url(myurl));
request.setOpt(new Options::SslVerifyPeer(false));
request.setOpt(new Options::SslCertType("PEM"));
request.setOpt(new Options::SslCert(cert));
request.perform();
Me lo imaginé. El problema es que el certificado X509 por sí mismo no es suficiente. También necesitaba colocar la clave privada en el almacén de claves generado dinámicamente. No parece que BouncyCastle PEMReader pueda manejar un archivo PEM con certificado y clave privada de una sola vez, pero puede manejar cada pieza por separado. Yo mismo puedo leer el PEM en la memoria y dividirlo en dos flujos separados y luego enviar cada uno a un PEMReader separado. Como sé que los archivos PEM con los que estoy tratando tendrán primero el certificado y luego la clave privada, puedo simplificar el código a costa de la robustez. También sé que el delimitador de CERTIFICADO FINAL siempre estará rodeado de cinco guiones. La implementación que me funciona es:
protected static SSLSocketFactory getSocketFactoryPEM(String pemPath) throws Exception {
Security.addProvider(new BouncyCastleProvider());
SSLContext context = SSLContext.getInstance("TLS");
byte[] certAndKey = fileToBytes(new File(pemPath));
String delimiter = "-----END CERTIFICATE-----";
String[] tokens = new String(certAndKey).split(delimiter);
byte[] certBytes = tokens[0].concat(delimiter).getBytes();
byte[] keyBytes = tokens[1].getBytes();
PEMReader reader;
reader = new PEMReader(new InputStreamReader(new ByteArrayInputStream(certBytes)));
X509Certificate cert = (X509Certificate)reader.readObject();
reader = new PEMReader(new InputStreamReader(new ByteArrayInputStream(keyBytes)));
PrivateKey key = (PrivateKey)reader.readObject();
KeyStore keystore = KeyStore.getInstance("JKS");
keystore.load(null);
keystore.setCertificateEntry("cert-alias", cert);
keystore.setKeyEntry("key-alias", key, "changeit".toCharArray(), new Certificate[] {cert});
KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
kmf.init(keystore, "changeit".toCharArray());
KeyManager[] km = kmf.getKeyManagers();
context.init(km, null, null);
return context.getSocketFactory();
}
Actualización : Parece que esto se puede hacer sin BouncyCastle:
byte[] certAndKey = fileToBytes(new File(pemPath));
byte[] certBytes = parseDERFromPEM(certAndKey, "-----BEGIN CERTIFICATE-----", "-----END CERTIFICATE-----");
byte[] keyBytes = parseDERFromPEM(certAndKey, "-----BEGIN PRIVATE KEY-----", "-----END PRIVATE KEY-----");
X509Certificate cert = generateCertificateFromDER(certBytes);
RSAPrivateKey key = generatePrivateKeyFromDER(keyBytes);
...
protected static byte[] parseDERFromPEM(byte[] pem, String beginDelimiter, String endDelimiter) {
String data = new String(pem);
String[] tokens = data.split(beginDelimiter);
tokens = tokens[1].split(endDelimiter);
return DatatypeConverter.parseBase64Binary(tokens[0]);
}
protected static RSAPrivateKey generatePrivateKeyFromDER(byte[] keyBytes) throws InvalidKeySpecException, NoSuchAlgorithmException {
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory factory = KeyFactory.getInstance("RSA");
return (RSAPrivateKey)factory.generatePrivate(spec);
}
protected static X509Certificate generateCertificateFromDER(byte[] certBytes) throws CertificateException {
CertificateFactory factory = CertificateFactory.getInstance("X.509");
return (X509Certificate)factory.generateCertificate(new ByteArrayInputStream(certBytes));
}