Serge Bazanski | 439b95e | 2021-06-30 23:16:13 +0200 | [diff] [blame] | 1 | package cluster |
| 2 | |
| 3 | import ( |
| 4 | "crypto/ed25519" |
| 5 | "crypto/subtle" |
Serge Bazanski | 5b2ae55 | 2021-08-17 13:00:14 +0200 | [diff] [blame^] | 6 | "crypto/tls" |
Serge Bazanski | 439b95e | 2021-06-30 23:16:13 +0200 | [diff] [blame] | 7 | "crypto/x509" |
| 8 | "fmt" |
| 9 | |
Serge Bazanski | 5b2ae55 | 2021-08-17 13:00:14 +0200 | [diff] [blame^] | 10 | "google.golang.org/grpc/credentials" |
| 11 | |
Serge Bazanski | 439b95e | 2021-06-30 23:16:13 +0200 | [diff] [blame] | 12 | "source.monogon.dev/metropolis/node/core/curator" |
| 13 | "source.monogon.dev/metropolis/node/core/localstorage" |
| 14 | ) |
| 15 | |
| 16 | // NodeCertificate is the public part of the credentials of a node. They are |
| 17 | // emitted for a node by the cluster CA contained within the curator. |
| 18 | type NodeCertificate struct { |
| 19 | node *x509.Certificate |
| 20 | ca *x509.Certificate |
| 21 | } |
| 22 | |
| 23 | // NodeCredentials are the public and private part of the credentials of a node. |
| 24 | // |
| 25 | // It represents all the data necessary for a node to authenticate over mTLS to |
| 26 | // other nodes and the rest of the cluster. |
| 27 | // |
| 28 | // It must never be made available to any node other than the node it has been |
| 29 | // emitted for. |
| 30 | type NodeCredentials struct { |
| 31 | NodeCertificate |
| 32 | private ed25519.PrivateKey |
| 33 | } |
| 34 | |
| 35 | // NewNodeCertificate wraps a pair CA and node DER-encoded certificates into |
| 36 | // NodeCertificate, ensuring the given certificate data is valid and compatible |
| 37 | // Metropolis assumptions. |
| 38 | // |
| 39 | // It does _not_ verify that the given CA is a known/trusted Metropolis CA for a |
| 40 | // running cluster. |
| 41 | func NewNodeCertificate(cert, ca []byte) (*NodeCertificate, error) { |
| 42 | certParsed, err := x509.ParseCertificate(cert) |
| 43 | if err != nil { |
| 44 | return nil, fmt.Errorf("could not parse node certificate: %w", err) |
| 45 | } |
| 46 | caCertParsed, err := x509.ParseCertificate(ca) |
| 47 | if err != nil { |
| 48 | return nil, fmt.Errorf("could not parse ca certificate: %w", err) |
| 49 | } |
| 50 | |
| 51 | // Ensure both CA and node certs use ED25519. |
| 52 | if certParsed.PublicKeyAlgorithm != x509.Ed25519 { |
| 53 | return nil, fmt.Errorf("node certificate must use ED25519, is %s", certParsed.PublicKeyAlgorithm.String()) |
| 54 | } |
| 55 | if pub, ok := certParsed.PublicKey.(ed25519.PublicKey); !ok || len(pub) != ed25519.PublicKeySize { |
| 56 | return nil, fmt.Errorf("node certificate ED25519 key invalid") |
| 57 | } |
| 58 | if caCertParsed.PublicKeyAlgorithm != x509.Ed25519 { |
| 59 | return nil, fmt.Errorf("CA certificate must use ED25519, is %s", caCertParsed.PublicKeyAlgorithm.String()) |
| 60 | } |
| 61 | if pub, ok := caCertParsed.PublicKey.(ed25519.PublicKey); !ok || len(pub) != ed25519.PublicKeySize { |
| 62 | return nil, fmt.Errorf("CA certificate ED25519 key invalid") |
| 63 | } |
| 64 | |
| 65 | // Ensure that the certificate is signed by the CA certificate. |
| 66 | if err := certParsed.CheckSignatureFrom(caCertParsed); err != nil { |
| 67 | return nil, fmt.Errorf("certificate not signed by given CA: %w", err) |
| 68 | } |
| 69 | |
| 70 | // Ensure that the certificate has the node's calculated ID in its DNS names. |
| 71 | found := false |
| 72 | nid := curator.NodeID(certParsed.PublicKey.(ed25519.PublicKey)) |
| 73 | for _, n := range certParsed.DNSNames { |
| 74 | if n == nid { |
| 75 | found = true |
| 76 | break |
| 77 | } |
| 78 | } |
| 79 | if !found { |
| 80 | return nil, fmt.Errorf("calculated node ID %q not found in node certificate's DNS names (%v)", nid, certParsed.DNSNames) |
| 81 | } |
| 82 | |
| 83 | return &NodeCertificate{ |
| 84 | node: certParsed, |
| 85 | ca: caCertParsed, |
| 86 | }, nil |
| 87 | } |
| 88 | |
| 89 | // NewNodeCredentials wraps a pair of CA and node DER-encoded certificates plus |
| 90 | // a private key into NodeCredentials, ensuring that the given data is valid and |
| 91 | // compatible with Metropolis assumptions. |
| 92 | // |
| 93 | // It does _not_ verify that the given CA is a known/trusted Metropolis CA for a |
| 94 | // running cluster. |
| 95 | func NewNodeCredentials(priv, cert, ca []byte) (*NodeCredentials, error) { |
| 96 | nc, err := NewNodeCertificate(cert, ca) |
| 97 | if err != nil { |
| 98 | return nil, err |
| 99 | } |
| 100 | |
| 101 | // Ensure that the private key is a valid length. |
| 102 | if want, got := ed25519.PrivateKeySize, len(priv); want != got { |
| 103 | return nil, fmt.Errorf("private key is not the correct length, wanted %d, got %d", want, got) |
| 104 | } |
| 105 | |
| 106 | // Ensure that the given private key matches the given public key. |
| 107 | if want, got := ed25519.PrivateKey(priv).Public().(ed25519.PublicKey), nc.PublicKey(); subtle.ConstantTimeCompare(want, got) != 1 { |
| 108 | return nil, fmt.Errorf("public key does not match private key") |
| 109 | } |
| 110 | |
| 111 | return &NodeCredentials{ |
| 112 | NodeCertificate: *nc, |
| 113 | private: ed25519.PrivateKey(priv), |
| 114 | }, nil |
| 115 | } |
| 116 | |
| 117 | // Save stores the given node credentials in local storage. |
| 118 | func (c *NodeCredentials) Save(d *localstorage.PKIDirectory) error { |
| 119 | if err := d.CACertificate.Write(c.ca.Raw, 0400); err != nil { |
| 120 | return fmt.Errorf("when writing CA certificate: %w", err) |
| 121 | } |
| 122 | if err := d.Certificate.Write(c.node.Raw, 0400); err != nil { |
| 123 | return fmt.Errorf("when writing node certificate: %w", err) |
| 124 | } |
| 125 | if err := d.Key.Write(c.private, 0400); err != nil { |
| 126 | return fmt.Errorf("when writing node private key: %w", err) |
| 127 | } |
| 128 | return nil |
| 129 | } |
| 130 | |
| 131 | // PublicKey returns the ED25519 public key corresponding to this node's |
| 132 | // certificate/credentials. |
| 133 | func (nc *NodeCertificate) PublicKey() ed25519.PublicKey { |
| 134 | // Safe: we have ensured that the given certificate has an ed25519 public key on |
| 135 | // NewNodeCertificate. |
| 136 | return nc.node.PublicKey.(ed25519.PublicKey) |
| 137 | } |
| 138 | |
| 139 | // ID returns the canonical ID/name of the node for which this |
| 140 | // certificate/credentials were emitted. |
| 141 | func (nc *NodeCertificate) ID() string { |
| 142 | return curator.NodeID(nc.PublicKey()) |
| 143 | } |
Serge Bazanski | 5b2ae55 | 2021-08-17 13:00:14 +0200 | [diff] [blame^] | 144 | |
| 145 | // PublicGRPCServerCredentials returns gRPC TransportCredentials that should be |
| 146 | // used by this node to run public gRPC services (ie. the AAA service and any |
| 147 | // other management/user services). |
| 148 | // |
| 149 | // SECURITY: The returned TransportCredentials accepts _any_ client certificate |
| 150 | // served by the client and does not perform any verification. The gRPC service |
| 151 | // instance (via per-method checks or middleware) should perform user |
| 152 | // authentication/authorization. |
| 153 | func (nc *NodeCredentials) PublicGRPCServerCredentials() credentials.TransportCredentials { |
| 154 | tlsCert := tls.Certificate{ |
| 155 | Certificate: [][]byte{nc.node.Raw}, |
| 156 | PrivateKey: nc.private, |
| 157 | } |
| 158 | return credentials.NewTLS(&tls.Config{ |
| 159 | Certificates: []tls.Certificate{tlsCert}, |
| 160 | ClientAuth: tls.RequireAnyClientCert, |
| 161 | }) |
| 162 | } |