internal/certs/certs.go (305 lines of code) (raw):

// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one // or more contributor license agreements. Licensed under the Elastic License; // you may not use this file except in compliance with the Elastic License. package certs import ( "crypto" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/rsa" "crypto/sha256" "crypto/tls" "crypto/x509" "encoding/hex" "encoding/pem" "fmt" "io" "math/big" "net" "os" "path/filepath" "slices" "strings" "time" ) const ( IssueCertTypeCA = iota IssueCertTypeService IssueCertTypeClient ) // Certificate contains the key and certificate for an issued certificate. type Certificate struct { key crypto.Signer cert *x509.Certificate issuer *Certificate } // LoadCertificate loads a certificate and key from disk. func LoadCertificate(certFile, keyFile string) (*Certificate, error) { pair, err := tls.LoadX509KeyPair(certFile, keyFile) if err != nil { return nil, err } if len(pair.Certificate) == 0 { return nil, fmt.Errorf("no certificates in %q?", certFile) } chain := make([]*x509.Certificate, len(pair.Certificate)) for i := range pair.Certificate { cert, err := x509.ParseCertificate(pair.Certificate[i]) if err != nil { return nil, fmt.Errorf("failed to parse #%d certificate loaded from %q", i, certFile) } chain[i] = cert } var key crypto.Signer switch privKey := pair.PrivateKey.(type) { case crypto.Signer: key = privKey default: return nil, fmt.Errorf("key of type %T cannot be used", privKey) } cert := &Certificate{ key: key, cert: chain[0], } if len(chain) > 1 { // This is an intermediate certificate, rebuild the full chain. c := cert for _, cert := range chain[1:] { c.issuer = &Certificate{ // Parent keys are not known here, but that's ok // as these certs are only used for the cert chain. cert: cert, } c = c.issuer } } return cert, nil } // Issuer is a certificate that can issue other certificates. type Issuer struct { *Certificate } // NewCA creates a new self-signed root CA. func NewCA() (*Issuer, error) { return newCA(nil) } // LoadCA loads a CA certificate and key from disk. func LoadCA(certFile, keyFile string) (*Issuer, error) { cert, err := LoadCertificate(certFile, keyFile) if err != nil { return nil, err } return &Issuer{cert}, nil } func newCA(parent *Issuer) (*Issuer, error) { cert, err := New(IssueCertTypeCA, parent) if err != nil { return nil, err } return &Issuer{Certificate: cert}, nil } // IssueIntermediate issues an intermediate CA signed by the issuer. func (i *Issuer) IssueIntermediate() (*Issuer, error) { return newCA(i) } // Issue issues a certificate with the given options. This certificate // can be used to configure a TLS server. func (i *Issuer) Issue(opts ...Option) (*Certificate, error) { return New(IssueCertTypeService, i, opts...) } // IssueClient issues a certificate with the given options. This certificate // can be used to configure a TLS client. func (i *Issuer) IssueClient(opts ...Option) (*Certificate, error) { return New(IssueCertTypeClient, i, opts...) } // NewSelfSignedCert issues a self-signed certificate with the given options. // This certificate can be used to configure a TLS server. func NewSelfSignedCert(opts ...Option) (*Certificate, error) { return New(IssueCertTypeService, nil, opts...) } // Option is a function that can modify a certificate template. To be used // when issuing certificates. type Option func(template *x509.Certificate) // WithName is an option to configure the common and alternate DNS names of a certificate. func WithName(name string) Option { return func(template *x509.Certificate) { template.Subject.CommonName = name if !slices.Contains(template.DNSNames, name) { template.DNSNames = append(template.DNSNames, name) } } } // New is the main helper to create a certificate, it is recommended to // use the more specific ones for specific use cases. func New(certType int, issuer *Issuer, opts ...Option) (*Certificate, error) { key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { return nil, fmt.Errorf("failed to generate key: %w", err) } sn, err := newSerialNumber() if err != nil { return nil, fmt.Errorf("failed to get a unique serial number: %w", err) } // Don't use a expiration time longer than 825 days. // See https://rahulkj.github.io/openssl,/certificates/2022/09/09/self-signed-certificates.html. const longTime = 800 * 24 * time.Hour template := x509.Certificate{ NotBefore: time.Now(), NotAfter: time.Now().Add(longTime), SerialNumber: sn, KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, BasicConstraintsValid: true, } switch certType { case IssueCertTypeCA: template.IsCA = true template.KeyUsage |= x509.KeyUsageCRLSign | x509.KeyUsageCertSign if issuer == nil { template.Subject.CommonName = "elastic-package CA" } else { template.Subject.CommonName = "intermediate elastic-package CA" } case IssueCertTypeClient: // If the requester is a client we set clientAuth instead template.ExtKeyUsage = []x509.ExtKeyUsage{ x509.ExtKeyUsageClientAuth, } // Include local hostname and ips as alternates in service certificates. template.DNSNames = []string{"localhost"} template.IPAddresses = []net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("::1")} case IssueCertTypeService: template.ExtKeyUsage = []x509.ExtKeyUsage{ // Required for Chrome in OSX to show the "Proceed anyway" link. // https://stackoverflow.com/a/64309893/28855 x509.ExtKeyUsageServerAuth, } // Include local hostname and ips as alternates in service certificates. template.DNSNames = []string{"localhost"} template.IPAddresses = []net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("::1")} default: return nil, fmt.Errorf("unknown certificate type %d", certType) } for _, opt := range opts { opt(&template) } // Self-signed unless an issuer has been received. var parent *x509.Certificate = &template var signer crypto.Signer = key var issuerCert *Certificate if issuer != nil { parent = issuer.cert signer = issuer.key issuerCert = issuer.Certificate template.Issuer = issuer.cert.Subject } der, err := x509.CreateCertificate(rand.Reader, &template, parent, key.Public(), signer) if err != nil { return nil, fmt.Errorf("failed to generate certificate: %w", err) } cert, err := x509.ParseCertificate(der) if err != nil { return nil, fmt.Errorf("failed to parse certificate: %w", err) } return &Certificate{ key: key, cert: cert, issuer: issuerCert, }, nil } func newSerialNumber() (*big.Int, error) { // This implementation attempts to get unique serial numbers // by getting random ones between 0 and 2^128. max := new(big.Int).Exp(big.NewInt(2), big.NewInt(128), nil) return rand.Int(rand.Reader, max) } // WriteKey writes the PEM-encoded key in the given writer. func (c *Certificate) WriteKey(w io.Writer) error { keyPem, err := keyPemBlock(c.key) if err != nil { return fmt.Errorf("failed to encode key PEM block: %w", err) } return encodePem(w, keyPem) } // WriteKeyFile writes the PEM-encoded key in the given file. func (c *Certificate) WriteKeyFile(path string) error { err := os.MkdirAll(filepath.Dir(path), 0755) if err != nil { return fmt.Errorf("error creating directory for key file: %w", err) } f, err := os.Create(path) if err != nil { return fmt.Errorf("failed to create key file %q: %w", path, err) } defer f.Close() return c.WriteKey(f) } // WriteCert writes the PEM-encoded certificate chain in the given writer. func (c *Certificate) WriteCert(w io.Writer) error { for i := c; i != nil; i = i.issuer { err := encodePem(w, certPemBlock(i.cert.Raw)) if err != nil { return err } } return nil } // WriteEnv writes the environment variables about the certificate in the given writer. func (c *Certificate) WriteEnv(w io.Writer) error { fingerprint := c.Fingerprint() _, err := fmt.Fprintf(w, "%s=%s\n", "ELASTIC_PACKAGE_CA_TRUSTED_FINGERPRINT", strings.ToUpper(hex.EncodeToString(fingerprint))) return err } // Fingerprint returns the fingerprint of the certificate. The fingerprint // of a CA can be used to verify certificates. func (c *Certificate) Fingerprint() []byte { f := sha256.Sum256(c.cert.Raw) return f[:] } // WriteCertFile writes the PEM-encoded certifiacte in the given file. func (c *Certificate) WriteCertFile(path string) error { err := os.MkdirAll(filepath.Dir(path), 0755) if err != nil { return fmt.Errorf("error creating directory for certificate file: %w", err) } f, err := os.Create(path) if err != nil { return fmt.Errorf("failed to create cert file %q: %w", path, err) } defer f.Close() return c.WriteCert(f) } // Verify verifies a certificate with the given verification options. func (c *Certificate) Verify(options x509.VerifyOptions) error { err := checkExpectedCertUsage(c.cert) if err != nil { return err } _, err = c.cert.Verify(options) return err } func checkExpectedCertUsage(cert *x509.Certificate) error { expectedKeyUsage := x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature if cert.IsCA { expectedKeyUsage |= x509.KeyUsageCRLSign | x509.KeyUsageCertSign } if cert.KeyUsage&expectedKeyUsage != expectedKeyUsage { return fmt.Errorf("missing expected usage flags in certificate") } if !cert.IsCA { // Required for Chrome in OSX to show the "Proceed anyway" link. // https://stackoverflow.com/a/64309893/28855 if !(containsExtKeyUsage(cert.ExtKeyUsage, x509.ExtKeyUsageServerAuth) || containsExtKeyUsage(cert.ExtKeyUsage, x509.ExtKeyUsageClientAuth)) { return fmt.Errorf("missing either of server/client auth key usage in certificate") } } return nil } func containsExtKeyUsage(us []x509.ExtKeyUsage, u x509.ExtKeyUsage) bool { for _, candidate := range us { if u == candidate { return true } } return false } func certPemBlock(cert []byte) *pem.Block { const certificatePemType = "CERTIFICATE" return &pem.Block{ Type: certificatePemType, Bytes: cert, } } func keyPemBlock(key crypto.Signer) (*pem.Block, error) { const ( ecPrivateKeyPemType = "EC PRIVATE KEY" rsaPrivateKeyPemType = "RSA PRIVATE KEY" ) switch key := key.(type) { case *rsa.PrivateKey: d := x509.MarshalPKCS1PrivateKey(key) return &pem.Block{ Type: rsaPrivateKeyPemType, Bytes: d, }, nil case *ecdsa.PrivateKey: d, err := x509.MarshalECPrivateKey(key) if err != nil { return nil, fmt.Errorf("failed to encode EC private key: %w", err) } return &pem.Block{ Type: ecPrivateKeyPemType, Bytes: d, }, nil default: return nil, fmt.Errorf("unsupported key type %T", key) } } func encodePem(w io.Writer, blocks ...*pem.Block) error { for _, block := range blocks { err := pem.Encode(w, block) if err != nil { return err } } return nil }