- Author: @gtcooke94
- Approver: ejona86
- Implemented in: C++
- Last updated: 2026-04-03
- Discussion at: https://groups.google.com/g/grpc-io/c/N02jVxPd_4Y/m/n34PWOyKBgAJ?e=48417069
This document outlines gRPC's plan to support TLS private key offloading, allowing a separate module (e.g., hardware) to handle private key signing during TLS handshakes for enhanced security and flexibility.
A server possesses an identity certificate and a private key to identify itself during a handshake for authentication. During the TLS handshake, the server uses its private key to sign the transcript of the handshake, thereby validating ownership of said private key to the client. Currently, gRPC exclusively supports directly accessible, in-memory private keys; however, specialized private key storage solutions exist. TLS Private Key Offloading is a process by which an application performing a TLS handshake can use a private key to sign data without having direct access to the private key itself. This document will detail gRPC's prospective support for private key offloading.
This feature will have the following requirements/assumptions:
- TLS1.3 or TLS1.2 with modern ciphersuites that use ECDHE (Elliptic Curve Diffie-Hellman Ephemeral) for key exchange.
- HTTP/2 mandates TLS_ECDHE_{RSA/ECDSA}_WITH_AES_128_GCM_SHA256 as a baseline ciphersuite, explicitly prohibiting older, non-ephemeral Diffie-Hellman methods.
- In non-ephemeral Diffe-Hellman key exchange, the private key could be used for other cryptographic operations (e.g. decryption with an RSA key). These will not be supported.
The crypto libraries that each gRPC implementation uses have support for TLS Private Key Offloading. BoringSSL, for example, has the following interface:
struct ssl_private_key_method_st {
enum ssl_private_key_result_t (*sign)(SSL *ssl, uint8_t *out, size_t *out_len,
size_t max_out,
uint16_t signature_algorithm,
const uint8_t *in, size_t in_len);
enum ssl_private_key_result_t (*decrypt)(SSL *ssl, uint8_t *out,
size_t *out_len, size_t max_out,
const uint8_t *in, size_t in_len);
enum ssl_private_key_result_t (*complete)(SSL *ssl, uint8_t *out,
size_t *out_len, size_t max_out);
};For TLS>=1.2 with ECDHE, a requirement for this feature, the decrypt method is not needed. We will provide an
interface through which users can provide a custom private key signer, with the
higher-level functionality being a function that takes in unsigned bytes and a
signature algorithm and returns signed bytes.
string signed_bytes sign(string algorithm, string unsigned_bytes)
We are largely restricted by the underlying security libraries in each language. In the following sections, each language's API will be discussed as they are dependent upon the SSL library interfaces. Further, each language has different expectations for the sign functions on whether raw bytes or a digest is expected.
There are three distinct changes:
- Creating an InMemory Certificate Provider
- Must be done to cleanly accept the PrivateKeySigner instead of just a static PEM key or a file path.
- Splitting Certificate Providers into Root and Identity Providers
- Must be done to decouple how identity (certificate and key signing) and roots of trust are defined. Prior to this, roots had to be provided the same way that identities were provided, which wouldn't work for a PrivateKeySigner API.
- The PrivateKeySigner API itself.
We will create an InMemoryCertificateProvider that takes a set of root certs
and a list of PemKeyCertPair. Further, these values can be manually updated by
the user. This provides a strict super-set of functionality of the current
StaticDataCertificateProvider - calling UpdateRoot and UpdateIdentity on
an InMemoryCertificateProvider at setup is the same as a
StaticDataCertificateProvider. Further, we will be deprecating
StaticDataCertificateProvider.
The updates will return an absl::Status to the caller, and further a
ValidateCredentials API is provided.
// Implements a provider that uses in-memory data that can be modified in a thread-safe manner.
class InMemoryCertificateProvider : grpc_tls_certificate_provider {
public:
InMemoryCertificateProvider(std::string root_certificates, PemKeyCertPairList pem_key_cert_pairs);
// Thread safe updates
absl::Status UpdateRoot(std::string root_certificates);
absl::Status UpdateIdentityKeyCertPair(
const std::vector<IdentityKeyCertPair>& identity_key_cert_pairs);
// Returns an OK status if the following conditions hold:
// - the root certificates consist of one or more valid PEM blocks, and
// - every identity key-cert pair has a certificate chain that consists of
// chain that consists of valid PEM blocks and has a private key is a valid
// PEM block.
absl::Status ValidateCredentials() const;
}We will also decouple the root provider and the identity provider in the
tls_credentials_options
and through the stack. Users should be able to specify, for example, an
InMemoryCertificateProvider that implements private key offloading and a
FileWatcherCertificateProvider for the root certificates. We will mark the
coupled
API
as deprecated. This is required for this effort, as specifying a private key
signer must be done for the identity, but is unrelated to how the root is
provided. Currently, identity (private key and certificate chain) presentation
to gRPC is coupled with the roots presentation to gRPC.
class TlsCredentialsOptions {
public:
// The existing method.
[[deprecated(
"Use set_root_certificate_provider() or "
"set_identity_certificate_provider() instead.")]]
void set_certificate_provider(
std::shared_ptr<CertificateProviderInterface> certificate_provider);
// The new method proposed in this gRFC.
void set_root_certificate_provider(
std::shared_ptr<CertificateProviderInterface> certificate_provider);
// The new method proposed in this gRFC.
void set_identity_certificate_provider(
std::shared_ptr<CertificateProviderInterface> certificate_provider);BoringSSL provides an asynchronous API for private key signing, so we will
provide an asynchronous, cancellable API using callbacks. Thus, this is only
supported in gRPC builds with BoringSSL. If a PrivateKeySigner is used in a
non-BoringSSL build, the user should expect failure when configuring the
PrivateKeySigner in the InMemoryCertificateProvider.
The implementation of PrivateKeySigner::Sign can choose to return
synchronously or asynchronously via a callback. The implementer must also
implement cancellation. Cancel will be called by gRPC in the case that an
in-flight operation must be shutdown, and the implementer should use this
function to ensure any resources being used for an asynchronous signing
operation are properly shut down and released.
// Implementations of this class must be thread-safe.
class PrivateKeySigner {
public:
// A handle for an asynchronous signing operation.
//
// When `PrivateKeySigner::Sign` is implemented asynchronously, it returns an
// instance of a concrete implementation of this class. This handle is used
// to manage the asynchronous signing operation and can be used to cancel the
// operation via `PrivateKeySigner::Cancel`.
//
// Users must provide their own concrete implementation of this class. The
// handle can store any state needed for the asynchronous operation.
class AsyncSigningHandle {
public:
virtual ~AsyncSigningHandle() = default;
};
// Enum class representing TLS signature algorithm identifiers from BoringSSL.
// The values correspond to the SSL_SIGN_* macros in <openssl/ssl.h>.
enum class SignatureAlgorithm {
kRsaPkcs1Sha256,
kRsaPkcs1Sha384,
kRsaPkcs1Sha512,
kEcdsaSecp256r1Sha256,
kEcdsaSecp384r1Sha384,
kEcdsaSecp521r1Sha512,
kRsaPssRsaeSha256,
kRsaPssRsaeSha384,
kRsaPssRsaeSha512,
};
// A callback that is invoked when an asynchronous signing operation is
// complete. The argument should contain the signed bytes on success, or a
// non-OK status on failure.
using OnSignComplete = absl::AnyInvocable<void(absl::StatusOr<std::string>)>;
virtual ~PrivateKeySigner() = default;
// Signs data_to_sign.
// May return either synchronously or asynchronously.
// For synchronous returns, directly returns either the signed bytes
// or a failed status, and the callback will never be invoked.
// For asynchronous implementations, returns a handle for the asynchronous
// signing operation. The function argument on_sign_complete must be called by
// the implementer when the async signing operation is complete.
// on_sign_complete must not be invoked synchronously within Sign().
virtual std::variant<absl::StatusOr<std::string>,
std::shared_ptr<AsyncSigningHandle>>
Sign(absl::string_view data_to_sign, SignatureAlgorithm signature_algorithm,
OnSignComplete on_sign_complete) = 0;
// Cancels an in-flight async signing operation using a handle returned
// from a previous call to Sign().
virtual void Cancel(std::shared_ptr<AsyncSigningHandle> handle) = 0;
};The private key signer can be configured via the InMemoryCertificateProvider
using a new IdentityKeyOrSignerCertPair struct, as shown below.
// This already exists - it will be deprecated and replaced with `IdentityKeyOrSignerCertPair` where necessary.
struct [[deprecated("Use IdentityKeyOrSignerCertPair instead")]] GRPCXX_DLL
IdentityKeyCertPair {
std::string private_key;
std::string certificate_chain;
};
// A struct that stores the credential data presented to the peer in handshake
// to show local identity. The private_key and certificate_chain should always
// match. The private_key can be either a PEM string or a PrivateKeySigner.
// The PrivateKeySigner will only work with gRPC binaries compiled with
// BoringSSL.
struct GRPCXX_DLL IdentityKeyOrSignerCertPair {
std::variant<std::string, std::shared_ptr<PrivateKeySigner>> private_key;
std::string certificate_chain;
};
// A new overload on the InMemoryCertificateProvider to take this new struct
absl::Status UpdateIdentityKeyCertPair(
std::vector<IdentityKeyOrSignerCertPair>
identity_key_or_signer_cert_pairs);Python wraps the C-Core implementation. Currently, Python's security
configuration wraps the SslCredentials instead of the TlsCredentials. We
will update gRPC-Python internally to use TlsCredentials (PR
#40878). Then, the task of
supporting this feature in Python is similar to C++. We will wrap the new types
and split the certificate provider on tls_credentials_options to support
different root and identity providers.
The current Python API takes a function rather than attempting to do the interface-based approach from C++. It returns either how to cancel the async operation in the async case or a signature in the sync case.
Note: gevent is NOT supported.
# A Callable to return in the async case
# See the `ssl_channel_credentials_with_custom_signer` docstring for more detail on usage.
PrivateKeySignCancel = Callable[[], None]
PrivateKeySignatureAlgorithm = _cygrpc.PrivateKeySignatureAlgorithm
PrivateKeySignOnComplete = Callable[[Union[bytes, Exception]], None]
# See the `ssl_channel_credentials_with_custom_signer` docstring for more detail on usage.
# The custom signing function for a user to implement and pass to gRPC Python.
CustomPrivateKeySign = Callable[
[
bytes,
PrivateKeySignatureAlgorithm,
"PrivateKeySignOnComplete",
],
Union[bytes, "PrivateKeySignCancel"],
]
@experimental_api
def ssl_channel_credentials_with_custom_signer(
*,
private_key_sign_fn: "CustomPrivateKeySign",
root_certificates: Optional[bytes] = None,
certificate_chain: bytes,
) -> grpc.ChannelCredentials:
"""Creates a ChannelCredentials for use with an SSL-enabled Channel with a custom signer.
THIS IS AN EXPERIMENTAL API.
This API will be removed in a future version and combined with `grpc.ssl_channel_credentials`.
This API is not compatible with implementations using gevent.
Args:
private_key_sign_fn: a function with the signature of
`CustomPrivateKeySign`. This function can return synchronously or
asynchronously. To return synchronously, return the signed bytes. To
return asynchronously, return a callable matching the
`PrivateKeySignCancel` signature.This can be a no-op if no cancellation is
needed. In the async case, this function must return this callable
quickly, then call the passed in `PrivateKeySignOnComplete` when the async
signing operation is complete to trigger gRPC to continue the handshake.
root_certificates: The PEM-encoded root certificates as a byte string,
or None to retrieve them from a default location chosen by gRPC
runtime.
certificate_chain: The PEM-encoded certificate chain as a byte string
to use
Returns:
A ChannelCredentials for use with an SSL-enabled Channel.
"""
The most complex piece of this is the implementation - C-Core/Cython/Python
must handle the calling of the user provided Python signing function from C
which must invoke a callback that is passed to it. This will involve creating
bridge types between the Python user sign function and the expected
absl::AnyInvocable as well as bridging the callback that is passed to the user
sign function while managing the GIL and asynchronous nature of the signing.
This is technically feasible with Cython.
Example Usage:
# In the Python code configuring gRPC, create creds with the custom signer
creds = grpc.experimental.ssl_channel_credentials_with_custom_signer(
private_key_sign_fn=your_signer_fn
certificate_chain=your_cert_chain,
root_certificates=your_root_certs,
)Synchronous (blocking) implementation example:
def sync_client_private_key_signer(
data_to_sign,
signature_algorithm,
on_complete,
):
"""
Takes in data_to_sign and signs it using the test private key with a sync return
"""
private_key_bytes = client_private_key()
signature = sign_private_key(
data_to_sign, private_key_bytes, signature_algorithm
)
return signatureConcurrent example:
import multiprocessing
def _sign_worker(data_to_sign, signature_algorithm, on_complete):
"""Worker function to perform signing in a separate process."""
try:
# In a real use case, this would use a secure method to access the key.
signature = sign_private_key(
data_to_sign, client_private_key(), signature_algorithm
)
on_complete(signature)
except Exception as e:
on_complete(e)
def concurrent_client_private_key_signer(
data_to_sign, signature_algorithm, on_complete
):
"""Takes in data_to_sign and signs it concurrently using multiprocessing."""
p = multiprocessing.Process(
target=_sign_worker,
args=(data_to_sign, signature_algorithm, on_complete),
)
p.start()
# Per the Python API, this will be called when gRPC has a reason to shutdown
# or cancel the request and allows the implementer to clean up resources.
def cancel():
p.terminate()
p.join()
return cancelGolang's crypto/tls package does not directly support asynchronous operations
for the Signer interface, but this is not a problem due to the structure of
goroutines. The API will simply look like implementing the crypto/tls Signer
API. In this library, the Certificate
type's
PrivateKey is a crypto.PrivateKey,
which is Any, but it must implement the Signer
interface. Notably, in Golang, the user's
function should expect a digest of the transcript as input.
// From crypto.tls
type Signer interface {
// Public returns the public key corresponding to the opaque,
// private key.
Public() PublicKey
// Sign signs digest with the private key, possibly using entropy from
// rand. For an RSA key, the resulting signature should be either a
// PKCS #1 v1.5 or PSS signature (as indicated by opts). For an (EC)DSA
// key, it should be a DER-serialised, ASN.1 signature structure.
//
// Hash implements the SignerOpts interface and, in most cases, one can
// simply pass in the hash function used as opts. Sign may also attempt
// to type assert opts to other types in order to obtain algorithm
// specific values. See the documentation in each package for details.
//
// Note that when a signature of a hash of a larger message is needed,
// the caller is responsible for hashing the larger message and passing
// the hash (as digest) and the hash function (as opts) to Sign.
Sign(rand io.Reader, digest []byte, opts SignerOpts) (signature []byte, err error)
}
cert = tls.Certificate{
Certificate: derChain, //[][]byte
PrivateKey: PrivateKeySigner, //crypto.PrivateKey
}This is the interface that a user will implement and pass to gRPC-Go via a
tls.Certificate. We will create a similar InMemoryCertProvider that takes
and provides a tls.Certificate. This will live in the advancedTLS package.
// InMemoryCertProvider is a certprovider.Provider implementation that holds and serves a
// single, predefined tls.Certificate instance.
type InMemoryCertProvider struct {
tlsCert *tls.Certificate
}
func NewInMemoryCertProvider(cert *tls.Certificate) (*InMemoryCertProvider, error) {
// Checks
return &InMemoryCertProvider{tlsCert: *cert}, nil
}
// Implements the `KeyMaterial` interface
// Thread safe access
func (i *InMemoryCertProvider) KeyMaterial(_ context.Context) (*KeyMaterial, error) {...}
// Allow the user to update with new data
// Thread safe updates
func (provider *InMemoryCertProvider) UpdateCertificate(tlsCert *tls.Certificate) {...}This will be piped around by the existing infrastructure to configure the
crypto/tls library with no additional changes needed.
// Example Usage
// ExampleSigner is a basic implementation of crypto.Signer
type ExampleSigner struct {}
// Public returns the public key corresponding to the opaque, private key.
func (s *ExampleSigner) Public() crypto.PublicKey {
return <some key>
}
// Sign signs the digest with the private key.
func (s *ExampleSigner) Sign(r io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) {
signed_bytes := <do stuff with digest>
return signed_bytes, nil
}
// Now the user is in their application configuring gRPC
// Create the custom signer
customSigner, err := NewExampleSigner()
// Create the tls.Certificate
tlsCert := &tls.Certificate{
Certificate: <pem-certificate>
Signer: customSigner
}
// Create the InMemoryCertProvider and configure gRPC with it
provider, err := NewInMemoryCertProvider(tlsCert)
clientOpts := &advancedtls.Options{
IdentityOptions: advancedtls.IdentityCertificateOptions{
IdentityProvider: provider,
},
}
clientTLSCreds, err := advancedtls.NewClientCreds(clientOpts)
conn, err := grpc.NewClient(fullServerAddr, grpc.WithTransportCredentials(clientTLSCreds))After initial investigation, we will not pursue implementing this feature in
Java right now. Due to the different designs of the options of underlying
security library APIs, there is no cohesive convenience API that we could add to
gRPC-Java. A user can still implement this themselves and use the
AdvancedTlsKeyProvider APIs to do whatever they want with the PrivateKey
interface. Particularly, a user could create their own signature provider and
globally register it to be used. gRPC-Java, as a library, will not do global
registration.
This feature will be explicitly configured by users, thus no environment variable protection is needed. If a user does not configure TLS Private Key Offloading, it will not happen.
Private key offloading is designed to support signing outside of the existing process, for example in a hardware module or via an RPC - thus this API should support asynchronous operations in languages where that is possible (Golang's crypto/tls does not support asynchronous private key signing).
-
C-Core/C++
- grpc/grpc#41490 - Separate cert provider into a root and identity provider
- grpc/grpc#41484 - Create InMemoryCertificateProvider
- grpc/grpc#41606 - Implement PrivateKeySigner in C-Core and C++
-
Python
- grpc/grpc#40878 - Migrate Python to TlsCredentials under the hood
- grpc/grpc#41701 - Implement in Python and Cython