Files
orca/internal/security/fingerprint.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

59 lines
1.7 KiB
Go

package security
import (
"crypto/sha256"
"crypto/x509"
"encoding/hex"
"encoding/pem"
"errors"
"fmt"
"os"
)
// Fingerprint returns the SHA-256 hex digest of the certificate's DER bytes,
// computed from the on-disk PEM at certPath. The output is lowercase hex
// (64 chars) and matches the value operators see with `openssl x509 -fingerprint
// -sha256 -noout`. Used for the `orca node join --ca-fingerprint <sha>` pin.
func Fingerprint(certPath string) (string, error) {
if certPath == "" {
return "", errors.New("Fingerprint: certPath is required")
}
pemBytes, err := os.ReadFile(certPath)
if err != nil {
return "", fmt.Errorf("Fingerprint: read cert: %w", err)
}
der, err := firstCertDER(pemBytes)
if err != nil {
return "", fmt.Errorf("Fingerprint: %w", err)
}
return FingerprintOf(der), nil
}
// FingerprintOf returns the SHA-256 hex digest of a DER-encoded certificate.
// Lowercase hex; matches `openssl ... -fingerprint -sha256` output.
func FingerprintOf(der []byte) string {
sum := sha256.Sum256(der)
return hex.EncodeToString(sum[:])
}
// firstCertDER decodes PEM bytes and returns the DER of the first
// CERTIFICATE block. Errors if the input is empty or no CERTIFICATE block
// is present.
func firstCertDER(pemBytes []byte) ([]byte, error) {
if len(pemBytes) == 0 {
return nil, errors.New("empty input")
}
block, _ := pem.Decode(pemBytes)
if block == nil {
return nil, errors.New("no PEM data found")
}
if block.Type != "CERTIFICATE" {
return nil, fmt.Errorf("unexpected PEM type %q, want CERTIFICATE", block.Type)
}
// Re-parse through x509 to validate the cert is well-formed.
if _, err := x509.ParseCertificate(block.Bytes); err != nil {
return nil, fmt.Errorf("parse certificate: %w", err)
}
return block.Bytes, nil
}