testing/certutil/cmd/main.go (167 lines of code) (raw):

// Licensed to Elasticsearch B.V. under one or more contributor // license agreements. See the NOTICE file distributed with // this work for additional information regarding copyright // ownership. Elasticsearch B.V. licenses this file to you under // the Apache License, Version 2.0 (the "License"); you may // not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. //nolint:errorlint,forbidigo // it's a cli application package main import ( "crypto" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/rsa" "crypto/tls" "crypto/x509" "flag" "fmt" "net" "os" "path/filepath" "strings" "time" "github.com/elastic/elastic-agent-libs/testing/certutil" ) func main() { var caPath, caKeyPath, dest, name, names, ipList, prefix, pass string var client, rsaflag, noip bool flag.StringVar(&caPath, "ca", "", "File path for CA in PEM format") flag.StringVar(&caKeyPath, "ca-key", "", "File path for the CA key in PEM format") flag.BoolVar(&rsaflag, "rsa", false, "generate a RSA with a 2048-bit key certificate") flag.BoolVar(&client, "client", false, "generates a client certificate without any IP or SAN/DNS") flag.StringVar(&name, "name", "localhost", "a single \"Subject Alternate Name values\" for the child certificate. It's added to 'names' if set") flag.StringVar(&names, "names", "", "a comma separated list of \"Subject Alternate Name values\" for the child certificate") flag.BoolVar(&noip, "noip", false, "generate a certificate with no IP. It overrides -ips.") flag.StringVar(&ipList, "ips", "127.0.0.1", "a comma separated list of IP addresses for the child certificate") flag.StringVar(&prefix, "prefix", "current timestamp", "a prefix to be added to the file name. If not provided a timestamp will be used") flag.StringVar(&pass, "pass", "", "a passphrase to encrypt the certificate key") flag.Parse() if caPath == "" && caKeyPath != "" || caPath != "" && caKeyPath == "" { flag.Usage() fmt.Fprintf(flag.CommandLine.Output(), "Both 'ca' and 'ca-key' must be specified, or neither should be provided.\nGot ca: %s, ca-key: %s\n", caPath, caKeyPath) } if prefix == "current timestamp" { prefix = fmt.Sprintf("%d", time.Now().Unix()) } filePrefix := prefix + "-" wd, err := os.Getwd() if err != nil { fmt.Printf("error getting current working directory: %v\n", err) } fmt.Println("files will be witten to:", wd) var netIPs []net.IP if !noip { ips := strings.Split(ipList, ",") for _, ip := range ips { netIPs = append(netIPs, net.ParseIP(ip)) } } var dnsNames []string if names != "" { dnsNames = strings.Split(names, ",") } rootCert, rootKey := getCA(rsaflag, caPath, caKeyPath, dest, prefix) priv, pub := generateKey(rsaflag) childCert, childPair, err := certutil.GenerateGenericChildCert( name, netIPs, priv, pub, rootKey, rootCert, certutil.WithCNPrefix(prefix), certutil.WithDNSNames(dnsNames...), certutil.WithClientCert(client)) if err != nil { panic(fmt.Errorf("error generating child certificate: %w", err)) } if client { name = "client" } savePair(dest, filePrefix+name, childPair) if pass != "" { fmt.Printf("passphrase present, encrypting \"%s\" certificate key\n", name) err = os.WriteFile(filePrefix+name+"-passphrase", []byte(pass), 0o600) if err != nil { panic(fmt.Errorf("error writing passphrase file: %w", err)) } certKeyEnc, err := certutil.EncryptKey(childCert.PrivateKey, pass) if err != nil { panic(err) } err = os.WriteFile(filepath.Join(dest, filePrefix+name+"_enc-key.pem"), certKeyEnc, 0o600) if err != nil { panic(fmt.Errorf("could not save %s certificate encrypted key: %w", filePrefix+name+"_enc-key.pem", err)) } } } func generateKey(useRSA bool) (crypto.PrivateKey, crypto.PublicKey) { if useRSA { priv, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { panic(fmt.Errorf("failed to generate RSA key: %v", err)) } return priv, &priv.PublicKey } priv, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) if err != nil { panic(fmt.Errorf("failed to generate EC key: %v", err)) } return priv, &priv.PublicKey } func getCA(rsa bool, caPath, caKeyPath, dest, prefix string) (*x509.Certificate, crypto.PrivateKey) { var rootCert *x509.Certificate var rootKey crypto.PrivateKey var err error if caPath == "" && caKeyPath == "" { caFn := certutil.NewRootCA if rsa { caFn = certutil.NewRSARootCA } var pair certutil.Pair rootKey, rootCert, pair, err = caFn(certutil.WithCNPrefix(prefix)) if err != nil { panic(fmt.Errorf("could not create root CA certificate: %w", err)) } savePair(dest, prefix+"-ca", pair) } else { rootKey, rootCert = loadCA(caPath, caKeyPath) } return rootCert, rootKey } func loadCA(caPath string, keyPath string) (crypto.PrivateKey, *x509.Certificate) { caBytes, err := os.ReadFile(caPath) if err != nil { panic(fmt.Errorf("failed reading CA file: %w", err)) } keyBytes, err := os.ReadFile(keyPath) if err != nil { panic(fmt.Errorf("failed reading CA key file: %w", err)) } tlsCert, err := tls.X509KeyPair(caBytes, keyBytes) if err != nil { panic(fmt.Errorf("failed generating TLS key pair: %w", err)) } rootCACert, err := x509.ParseCertificate(tlsCert.Certificate[0]) if err != nil { panic(fmt.Errorf("could not parse certificate: %w", err)) } return tlsCert.PrivateKey, rootCACert } func savePair(dest string, name string, pair certutil.Pair) { err := os.WriteFile(filepath.Join(dest, name+".pem"), pair.Cert, 0o600) if err != nil { panic(fmt.Errorf("could not save %s certificate: %w", name, err)) } err = os.WriteFile(filepath.Join(dest, name+"_key.pem"), pair.Key, 0o600) if err != nil { panic(fmt.Errorf("could not save %s certificate key: %w", name, err)) } }