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---
59 lines
1.7 KiB
Go
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
|
|
}
|