97b88a703c
orca ns inherit <name> --parent (cycle detection), ns set-constraint <key>=value>. Deprecation warnings on orca cert ca-init/gen/renew (step-ca replaces) and .hcl jobspec (R-013). --no-deprecation-warnings suppresses all. ---ci--- project: orca phase: 13 milestone: v0.11 status: execute ---/ci---
273 lines
8.8 KiB
Go
273 lines
8.8 KiB
Go
// cert.go implements the `orca cert` subcommand family.
|
|
//
|
|
// Subcommands:
|
|
//
|
|
// orca cert ca-init — bootstrap a local CA in ~/.orca/
|
|
// orca cert gen — generate a server CSR + sign it with the local CA
|
|
// orca cert show — print the active server cert (redacted; REQ-035)
|
|
// orca cert renew — re-issue and rotate the server cert
|
|
// orca cert fingerprint — print the SHA-256 of ca.crt or server.crt
|
|
//
|
|
// All subcommands refuse to operate if the on-disk CA / cert file modes
|
|
// do not match REQ-033 (0600 for keys, 0644 for certs).
|
|
package cli
|
|
|
|
import (
|
|
"encoding/pem"
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
"git.cloudinit.dev/coreci/orca/internal/certpaths"
|
|
"git.cloudinit.dev/coreci/orca/internal/security"
|
|
)
|
|
|
|
// CADir returns the directory the local CA lives in. Re-exported for
|
|
// backward compatibility with callers that imported this from the cli
|
|
// package directly.
|
|
func CADir() string { return certpaths.Dir() }
|
|
|
|
// CACertPath returns the path to ca.crt.
|
|
func CACertPath() string { return certpaths.CACertPath() }
|
|
|
|
// CAKeyPath returns the path to ca.key.
|
|
func CAKeyPath() string { return certpaths.CAKeyPath() }
|
|
|
|
// ServerCertPath returns the path to server.crt.
|
|
func ServerCertPath() string { return certpaths.ServerCertPath() }
|
|
|
|
// ServerKeyPath returns the path to server.key.
|
|
func ServerKeyPath() string { return certpaths.ServerKeyPath() }
|
|
|
|
// NewCommand builds the `orca cert` command tree.
|
|
//
|
|
// Deprecated: v0.9 re-architecture replaces the internal CA with step-ca
|
|
// (D-101/REQ-076). The `orca cert` command tree is retained for the
|
|
// dual-write window and scheduled for deletion in v0.10. See
|
|
// .ciagent/PRD_v0.9.md.
|
|
func NewCommand(log *slog.Logger) *cobra.Command {
|
|
if log == nil {
|
|
log = slog.Default()
|
|
}
|
|
certCmd := &cobra.Command{
|
|
Use: "cert",
|
|
Short: "Manage orca certificates (CA, server, rotation)",
|
|
Long: `Manage orca certificates (CA, server, rotation).
|
|
|
|
Deprecated: v0.9 re-architecture replaces the internal CA with step-ca
|
|
(D-101/REQ-076). The ` + "`orca cert`" + ` command tree is retained for the
|
|
dual-write window and scheduled for deletion in v0.10. See
|
|
.ciagent/PRD_v0.9.md.`,
|
|
}
|
|
|
|
certCmd.AddCommand(newCAInitCmd(log))
|
|
certCmd.AddCommand(newGenCmd(log))
|
|
certCmd.AddCommand(newShowCmd(log))
|
|
certCmd.AddCommand(newRenewCmd(log))
|
|
certCmd.AddCommand(newFingerprintCmd(log))
|
|
return certCmd
|
|
}
|
|
|
|
func newCAInitCmd(log *slog.Logger) *cobra.Command {
|
|
var cn string
|
|
cmd := &cobra.Command{
|
|
Use: "ca-init",
|
|
Short: "Initialize a local orca CA (ca.crt + ca.key) under ~/.orca",
|
|
Long: "Generates a new RSA CA cert and writes it to ~/.orca/ca.crt (0644) and ~/.orca/ca.key (0600) per REQ-033.",
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
warnDeprecated("orca cert ca-init is deprecated: use step-ca (R-006); the internal CA is replaced by step-ca (D-101) — see .ciagent/PRD_v0.9.md")
|
|
dir := CADir()
|
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
return fmt.Errorf("mkdir %s: %w", dir, err)
|
|
}
|
|
ca, err := security.CAInit(dir, cn)
|
|
if err != nil {
|
|
return fmt.Errorf("ca-init: %w", err)
|
|
}
|
|
fp := ca.Fingerprint()
|
|
if _, err := fmt.Fprintf(cmd.OutOrStdout(), "✓ CA initialized at %s\n fingerprint (sha256): %s\n not_after: %s\n",
|
|
dir, fp, ca.NotAfter.UTC().Format("2006-01-02")); err != nil {
|
|
return err
|
|
}
|
|
log.Info("cert.ca_init",
|
|
slog.String("event", "cert.ca_init"),
|
|
slog.String("dir", dir),
|
|
slog.String("cert_fp", fp),
|
|
)
|
|
return nil
|
|
},
|
|
}
|
|
cmd.Flags().StringVar(&cn, "cn", "orca-local-ca", "CA common name")
|
|
return cmd
|
|
}
|
|
|
|
func newGenCmd(log *slog.Logger) *cobra.Command {
|
|
var cn string
|
|
var sans []string
|
|
cmd := &cobra.Command{
|
|
Use: "gen",
|
|
Short: "Generate a server cert (CSR + sign) under ~/.orca",
|
|
Long: "Builds a CSR with the requested SANs, signs it with the local CA, and writes server.crt + server.key.",
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
warnDeprecated("orca cert gen is deprecated: use step-ca + orca node join for cert generation (D-101) — see .ciagent/PRD_v0.9.md")
|
|
dir := CADir()
|
|
if cn == "" {
|
|
cn = "orca-server"
|
|
}
|
|
ca, err := security.LoadCA(dir)
|
|
if err != nil {
|
|
return fmt.Errorf("load CA (run `orca cert ca-init` first): %w", err)
|
|
}
|
|
keyPEM, csrPEM, err := security.GenerateCSR(cn, sans)
|
|
if err != nil {
|
|
return fmt.Errorf("generate CSR: %w", err)
|
|
}
|
|
certPEM, err := ca.SignCSR(csrPEM)
|
|
if err != nil {
|
|
return fmt.Errorf("sign CSR: %w", err)
|
|
}
|
|
certPath := ServerCertPath()
|
|
keyPath := ServerKeyPath()
|
|
if err := security.WriteCert(certPath, certPEM); err != nil {
|
|
return fmt.Errorf("write cert: %w", err)
|
|
}
|
|
if err := security.WriteKey(keyPath, keyPEM); err != nil {
|
|
return fmt.Errorf("write key: %w", err)
|
|
}
|
|
fp := security.FingerprintOf(parseFirstCertDER(certPEM))
|
|
if _, err := fmt.Fprintf(cmd.OutOrStdout(), "✓ Server cert generated\n cert: %s\n key: %s\n fingerprint (sha256): %s\n",
|
|
certPath, keyPath, fp); err != nil {
|
|
return err
|
|
}
|
|
log.Info("cert.issued",
|
|
slog.String("event", "cert.issued"),
|
|
slog.String("cn", cn),
|
|
slog.String("cert_fp", fp),
|
|
)
|
|
return nil
|
|
},
|
|
}
|
|
cmd.Flags().StringVar(&cn, "cn", "orca-server", "server cert common name")
|
|
cmd.Flags().StringSliceVar(&sans, "san", []string{"localhost", "127.0.0.1"}, "SAN entries (DNS or IP) — at least one required (REQ-036)")
|
|
return cmd
|
|
}
|
|
|
|
func newShowCmd(log *slog.Logger) *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "show",
|
|
Short: "Print the server cert (private keys redacted; REQ-035)",
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
pem, err := os.ReadFile(ServerCertPath())
|
|
if err != nil {
|
|
return fmt.Errorf("read server cert: %w", err)
|
|
}
|
|
// Per REQ-035, strip private key material before display.
|
|
out := security.Redact(pem)
|
|
if _, err := cmd.OutOrStdout().Write(out); err != nil {
|
|
return err
|
|
}
|
|
log.Debug("cert.show", slog.String("event", "cert.show"))
|
|
return nil
|
|
},
|
|
}
|
|
return cmd
|
|
}
|
|
|
|
func newRenewCmd(log *slog.Logger) *cobra.Command {
|
|
var cn string
|
|
var sans []string
|
|
cmd := &cobra.Command{
|
|
Use: "renew",
|
|
Short: "Rotate the server cert (hot-swapped by the daemon; REQ-034)",
|
|
Long: "Re-runs `cert gen` and overwrites server.crt / server.key in place. The daemon's GetCertificate callback picks up the new cert on the next handshake — no restart required.",
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
warnDeprecated("orca cert renew is deprecated: use step-ca for cert rotation (D-101) — see .ciagent/PRD_v0.9.md")
|
|
dir := CADir()
|
|
if cn == "" {
|
|
cn = "orca-server"
|
|
}
|
|
ca, err := security.LoadCA(dir)
|
|
if err != nil {
|
|
return fmt.Errorf("load CA: %w", err)
|
|
}
|
|
keyPEM, csrPEM, err := security.GenerateCSR(cn, sans)
|
|
if err != nil {
|
|
return fmt.Errorf("generate CSR: %w", err)
|
|
}
|
|
certPEM, err := ca.SignCSR(csrPEM)
|
|
if err != nil {
|
|
return fmt.Errorf("sign CSR: %w", err)
|
|
}
|
|
if err := security.WriteCert(ServerCertPath(), certPEM); err != nil {
|
|
return fmt.Errorf("write cert: %w", err)
|
|
}
|
|
if err := security.WriteKey(ServerKeyPath(), keyPEM); err != nil {
|
|
return fmt.Errorf("write key: %w", err)
|
|
}
|
|
fp := security.FingerprintOf(parseFirstCertDER(certPEM))
|
|
if _, err := fmt.Fprintln(cmd.OutOrStdout(), "✓ Server cert rotated"); err != nil {
|
|
return err
|
|
}
|
|
log.Info("cert.renewed",
|
|
slog.String("event", "cert.renewed"),
|
|
slog.String("cert_fp", fp),
|
|
)
|
|
return nil
|
|
},
|
|
}
|
|
cmd.Flags().StringVar(&cn, "cn", "orca-server", "server cert common name")
|
|
cmd.Flags().StringSliceVar(&sans, "san", []string{"localhost", "127.0.0.1"}, "SAN entries (DNS or IP)")
|
|
return cmd
|
|
}
|
|
|
|
func newFingerprintCmd(log *slog.Logger) *cobra.Command {
|
|
var which string
|
|
cmd := &cobra.Command{
|
|
Use: "fingerprint",
|
|
Short: "Print the SHA-256 fingerprint of ca.crt or server.crt",
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
var path string
|
|
switch which {
|
|
case "ca", "":
|
|
path = CACertPath()
|
|
case "server":
|
|
path = ServerCertPath()
|
|
default:
|
|
return fmt.Errorf("--which must be 'ca' or 'server'")
|
|
}
|
|
fp, err := security.Fingerprint(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, err := fmt.Fprintln(cmd.OutOrStdout(), fp); err != nil {
|
|
return err
|
|
}
|
|
log.Debug("cert.fingerprint",
|
|
slog.String("event", "cert.fingerprint"),
|
|
slog.String("path", path),
|
|
slog.String("cert_fp", fp),
|
|
)
|
|
return nil
|
|
},
|
|
}
|
|
cmd.Flags().StringVar(&which, "which", "ca", "which cert: 'ca' or 'server'")
|
|
return cmd
|
|
}
|
|
|
|
// parseFirstCertDER decodes the first CERTIFICATE PEM block in pemBytes
|
|
// and returns the DER bytes. Used by the cert cli for fingerprint calc
|
|
// after a fresh issuance.
|
|
func parseFirstCertDER(pemBytes []byte) []byte {
|
|
block, _ := pem.Decode(pemBytes)
|
|
if block == nil {
|
|
return nil
|
|
}
|
|
return block.Bytes
|
|
}
|
|
|
|
func init() {
|
|
rootCmd.AddCommand(NewCommand(slog.Default()))
|
|
}
|