| package certs |
| |
| import ( |
| "bytes" |
| "crypto" |
| "crypto/ed25519" |
| "crypto/rand" |
| "crypto/x509" |
| "encoding/pem" |
| "errors" |
| "fmt" |
| "log" |
| "math/big" |
| "net" |
| "os" |
| "path/filepath" |
| "time" |
| ) |
| |
| // Certificate is a higher-level descriptor of an intent to generate a |
| // certificate and corresponding Ed25519 keypair on disk. |
| type Certificate struct { |
| // uniquer name for this cert, used to calculate filesystem paths. |
| name string |
| // root directory where all certs are stored. |
| root string |
| // duration used to determine TimeAfter. If not set, the certificate will |
| // never expire. |
| duration time.Duration |
| |
| kind certificateKind |
| |
| // cn is the subject common name that's going to be produced in the X.509 |
| // certificate. |
| cn string |
| // o is the subject organziation that's going to be produced in the X.509 |
| // certificate. |
| o string |
| // san are the DNS alternate names that are going to be produced in the |
| // X.509 certificate. |
| san []string |
| // ips are the IP alternate names that are going to be produced in the |
| // X.509 certificate. |
| ips []net.IP |
| |
| // issuer, if set, is the certificate that will sign this certificate. If |
| // not set, the certificate will be self-signed. |
| issuer *Certificate |
| } |
| |
| // Paths returns local filesystem paths to the CA certificate, certificate and |
| // key respectively. If the certificate is self signed, the CA path returned |
| // will be empty. These files might or might not live on the file system - you |
| // should first call Ensure to make sure they do. |
| func (c *Certificate) Paths() (caPath, certPath, keyPath string) { |
| if c.issuer != nil { |
| caPath = c.issuer.path(fileKindCert) |
| } |
| certPath = c.path(fileKindCert) |
| keyPath = c.path(fileKindKey) |
| return |
| } |
| |
| type certificateKind string |
| |
| const ( |
| kindServer certificateKind = "server" |
| kindClient certificateKind = "client" |
| kindClientServer certificateKind = "client-server" |
| kindCA certificateKind = "ca" |
| kindProdvider certificateKind = "prodvider" |
| ) |
| |
| type fileKind string |
| |
| const ( |
| fileKindKey fileKind = "key" |
| fileKindKeyEncrypted fileKind = "key-encrypted" |
| fileKindCert fileKind = "cert" |
| ) |
| |
| // path returns the path to the generated fileKind for this Certificate. |
| func (c *Certificate) path(k fileKind) string { |
| switch k { |
| case fileKindKeyEncrypted: |
| return filepath.Join(c.root, "secrets", "cipher", c.name+".key") |
| case fileKindKey: |
| return filepath.Join(c.root, "secrets", "plain", c.name+".key") |
| case fileKindCert: |
| // clustercfg.py compat: CA certs end in .crt, non-CA certs end in .cert. |
| // We're keeping this accidental convention to avoid spurious nix rebuilds |
| // when migrating. |
| // |
| // Feel free to fix it if it annoys you. |
| extension := ".cert" |
| if c.kind == kindCA { |
| extension = ".crt" |
| } |
| return filepath.Join(c.root, "certs", c.name+extension) |
| default: |
| panic("unexpected file kind type " + k) |
| } |
| } |
| |
| // ensureKey loads or generates-then-saves the private key for this |
| // Certificate. |
| func (c *Certificate) ensureKey() (crypto.Signer, error) { |
| path := c.path(fileKindKey) |
| _, err := os.Stat(path) |
| switch { |
| case err == nil: |
| return c.loadKey() |
| case errors.Is(err, os.ErrNotExist): |
| epath := c.path(fileKindKeyEncrypted) |
| if _, err = os.Stat(epath); err == nil { |
| return nil, fmt.Errorf("plaintext key at %q not found, but exists encrypted at %q - please decrypt using secretstore", path, epath) |
| } |
| return c.generateKey() |
| default: |
| return nil, fmt.Errorf("could not read key: %w", err) |
| } |
| } |
| |
| func (c *Certificate) loadKey() (crypto.Signer, error) { |
| path := c.path(fileKindKey) |
| bytes, err := os.ReadFile(path) |
| if err != nil { |
| return nil, err |
| } |
| block, _ := pem.Decode(bytes) |
| if block == nil { |
| return nil, fmt.Errorf("no PEM block found") |
| } |
| if block.Type != "PRIVATE KEY" { |
| return nil, fmt.Errorf("unexpected PEM block: %q", block.Type) |
| } |
| key, err := x509.ParsePKCS8PrivateKey(block.Bytes) |
| if err != nil { |
| return nil, err |
| } |
| if k, ok := key.(ed25519.PrivateKey); ok { |
| return k, nil |
| } |
| return nil, fmt.Errorf("not an ED25519 key") |
| } |
| |
| func (c *Certificate) generateKey() (crypto.Signer, error) { |
| _, priv, err := ed25519.GenerateKey(rand.Reader) |
| if err != nil { |
| return nil, err |
| } |
| |
| pkcs8, err := x509.MarshalPKCS8PrivateKey(priv) |
| if err != nil { |
| return nil, err |
| } |
| |
| block := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: pkcs8}) |
| path := c.path(fileKindKey) |
| os.MkdirAll(filepath.Dir(path), 0700) |
| log.Printf("Saving %s key to %s ...", c.name, path) |
| if err := os.WriteFile(path, block, 0600); err != nil { |
| return nil, err |
| } |
| |
| return priv, nil |
| } |
| |
| // ensureCert loads or generates-then-saves the X.509 certificate for the |
| // Certificate. |
| func (c *Certificate) ensureCert() (*x509.Certificate, error) { |
| path := c.path(fileKindCert) |
| _, err := os.Stat(path) |
| switch { |
| case err == nil: |
| cert, err := c.loadCert() |
| switch err { |
| case nil: |
| return cert, nil |
| case errExpired: |
| return c.generateCert() |
| default: |
| return nil, err |
| } |
| case errors.Is(err, os.ErrNotExist): |
| return c.generateCert() |
| default: |
| return nil, fmt.Errorf("could not read cert: %w", err) |
| } |
| } |
| |
| func (c *Certificate) generateCert() (*x509.Certificate, error) { |
| serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 127) |
| serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) |
| if err != nil { |
| return nil, err |
| } |
| |
| notAfter := unknownNotAfter |
| if c.duration != 0 { |
| notAfter = time.Now().Add(c.duration) |
| } |
| template := c.template() |
| template.SerialNumber = serialNumber |
| template.NotBefore = time.Now() |
| template.NotAfter = notAfter |
| |
| parent := template |
| skey, err := c.ensureKey() |
| if err != nil { |
| return nil, fmt.Errorf("when ensuring key: %w", err) |
| } |
| pkey := skey.Public() |
| caskey := skey |
| if c.issuer != nil { |
| caskey, err = c.issuer.ensureKey() |
| if err != nil { |
| return nil, fmt.Errorf("when ensuring CA key: %w", err) |
| } |
| cacert, err := c.issuer.ensureCert() |
| if err != nil { |
| return nil, fmt.Errorf("when ensuring CA cert: %w", err) |
| } |
| parent = cacert |
| } |
| |
| bytes, err := x509.CreateCertificate(rand.Reader, template, parent, pkey, caskey) |
| if err != nil { |
| return nil, fmt.Errorf("issuing certificate failed: %w", err) |
| } |
| |
| block := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: bytes}) |
| path := c.path(fileKindCert) |
| os.MkdirAll(filepath.Dir(path), 0700) |
| log.Printf("Saving %s cert to %s ...", c.name, path) |
| if err := os.WriteFile(path, block, 0600); err != nil { |
| return nil, err |
| } |
| |
| return x509.ParseCertificate(bytes) |
| } |
| |
| // errExpired is returned if the cert exists on disk but has (nearly) expired. |
| var errExpired = errors.New("certificate expired") |
| |
| func (c *Certificate) loadCert() (*x509.Certificate, error) { |
| path := c.path(fileKindCert) |
| b, err := os.ReadFile(path) |
| if err != nil { |
| return nil, err |
| } |
| |
| block, _ := pem.Decode(b) |
| if block == nil { |
| return nil, fmt.Errorf("no PEM block found") |
| } |
| if block.Type != "CERTIFICATE" { |
| return nil, fmt.Errorf("unexpected PEM block: %q", block.Type) |
| } |
| cert, err := x509.ParseCertificate(block.Bytes) |
| if err != nil { |
| return nil, err |
| } |
| if time.Now().Add(time.Hour).After(cert.NotAfter) { |
| return nil, errExpired |
| } |
| pkey, ok := cert.PublicKey.(ed25519.PublicKey) |
| if !ok { |
| return nil, fmt.Errorf("not a ED25519 cert") |
| } |
| skey, err := c.ensureKey() |
| if err != nil { |
| return nil, fmt.Errorf("when ensuring key: %w", err) |
| } |
| if !bytes.Equal(pkey, skey.Public().(ed25519.PublicKey)) { |
| return nil, fmt.Errorf("issued for different key") |
| } |
| |
| template := c.template() |
| if err := compareCertData(template, cert); err != nil { |
| return nil, err |
| } |
| return cert, nil |
| } |
| |
| // Ensure makes sure the given Certificate (and all of its' issuers) have |
| // corresponding private keys and X.509 certificates on disk, generating things |
| // as necessary. |
| func (c *Certificate) Ensure() error { |
| cert, err := c.ensureCert() |
| if err != nil { |
| return fmt.Errorf("when ensuring cert %s: %w", c.name, err) |
| } |
| _ = cert |
| |
| return nil |
| } |