272 lines
7.4 KiB
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
|
|
}
|