boulder/grpc/creds/creds.go

240 lines
8.9 KiB
Go

package creds
import (
"context"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"net"
"google.golang.org/grpc/credentials"
)
var (
ErrClientHandshakeNop = errors.New(
"boulder/grpc/creds: Client-side handshakes are not implemented with " +
"serverTransportCredentials")
ErrServerHandshakeNop = errors.New(
"boulder/grpc/creds: Server-side handshakes are not implemented with " +
"clientTransportCredentials")
ErrOverrideServerNameNop = errors.New(
"boulder/grpc/creds: OverrideServerName() is not implemented")
ErrNilServerConfig = errors.New(
"boulder/grpc/creds: `serverConfig` must not be nil")
ErrEmptyPeerCerts = errors.New(
"boulder/grpc/creds: validateClient given state with empty PeerCertificates")
)
type ErrSANNotAccepted struct {
got, expected []string
}
func (e ErrSANNotAccepted) Error() string {
return fmt.Sprintf("boulder/grpc/creds: client certificate SAN was invalid. "+
"Got %q, expected one of %q.", e.got, e.expected)
}
// clientTransportCredentials is a grpc/credentials.TransportCredentials which supports
// connecting to, and verifying multiple DNS names
type clientTransportCredentials struct {
roots *x509.CertPool
clients []tls.Certificate
// If set, this is used as the hostname to validate on certificates, instead
// of the value passed to ClientHandshake by grpc.
hostOverride string
}
// NewClientCredentials returns a new initialized grpc/credentials.TransportCredentials for client usage
func NewClientCredentials(rootCAs *x509.CertPool, clientCerts []tls.Certificate, hostOverride string) credentials.TransportCredentials {
return &clientTransportCredentials{rootCAs, clientCerts, hostOverride}
}
// ClientHandshake does the authentication handshake specified by the corresponding
// authentication protocol on rawConn for clients. It returns the authenticated
// connection and the corresponding auth information about the connection.
// Implementations must use the provided context to implement timely cancellation.
func (tc *clientTransportCredentials) ClientHandshake(ctx context.Context, addr string, rawConn net.Conn) (net.Conn, credentials.AuthInfo, error) {
var err error
host := tc.hostOverride
if host == "" {
// IMPORTANT: Don't wrap the errors returned from this method. gRPC expects to be
// able to check err.Temporary to spot temporary errors and reconnect when they happen.
host, _, err = net.SplitHostPort(addr)
if err != nil {
return nil, nil, err
}
}
conn := tls.Client(rawConn, &tls.Config{
ServerName: host,
RootCAs: tc.roots,
Certificates: tc.clients,
})
err = conn.HandshakeContext(ctx)
if err != nil {
_ = rawConn.Close()
return nil, nil, err
}
return conn, nil, nil
}
// ServerHandshake is not implemented for a `clientTransportCredentials`, use
// a `serverTransportCredentials` if you require `ServerHandshake`.
func (tc *clientTransportCredentials) ServerHandshake(rawConn net.Conn) (net.Conn, credentials.AuthInfo, error) {
return nil, nil, ErrServerHandshakeNop
}
// Info returns information about the transport protocol used
func (tc *clientTransportCredentials) Info() credentials.ProtocolInfo {
return credentials.ProtocolInfo{SecurityProtocol: "tls"}
}
// GetRequestMetadata returns nil, nil since TLS credentials do not have metadata.
func (tc *clientTransportCredentials) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
return nil, nil
}
// RequireTransportSecurity always returns true because TLS is transport security
func (tc *clientTransportCredentials) RequireTransportSecurity() bool {
return true
}
// Clone returns a copy of the clientTransportCredentials
func (tc *clientTransportCredentials) Clone() credentials.TransportCredentials {
return NewClientCredentials(tc.roots, tc.clients, tc.hostOverride)
}
// OverrideServerName is not implemented and here only to satisfy the interface
func (tc *clientTransportCredentials) OverrideServerName(serverNameOverride string) error {
return ErrOverrideServerNameNop
}
// serverTransportCredentials is a grpc/credentials.TransportCredentials which supports
// filtering acceptable peer connections by a list of accepted client certificate SANs
type serverTransportCredentials struct {
serverConfig *tls.Config
acceptedSANs map[string]struct{}
}
// NewServerCredentials returns a new initialized grpc/credentials.TransportCredentials for server usage
func NewServerCredentials(serverConfig *tls.Config, acceptedSANs map[string]struct{}) (credentials.TransportCredentials, error) {
if serverConfig == nil {
return nil, ErrNilServerConfig
}
return &serverTransportCredentials{serverConfig, acceptedSANs}, nil
}
// validateClient checks a peer's client certificate's SAN entries against
// a list of accepted SANs. If the client certificate does not have a SAN on the
// list it is rejected.
//
// Note 1: This function *only* verifies the SAN entries! Callers are expected to
// have provided the `tls.ConnectionState` returned from a validate (e.g.
// non-error producing) `conn.Handshake()`.
//
// Note 2: We do *not* consider the client certificate subject common name. The
// CN field is deprecated and should be present as a DNS SAN!
func (tc *serverTransportCredentials) validateClient(peerState tls.ConnectionState) error {
/*
* If there's no list of accepted SANs, all clients are OK
*
* TODO(@cpu): This should be converted to a hard error at initialization time
* once we have deployed & updated all gRPC configurations to have an accepted
* SAN list configured
*/
if len(tc.acceptedSANs) == 0 {
return nil
}
// If `conn.Handshake()` is called before `validateClient` this should not
// occur. We return an error in this event primarily for unit tests that may
// call `validateClient` with manufactured & artificial connection states.
if len(peerState.PeerCertificates) < 1 {
return ErrEmptyPeerCerts
}
// Since we call `conn.Handshake()` before `validateClient` and ensure
// a non-error response we don't need to validate anything except the presence
// of an acceptable SAN in the leaf entry of `PeerCertificates`. The tls
// package's `serverHandshake` and in particular, `processCertsFromClient`
// will address everything else as an error returned from `Handshake()`.
leaf := peerState.PeerCertificates[0]
// Combine both the DNS and IP address subjectAlternativeNames into a single
// list for checking.
var receivedSANs []string
receivedSANs = append(receivedSANs, leaf.DNSNames...)
for _, ip := range leaf.IPAddresses {
receivedSANs = append(receivedSANs, ip.String())
}
for _, name := range receivedSANs {
if _, ok := tc.acceptedSANs[name]; ok {
return nil
}
}
// If none of the DNS or IP SANs on the leaf certificate matched the
// acceptable list, the client isn't valid and we error
var acceptableSANs []string
for k := range tc.acceptedSANs {
acceptableSANs = append(acceptableSANs, k)
}
return ErrSANNotAccepted{receivedSANs, acceptableSANs}
}
// ServerHandshake does the authentication handshake for servers. It returns
// the authenticated connection and the corresponding auth information about
// the connection.
func (tc *serverTransportCredentials) ServerHandshake(rawConn net.Conn) (net.Conn, credentials.AuthInfo, error) {
// Perform the server <- client TLS handshake. This will validate the peer's
// client certificate.
conn := tls.Server(rawConn, tc.serverConfig)
err := conn.Handshake()
if err != nil {
return nil, nil, err
}
// In addition to the validation from `conn.Handshake()` we apply further
// constraints on what constitutes a valid peer
err = tc.validateClient(conn.ConnectionState())
if err != nil {
return nil, nil, err
}
return conn, credentials.TLSInfo{State: conn.ConnectionState()}, nil
}
// ClientHandshake is not implemented for a `serverTransportCredentials`, use
// a `clientTransportCredentials` if you require `ClientHandshake`.
func (tc *serverTransportCredentials) ClientHandshake(ctx context.Context, addr string, rawConn net.Conn) (net.Conn, credentials.AuthInfo, error) {
return nil, nil, ErrClientHandshakeNop
}
// Info provides the ProtocolInfo of this TransportCredentials.
func (tc *serverTransportCredentials) Info() credentials.ProtocolInfo {
return credentials.ProtocolInfo{SecurityProtocol: "tls"}
}
// GetRequestMetadata returns nil, nil since TLS credentials do not have metadata.
func (tc *serverTransportCredentials) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
return nil, nil
}
// RequireTransportSecurity always returns true because TLS is transport security
func (tc *serverTransportCredentials) RequireTransportSecurity() bool {
return true
}
// Clone returns a copy of the serverTransportCredentials
func (tc *serverTransportCredentials) Clone() credentials.TransportCredentials {
clone, _ := NewServerCredentials(tc.serverConfig, tc.acceptedSANs)
return clone
}
// OverrideServerName is not implemented and here only to satisfy the interface
func (tc *serverTransportCredentials) OverrideServerName(serverNameOverride string) error {
return ErrOverrideServerNameNop
}