fc94326b0e
P00 — Re-architecture Foundation (deprecation/migration/test-infra/persona/docs). Deprecation sweep (REQ-068, REQ-072, REQ-089): - Add // Deprecated: doc comments to internal/daemon (R-001), internal/transport (REQ-073), internal/security/ca.go+csr.go (D-101/REQ-076), internal/engine/ dispatcher.go+peer.go (CLI-side scheduler), internal/cli/daemon.go. - orca daemon emits slog.Warn deprecation banner on every run (ungated); fires R-001 + v0.10-P05 drain-and-stop + v0.10-P14 deletion. - orca cert and orca node join (mTLS path) emit deprecation warnings; proxmox SSH path (the v0.9 replacement) does not warn. - Add --no-deprecation-warnings global flag on root command (PersistentPreRunE) for orca upgrade migrations. - 12 new daemon/cert/node deprecation tests in internal/cli/daemon_test.go (cli coverage 81.9%, warnDeprecated 100%). - Add DEPRECATED banners to v0.8 sections of ARCHITECTURE.md (verified the v0.9 supersession section + Supersession Table from prior turn are present). Bash tooling gate (grill C-06, C-15, C-16, C-17, C-18): - scripts/tests/test_helper.bash + example_test.bash — bats framework + helpers. - scripts/lib/orca-log.sh — slog-compatible JSON logging to syslog (C-17). - scripts/orca-verify-render.sh — render-contract validator skeleton (C-16). - scripts/tests/orca-log_test.bash + orca-verify-render_test.bash — 20 bats tests total (happy + failure paths per C-15). - .shellcheckrc — project shellcheck config. - Makefile: test-bash + lint-bash targets (graceful skip if tools missing); wired into test + lint targets. - internal/emit/contract.go + contract_test.go — versioned JSON render contract (orca.emit/v1) between Go emitters and bash appliers (C-16). - .ciagent/BASH_CAPABILITY_MAP_v0.9.md — maps shipped internal/transport capabilities to bash-side equivalents or accepted drops (C-18). - D-186 recorded in PROJECT.md: bash exempt from Go coverage gate; compensating control is bats + shellcheck + shfmt (C-06). verify-reqs: 90 requirements consistent. Build/test/lint/fmt all green. 20 bats tests pass. Go tests pass. No v0.8 code deleted — only marked deprecated (deletion deferred to v0.10-P14 per REQ-090 dual-write window). ---ci--- project: orca phase: P00 milestone: v0.9 status: execute ---/ci---
274 lines
8.6 KiB
Go
274 lines
8.6 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.`,
|
|
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
|
warnDeprecated("orca cert is deprecated in v0.9: step-ca (D-101) now handles CA; orca cert will be removed in v0.10 — see .ciagent/PRD_v0.9.md")
|
|
return nil
|
|
},
|
|
}
|
|
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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()))
|
|
}
|