148 lines
5.0 KiB
Go
148 lines
5.0 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto"
|
|
"crypto/x509"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/lixenwraith/logwisp/internal/tlsx"
|
|
)
|
|
|
|
// maxDays keeps a validity well inside time.Duration, whose overflow would
|
|
// date a certificate in the past
|
|
const maxDays = 36500
|
|
|
|
var tlsCommands = []subcommand{
|
|
{"ca", "--dir DIR [--name NAME] [--days N]",
|
|
"Create a certificate authority, DIR/ca.crt and DIR/ca.key, that signs certificates only",
|
|
[]string{"dir"}, defineCA},
|
|
{"cert", "--ca-dir DIR --name NAME [--host NAME,...] [--server] [--client] [--days N] [--out DIR]",
|
|
"Issue a certificate and key, OUT/NAME.crt and OUT/NAME.key, from DIR/ca.crt and DIR/ca.key",
|
|
[]string{"ca-dir", "name"}, defineCert},
|
|
}
|
|
|
|
func defineCA(fs *flag.FlagSet) func(stdout, stderr io.Writer) error {
|
|
dir := fs.String("dir", "", "`dir`ectory to write ca.crt and ca.key in, created when missing")
|
|
name := fs.String("name", "logwisp CA", "subject common `name`")
|
|
days := fs.Int("days", int(tlsx.CAValidity/(24*time.Hour)), "validity in `n` days")
|
|
return func(_, stderr io.Writer) error {
|
|
if *days < 1 || *days > maxDays {
|
|
return usageError(fmt.Sprintf("--days must be 1 to %d", maxDays))
|
|
}
|
|
cert, key, err := tlsx.NewCA(*name, time.Duration(*days)*24*time.Hour)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := writePair(*dir, "ca", cert, key); err != nil {
|
|
return err
|
|
}
|
|
fmt.Fprintf(stderr, "wrote %s and %s, valid until %s\n"+
|
|
"ca.crt verifies: dialers' tls.ca_file, listeners' tls.client_ca_file; with ca.key, it signs: lw tls cert, tls.issuer_cert_file\n",
|
|
filepath.Join(*dir, "ca.crt"), filepath.Join(*dir, "ca.key"), cert.NotAfter.UTC().Format(time.DateOnly))
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func defineCert(fs *flag.FlagSet) func(stdout, stderr io.Writer) error {
|
|
caDir := fs.String("ca-dir", "", "`dir`ectory holding ca.crt and ca.key (lw tls ca)")
|
|
name := fs.String("name", "", "subject common `name`, the mtls identity, and the file names")
|
|
hosts := fs.String("host", "", "DNS `names` and IP addresses, ',' between them (default with --server: --name)")
|
|
server := fs.Bool("server", false, "for listeners (TLS server authentication)")
|
|
client := fs.Bool("client", false, "for dialers presenting a certificate (TLS client authentication)")
|
|
days := fs.Int("days", int(tlsx.LeafValidity/(24*time.Hour)), "validity in `n` days, at most the CA's")
|
|
out := fs.String("out", "", "`dir`ectory to write in (default: --ca-dir)")
|
|
return func(_, stderr io.Writer) error {
|
|
switch {
|
|
case !*server && !*client:
|
|
return usageError("--server, --client or both is required")
|
|
case *days < 1 || *days > maxDays:
|
|
return usageError(fmt.Sprintf("--days must be 1 to %d", maxDays))
|
|
case strings.ContainsAny(*name, `/\`) || *name == "ca" || strings.HasPrefix(*name, "."):
|
|
return usageError(fmt.Sprintf("--name %q cannot name the files", *name))
|
|
}
|
|
issuer, signer, err := tlsx.LoadIssuer(filepath.Join(*caDir, "ca.crt"), filepath.Join(*caDir, "ca.key"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
leaf := tlsx.Leaf{Name: *name, Server: *server, Client: *client, Validity: time.Duration(*days) * 24 * time.Hour}
|
|
for h := range strings.SplitSeq(*hosts, ",") {
|
|
if h = strings.TrimSpace(h); h != "" {
|
|
leaf.Hosts = append(leaf.Hosts, h)
|
|
}
|
|
}
|
|
if len(leaf.Hosts) == 0 && *server {
|
|
leaf.Hosts = []string{*name}
|
|
}
|
|
key, err := tlsx.NewKey()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cert, err := leaf.Issue(&key.PublicKey, issuer, signer)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
dir := *out
|
|
if dir == "" {
|
|
dir = *caDir
|
|
}
|
|
if err := writePair(dir, *name, cert, key); err != nil {
|
|
return err
|
|
}
|
|
fmt.Fprintf(stderr, "wrote %s and %s for %v, valid until %s, pin_sha256 %s\n",
|
|
filepath.Join(dir, *name+".crt"), filepath.Join(dir, *name+".key"),
|
|
append(leaf.Hosts, "CN="+*name), cert.NotAfter.UTC().Format(time.DateOnly), tlsx.PinSHA256(cert))
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// writePair writes NAME.crt (0644) and NAME.key (0600) in dir, refusing to
|
|
// replace either: a reissued key would silently break every pin and peer.
|
|
func writePair(dir, name string, cert *x509.Certificate, key crypto.PrivateKey) error {
|
|
keyPEM, err := tlsx.EncodeKey(key)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
return err
|
|
}
|
|
certPath, keyPath := filepath.Join(dir, name+".crt"), filepath.Join(dir, name+".key")
|
|
for _, p := range []string{certPath, keyPath} {
|
|
if _, err := os.Lstat(p); !errors.Is(err, os.ErrNotExist) {
|
|
return fmt.Errorf("%s exists; remove it to replace it", p)
|
|
}
|
|
}
|
|
if err := writeNew(keyPath, keyPEM, 0o600); err != nil {
|
|
return err
|
|
}
|
|
if err := writeNew(certPath, tlsx.EncodeCert(cert), 0o644); err != nil {
|
|
os.Remove(keyPath)
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func writeNew(path string, data []byte, mode os.FileMode) error {
|
|
f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, mode)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = f.Write(data)
|
|
if err == nil {
|
|
err = f.Sync()
|
|
}
|
|
if cerr := f.Close(); err == nil {
|
|
err = cerr
|
|
}
|
|
if err != nil {
|
|
os.Remove(path)
|
|
}
|
|
return err
|
|
}
|