// A certificate database management functions package main import ( "crypto/x509" "crypto/x509/pkix" "encoding/asn1" "encoding/json" "encoding/pem" "fmt" "math/big" "os" "path/filepath" "time" ) // _CAState represents the persisted CA state in JSON type _CAState struct { CreatedAt string `json:"createdAt"` UpdatedAt string `json:"updatedAt"` Serial int `json:"serial,omitempty"` CRLNumber int `json:"crlNumber"` Certificates []CertificateRecord `json:"certificates"` } // CertificateRecord represents a single certificate record in the CA state type CertificateRecord struct { Name string `json:"name"` Issued string `json:"issued"` Expires string `json:"expires"` Serial string `json:"serial"` RevokedAt string `json:"revokedAt,omitempty"` RevokeReason int `json:"revokeReason,omitempty"` } func caStatePath() string { return filepath.Join(filepath.Dir(CAConfigPath), CAConfig.StateName()) } // LoadCAState loads the CA state from a JSON file func LoadCAState() error { path := caStatePath() fmt.Printf("Loading CA state from %s\n", path) f, err := os.Open(path) if err != nil { return err } defer f.Close() CAState = &_CAState{} if err := json.NewDecoder(f).Decode(CAState); err != nil { return err } return nil } // SaveCAState saves the CA state to a JSON file func SaveCAState() error { CAState.UpdatedAt = time.Now().UTC().Format(time.RFC3339) f, err := os.Create(caStatePath()) if err != nil { return err } defer f.Close() enc := json.NewEncoder(f) enc.SetIndent("", " ") return enc.Encode(CAState) } // UpdateCAStateAfterIssue updates the CA state JSON after issuing a certificate func (s *_CAState) UpdateCAStateAfterIssue(serialType, basename string, serialNumber any, validity time.Duration) error { if s == nil { fmt.Fprintf(os.Stderr, "FATAL: CAState is nil in UpdateCAStateAfterIssue. This indicates a programming error.\n") os.Exit(1) } issued := time.Now().UTC().Format(time.RFC3339) expires := time.Now().Add(validity).UTC().Format(time.RFC3339) serialStr := "" switch serialType { case "sequential": serialStr = fmt.Sprintf("%d", CAState.Serial) CAState.Serial++ case "random": serialStr = fmt.Sprintf("%x", serialNumber) default: serialStr = fmt.Sprintf("%v", serialNumber) } s.AddCertificate(basename, issued, expires, serialStr) return nil } func (s *_CAState) AddCertificate(name, issued, expires, serial string) { if s == nil { fmt.Fprintf(os.Stderr, "FATAL: CAState is nil in AddCertificate. This indicates a programming error.\n") os.Exit(1) } rec := CertificateRecord{ Name: name, Issued: issued, Expires: expires, Serial: serial, } s.Certificates = append(s.Certificates, rec) } // RevokeCertificate revokes a certificate by serial number and reason code, updates state, and saves to disk func (s *_CAState) RevokeCertificate(serial string, reason int) error { if s == nil { fmt.Fprintf(os.Stderr, "FATAL: CAState is nil in RevokeCertificate. This indicates a programming error.\n") os.Exit(1) } revoked := false revokedAt := time.Now().UTC().Format(time.RFC3339) for i, rec := range s.Certificates { if rec.Serial == serial && rec.RevokedAt == "" { s.Certificates[i].RevokedAt = revokedAt s.Certificates[i].RevokeReason = reason revoked = true } } if !revoked { return fmt.Errorf("certificate with serial %s not found or already revoked", serial) } s.UpdatedAt = time.Now().UTC().Format(time.RFC3339) if err := SaveCAState(); err != nil { return fmt.Errorf("failed to save CA state after revocation: %v", err) } return nil } // GenerateCRL generates a CRL file from revoked certificates and writes it to the given path // validityDays defines the number of days for which the CRL is valid (NextUpdate - ThisUpdate) func (s *_CAState) GenerateCRL(crlPath string, validityDays int) error { if s == nil { return fmt.Errorf("CAState is nil in GenerateCRL") } if CACert == nil || CAKey == nil { return fmt.Errorf("CA certificate or key not loaded") } var revokedCerts []pkix.RevokedCertificate for _, rec := range s.Certificates { if rec.RevokedAt != "" { serial := new(big.Int) serial.SetString(rec.Serial, 16) // Parse serial as hex revokedTime, err := time.Parse(time.RFC3339, rec.RevokedAt) if err != nil { return fmt.Errorf("invalid revocation time for serial %s: %v", rec.Serial, err) } reasonCode := rec.RevokeReason // RFC 5280: Reason code must be encoded as ASN.1 ENUMERATED, not a raw byte // Use ASN.1 encoding for ENUMERATED asn1Reason, err := asn1.Marshal(asn1.Enumerated(reasonCode)) if err != nil { return fmt.Errorf("failed to ASN.1 encode reason code: %v", err) } revokedCerts = append(revokedCerts, pkix.RevokedCertificate{ SerialNumber: serial, RevocationTime: revokedTime, Extensions: []pkix.Extension{{Id: []int{2, 5, 29, 21}, Value: asn1Reason}}, }) } } now := time.Now().UTC() nextUpdate := now.Add(time.Duration(validityDays) * 24 * time.Hour) // validityDays * 24 * 60 * 60 * 1000 milliseconds template := &x509.RevocationList{ SignatureAlgorithm: CACert.SignatureAlgorithm, RevokedCertificates: revokedCerts, Number: big.NewInt(int64(s.CRLNumber + 1)), ThisUpdate: now, NextUpdate: nextUpdate, Issuer: CACert.Subject, } crlBytes, err := x509.CreateRevocationList(nil, template, CACert, CAKey) if err != nil { return fmt.Errorf("failed to create CRL: %v", err) } f, err := os.Create(crlPath) if err != nil { return fmt.Errorf("failed to create CRL file: %v", err) } defer f.Close() if err := pem.Encode(f, &pem.Block{Type: "X509 CRL", Bytes: crlBytes}); err != nil { return fmt.Errorf("failed to write CRL PEM: %v", err) } // Update CRL number and save state s.CRLNumber++ s.UpdatedAt = time.Now().UTC().Format(time.RFC3339) if err := SaveCAState(); err != nil { return fmt.Errorf("failed to update CA state after CRL generation: %v", err) } return nil }