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