blob: 656fee54be4d79a4f97bd6f9bd946421908de463 [file] [log] [blame]
package rpc
import (
"context"
"crypto/ed25519"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"fmt"
"math/big"
"time"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/status"
"source.monogon.dev/metropolis/node/core/identity"
"source.monogon.dev/metropolis/pkg/pki"
apb "source.monogon.dev/metropolis/proto/api"
)
type verifyPeerCertificate func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error
func verifyClusterCertificateAndNodeID(ca *x509.Certificate, nodeID string) verifyPeerCertificate {
return func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
if len(rawCerts) != 1 {
return fmt.Errorf("server presented %d certificates, wanted exactly one", len(rawCerts))
}
serverCert, err := x509.ParseCertificate(rawCerts[0])
if err != nil {
return fmt.Errorf("server presented unparseable certificate: %w", err)
}
pkey, err := identity.VerifyNodeInCluster(serverCert, ca)
if err != nil {
return fmt.Errorf("node certificate verification failed: %w", err)
}
if nodeID != "" {
id := identity.NodeID(pkey)
if id != nodeID {
return fmt.Errorf("wanted to reach node %q, got %q", nodeID, id)
}
}
return nil
}
}
func verifyFail(err error) verifyPeerCertificate {
return func(_ [][]byte, _ [][]*x509.Certificate) error {
return err
}
}
// NewEphemeralCredentials returns gRPC TransportCredentials that can be used to
// dial a cluster without authenticating with a certificate, but instead
// authenticating by proving the possession of a private key, via an ephemeral
// self-signed certificate.
//
// Currently these credentials are used in two flows:
//
// 1. Registration of nodes into a cluster, after which a node receives a proper
// node certificate
//
// 2. Escrow of initial owner credentials into a proper manager
// certificate
//
// If 'ca' is given, the remote side will be cryptographically verified to be a
// node that's part of the cluster represented by the ca. Otherwise, no
// verification is performed and this function is unsafe.
func NewEphemeralCredentials(private ed25519.PrivateKey, ca *x509.Certificate) (credentials.TransportCredentials, error) {
template := x509.Certificate{
SerialNumber: big.NewInt(1),
NotBefore: time.Now(),
NotAfter: pki.UnknownNotAfter,
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
BasicConstraintsValid: true,
}
certificateBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, private.Public(), private)
if err != nil {
return nil, fmt.Errorf("when generating self-signed certificate: %w", err)
}
certificate := tls.Certificate{
Certificate: [][]byte{certificateBytes},
PrivateKey: private,
}
var opts []AuthenticatedCredentialsOpt
if ca != nil {
opts = append(opts, WantRemoteCluster(ca))
} else {
opts = append(opts, WantInsecure())
}
return NewAuthenticatedCredentials(certificate, opts...), nil
}
// AuthenticatedCredentialsOpt are created using WantXXX functions and used in
// NewAuthenticatedCredentials.
type AuthenticatedCredentialsOpt struct {
wantCA *x509.Certificate
wantNodeID string
insecureOkay bool
}
func (a *AuthenticatedCredentialsOpt) merge(o *AuthenticatedCredentialsOpt) {
if a.wantNodeID == "" && o.wantNodeID != "" {
a.wantNodeID = o.wantNodeID
}
if a.wantCA == nil && o.wantCA != nil {
a.wantCA = o.wantCA
}
if !a.insecureOkay && o.insecureOkay {
a.insecureOkay = o.insecureOkay
}
}
// WantRemoteCluster enables the verification of the remote cluster identity when
// using NewAuthanticatedCredentials. If the connection is not terminated at a
// cluster with the given CA certificate, an error will be returned.
//
// This is the bare minimum option required to implement secure connections to
// clusters.
func WantRemoteCluster(ca *x509.Certificate) AuthenticatedCredentialsOpt {
return AuthenticatedCredentialsOpt{
wantCA: ca,
}
}
// WantRemoteNode enables the verification of the remote node identity when using
// NewAuthenticatedCredentials. If the connection is not terminated at the node
// ID 'id', an error will be returned. For this function to work,
// WantRemoteCluster must also be set.
func WantRemoteNode(id string) AuthenticatedCredentialsOpt {
return AuthenticatedCredentialsOpt{
wantNodeID: id,
}
}
// WantInsecure disables the verification of the remote side of the connection
// via NewAuthenticatedCredentials. This is unsafe.
func WantInsecure() AuthenticatedCredentialsOpt {
return AuthenticatedCredentialsOpt{
insecureOkay: true,
}
}
// NewAuthenticatedCredentials returns gRPC TransportCredentials that can be
// used to dial a cluster with a given TLS certificate (from node or manager
// credentials).
//
// The provided AuthenticatedCredentialsOpt specify the verification of the
// remote side of the connection. When connecting to a cluster (any node), use
// WantRemoteCluster. If you also want to verify the connection to a particular
// node, specify WantRemoteNode alongside it. If no verification should be
// performed use WantInsecure.
//
// The given options are parsed on a first-wins basis.
func NewAuthenticatedCredentials(cert tls.Certificate, opts ...AuthenticatedCredentialsOpt) credentials.TransportCredentials {
config := &tls.Config{
Certificates: []tls.Certificate{cert},
InsecureSkipVerify: true,
}
var merged AuthenticatedCredentialsOpt
for _, o := range opts {
merged.merge(&o)
}
if merged.insecureOkay {
if merged.wantNodeID != "" || merged.wantCA != nil {
config.VerifyPeerCertificate = verifyFail(fmt.Errorf("WantInsecure specified alongside WantRemoteNode/WantRemoteCluster"))
}
} else {
switch {
case merged.wantNodeID != "" && merged.wantCA == nil:
config.VerifyPeerCertificate = verifyFail(fmt.Errorf("WantRemoteNode also requires WantRemoteCluster"))
case merged.wantCA == nil:
config.VerifyPeerCertificate = verifyFail(fmt.Errorf("no AuthenticaedCreentialsOpts specified"))
default:
config.VerifyPeerCertificate = verifyClusterCertificateAndNodeID(merged.wantCA, merged.wantNodeID)
}
}
return credentials.NewTLS(config)
}
// RetrieveOwnerCertificate uses AAA.Escrow to retrieve a cluster manager
// certificate for the initial owner of the cluster, authenticated by the
// public/private key set in the clusters NodeParameters.ClusterBoostrap.
//
// The retrieved certificate can be used to dial further cluster RPCs.
func RetrieveOwnerCertificate(ctx context.Context, aaa apb.AAAClient, private ed25519.PrivateKey) (*tls.Certificate, error) {
srv, err := aaa.Escrow(ctx)
if err != nil {
if st, ok := status.FromError(err); ok {
return nil, status.Errorf(st.Code(), "Escrow call failed: %s", st.Message())
}
return nil, err
}
if err := srv.Send(&apb.EscrowFromClient{
Parameters: &apb.EscrowFromClient_Parameters{
RequestedIdentityName: "owner",
PublicKey: private.Public().(ed25519.PublicKey),
},
}); err != nil {
return nil, fmt.Errorf("when sending client parameters: %w", err)
}
resp, err := srv.Recv()
if err != nil {
return nil, fmt.Errorf("when receiving server message: %w", err)
}
if len(resp.EmittedCertificate) == 0 {
return nil, fmt.Errorf("expected certificate, instead got needed proofs: %+v", resp.Needed)
}
return &tls.Certificate{
Certificate: [][]byte{resp.EmittedCertificate},
PrivateKey: private,
}, nil
}