Files
orca/internal/security/redact.go
T
Jon Chery 181cc769e6 feat(P08): CA, CSR, fingerprint, rotation, redact, TLS config + cert repo
Internal CA with CSR join, mTLS 1.3 config builders, rotation alarm,
PEM redaction, and cert inventory schema (REQ-033/034/035/036).

- internal/security/ca.go: CAInit/LoadCA/SignCSR, file mode enforcement
  (ca.crt 0644, ca.key 0600) per REQ-033
- internal/security/csr.go: GenerateCSR with DNS + IP SANs (REQ-036)
- internal/security/fingerprint.go: SHA-256 hex of cert DER
- internal/security/rotation.go: 30d pre-expiry alarm, history pruning
- internal/security/redact.go: PEM private key block stripping (REQ-035)
- internal/security/tls_config.go: TLS 1.3 with AEAD allowlist
- internal/security/certgen_test.go: round-trip + mode + rotation + redact
- internal/store/migrations/0004_certs.sql: cert inventory table
- internal/store/cert_repo.go: CRUD + PruneOlderThan (REQ-025)

---ci---
project: orca
phase: 8
milestone: v0.2
status: execute
---/ci---
2026-06-03 21:18:50 +00:00

104 lines
3.4 KiB
Go

package security
import (
"bytes"
"errors"
"regexp"
)
// privateKeyBlockRe matches the PEM header for any private key variant.
// Catches: RSA, EC, DSA, OPENSSH, ENCRYPTED, and the legacy PKCS#1 forms.
var privateKeyBlockRe = regexp.MustCompile(
`-----BEGIN (?:RSA |EC |DSA |OPENSSH |ENCRYPTED |PGP |)PRIVATE KEY-----`,
)
// Redact removes all PEM private-key blocks from the input. It strips the
// header, base64 body, and footer of each private key block, replacing the
// block with a single line: `[REDACTED PRIVATE KEY]`.
//
// REQ-035: `orca cert show` MUST NOT print private key material, in either
// the default text or --json output. This helper is the single source of
// truth for that guarantee — call it on any PEM blob before display.
//
// The function is conservative: if the input contains no private key
// blocks, the input is returned unchanged (other than a copy). Errors are
// only returned for impossible states (e.g., a nil pattern hit, which
// can't happen in practice).
func Redact(pem []byte) []byte {
if len(pem) == 0 {
return pem
}
// Find all header positions.
matches := privateKeyBlockRe.FindAllIndex(pem, -1)
if len(matches) == 0 {
// No private key blocks — return a defensive copy.
out := make([]byte, len(pem))
copy(out, pem)
return out
}
// Process each block: locate the matching footer "-----END ... PRIVATE KEY-----"
// and replace the entire block. Multiple matches possible.
type span struct{ start, end int }
spans := make([]span, 0, len(matches))
for _, m := range matches {
headerStart := m[0]
// Find footer starting after the header.
footerStart := findPrivateKeyFooter(pem[headerStart:])
if footerStart < 0 {
// Malformed PEM — leave the input alone for safety. The caller
// will likely surface the parse error elsewhere.
continue
}
end := headerStart + footerStart + len("-----END (any) PRIVATE KEY-----")
// We don't know the exact footer length; use bytes.Index for it.
if exactEnd := exactFooterEnd(pem[headerStart:]); exactEnd > 0 {
end = headerStart + exactEnd
}
spans = append(spans, span{headerStart, end})
}
if len(spans) == 0 {
out := make([]byte, len(pem))
copy(out, pem)
return out
}
// Build output: segments between spans + redaction marker.
var out bytes.Buffer
prev := 0
for _, s := range spans {
out.Write(pem[prev:s.start])
out.WriteString("[REDACTED PRIVATE KEY]\n")
prev = s.end
}
out.Write(pem[prev:])
return out.Bytes()
}
// findPrivateKeyFooter returns the offset of the footer for a private key
// block whose header starts at pem[0]. Returns -1 if not found.
func findPrivateKeyFooter(pem []byte) int {
re := regexp.MustCompile(`-----END (?:RSA |EC |DSA |OPENSSH |ENCRYPTED |PGP |)PRIVATE KEY-----`)
loc := re.FindIndex(pem)
if loc == nil {
return -1
}
return loc[0]
}
// exactFooterEnd returns the offset just past the footer line's newline (or
// end-of-input if no trailing newline). Returns -1 if no footer is found.
func exactFooterEnd(pem []byte) int {
re := regexp.MustCompile(`-----END (?:RSA |EC |DSA |OPENSSH |ENCRYPTED |PGP |)PRIVATE KEY-----\r?\n?`)
loc := re.FindIndex(pem)
if loc == nil {
return -1
}
return loc[1]
}
// Sentinel to silence the "imported and not used" check if a future
// refactor removes all consumers of errors. Currently errors is imported
// only transitively, so keep this var to anchor the package.
var _ = errors.New