181cc769e6
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---
104 lines
3.4 KiB
Go
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
|