Add certificate issuance, CSR signing, and cert listing to web UI
- Add SignCSR RPC to v2 CA proto and regenerate; implement handleSignCSR
in CA engine and caServer gRPC layer; add SignCSR client method and
POST /pki/sign-csr web route with result display in pki.html
- Fix issuer detail cert listing: template was using map-style index on
CertSummary structs; switch to struct field access and populate
IssuedBy/IssuedAt fields from proto response
- Add certificate detail view (cert_detail.html) with GET /cert/{serial}
and GET /cert/{serial}/download routes
- Update Makefile proto target to generate both v1 and v2 protos
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -300,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 "sign-csr":
|
||||
return e.handleSignCSR(ctx, req)
|
||||
case "import-root":
|
||||
return e.handleImportRoot(ctx, req)
|
||||
default:
|
||||
@@ -1014,6 +1016,109 @@ func (e *CAEngine) handleRenew(ctx context.Context, req *engine.Request) (*engin
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (e *CAEngine) handleSignCSR(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")
|
||||
}
|
||||
|
||||
csrPEM, _ := req.Data["csr_pem"].(string)
|
||||
if csrPEM == "" {
|
||||
return nil, fmt.Errorf("ca: csr_pem is required")
|
||||
}
|
||||
|
||||
block, _ := pem.Decode([]byte(csrPEM))
|
||||
if block == nil || block.Type != "CERTIFICATE REQUEST" {
|
||||
return nil, fmt.Errorf("ca: invalid CSR PEM")
|
||||
}
|
||||
csr, err := x509.ParseCertificateRequest(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ca: parse CSR: %w", err)
|
||||
}
|
||||
if err := csr.CheckSignature(); err != nil {
|
||||
return nil, fmt.Errorf("ca: invalid CSR signature: %w", err)
|
||||
}
|
||||
|
||||
profileName, _ := req.Data["profile"].(string)
|
||||
if profileName == "" {
|
||||
profileName = "server"
|
||||
}
|
||||
profile, ok := GetProfile(profileName)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%w: %s", ErrUnknownProfile, profileName)
|
||||
}
|
||||
|
||||
if v, ok := req.Data["ttl"].(string); ok && v != "" {
|
||||
profile.Expiry = v
|
||||
}
|
||||
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
if e.rootCert == nil {
|
||||
return nil, ErrSealed
|
||||
}
|
||||
|
||||
is, ok := e.issuers[issuerName]
|
||||
if !ok {
|
||||
return nil, ErrIssuerNotFound
|
||||
}
|
||||
|
||||
leafCert, err := profile.SignRequest(is.cert, csr, is.key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ca: sign CSR: %w", err)
|
||||
}
|
||||
|
||||
leafCertPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: leafCert.Raw})
|
||||
|
||||
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)
|
||||
cn := csr.Subject.CommonName
|
||||
allSANs := append(leafCert.DNSNames, ipStrings(leafCert.IPAddresses)...)
|
||||
|
||||
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),
|
||||
"chain_pem": string(chainPEM),
|
||||
"cn": cn,
|
||||
"sans": allSANs,
|
||||
"issued_by": req.CallerInfo.Username,
|
||||
"expires_at": leafCert.NotAfter.Format(time.RFC3339),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// --- Helpers ---
|
||||
|
||||
func defaultCAConfig() *CAConfig {
|
||||
|
||||
Reference in New Issue
Block a user