Add architecture docs, fix gRPC/REST API parity, project conventions

- Add ARCHITECTURE.md with full system specification
- Add Project Structure and API Sync Rule to CLAUDE.md; ignore srv/
- Fix engine.proto MountRequest missing config field
- Add pki.proto PKIService to match unauthenticated REST PKI routes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-14 23:29:51 -07:00
parent 8f77050a84
commit 658d067d78
15 changed files with 923 additions and 201 deletions

View File

@@ -87,35 +87,62 @@ func (e *CAEngine) Initialize(ctx context.Context, b barrier.Barrier, mountPath
return fmt.Errorf("ca: store config: %w", err)
}
// 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 rootCert *x509.Certificate
var rootKey crypto.PrivateKey
var certPEM, keyPEM []byte
rootCert, rootKey, err := certgen.GenerateSelfSigned(creq)
if err != nil {
return fmt.Errorf("ca: generate root CA: %w", err)
}
// 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)
// Store root cert and key in barrier.
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 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 {
@@ -273,6 +300,8 @@ func (e *CAEngine) HandleRequest(ctx context.Context, req *engine.Request) (*eng
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)
}
@@ -327,6 +356,67 @@ func (e *CAEngine) GetChainPEM(issuerName string) ([]byte, error) {
// --- 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()