api: Add mTLS and Trust/KeyManager Credentials API
diff --git a/api/src/main/java/io/grpc/TlsChannelCredentials.java b/api/src/main/java/io/grpc/TlsChannelCredentials.java
index b529bf5..2d3751f 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 @@
}
private final boolean fakeFeature;
+ private final byte[] certificateChain;
+ private final byte[] privateKey;
+ private final String privateKeyPassword;
+ private final List<KeyManager> keyManagers;
+ private final byte[] rootCertificates;
+ private final List<TrustManager> 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<KeyManager> 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<TrustManager> getTrustManagers() {
+ return trustManagers;
}
/**
@@ -71,6 +154,12 @@
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 @@
* 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).
+ *
+ * <p>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 <em>must</em> first
+ * check that it understands that alternative representation by using {@link #incomprehensible}
+ * <em>without</em> the {@code CUSTOM_MANAGERS} feature.
+ */
+ CUSTOM_MANAGERS,
;
}
@@ -107,6 +224,12 @@
@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<KeyManager> keyManagers;
+ private byte[] rootCertificates;
+ private List<TrustManager> trustManagers;
private Builder() {}
@@ -119,6 +242,126 @@
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<KeyManager> 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<TrustManager> 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 7619716..8ed1482 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 @@
private final byte[] certificateChain;
private final byte[] privateKey;
private final String privateKeyPassword;
+ private final List<KeyManager> keyManagers;
+ private final ClientAuth clientAuth;
+ private final byte[] rootCertificates;
+ private final List<TrustManager> 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);
}
@@ -89,6 +111,42 @@
}
/**
+ * 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<KeyManager> 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<TrustManager> 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
* to understand the configuration to be used for manual debugging.
@@ -120,6 +178,12 @@
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 @@
* 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).
+ *
+ * <p>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 <em>must</em> first
+ * check that it understands that alternative representation by using {@link #incomprehensible}
+ * <em>without</em> 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 @@
private byte[] certificateChain;
private byte[] privateKey;
private String privateKeyPassword;
+ private List<KeyManager> keyManagers;
+ private ClientAuth clientAuth = ClientAuth.NONE;
+ private byte[] rootCertificates;
+ private List<TrustManager> trustManagers;
private Builder() {}
@@ -167,8 +262,8 @@
}
/**
- * 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 @@
}
/**
- * 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 @@
}
/**
- * 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 @@
}
/**
- * 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<KeyManager> 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<TrustManager> 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;
+ }
}