From d937ec5baf195fb80b45b475891666263ea84734 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Thu, 4 Feb 2021 16:49:16 -0800 Subject: [PATCH] api: Add mTLS and Trust/KeyManager Credentials API --- .../java/io/grpc/TlsChannelCredentials.java | 243 ++++++++++++++++++ .../java/io/grpc/TlsServerCredentials.java | 221 ++++++++++++++-- 2 files changed, 448 insertions(+), 16 deletions(-) diff --git a/api/src/main/java/io/grpc/TlsChannelCredentials.java b/api/src/main/java/io/grpc/TlsChannelCredentials.java index b529bf504f..2d3751f80c 100644 --- a/api/src/main/java/io/grpc/TlsChannelCredentials.java +++ b/api/src/main/java/io/grpc/TlsChannelCredentials.java @@ -16,9 +16,19 @@ package io.grpc; +import com.google.common.io.ByteStreams; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.EnumSet; +import java.util.List; import java.util.Set; +import javax.net.ssl.KeyManager; +import javax.net.ssl.TrustManager; /** * TLS credentials, providing server authentication and encryption. Consumers of this credential @@ -34,9 +44,82 @@ public final class TlsChannelCredentials extends ChannelCredentials { } private final boolean fakeFeature; + private final byte[] certificateChain; + private final byte[] privateKey; + private final String privateKeyPassword; + private final List keyManagers; + private final byte[] rootCertificates; + private final List trustManagers; TlsChannelCredentials(Builder builder) { fakeFeature = builder.fakeFeature; + certificateChain = builder.certificateChain; + privateKey = builder.privateKey; + privateKeyPassword = builder.privateKeyPassword; + keyManagers = builder.keyManagers; + rootCertificates = builder.rootCertificates; + trustManagers = builder.trustManagers; + } + + /** + * The certificate chain for the client's identity, as a new byte array. Generally should be + * PEM-encoded. If {@code null}, some feature is providing key manager information via a different + * method or no client identity is available. + */ + public byte[] getCertificateChain() { + if (certificateChain == null) { + return null; + } + return Arrays.copyOf(certificateChain, certificateChain.length); + } + + /** + * The private key for the client's identity, as a new byte array. Generally should be in PKCS#8 + * format. If encrypted, {@link #getPrivateKeyPassword} is the decryption key. If unencrypted, the + * password will be {@code null}. If {@code null}, some feature is providing key manager + * information via a different method or no client identity is available. + */ + public byte[] getPrivateKey() { + if (privateKey == null) { + return null; + } + return Arrays.copyOf(privateKey, privateKey.length); + } + + /** Returns the password to decrypt the private key, or {@code null} if unencrypted. */ + public String getPrivateKeyPassword() { + return privateKeyPassword; + } + + /** + * Returns the key manager list which provides the client's identity. Entries are scanned checking + * for specific types, like {@link javax.net.ssl.X509KeyManager}. Only a single entry for a type + * is used. Entries earlier in the list are higher priority. If {@code null}, key manager + * information is provided via a different method or no client identity is available. + */ + public List getKeyManagers() { + return keyManagers; + } + + /** + * Root trust certificates for verifying the server's identity that override the system's + * defaults. Generally PEM-encoded with multiple certificates concatenated. + */ + public byte[] getRootCertificates() { + if (rootCertificates == null) { + return null; + } + return Arrays.copyOf(rootCertificates, rootCertificates.length); + } + + /** + * Returns the trust manager list which verifies the server's identity. Entries are scanned + * checking for specific types, like {@link javax.net.ssl.X509TrustManager}. Only a single entry + * for a type is used. Entries earlier in the list are higher priority. If {@code null}, trust + * manager information is provided via the system's default or a different method. + */ + public List getTrustManagers() { + return trustManagers; } /** @@ -71,6 +154,12 @@ public final class TlsChannelCredentials extends ChannelCredentials { if (fakeFeature) { requiredFeature(understoodFeatures, incomprehensible, Feature.FAKE); } + if (rootCertificates != null || privateKey != null || keyManagers != null) { + requiredFeature(understoodFeatures, incomprehensible, Feature.MTLS); + } + if (keyManagers != null || trustManagers != null) { + requiredFeature(understoodFeatures, incomprehensible, Feature.CUSTOM_MANAGERS); + } return Collections.unmodifiableSet(incomprehensible); } @@ -95,6 +184,34 @@ public final class TlsChannelCredentials extends ChannelCredentials { * a call to {@link #incomprehensible incomprehensible()} is implemented properly. */ FAKE, + /** + * Client identity may be provided and server verification can be tuned. This feature requires + * observing {@link #getCertificateChain}, {@link #getPrivateKey}, and {@link + * #getPrivateKeyPassword} as well as {@link #getRootCertificates()}. The certificate chain and + * private key are used to configure a key manager to provide the client's identity. If no + * certificate chain and private key are provided the client will have no identity. The root + * certificates are used to configure a trust manager for verifying the server's identity. If no + * root certificates are provided the trust manager will default to the system's root + * certificates. + */ + MTLS, + /** + * Key managers and trust managers may be specified as {@link KeyManager} and {@link + * TrustManager} objects. This feature requires observing {@link #getKeyManagers()} and {@link + * #getTrustManagers()}. Generally {@link #MTLS} should also be supported, as that is the more + * common method of configuration. When a manager is non-{@code null}, then it is wholly + * responsible for key or trust material and usage; there is no need to check other manager + * sources like {@link #getCertificateChain()} or {@link #getPrivateKey()} (if {@code + * KeyManager} is available), or {@link #getRootCertificates()} (if {@code TrustManager} is + * available). + * + *

If other manager sources are available (e.g., {@code getPrivateKey() != null}), then they + * may be alternative representations of the same configuration and the consumer is free to use + * those alternative representations if it prefers. But before doing so it must first + * check that it understands that alternative representation by using {@link #incomprehensible} + * without the {@code CUSTOM_MANAGERS} feature. + */ + CUSTOM_MANAGERS, ; } @@ -107,6 +224,12 @@ public final class TlsChannelCredentials extends ChannelCredentials { @ExperimentalApi("https://github.com/grpc/grpc-java/issues/7479") public static final class Builder { private boolean fakeFeature; + private byte[] certificateChain; + private byte[] privateKey; + private String privateKeyPassword; + private List keyManagers; + private byte[] rootCertificates; + private List trustManagers; private Builder() {} @@ -119,6 +242,126 @@ public final class TlsChannelCredentials extends ChannelCredentials { return this; } + /** + * Use the provided certificate chain and private key as the client's identity. Generally they + * should be PEM-encoded and the key is an unencrypted PKCS#8 key (file headers have "BEGIN + * CERTIFICATE" and "BEGIN PRIVATE KEY"). + */ + public Builder keyManager(File certChain, File privateKey) throws IOException { + return keyManager(certChain, privateKey, null); + } + + /** + * Use the provided certificate chain and possibly-encrypted private key as the client's + * identity. Generally they should be PEM-encoded and the key is a PKCS#8 key. If the private + * key is unencrypted, then password must be {@code null}. + */ + public Builder keyManager(File certChain, File privateKey, String privateKeyPassword) + throws IOException { + InputStream certChainIs = new FileInputStream(certChain); + try { + InputStream privateKeyIs = new FileInputStream(privateKey); + try { + return keyManager(certChainIs, privateKeyIs, privateKeyPassword); + } finally { + privateKeyIs.close(); + } + } finally { + certChainIs.close(); + } + } + + /** + * Use the provided certificate chain and private key as the client's identity. Generally they + * should be PEM-encoded and the key is an unencrypted PKCS#8 key (file headers have "BEGIN + * CERTIFICATE" and "BEGIN PRIVATE KEY"). + */ + public Builder keyManager(InputStream certChain, InputStream privateKey) throws IOException { + return keyManager(certChain, privateKey, null); + } + + /** + * Use the provided certificate chain and possibly-encrypted private key as the client's + * identity. Generally they should be PEM-encoded and the key is a PKCS#8 key. If the private + * key is unencrypted, then password must be {@code null}. + */ + public Builder keyManager( + InputStream certChain, InputStream privateKey, String privateKeyPassword) + throws IOException { + byte[] certChainBytes = ByteStreams.toByteArray(certChain); + byte[] privateKeyBytes = ByteStreams.toByteArray(privateKey); + clearKeyManagers(); + this.certificateChain = certChainBytes; + this.privateKey = privateKeyBytes; + this.privateKeyPassword = privateKeyPassword; + return this; + } + + /** + * Have the provided key manager select the client's identity. Although multiple are allowed, + * only the first instance implementing a particular interface is used. So generally there will + * just be a single entry and it implements {@link javax.net.ssl.X509KeyManager}. + */ + public Builder keyManager(KeyManager... keyManagers) { + List keyManagerList = Collections.unmodifiableList(new ArrayList<>( + Arrays.asList(keyManagers))); + clearKeyManagers(); + this.keyManagers = keyManagerList; + return this; + } + + private void clearKeyManagers() { + this.certificateChain = null; + this.privateKey = null; + this.privateKeyPassword = null; + this.keyManagers = null; + } + + /** + * Use the provided root certificates to verify the server's identity instead of the system's + * default. Generally they should be PEM-encoded with all the certificates concatenated together + * (file header has "BEGIN CERTIFICATE", and would occur once per certificate). + */ + public Builder trustManager(File rootCerts) throws IOException { + InputStream rootCertsIs = new FileInputStream(rootCerts); + try { + return trustManager(rootCertsIs); + } finally { + rootCertsIs.close(); + } + } + + /** + * Use the provided root certificates to verify the server's identity instead of the system's + * default. Generally they should be PEM-encoded with all the certificates concatenated together + * (file header has "BEGIN CERTIFICATE", and would occur once per certificate). + */ + public Builder trustManager(InputStream rootCerts) throws IOException { + byte[] rootCertsBytes = ByteStreams.toByteArray(rootCerts); + clearTrustManagers(); + this.rootCertificates = rootCertsBytes; + return this; + } + + /** + * Have the provided trust manager verify the server's identity instead of the system's default. + * Although multiple are allowed, only the first instance implementing a particular interface is + * used. So generally there will just be a single entry and it implements {@link + * javax.net.ssl.X509TrustManager}. + */ + public Builder trustManager(TrustManager... trustManagers) { + List trustManagerList = Collections.unmodifiableList(new ArrayList<>( + Arrays.asList(trustManagers))); + clearTrustManagers(); + this.trustManagers = trustManagerList; + return this; + } + + private void clearTrustManagers() { + this.rootCertificates = null; + this.trustManagers = null; + } + /** Construct the credentials. */ public ChannelCredentials build() { return new TlsChannelCredentials(this); diff --git a/api/src/main/java/io/grpc/TlsServerCredentials.java b/api/src/main/java/io/grpc/TlsServerCredentials.java index 7619716764..8ed1482c40 100644 --- a/api/src/main/java/io/grpc/TlsServerCredentials.java +++ b/api/src/main/java/io/grpc/TlsServerCredentials.java @@ -16,15 +16,20 @@ package io.grpc; +import com.google.common.base.Preconditions; import com.google.common.io.ByteStreams; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.EnumSet; +import java.util.List; import java.util.Set; +import javax.net.ssl.KeyManager; +import javax.net.ssl.TrustManager; /** * TLS credentials, providing server identity and encryption. Consumers of this credential must @@ -59,27 +64,44 @@ public final class TlsServerCredentials extends ServerCredentials { private final byte[] certificateChain; private final byte[] privateKey; private final String privateKeyPassword; + private final List keyManagers; + private final ClientAuth clientAuth; + private final byte[] rootCertificates; + private final List trustManagers; TlsServerCredentials(Builder builder) { fakeFeature = builder.fakeFeature; certificateChain = builder.certificateChain; privateKey = builder.privateKey; privateKeyPassword = builder.privateKeyPassword; + keyManagers = builder.keyManagers; + clientAuth = builder.clientAuth; + rootCertificates = builder.rootCertificates; + trustManagers = builder.trustManagers; } /** - * The certificate chain, as a new byte array. Generally should be PEM-encoded. + * The certificate chain for the server's identity, as a new byte array. Generally should be + * PEM-encoded. If {@code null}, some feature is providing key manager information via a different + * method. */ public byte[] getCertificateChain() { + if (certificateChain == null) { + return null; + } return Arrays.copyOf(certificateChain, certificateChain.length); } /** - * The private key, as a new byte array. Generally should be in PKCS#8 format. If encrypted, - * {@link #getPrivateKeyPassword} is the decryption key. If unencrypted, the password will be - * {@code null}. + * The private key for the server's identity, as a new byte array. Generally should be in PKCS#8 + * format. If encrypted, {@link #getPrivateKeyPassword} is the decryption key. If unencrypted, the + * password will be {@code null}. If {@code null}, some feature is providing key manager + * information via a different method. */ public byte[] getPrivateKey() { + if (privateKey == null) { + return null; + } return Arrays.copyOf(privateKey, privateKey.length); } @@ -88,6 +110,42 @@ public final class TlsServerCredentials extends ServerCredentials { return privateKeyPassword; } + /** + * Returns the key manager list which provides the server's identity. Entries are scanned checking + * for specific types, like {@link javax.net.ssl.X509KeyManager}. Only a single entry for a type + * is used. Entries earlier in the list are higher priority. If {@code null}, key manager + * information is provided via a different method. + */ + public List getKeyManagers() { + return keyManagers; + } + + /** Non-{@code null} setting indicating whether the server should expect a client's identity. */ + public ClientAuth getClientAuth() { + return clientAuth; + } + + /** + * Root trust certificates for verifying the client's identity that override the system's + * defaults. Generally PEM-encoded with multiple certificates concatenated. + */ + public byte[] getRootCertificates() { + if (rootCertificates == null) { + return null; + } + return Arrays.copyOf(rootCertificates, rootCertificates.length); + } + + /** + * Returns the trust manager list which verifies the client's identity. Entries are scanned + * checking for specific types, like {@link javax.net.ssl.X509TrustManager}. Only a single entry + * for a type is used. Entries earlier in the list are higher priority. If {@code null}, trust + * manager information is provided via the system's default or a different method. + */ + public List getTrustManagers() { + return trustManagers; + } + /** * Returns an empty set if this credential can be adequately understood via * the features listed, otherwise returns a hint of features that are lacking @@ -120,6 +178,12 @@ public final class TlsServerCredentials extends ServerCredentials { if (fakeFeature) { requiredFeature(understoodFeatures, incomprehensible, Feature.FAKE); } + if (clientAuth != ClientAuth.NONE) { + requiredFeature(understoodFeatures, incomprehensible, Feature.MTLS); + } + if (keyManagers != null || trustManagers != null) { + requiredFeature(understoodFeatures, incomprehensible, Feature.CUSTOM_MANAGERS); + } return Collections.unmodifiableSet(incomprehensible); } @@ -139,10 +203,37 @@ public final class TlsServerCredentials extends ServerCredentials { * a call to {@link #incomprehensible incomprehensible()} is implemented properly. */ FAKE, + /** + * Client certificates may be requested and verified. This feature requires observing {@link + * #getRootCertificates()} and {@link #getClientAuth()}. The root certificates are used to + * configure a trust manager for verifying the client's identity. If no root certificates are + * provided the trust manager will default to the system's root certificates. + */ + MTLS, + /** + * Key managers and trust managers may be specified as {@link KeyManager} and {@link + * TrustManager} objects. This feature by itself only implies {@link #getKeyManagers()} needs to + * be observed. But along with {@link #MTLS}, then {@link #getTrustManagers()} needs to be + * observed as well. When a manager is non-{@code null}, then it is wholly responsible for key + * or trust material and usage; there is no need to check other manager sources like {@link + * #getCertificateChain()} or {@link #getPrivateKey()} (if {@code KeyManager} is available), or + * {@link #getRootCertificates()} (if {@code TrustManager} is available). + * + *

If other manager sources are available (e.g., {@code getPrivateKey() != null}), then they + * may be alternative representations of the same configuration and the consumer is free to use + * those alternative representations if it prefers. But before doing so it must first + * check that it understands that alternative representation by using {@link #incomprehensible} + * without the {@code CUSTOM_MANAGERS} feature. + */ + CUSTOM_MANAGERS, ; } - /** Creates a builder for changing default configuration. */ + /** + * Creates a builder for changing default configuration. There is no default key manager, so key + * material must be specified. The default trust manager uses the system's root certificates. By + * default no client authentication will occur. + */ public static Builder newBuilder() { return new Builder(); } @@ -154,6 +245,10 @@ public final class TlsServerCredentials extends ServerCredentials { private byte[] certificateChain; private byte[] privateKey; private String privateKeyPassword; + private List keyManagers; + private ClientAuth clientAuth = ClientAuth.NONE; + private byte[] rootCertificates; + private List trustManagers; private Builder() {} @@ -167,8 +262,8 @@ public final class TlsServerCredentials extends ServerCredentials { } /** - * Creates an instance using provided certificate chain and private key. Generally they should - * be PEM-encoded and the key is an unencrypted PKCS#8 key (file headers have "BEGIN + * Use the provided certificate chain and private key as the server's identity. Generally they + * should be PEM-encoded and the key is an unencrypted PKCS#8 key (file headers have "BEGIN * CERTIFICATE" and "BEGIN PRIVATE KEY"). */ public Builder keyManager(File certChain, File privateKey) throws IOException { @@ -176,9 +271,9 @@ public final class TlsServerCredentials extends ServerCredentials { } /** - * Creates an instance using provided certificate chain and possibly-encrypted private key. - * Generally they should be PEM-encoded and the key is a PKCS#8 key. If the private key is - * unencrypted, then password must be {@code null}. + * Use the provided certificate chain and possibly-encrypted private key as the server's + * identity. Generally they should be PEM-encoded and the key is a PKCS#8 key. If the private + * key is unencrypted, then password must be {@code null}. */ public Builder keyManager(File certChain, File privateKey, String privateKeyPassword) throws IOException { @@ -196,8 +291,8 @@ public final class TlsServerCredentials extends ServerCredentials { } /** - * Creates an instance using provided certificate chain and private key. Generally they should - * be PEM-encoded and the key is an unencrypted PKCS#8 key (file headers have "BEGIN + * Use the provided certificate chain and private key as the server's identity. Generally they + * should be PEM-encoded and the key is an unencrypted PKCS#8 key (file headers have "BEGIN * CERTIFICATE" and "BEGIN PRIVATE KEY"). */ public Builder keyManager(InputStream certChain, InputStream privateKey) throws IOException { @@ -205,27 +300,121 @@ public final class TlsServerCredentials extends ServerCredentials { } /** - * Creates an instance using provided certificate chain and possibly-encrypted private key. - * Generally they should be PEM-encoded and the key is a PKCS#8 key. If the private key is - * unencrypted, then password must be {@code null}. + * Use the provided certificate chain and possibly-encrypted private key as the server's + * identity. Generally they should be PEM-encoded and the key is a PKCS#8 key. If the private + * key is unencrypted, then password must be {@code null}. */ public Builder keyManager( InputStream certChain, InputStream privateKey, String privateKeyPassword) throws IOException { byte[] certChainBytes = ByteStreams.toByteArray(certChain); byte[] privateKeyBytes = ByteStreams.toByteArray(privateKey); + clearKeyManagers(); this.certificateChain = certChainBytes; this.privateKey = privateKeyBytes; this.privateKeyPassword = privateKeyPassword; return this; } + /** + * Have the provided key manager select the server's identity. Although multiple are allowed, + * only the first instance implementing a particular interface is used. So generally there will + * just be a single entry and it implements {@link javax.net.ssl.X509KeyManager}. + */ + public Builder keyManager(KeyManager... keyManagers) { + List keyManagerList = Collections.unmodifiableList(new ArrayList<>( + Arrays.asList(keyManagers))); + clearKeyManagers(); + this.keyManagers = keyManagerList; + return this; + } + + private void clearKeyManagers() { + this.certificateChain = null; + this.privateKey = null; + this.privateKeyPassword = null; + this.keyManagers = null; + } + + /** + * Indicates whether the server should expect a client's identity. Must not be {@code null}. + * Defaults to {@link ClientAuth#NONE}. + */ + public Builder clientAuth(ClientAuth clientAuth) { + Preconditions.checkNotNull(clientAuth, "clientAuth"); + this.clientAuth = clientAuth; + return this; + } + + /** + * Use the provided root certificates to verify the client's identity instead of the system's + * default. Generally they should be PEM-encoded with all the certificates concatenated together + * (file header has "BEGIN CERTIFICATE", and would occur once per certificate). + */ + public Builder trustManager(File rootCerts) throws IOException { + InputStream rootCertsIs = new FileInputStream(rootCerts); + try { + return trustManager(rootCertsIs); + } finally { + rootCertsIs.close(); + } + } + + /** + * Use the provided root certificates to verify the client's identity instead of the system's + * default. Generally they should be PEM-encoded with all the certificates concatenated together + * (file header has "BEGIN CERTIFICATE", and would occur once per certificate). + */ + public Builder trustManager(InputStream rootCerts) throws IOException { + byte[] rootCertsBytes = ByteStreams.toByteArray(rootCerts); + clearTrustManagers(); + this.rootCertificates = rootCertsBytes; + return this; + } + + /** + * Have the provided trust manager verify the client's identity instead of the system's default. + * Although multiple are allowed, only the first instance implementing a particular interface is + * used. So generally there will just be a single entry and it implements {@link + * javax.net.ssl.X509TrustManager}. + */ + public Builder trustManager(TrustManager... trustManagers) { + List trustManagerList = Collections.unmodifiableList(new ArrayList<>( + Arrays.asList(trustManagers))); + clearTrustManagers(); + this.trustManagers = trustManagerList; + return this; + } + + private void clearTrustManagers() { + this.rootCertificates = null; + this.trustManagers = null; + } + /** Construct the credentials. */ public ServerCredentials build() { - if (certificateChain == null) { + if (certificateChain == null && keyManagers == null) { throw new IllegalStateException("A key manager is required"); } return new TlsServerCredentials(this); } } + + /** The level of authentication the server should expect from the client. */ + public enum ClientAuth { + /** Clients will not present any identity. */ + NONE, + + /** + * Clients are requested to present their identity, but clients without identities are + * permitted. + */ + OPTIONAL, + + /** + * Clients are requested to present their identity, and are required to provide a valid + * identity. + */ + REQUIRE; + } }