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:
2026-03-15 13:21:13 -07:00
parent 65c92fe5ec
commit b4dbc088cb
12 changed files with 785 additions and 82 deletions

View File

@@ -371,6 +371,55 @@ func (cs *caServer) RenewCert(ctx context.Context, req *pb.RenewCertRequest) (*p
}, nil
}
func (cs *caServer) SignCSR(ctx context.Context, req *pb.SignCSRRequest) (*pb.SignCSRResponse, error) {
if req.Mount == "" || req.Issuer == "" {
return nil, status.Error(codes.InvalidArgument, "mount and issuer are required")
}
if len(req.CsrPem) == 0 {
return nil, status.Error(codes.InvalidArgument, "csr_pem is required")
}
data := map[string]interface{}{
"issuer": req.Issuer,
"csr_pem": string(req.CsrPem),
}
if req.Profile != "" {
data["profile"] = req.Profile
}
if req.Ttl != "" {
data["ttl"] = req.Ttl
}
resp, err := cs.caHandleRequest(ctx, req.Mount, "sign-csr", &engine.Request{
Operation: "sign-csr",
CallerInfo: cs.callerInfo(ctx),
Data: data,
})
if err != nil {
return nil, err
}
serial, _ := resp.Data["serial"].(string)
cn, _ := resp.Data["cn"].(string)
issuedBy, _ := resp.Data["issued_by"].(string)
certPEM, _ := resp.Data["cert_pem"].(string)
chainPEM, _ := resp.Data["chain_pem"].(string)
sans := toStringSliceFromInterface(resp.Data["sans"])
var expiresAt *timestamppb.Timestamp
if s, ok := resp.Data["expires_at"].(string); ok {
if t, err := time.Parse(time.RFC3339, s); err == nil {
expiresAt = timestamppb.New(t)
}
}
cs.s.logger.Info("audit: CSR signed", "mount", req.Mount, "issuer", req.Issuer, "cn", cn, "serial", serial, "username", callerUsername(ctx))
return &pb.SignCSRResponse{
Serial: serial,
CommonName: cn,
Sans: sans,
IssuedBy: issuedBy,
ExpiresAt: expiresAt,
CertPem: []byte(certPEM),
ChainPem: []byte(chainPEM),
}, nil
}
// --- helpers ---
func certRecordFromData(d map[string]interface{}) *pb.CertRecord {