// Package ca implements the CA (PKI) engine for X.509 certificate issuance. package ca import ( "context" "crypto" "crypto/ecdsa" "crypto/ed25519" "crypto/rsa" "crypto/x509" "encoding/json" "encoding/pem" "errors" "fmt" "net" "strings" "sync" "time" "git.wntrmute.dev/kyle/goutils/certlib/certgen" "git.wntrmute.dev/kyle/metacrypt/internal/barrier" "git.wntrmute.dev/kyle/metacrypt/internal/engine" ) var ( ErrSealed = errors.New("ca: engine is sealed") ErrIssuerNotFound = errors.New("ca: issuer not found") ErrIssuerExists = errors.New("ca: issuer already exists") ErrCertNotFound = errors.New("ca: certificate not found") ErrUnknownProfile = errors.New("ca: unknown profile") ErrForbidden = errors.New("ca: forbidden") ErrUnauthorized = errors.New("ca: authentication required") ) // issuerState holds in-memory state for a loaded issuer. type issuerState struct { cert *x509.Certificate key crypto.PrivateKey config *IssuerConfig } // CAEngine implements the CA (PKI) engine. type CAEngine struct { mu sync.RWMutex barrier barrier.Barrier mountPath string config *CAConfig rootCert *x509.Certificate rootKey crypto.PrivateKey issuers map[string]*issuerState } // NewCAEngine creates a new CA engine instance. func NewCAEngine() engine.Engine { return &CAEngine{ issuers: make(map[string]*issuerState), } } func (e *CAEngine) Type() engine.EngineType { return engine.EngineTypeCA } // Initialize sets up the CA engine for first use: generates a self-signed root CA. func (e *CAEngine) Initialize(ctx context.Context, b barrier.Barrier, mountPath string, config map[string]interface{}) error { e.mu.Lock() defer e.mu.Unlock() e.barrier = b e.mountPath = mountPath cfg := defaultCAConfig() if config != nil { if err := mapToCAConfig(config, cfg); err != nil { return fmt.Errorf("ca: parse config: %w", err) } } e.config = cfg // Store config. configData, err := json.Marshal(cfg) if err != nil { return fmt.Errorf("ca: marshal config: %w", err) } if err := b.Put(ctx, mountPath+"config.json", configData); err != nil { return fmt.Errorf("ca: store config: %w", err) } var rootCert *x509.Certificate var rootKey crypto.PrivateKey var certPEM, keyPEM []byte // If root_cert_pem and root_key_pem are provided, import them // instead of generating a new root CA. rootCertStr, _ := config["root_cert_pem"].(string) rootKeyStr, _ := config["root_key_pem"].(string) if rootCertStr != "" && rootKeyStr != "" { certPEM = []byte(rootCertStr) keyPEM = []byte(rootKeyStr) var err error rootCert, err = parseCertPEM(certPEM) if err != nil { return fmt.Errorf("ca: parse imported root cert: %w", err) } if !rootCert.IsCA { return fmt.Errorf("ca: imported certificate is not a CA") } rootKey, err = parsePrivateKeyPEM(keyPEM) if err != nil { return fmt.Errorf("ca: parse imported root key: %w", err) } } else { // Generate self-signed root CA. creq := &certgen.CertificateRequest{ KeySpec: certgen.KeySpec{ Algorithm: cfg.KeyAlgorithm, Size: cfg.KeySize, }, Subject: certgen.Subject{ CommonName: cfg.Organization + " Root CA", Organization: cfg.Organization, Country: cfg.Country, }, Profile: certgen.Profile{ IsCA: true, PathLen: 1, KeyUse: []string{"cert sign", "crl sign"}, Expiry: cfg.RootExpiry, }, } var err error rootCert, rootKey, err = certgen.GenerateSelfSigned(creq) if err != nil { return fmt.Errorf("ca: generate root CA: %w", err) } certPEM = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: rootCert.Raw}) keyPEM, err = marshalPrivateKey(rootKey) if err != nil { return fmt.Errorf("ca: marshal root key: %w", err) } } if err := b.Put(ctx, mountPath+"root/cert.pem", certPEM); err != nil { return fmt.Errorf("ca: store root cert: %w", err) } if err := b.Put(ctx, mountPath+"root/key.pem", keyPEM); err != nil { return fmt.Errorf("ca: store root key: %w", err) } e.rootCert = rootCert e.rootKey = rootKey return nil } // Unseal loads the CA state from the barrier into memory. func (e *CAEngine) Unseal(ctx context.Context, b barrier.Barrier, mountPath string) error { e.mu.Lock() defer e.mu.Unlock() e.barrier = b e.mountPath = mountPath // Load config. configData, err := b.Get(ctx, mountPath+"config.json") if err != nil { return fmt.Errorf("ca: load config: %w", err) } var cfg CAConfig if err := json.Unmarshal(configData, &cfg); err != nil { return fmt.Errorf("ca: parse config: %w", err) } e.config = &cfg // Load root cert and key. certPEM, err := b.Get(ctx, mountPath+"root/cert.pem") if err != nil { return fmt.Errorf("ca: load root cert: %w", err) } keyPEM, err := b.Get(ctx, mountPath+"root/key.pem") if err != nil { return fmt.Errorf("ca: load root key: %w", err) } rootCert, err := parseCertPEM(certPEM) if err != nil { return fmt.Errorf("ca: parse root cert: %w", err) } rootKey, err := parsePrivateKeyPEM(keyPEM) if err != nil { return fmt.Errorf("ca: parse root key: %w", err) } e.rootCert = rootCert e.rootKey = rootKey e.issuers = make(map[string]*issuerState) // Load all issuers. issuerPaths, err := b.List(ctx, mountPath+"issuers/") if err != nil { return fmt.Errorf("ca: list issuers: %w", err) } // Collect unique issuer names from paths like "name/cert.pem", "name/key.pem", "name/config.json". issuerNames := make(map[string]bool) for _, p := range issuerPaths { parts := strings.SplitN(p, "/", 2) if len(parts) > 0 && parts[0] != "" { issuerNames[parts[0]] = true } } for name := range issuerNames { is, err := e.loadIssuer(ctx, b, mountPath, name) if err != nil { return fmt.Errorf("ca: load issuer %q: %w", name, err) } e.issuers[name] = is } return nil } func (e *CAEngine) loadIssuer(ctx context.Context, b barrier.Barrier, mountPath, name string) (*issuerState, error) { prefix := mountPath + "issuers/" + name + "/" certPEM, err := b.Get(ctx, prefix+"cert.pem") if err != nil { return nil, fmt.Errorf("load cert: %w", err) } keyPEM, err := b.Get(ctx, prefix+"key.pem") if err != nil { return nil, fmt.Errorf("load key: %w", err) } configData, err := b.Get(ctx, prefix+"config.json") if err != nil { return nil, fmt.Errorf("load config: %w", err) } cert, err := parseCertPEM(certPEM) if err != nil { return nil, fmt.Errorf("parse cert: %w", err) } key, err := parsePrivateKeyPEM(keyPEM) if err != nil { return nil, fmt.Errorf("parse key: %w", err) } var cfg IssuerConfig if err := json.Unmarshal(configData, &cfg); err != nil { return nil, fmt.Errorf("parse config: %w", err) } return &issuerState{cert: cert, key: key, config: &cfg}, nil } // Seal zeroizes all in-memory key material. func (e *CAEngine) Seal() error { e.mu.Lock() defer e.mu.Unlock() zeroizeKey(e.rootKey) e.rootKey = nil e.rootCert = nil e.config = nil for name, is := range e.issuers { zeroizeKey(is.key) delete(e.issuers, name) } e.issuers = nil return nil } // HandleRequest dispatches CA operations. func (e *CAEngine) HandleRequest(ctx context.Context, req *engine.Request) (*engine.Response, error) { switch req.Operation { case "get-root": return e.handleGetRoot(ctx) case "get-chain": return e.handleGetChain(ctx, req) case "get-issuer": return e.handleGetIssuer(ctx, req) case "create-issuer": return e.handleCreateIssuer(ctx, req) case "delete-issuer": return e.handleDeleteIssuer(ctx, req) case "list-issuers": return e.handleListIssuers(ctx, req) case "issue": return e.handleIssue(ctx, req) case "get-cert": return e.handleGetCert(ctx, req) case "list-certs": return e.handleListCerts(ctx, req) case "renew": return e.handleRenew(ctx, req) case "import-root": return e.handleImportRoot(ctx, req) default: return nil, fmt.Errorf("ca: unknown operation: %s", req.Operation) } } // --- Public methods for unauthenticated PKI routes --- // GetRootCertPEM returns the root CA certificate in PEM format. func (e *CAEngine) GetRootCertPEM() ([]byte, error) { e.mu.RLock() defer e.mu.RUnlock() if e.rootCert == nil { return nil, ErrSealed } return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: e.rootCert.Raw}), nil } // GetIssuerCertPEM returns the named issuer's certificate in PEM format. func (e *CAEngine) GetIssuerCertPEM(name string) ([]byte, error) { e.mu.RLock() defer e.mu.RUnlock() if e.rootCert == nil { return nil, ErrSealed } is, ok := e.issuers[name] if !ok { return nil, ErrIssuerNotFound } return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: is.cert.Raw}), nil } // GetChainPEM returns the full certificate chain (issuer + root) in PEM format. func (e *CAEngine) GetChainPEM(issuerName string) ([]byte, error) { e.mu.RLock() defer e.mu.RUnlock() if e.rootCert == nil { return nil, ErrSealed } is, ok := e.issuers[issuerName] if !ok { return nil, ErrIssuerNotFound } var chain []byte chain = append(chain, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: is.cert.Raw})...) chain = append(chain, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: e.rootCert.Raw})...) return chain, nil } // --- Operation handlers --- func (e *CAEngine) handleImportRoot(ctx context.Context, req *engine.Request) (*engine.Response, error) { if req.CallerInfo == nil { return nil, ErrUnauthorized } if !req.CallerInfo.IsAdmin { return nil, ErrForbidden } certStr, _ := req.Data["cert_pem"].(string) keyStr, _ := req.Data["key_pem"].(string) if certStr == "" || keyStr == "" { return nil, fmt.Errorf("ca: cert_pem and key_pem are required") } newCert, err := parseCertPEM([]byte(certStr)) if err != nil { return nil, fmt.Errorf("ca: parse imported cert: %w", err) } if !newCert.IsCA { return nil, fmt.Errorf("ca: imported certificate is not a CA") } newKey, err := parsePrivateKeyPEM([]byte(keyStr)) if err != nil { return nil, fmt.Errorf("ca: parse imported key: %w", err) } e.mu.Lock() defer e.mu.Unlock() // Only allow import if there is no root or the current root is expired. if e.rootCert != nil && time.Now().Before(e.rootCert.NotAfter) { return nil, fmt.Errorf("ca: current root is still valid (expires %s); cannot replace", e.rootCert.NotAfter.Format(time.RFC3339)) } // Zeroize old key if present. if e.rootKey != nil { zeroizeKey(e.rootKey) } // Store in barrier. certPEM := []byte(certStr) keyPEM := []byte(keyStr) if err := e.barrier.Put(ctx, e.mountPath+"root/cert.pem", certPEM); err != nil { return nil, fmt.Errorf("ca: store root cert: %w", err) } if err := e.barrier.Put(ctx, e.mountPath+"root/key.pem", keyPEM); err != nil { return nil, fmt.Errorf("ca: store root key: %w", err) } e.rootCert = newCert e.rootKey = newKey return &engine.Response{ Data: map[string]interface{}{ "cn": newCert.Subject.CommonName, "expires_at": newCert.NotAfter, }, }, nil } func (e *CAEngine) handleGetRoot(_ context.Context) (*engine.Response, error) { e.mu.RLock() defer e.mu.RUnlock() if e.rootCert == nil { return nil, ErrSealed } certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: e.rootCert.Raw}) return &engine.Response{ Data: map[string]interface{}{ "cert_pem": string(certPEM), }, }, nil } func (e *CAEngine) handleGetChain(_ context.Context, req *engine.Request) (*engine.Response, error) { issuerName, _ := req.Data["issuer"].(string) if issuerName == "" { issuerName = req.Path } chain, err := e.GetChainPEM(issuerName) if err != nil { return nil, err } return &engine.Response{ Data: map[string]interface{}{ "chain_pem": string(chain), }, }, nil } func (e *CAEngine) handleGetIssuer(_ context.Context, req *engine.Request) (*engine.Response, error) { name := req.Path certPEM, err := e.GetIssuerCertPEM(name) if err != nil { return nil, err } return &engine.Response{ Data: map[string]interface{}{ "cert_pem": string(certPEM), }, }, nil } func (e *CAEngine) handleCreateIssuer(ctx context.Context, req *engine.Request) (*engine.Response, error) { if req.CallerInfo == nil { return nil, ErrUnauthorized } if !req.CallerInfo.IsAdmin { return nil, ErrForbidden } name, _ := req.Data["name"].(string) if name == "" { return nil, fmt.Errorf("ca: issuer name is required") } e.mu.Lock() defer e.mu.Unlock() if e.rootCert == nil { return nil, ErrSealed } if _, exists := e.issuers[name]; exists { return nil, ErrIssuerExists } // Determine key spec: use issuer-specific overrides or fall back to CA config. keyAlg := e.config.KeyAlgorithm keySize := e.config.KeySize if v, ok := req.Data["key_algorithm"].(string); ok && v != "" { keyAlg = v } if v, ok := req.Data["key_size"].(float64); ok { keySize = int(v) } expiry := "43800h" // 5 years default if v, ok := req.Data["expiry"].(string); ok && v != "" { expiry = v } maxTTL := "2160h" // 90 days default if v, ok := req.Data["max_ttl"].(string); ok && v != "" { maxTTL = v } // Generate issuer key pair and CSR. ks := certgen.KeySpec{Algorithm: keyAlg, Size: keySize} _, priv, err := ks.Generate() if err != nil { return nil, fmt.Errorf("ca: generate issuer key: %w", err) } creq := certgen.CertificateRequest{ KeySpec: ks, Subject: certgen.Subject{ CommonName: name, Organization: e.config.Organization, Country: e.config.Country, }, } csr, err := creq.Request(priv) if err != nil { return nil, fmt.Errorf("ca: create issuer CSR: %w", err) } // Sign with root CA using an intermediate CA profile. profile := certgen.Profile{ IsCA: true, PathLen: 0, KeyUse: []string{"cert sign", "crl sign"}, Expiry: expiry, } issuerCert, err := profile.SignRequest(e.rootCert, csr, e.rootKey) if err != nil { return nil, fmt.Errorf("ca: sign issuer cert: %w", err) } // Store in barrier. prefix := e.mountPath + "issuers/" + name + "/" certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: issuerCert.Raw}) keyPEM, err := marshalPrivateKey(priv) if err != nil { return nil, fmt.Errorf("ca: marshal issuer key: %w", err) } issuerCfg := &IssuerConfig{ Name: name, KeyAlgorithm: keyAlg, KeySize: keySize, Expiry: expiry, MaxTTL: maxTTL, CreatedBy: req.CallerInfo.Username, CreatedAt: time.Now(), } cfgData, err := json.Marshal(issuerCfg) if err != nil { return nil, fmt.Errorf("ca: marshal issuer config: %w", err) } if err := e.barrier.Put(ctx, prefix+"cert.pem", certPEM); err != nil { return nil, fmt.Errorf("ca: store issuer cert: %w", err) } if err := e.barrier.Put(ctx, prefix+"key.pem", keyPEM); err != nil { return nil, fmt.Errorf("ca: store issuer key: %w", err) } if err := e.barrier.Put(ctx, prefix+"config.json", cfgData); err != nil { return nil, fmt.Errorf("ca: store issuer config: %w", err) } e.issuers[name] = &issuerState{cert: issuerCert, key: priv, config: issuerCfg} return &engine.Response{ Data: map[string]interface{}{ "name": name, "cert_pem": string(certPEM), }, }, nil } func (e *CAEngine) handleDeleteIssuer(ctx context.Context, req *engine.Request) (*engine.Response, error) { if req.CallerInfo == nil { return nil, ErrUnauthorized } if !req.CallerInfo.IsAdmin { return nil, ErrForbidden } name, _ := req.Data["name"].(string) if name == "" { name = req.Path } e.mu.Lock() defer e.mu.Unlock() if e.rootCert == nil { return nil, ErrSealed } is, exists := e.issuers[name] if !exists { return nil, ErrIssuerNotFound } // Zeroize key material. zeroizeKey(is.key) // Delete from barrier. prefix := e.mountPath + "issuers/" + name + "/" for _, suffix := range []string{"cert.pem", "key.pem", "config.json"} { if err := e.barrier.Delete(ctx, prefix+suffix); err != nil { return nil, fmt.Errorf("ca: delete issuer %s: %w", suffix, err) } } delete(e.issuers, name) return &engine.Response{ Data: map[string]interface{}{"ok": true}, }, nil } func (e *CAEngine) handleListIssuers(_ context.Context, req *engine.Request) (*engine.Response, error) { if req.CallerInfo == nil { return nil, ErrUnauthorized } e.mu.RLock() defer e.mu.RUnlock() if e.rootCert == nil { return nil, ErrSealed } names := make([]string, 0, len(e.issuers)) for name := range e.issuers { names = append(names, name) } return &engine.Response{ Data: map[string]interface{}{ "issuers": names, }, }, nil } func (e *CAEngine) handleIssue(ctx context.Context, req *engine.Request) (*engine.Response, error) { if req.CallerInfo == nil { return nil, ErrUnauthorized } issuerName, _ := req.Data["issuer"].(string) if issuerName == "" { issuerName = req.Path } if issuerName == "" { return nil, fmt.Errorf("ca: issuer name is required") } profileName, _ := req.Data["profile"].(string) if profileName == "" { profileName = "server" } cn, _ := req.Data["common_name"].(string) if cn == "" { return nil, fmt.Errorf("ca: common_name is required") } e.mu.Lock() defer e.mu.Unlock() if e.rootCert == nil { return nil, ErrSealed } is, ok := e.issuers[issuerName] if !ok { return nil, ErrIssuerNotFound } profile, ok := GetProfile(profileName) if !ok { return nil, fmt.Errorf("%w: %s", ErrUnknownProfile, profileName) } // Apply user overrides. if v, ok := req.Data["ttl"].(string); ok && v != "" { profile.Expiry = v } if v, ok := req.Data["key_usages"].([]interface{}); ok { profile.KeyUse = toStringSlice(v) } if v, ok := req.Data["ext_key_usages"].([]interface{}); ok { profile.ExtKeyUsages = toStringSlice(v) } // Determine leaf key spec. keyAlg := is.config.KeyAlgorithm keySize := is.config.KeySize if v, ok := req.Data["key_algorithm"].(string); ok && v != "" { keyAlg = v } if v, ok := req.Data["key_size"].(float64); ok { keySize = int(v) } // Parse SANs. var dnsNames []string var ipAddrs []string if v, ok := req.Data["dns_names"].([]interface{}); ok { dnsNames = toStringSlice(v) } if v, ok := req.Data["ip_addresses"].([]interface{}); ok { ipAddrs = toStringSlice(v) } // Generate leaf key pair and CSR. ks := certgen.KeySpec{Algorithm: keyAlg, Size: keySize} _, leafKey, err := ks.Generate() if err != nil { return nil, fmt.Errorf("ca: generate leaf key: %w", err) } creq := certgen.CertificateRequest{ KeySpec: ks, Subject: certgen.Subject{ CommonName: cn, Organization: e.config.Organization, DNSNames: dnsNames, IPAddresses: ipAddrs, }, } csr, err := creq.Request(leafKey) if err != nil { return nil, fmt.Errorf("ca: create leaf CSR: %w", err) } leafCert, err := profile.SignRequest(is.cert, csr, is.key) if err != nil { return nil, fmt.Errorf("ca: sign leaf cert: %w", err) } // Build PEM outputs. leafCertPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: leafCert.Raw}) leafKeyPEM, err := marshalPrivateKey(leafKey) if err != nil { return nil, fmt.Errorf("ca: marshal leaf key: %w", err) } var chainPEM []byte chainPEM = append(chainPEM, leafCertPEM...) chainPEM = append(chainPEM, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: is.cert.Raw})...) chainPEM = append(chainPEM, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: e.rootCert.Raw})...) serial := fmt.Sprintf("%x", leafCert.SerialNumber) // Collect all SANs for the record. allSANs := append(leafCert.DNSNames, ipStrings(leafCert.IPAddresses)...) // Store cert record (NO private key). record := &CertRecord{ Serial: serial, Issuer: issuerName, CN: cn, SANs: allSANs, Profile: profileName, CertPEM: string(leafCertPEM), IssuedBy: req.CallerInfo.Username, IssuedAt: time.Now(), ExpiresAt: leafCert.NotAfter, } recordData, err := json.Marshal(record) if err != nil { return nil, fmt.Errorf("ca: marshal cert record: %w", err) } if err := e.barrier.Put(ctx, e.mountPath+"certs/"+serial+".json", recordData); err != nil { return nil, fmt.Errorf("ca: store cert record: %w", err) } return &engine.Response{ Data: map[string]interface{}{ "serial": serial, "cert_pem": string(leafCertPEM), "key_pem": string(leafKeyPEM), "chain_pem": string(chainPEM), "cn": cn, "sans": allSANs, "issued_by": req.CallerInfo.Username, "expires_at": leafCert.NotAfter, }, }, nil } func (e *CAEngine) handleGetCert(ctx context.Context, req *engine.Request) (*engine.Response, error) { if req.CallerInfo == nil { return nil, ErrUnauthorized } serial, _ := req.Data["serial"].(string) if serial == "" { serial = req.Path } if serial == "" { return nil, fmt.Errorf("ca: serial is required") } e.mu.RLock() defer e.mu.RUnlock() recordData, err := e.barrier.Get(ctx, e.mountPath+"certs/"+serial+".json") if err != nil { if errors.Is(err, barrier.ErrNotFound) { return nil, ErrCertNotFound } return nil, fmt.Errorf("ca: load cert record: %w", err) } var record CertRecord if err := json.Unmarshal(recordData, &record); err != nil { return nil, fmt.Errorf("ca: parse cert record: %w", err) } return &engine.Response{ Data: map[string]interface{}{ "serial": record.Serial, "issuer": record.Issuer, "cn": record.CN, "sans": record.SANs, "profile": record.Profile, "cert_pem": record.CertPEM, "issued_by": record.IssuedBy, "issued_at": record.IssuedAt, "expires_at": record.ExpiresAt, }, }, nil } func (e *CAEngine) handleListCerts(ctx context.Context, req *engine.Request) (*engine.Response, error) { if req.CallerInfo == nil { return nil, ErrUnauthorized } e.mu.RLock() defer e.mu.RUnlock() paths, err := e.barrier.List(ctx, e.mountPath+"certs/") if err != nil { return nil, fmt.Errorf("ca: list certs: %w", err) } var certs []map[string]interface{} for _, p := range paths { if !strings.HasSuffix(p, ".json") { continue } recordData, err := e.barrier.Get(ctx, e.mountPath+"certs/"+p) if err != nil { continue } var record CertRecord if err := json.Unmarshal(recordData, &record); err != nil { continue } certs = append(certs, map[string]interface{}{ "serial": record.Serial, "issuer": record.Issuer, "cn": record.CN, "profile": record.Profile, "issued_by": record.IssuedBy, "issued_at": record.IssuedAt, "expires_at": record.ExpiresAt, }) } return &engine.Response{ Data: map[string]interface{}{ "certs": certs, }, }, nil } func (e *CAEngine) handleRenew(ctx context.Context, req *engine.Request) (*engine.Response, error) { if req.CallerInfo == nil { return nil, ErrUnauthorized } serial, _ := req.Data["serial"].(string) if serial == "" { serial = req.Path } if serial == "" { return nil, fmt.Errorf("ca: serial is required") } e.mu.Lock() defer e.mu.Unlock() if e.rootCert == nil { return nil, ErrSealed } // Load original cert record. recordData, err := e.barrier.Get(ctx, e.mountPath+"certs/"+serial+".json") if err != nil { if errors.Is(err, barrier.ErrNotFound) { return nil, ErrCertNotFound } return nil, fmt.Errorf("ca: load cert record: %w", err) } var record CertRecord if err := json.Unmarshal(recordData, &record); err != nil { return nil, fmt.Errorf("ca: parse cert record: %w", err) } // Look up issuer. is, ok := e.issuers[record.Issuer] if !ok { return nil, fmt.Errorf("ca: original issuer %q no longer exists", record.Issuer) } // Parse original cert to extract attributes. origCert, err := parseCertPEM([]byte(record.CertPEM)) if err != nil { return nil, fmt.Errorf("ca: parse original cert: %w", err) } // Build profile from original cert's usages. profile, _ := GetProfile(record.Profile) // Use original TTL duration. origDuration := origCert.NotAfter.Sub(origCert.NotBefore) profile.Expiry = fmt.Sprintf("%dh", int(origDuration.Hours())) // Generate new key. ks := certgen.KeySpec{Algorithm: is.config.KeyAlgorithm, Size: is.config.KeySize} _, newKey, err := ks.Generate() if err != nil { return nil, fmt.Errorf("ca: generate renewal key: %w", err) } creq := certgen.CertificateRequest{ KeySpec: ks, Subject: certgen.Subject{ CommonName: origCert.Subject.CommonName, Organization: firstOrEmpty(origCert.Subject.Organization), DNSNames: origCert.DNSNames, IPAddresses: ipStrings(origCert.IPAddresses), }, } csr, err := creq.Request(newKey) if err != nil { return nil, fmt.Errorf("ca: create renewal CSR: %w", err) } newCert, err := profile.SignRequest(is.cert, csr, is.key) if err != nil { return nil, fmt.Errorf("ca: sign renewal cert: %w", err) } // Build PEMs. newCertPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: newCert.Raw}) newKeyPEM, err := marshalPrivateKey(newKey) if err != nil { return nil, fmt.Errorf("ca: marshal renewal key: %w", err) } var chainPEM []byte chainPEM = append(chainPEM, newCertPEM...) chainPEM = append(chainPEM, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: is.cert.Raw})...) chainPEM = append(chainPEM, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: e.rootCert.Raw})...) newSerial := fmt.Sprintf("%x", newCert.SerialNumber) allSANs := append(newCert.DNSNames, ipStrings(newCert.IPAddresses)...) newRecord := &CertRecord{ Serial: newSerial, Issuer: record.Issuer, CN: record.CN, SANs: allSANs, Profile: record.Profile, CertPEM: string(newCertPEM), IssuedBy: req.CallerInfo.Username, IssuedAt: time.Now(), ExpiresAt: newCert.NotAfter, } newRecordData, err := json.Marshal(newRecord) if err != nil { return nil, fmt.Errorf("ca: marshal renewal record: %w", err) } if err := e.barrier.Put(ctx, e.mountPath+"certs/"+newSerial+".json", newRecordData); err != nil { return nil, fmt.Errorf("ca: store renewal record: %w", err) } return &engine.Response{ Data: map[string]interface{}{ "serial": newSerial, "cert_pem": string(newCertPEM), "key_pem": string(newKeyPEM), "chain_pem": string(chainPEM), "cn": record.CN, "expires_at": newCert.NotAfter, }, }, nil } // --- Helpers --- func defaultCAConfig() *CAConfig { return &CAConfig{ Organization: "Metacircular", KeyAlgorithm: "ecdsa", KeySize: 521, RootExpiry: "87600h", // 10 years } } func mapToCAConfig(m map[string]interface{}, cfg *CAConfig) error { if v, ok := m["organization"].(string); ok { cfg.Organization = v } if v, ok := m["country"].(string); ok { cfg.Country = v } if v, ok := m["key_algorithm"].(string); ok { cfg.KeyAlgorithm = v } if v, ok := m["key_size"].(float64); ok { cfg.KeySize = int(v) } if v, ok := m["root_expiry"].(string); ok { cfg.RootExpiry = v } return nil } func marshalPrivateKey(key crypto.PrivateKey) ([]byte, error) { der, err := x509.MarshalPKCS8PrivateKey(key) if err != nil { return nil, err } return pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: der}), nil } func parseCertPEM(data []byte) (*x509.Certificate, error) { block, _ := pem.Decode(data) if block == nil { return nil, fmt.Errorf("no PEM block found") } return x509.ParseCertificate(block.Bytes) } func parsePrivateKeyPEM(data []byte) (crypto.PrivateKey, error) { block, _ := pem.Decode(data) if block == nil { return nil, fmt.Errorf("no PEM block found") } return x509.ParsePKCS8PrivateKey(block.Bytes) } func zeroizeKey(key crypto.PrivateKey) { if key == nil { return } switch k := key.(type) { case *ecdsa.PrivateKey: k.D.SetInt64(0) case *rsa.PrivateKey: k.D.SetInt64(0) for _, p := range k.Primes { p.SetInt64(0) } case ed25519.PrivateKey: for i := range k { k[i] = 0 } } } func toStringSlice(v []interface{}) []string { s := make([]string, 0, len(v)) for _, item := range v { if str, ok := item.(string); ok { s = append(s, str) } } return s } func ipStrings(ips []net.IP) []string { s := make([]string, 0, len(ips)) for _, ip := range ips { s = append(s, ip.String()) } return s } func firstOrEmpty(s []string) string { if len(s) > 0 { return s[0] } return "" }