clavitor/operations/pop-sync/ca/pem.go

272 lines
7.4 KiB
Go

package ca
import (
"crypto/x509"
"encoding/pem"
"fmt"
"os"
)
// SaveToDisk writes all CA certificates and keys to PEM files.
//
// Use this immediately after Init() to persist the CA to disk.
// CA files: root-ca.key, root-ca.crt, intermediate-ca.key, intermediate-ca.crt
func (ca *CA) SaveToDisk(paths CertFiles) error {
// Save root key
rootKeyBytes, err := x509.MarshalECPrivateKey(ca.RootKey)
if err != nil {
return fmt.Errorf("marshal root key: %w", err)
}
if err := writePEMFile(paths.RootKeyPath, "EC PRIVATE KEY", rootKeyBytes, 0600); err != nil {
return fmt.Errorf("save root key: %w", err)
}
// Save root cert
if err := writePEMFile(paths.RootCertPath, "CERTIFICATE", ca.RootCert.Raw, 0644); err != nil {
return fmt.Errorf("save root cert: %w", err)
}
// Save intermediate key
intKeyBytes, err := x509.MarshalECPrivateKey(ca.IntKey)
if err != nil {
return fmt.Errorf("marshal intermediate key: %w", err)
}
if err := writePEMFile(paths.IntKeyPath, "EC PRIVATE KEY", intKeyBytes, 0600); err != nil {
return fmt.Errorf("save intermediate key: %w", err)
}
// Save intermediate cert
if err := writePEMFile(paths.IntCertPath, "CERTIFICATE", ca.IntCert.Raw, 0644); err != nil {
return fmt.Errorf("save intermediate cert: %w", err)
}
return nil
}
// SavePOPToDisk writes a POP certificate, key, and CA chain to disk.
//
// Files written:
//
// <region>.key - POP private key (0600)
// <region>.crt - POP certificate (0644)
// ca-chain.crt - Intermediate + Root bundled (0644)
func (pop *POPCertificate) SaveToDisk(region, dir string) error {
keyPath := fmt.Sprintf("%s/%s.key", dir, region)
certPath := fmt.Sprintf("%s/%s.crt", dir, region)
chainPath := fmt.Sprintf("%s/ca-chain.crt", dir)
if err := os.WriteFile(keyPath, pop.KeyPEM, 0600); err != nil {
return fmt.Errorf("save POP key: %w", err)
}
if err := os.WriteFile(certPath, pop.CertPEM, 0644); err != nil {
return fmt.Errorf("save POP cert: %w", err)
}
if err := os.WriteFile(chainPath, pop.ChainPEM, 0644); err != nil {
return fmt.Errorf("save CA chain: %w", err)
}
return nil
}
// LoadCA loads an existing CA from PEM files on disk.
func LoadCA(paths CertFiles) (*CA, error) {
// Load root key
rootKeyPEM, err := os.ReadFile(paths.RootKeyPath)
if err != nil {
return nil, fmt.Errorf("read root key: %w", err)
}
rootKeyBlock, _ := pem.Decode(rootKeyPEM)
if rootKeyBlock == nil {
return nil, fmt.Errorf("decode root key PEM")
}
rootKey, err := x509.ParseECPrivateKey(rootKeyBlock.Bytes)
if err != nil {
return nil, fmt.Errorf("parse root key: %w", err)
}
// Load root cert
rootCertPEM, err := os.ReadFile(paths.RootCertPath)
if err != nil {
return nil, fmt.Errorf("read root cert: %w", err)
}
rootCertBlock, _ := pem.Decode(rootCertPEM)
if rootCertBlock == nil {
return nil, fmt.Errorf("decode root cert PEM")
}
rootCert, err := x509.ParseCertificate(rootCertBlock.Bytes)
if err != nil {
return nil, fmt.Errorf("parse root cert: %w", err)
}
// Load intermediate key
intKeyPEM, err := os.ReadFile(paths.IntKeyPath)
if err != nil {
return nil, fmt.Errorf("read intermediate key: %w", err)
}
intKeyBlock, _ := pem.Decode(intKeyPEM)
if intKeyBlock == nil {
return nil, fmt.Errorf("decode intermediate key PEM")
}
intKey, err := x509.ParseECPrivateKey(intKeyBlock.Bytes)
if err != nil {
return nil, fmt.Errorf("parse intermediate key: %w", err)
}
// Load intermediate cert
intCertPEM, err := os.ReadFile(paths.IntCertPath)
if err != nil {
return nil, fmt.Errorf("read intermediate cert: %w", err)
}
intCertBlock, _ := pem.Decode(intCertPEM)
if intCertBlock == nil {
return nil, fmt.Errorf("decode intermediate cert PEM")
}
intCert, err := x509.ParseCertificate(intCertBlock.Bytes)
if err != nil {
return nil, fmt.Errorf("parse intermediate cert: %w", err)
}
return &CA{
RootCert: rootCert,
RootKey: rootKey,
IntCert: intCert,
IntKey: intKey,
}, nil
}
// LoadIntermediateOnly loads just the intermediate CA (for signing without root key).
// This is useful for day-to-day operations where the root key is kept offline.
// It also loads the root certificate for chain building.
func LoadIntermediateOnly(intKeyPath, intCertPath, rootCertPath string) (*CA, error) {
// Load root cert (for chain building - no key needed)
var rootCert *x509.Certificate
if rootCertPath != "" {
rootCertPEM, err := os.ReadFile(rootCertPath)
if err != nil {
return nil, fmt.Errorf("read root cert: %w", err)
}
rootCertBlock, _ := pem.Decode(rootCertPEM)
if rootCertBlock == nil {
return nil, fmt.Errorf("decode root cert PEM")
}
rootCert, err = x509.ParseCertificate(rootCertBlock.Bytes)
if err != nil {
return nil, fmt.Errorf("parse root cert: %w", err)
}
}
// Load intermediate key
intKeyPEM, err := os.ReadFile(intKeyPath)
if err != nil {
return nil, fmt.Errorf("read intermediate key: %w", err)
}
intKeyBlock, _ := pem.Decode(intKeyPEM)
if intKeyBlock == nil {
return nil, fmt.Errorf("decode intermediate key PEM")
}
intKey, err := x509.ParseECPrivateKey(intKeyBlock.Bytes)
if err != nil {
return nil, fmt.Errorf("parse intermediate key: %w", err)
}
// Load intermediate cert
intCertPEM, err := os.ReadFile(intCertPath)
if err != nil {
return nil, fmt.Errorf("read intermediate cert: %w", err)
}
intCertBlock, _ := pem.Decode(intCertPEM)
if intCertBlock == nil {
return nil, fmt.Errorf("decode intermediate cert PEM")
}
intCert, err := x509.ParseCertificate(intCertBlock.Bytes)
if err != nil {
return nil, fmt.Errorf("parse intermediate cert: %w", err)
}
return &CA{
RootCert: rootCert,
IntCert: intCert,
IntKey: intKey,
}, nil
}
// writePEMFile writes PEM-encoded data to a file with the specified mode.
func writePEMFile(path, blockType string, bytes []byte, mode os.FileMode) error {
block := &pem.Block{
Type: blockType,
Bytes: bytes,
}
data := pem.EncodeToMemory(block)
return os.WriteFile(path, data, mode)
}
// LoadPOP loads a POP certificate and key from disk.
func LoadPOP(region, dir string) (*POPCertificate, error) {
keyPath := fmt.Sprintf("%s/%s.key", dir, region)
certPath := fmt.Sprintf("%s/%s.crt", dir, region)
keyPEM, err := os.ReadFile(keyPath)
if err != nil {
return nil, fmt.Errorf("read POP key: %w", err)
}
keyBlock, _ := pem.Decode(keyPEM)
if keyBlock == nil {
return nil, fmt.Errorf("decode POP key PEM")
}
key, err := x509.ParseECPrivateKey(keyBlock.Bytes)
if err != nil {
return nil, fmt.Errorf("parse POP key: %w", err)
}
certPEM, err := os.ReadFile(certPath)
if err != nil {
return nil, fmt.Errorf("read POP cert: %w", err)
}
certBlock, _ := pem.Decode(certPEM)
if certBlock == nil {
return nil, fmt.Errorf("decode POP cert PEM")
}
cert, err := x509.ParseCertificate(certBlock.Bytes)
if err != nil {
return nil, fmt.Errorf("parse POP cert: %w", err)
}
return &POPCertificate{
Cert: cert,
Key: key,
CertPEM: certPEM,
KeyPEM: keyPEM,
}, nil
}
// LoadChain loads the CA chain (intermediate + root) from disk.
func LoadChain(path string) ([][]byte, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read chain file: %w", err)
}
var chain [][]byte
for {
block, rest := pem.Decode(data)
if block == nil {
break
}
if block.Type == "CERTIFICATE" {
chain = append(chain, block.Bytes)
}
data = rest
if len(data) == 0 {
break
}
}
if len(chain) == 0 {
return nil, fmt.Errorf("no certificates found in chain file")
}
return chain, nil
}