From 39f4c809d1ca4af8b5401d49061be04170d77f15 Mon Sep 17 00:00:00 2001 From: Jon Chambers Date: Wed, 3 Feb 2016 10:45:56 -0500 Subject: [PATCH] Allow InputStreams for key/trust managers in SslContextBuilder Motivation: Sometimes it's easier to get keys/certificates as `InputStream`s than it is to get an actual `File`. This is especially true when operating in a container environment and `getResourceAsInputStream` is the best way to load resources packaged with an application. Modifications: - Add read-from-`InputStream` methods to `PemReader` - Allow `SslContext` to get keys/certificates from `InputStreams` - Add `InputStream`-based setters for key/trust managers to `SslContextBuilder` Result: Callers may pass an `InputStream` instead of a `File` to `SslContextBuilder`. --- .../java/io/netty/handler/ssl/PemReader.java | 45 ++++++++--- .../java/io/netty/handler/ssl/SslContext.java | 31 +++++++- .../netty/handler/ssl/SslContextBuilder.java | 75 +++++++++++++++++++ 3 files changed, 140 insertions(+), 11 deletions(-) diff --git a/handler/src/main/java/io/netty/handler/ssl/PemReader.java b/handler/src/main/java/io/netty/handler/ssl/PemReader.java index ee606000bf22..cec74861d134 100644 --- a/handler/src/main/java/io/netty/handler/ssl/PemReader.java +++ b/handler/src/main/java/io/netty/handler/ssl/PemReader.java @@ -26,6 +26,7 @@ import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -56,11 +57,25 @@ final class PemReader { Pattern.CASE_INSENSITIVE); static ByteBuf[] readCertificates(File file) throws CertificateException { + try { + InputStream in = new FileInputStream(file); + + try { + return readCertificates(in); + } finally { + safeClose(in); + } + } catch (FileNotFoundException e) { + throw new CertificateException("could not find certificate file: " + file); + } + } + + static ByteBuf[] readCertificates(InputStream in) throws CertificateException { String content; try { - content = readContent(file); + content = readContent(in); } catch (IOException e) { - throw new CertificateException("failed to read a file: " + file, e); + throw new CertificateException("failed to read certificate input stream", e); } List certs = new ArrayList(); @@ -80,23 +95,37 @@ static ByteBuf[] readCertificates(File file) throws CertificateException { } if (certs.isEmpty()) { - throw new CertificateException("found no certificates: " + file); + throw new CertificateException("found no certificates in input stream"); } return certs.toArray(new ByteBuf[certs.size()]); } static ByteBuf readPrivateKey(File file) throws KeyException { + try { + InputStream in = new FileInputStream(file); + + try { + return readPrivateKey(in); + } finally { + safeClose(in); + } + } catch (FileNotFoundException e) { + throw new KeyException("could not fine key file: " + file); + } + } + + static ByteBuf readPrivateKey(InputStream in) throws KeyException { String content; try { - content = readContent(file); + content = readContent(in); } catch (IOException e) { - throw new KeyException("failed to read a file: " + file, e); + throw new KeyException("failed to read key input stream", e); } Matcher m = KEY_PATTERN.matcher(content); if (!m.find()) { - throw new KeyException("found no private key: " + file); + throw new KeyException("found no private key in input stream"); } ByteBuf base64 = Unpooled.copiedBuffer(m.group(1), CharsetUtil.US_ASCII); @@ -105,8 +134,7 @@ static ByteBuf readPrivateKey(File file) throws KeyException { return der; } - private static String readContent(File file) throws IOException { - InputStream in = new FileInputStream(file); + private static String readContent(InputStream in) throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); try { byte[] buf = new byte[8192]; @@ -119,7 +147,6 @@ private static String readContent(File file) throws IOException { } return out.toString(CharsetUtil.US_ASCII.name()); } finally { - safeClose(in); safeClose(out); } } diff --git a/handler/src/main/java/io/netty/handler/ssl/SslContext.java b/handler/src/main/java/io/netty/handler/ssl/SslContext.java index b3d9dfc25a2e..d64495ab86e1 100644 --- a/handler/src/main/java/io/netty/handler/ssl/SslContext.java +++ b/handler/src/main/java/io/netty/handler/ssl/SslContext.java @@ -42,6 +42,7 @@ import javax.security.auth.x500.X500Principal; import java.io.File; import java.io.IOException; +import java.io.InputStream; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.KeyException; @@ -909,7 +910,23 @@ static PrivateKey toPrivateKey(File keyFile, String keyPassword) throws NoSuchAl if (keyFile == null) { return null; } - ByteBuf encodedKeyBuf = PemReader.readPrivateKey(keyFile); + return getPrivateKeyFromByteBuffer(PemReader.readPrivateKey(keyFile), keyPassword); + } + + static PrivateKey toPrivateKey(InputStream keyInputStream, String keyPassword) throws NoSuchAlgorithmException, + NoSuchPaddingException, InvalidKeySpecException, + InvalidAlgorithmParameterException, + KeyException, IOException { + if (keyInputStream == null) { + return null; + } + return getPrivateKeyFromByteBuffer(PemReader.readPrivateKey(keyInputStream), keyPassword); + } + + private static PrivateKey getPrivateKeyFromByteBuffer(ByteBuf encodedKeyBuf, String keyPassword) + throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeySpecException, + InvalidAlgorithmParameterException, KeyException, IOException { + byte[] encodedKey = new byte[encodedKeyBuf.readableBytes()]; encodedKeyBuf.readBytes(encodedKey).release(); @@ -952,8 +969,18 @@ static X509Certificate[] toX509Certificates(File file) throws CertificateExcepti if (file == null) { return null; } + return getCertificatesFromBuffers(PemReader.readCertificates(file)); + } + + static X509Certificate[] toX509Certificates(InputStream in) throws CertificateException { + if (in == null) { + return null; + } + return getCertificatesFromBuffers(PemReader.readCertificates(in)); + } + + private static X509Certificate[] getCertificatesFromBuffers(ByteBuf[] certs) throws CertificateException { CertificateFactory cf = CertificateFactory.getInstance("X.509"); - ByteBuf[] certs = PemReader.readCertificates(file); X509Certificate[] x509Certs = new X509Certificate[certs.length]; try { diff --git a/handler/src/main/java/io/netty/handler/ssl/SslContextBuilder.java b/handler/src/main/java/io/netty/handler/ssl/SslContextBuilder.java index ebce6caafc16..6b241cdfc1c5 100644 --- a/handler/src/main/java/io/netty/handler/ssl/SslContextBuilder.java +++ b/handler/src/main/java/io/netty/handler/ssl/SslContextBuilder.java @@ -22,6 +22,7 @@ import javax.net.ssl.SSLException; import javax.net.ssl.TrustManagerFactory; import java.io.File; +import java.io.InputStream; import java.security.PrivateKey; import java.security.cert.X509Certificate; @@ -48,6 +49,17 @@ public static SslContextBuilder forServer(File keyCertChainFile, File keyFile) { return new SslContextBuilder(true).keyManager(keyCertChainFile, keyFile); } + /** + * Creates a builder for new server-side {@link SslContext}. + * + * @param keyCertChainInputStream an input stream for an X.509 certificate chain in PEM format + * @param keyFile an input stream for a PKCS#8 private key in PEM format + * @see #keyManager(InputStream, InputStream) + */ + public static SslContextBuilder forServer(InputStream keyCertChainInputStream, InputStream keyInputStream) { + return new SslContextBuilder(true).keyManager(keyCertChainInputStream, keyInputStream); + } + /** * Creates a builder for new server-side {@link SslContext}. * @@ -73,6 +85,20 @@ public static SslContextBuilder forServer( return new SslContextBuilder(true).keyManager(keyCertChainFile, keyFile, keyPassword); } + /** + * Creates a builder for new server-side {@link SslContext}. + * + * @param keyCertChainInputStream an input stream for an X.509 certificate chain in PEM format + * @param keyInputStream an input stream for a PKCS#8 private key in PEM format + * @param keyPassword the password of the {@code keyFile}, or {@code null} if it's not + * password-protected + * @see #keyManager(InputStream, InputStream, String) + */ + public static SslContextBuilder forServer( + InputStream keyCertChainInputStream, InputStream keyInputStream, String keyPassword) { + return new SslContextBuilder(true).keyManager(keyCertChainInputStream, keyInputStream, keyPassword); + } + /** * Creates a builder for new server-side {@link SslContext}. * @@ -136,6 +162,18 @@ public SslContextBuilder trustManager(File trustCertChainFile) { } } + /** + * Trusted certificates for verifying the remote endpoint's certificate. The input stream should + * contain an X.509 certificate chain in PEM format. {@code null} uses the system default. + */ + public SslContextBuilder trustManager(InputStream trustCertChainInputStream) { + try { + return trustManager(SslContext.toX509Certificates(trustCertChainInputStream)); + } catch (Exception e) { + throw new IllegalArgumentException("Input stream does not contain valid certificates.", e); + } + } + /** * Trusted certificates for verifying the remote endpoint's certificate, {@code null} uses the system default. */ @@ -167,6 +205,17 @@ public SslContextBuilder keyManager(File keyCertChainFile, File keyFile) { return keyManager(keyCertChainFile, keyFile, null); } + /** + * Identifying certificate for this host. {@code keyCertChainInputStream} and {@code keyInputStream} may + * be {@code null} for client contexts, which disables mutual authentication. + * + * @param keyCertChainInputStream an input stream for an X.509 certificate chain in PEM format + * @param keyInputStream an input stream for a PKCS#8 private key in PEM format + */ + public SslContextBuilder keyManager(InputStream keyCertChainInputStream, InputStream keyInputStream) { + return keyManager(keyCertChainInputStream, keyInputStream, null); + } + /** * Identifying certificate for this host. {@code keyCertChain} and {@code key} may * be {@code null} for client contexts, which disables mutual authentication. @@ -203,6 +252,32 @@ public SslContextBuilder keyManager(File keyCertChainFile, File keyFile, String return keyManager(key, keyPassword, keyCertChain); } + /** + * Identifying certificate for this host. {@code keyCertChainInputStream} and {@code keyInputStream} may + * be {@code null} for client contexts, which disables mutual authentication. + * + * @param keyCertChainInputStream an input stream for an X.509 certificate chain in PEM format + * @param keyInputStream an input stream for a PKCS#8 private key in PEM format + * @param keyPassword the password of the {@code keyInputStream}, or {@code null} if it's not + * password-protected + */ + public SslContextBuilder keyManager(InputStream keyCertChainInputStream, InputStream keyInputStream, + String keyPassword) { + X509Certificate[] keyCertChain; + PrivateKey key; + try { + keyCertChain = SslContext.toX509Certificates(keyCertChainInputStream); + } catch (Exception e) { + throw new IllegalArgumentException("Input stream not contain valid certificates.", e); + } + try { + key = SslContext.toPrivateKey(keyInputStream, keyPassword); + } catch (Exception e) { + throw new IllegalArgumentException("Input stream does not contain valid private key.", e); + } + return keyManager(key, keyPassword, keyCertChain); + } + /** * Identifying certificate for this host. {@code keyCertChain} and {@code key} may * be {@code null} for client contexts, which disables mutual authentication.