Files
orca/internal/cli/cert.go
T
Jon Chery 97b88a703c feat(P13): ns subcommands (inherit, set-constraint) + deprecation warnings (REQ-068)
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---
2026-08-07 07:47:28 +00:00

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()))
}