blob: 5913f1410b8cd817e777f7af7ef7b05479b7d548 [file] [log] [blame]
package launch
import (
"context"
"crypto/ed25519"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"fmt"
"math/big"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
apb "source.monogon.dev/metropolis/proto/api"
)
// InitialClient implements a gRPC wrapper for dialing a Metropolis cluster
// while not (yet) authenticated, i.e. using only a self-signed public
// certificate to prove ownership of an ed25519 public key.
//
// This is used to dial a cluster's AAA.Escrow service after cluster bootstrap
// using the owner key configured in NodeParams.
type InitialClient struct {
// conn is the underlying dialed gRPC connection to the cluster.
conn *grpc.ClientConn
// aaa is a stub to the AAA service running on conn.
aaa apb.AAAClient
// options are the options this client has been opened with.
options *InitialClientOptions
}
type InitialClientOptions struct {
// Remote is an address:port to connect to. This should be a cluster node's
// curator port.
Remote string
// Private is the cluster owner private key, which should correspond to the
// owner public key defined in NodeParametrs.ClusterBootstrap when a cluster
// is bootstrapped.
Private ed25519.PrivateKey
}
// NewInitialClient dials a cluster's curator service using just a self-signed
// certificate and can be used to then escrow real cluster credentials for the
// owner.
//
// MVP SECURITY: this does not verify the identity of the cluster/node. However,
// because any intercepting party cannot forward the presented client
// certificate to any real cluster, no danger of intercepting administrative
// access to the expected cluster is possible. Instead, the interceptor can just
// pretend to be the cluster which was expected.
func NewInitialClient(o *InitialClientOptions) (*InitialClient, error) {
template := x509.Certificate{
SerialNumber: big.NewInt(1),
NotBefore: time.Now(),
NotAfter: time.Now().Add(time.Hour),
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
BasicConstraintsValid: true,
}
certificateBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, o.Private.Public(), o.Private)
if err != nil {
return nil, fmt.Errorf("when generating self-signed certificate: %w", err)
}
keyBytes, err := x509.MarshalPKCS8PrivateKey(o.Private)
if err != nil {
return nil, fmt.Errorf("when marshaling private key: %w", err)
}
key := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyBytes})
certificate := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certificateBytes})
clientCert, err := tls.X509KeyPair(certificate, key)
if err != nil {
return nil, fmt.Errorf("when building self-signed TLS client certificate: %w", err)
}
creds := credentials.NewTLS(&tls.Config{
Certificates: []tls.Certificate{
clientCert,
},
InsecureSkipVerify: true,
VerifyPeerCertificate: o.verify,
})
conn, err := grpc.Dial(o.Remote, grpc.WithTransportCredentials(creds))
if err != nil {
return nil, fmt.Errorf("when dialing: %w", err)
}
return &InitialClient{
conn: conn,
aaa: apb.NewAAAClient(conn),
options: o,
}, nil
}
// Close must be called when the InitialClient is not used anymore. This closes
// the underlying gRPC connection(s).
func (i *InitialClient) Close() error {
return i.conn.Close()
}
func (o *InitialClientOptions) verify(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
// SECURITY: Always permit all server certificates. See NewInitialClient godoc
// for more information.
return nil
}
// RetrieveOwnerCertificates 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 (i *InitialClient) RetrieveOwnerCertificate(ctx context.Context) (*tls.Certificate, error) {
srv, err := i.aaa.Escrow(ctx)
if err != nil {
return nil, fmt.Errorf("when opening Escrow RPC: %w", err)
}
if err := srv.Send(&apb.EscrowFromClient{
Parameters: &apb.EscrowFromClient_Parameters{
RequestedIdentityName: "owner",
PublicKey: i.options.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)
}
certificateBytes := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: resp.EmittedCertificate})
key, err := x509.MarshalPKCS8PrivateKey(i.options.Private)
if err != nil {
return nil, fmt.Errorf("while marshalling private key: %w", err)
}
keyBytes := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: key})
ownerCert, err := tls.X509KeyPair(certificateBytes, keyBytes)
if err != nil {
return nil, fmt.Errorf("could not build certificate from data received from cluster: %w", err)
}
return &ownerCert, nil
}