PKI Package (internal/pki/): - CA initialization with configurable validity and key size - Server certificate generation with DNS/IP SANs - Agent certificate generation (agent ID in CN) - Certificate revocation list (CRL) support - mTLS TLS configuration helpers - File-based certificate store with JSON persistence CLI Commands (cmd/tyto/): - `tyto pki init-ca` - Initialize new Certificate Authority - `tyto pki gen-server` - Generate server certificate - `tyto pki gen-agent` - Generate agent certificate - `tyto pki revoke` - Revoke certificate by serial - `tyto pki list` - List all certificates - `tyto pki info` - Show CA information Security Features: - RSA 4096-bit keys by default - TLS 1.2 minimum version - Client certificate verification for mTLS - CRL checking in TLS handshake - Agent ID extraction from verified certificates 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
171 lines
3.4 KiB
Go
171 lines
3.4 KiB
Go
package pki
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
)
|
|
|
|
// Store manages certificate records on disk.
|
|
type Store struct {
|
|
dir string
|
|
certs map[string]CertInfo // serial -> info
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
// storeData is the JSON structure for persistence.
|
|
type storeData struct {
|
|
Certificates []CertInfo `json:"certificates"`
|
|
}
|
|
|
|
// NewStore creates a new certificate store in the given directory.
|
|
func NewStore(dir string) *Store {
|
|
s := &Store{
|
|
dir: dir,
|
|
certs: make(map[string]CertInfo),
|
|
}
|
|
s.load()
|
|
return s
|
|
}
|
|
|
|
// load reads the store from disk.
|
|
func (s *Store) load() error {
|
|
path := filepath.Join(s.dir, "certs.json")
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
var sd storeData
|
|
if err := json.Unmarshal(data, &sd); err != nil {
|
|
return err
|
|
}
|
|
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
for _, cert := range sd.Certificates {
|
|
s.certs[cert.SerialNumber] = cert
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// save writes the store to disk.
|
|
func (s *Store) save() error {
|
|
s.mu.RLock()
|
|
certs := make([]CertInfo, 0, len(s.certs))
|
|
for _, cert := range s.certs {
|
|
certs = append(certs, cert)
|
|
}
|
|
s.mu.RUnlock()
|
|
|
|
sd := storeData{Certificates: certs}
|
|
data, err := json.MarshalIndent(sd, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
path := filepath.Join(s.dir, "certs.json")
|
|
return os.WriteFile(path, data, 0600)
|
|
}
|
|
|
|
// AddCertificate adds a certificate to the store.
|
|
func (s *Store) AddCertificate(info CertInfo) error {
|
|
s.mu.Lock()
|
|
s.certs[info.SerialNumber] = info
|
|
s.mu.Unlock()
|
|
|
|
return s.save()
|
|
}
|
|
|
|
// GetCertificate returns a certificate by serial number.
|
|
func (s *Store) GetCertificate(serial string) (CertInfo, bool) {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
|
|
info, ok := s.certs[serial]
|
|
return info, ok
|
|
}
|
|
|
|
// ListCertificates returns all certificates in the store.
|
|
func (s *Store) ListCertificates() []CertInfo {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
|
|
certs := make([]CertInfo, 0, len(s.certs))
|
|
for _, cert := range s.certs {
|
|
certs = append(certs, cert)
|
|
}
|
|
return certs
|
|
}
|
|
|
|
// ListByType returns certificates of a specific type.
|
|
func (s *Store) ListByType(certType CertType) []CertInfo {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
|
|
var certs []CertInfo
|
|
for _, cert := range s.certs {
|
|
if cert.Type == certType {
|
|
certs = append(certs, cert)
|
|
}
|
|
}
|
|
return certs
|
|
}
|
|
|
|
// MarkRevoked marks a certificate as revoked.
|
|
func (s *Store) MarkRevoked(serial string) error {
|
|
s.mu.Lock()
|
|
if info, ok := s.certs[serial]; ok {
|
|
info.Revoked = true
|
|
s.certs[serial] = info
|
|
}
|
|
s.mu.Unlock()
|
|
|
|
return s.save()
|
|
}
|
|
|
|
// IsRevoked checks if a certificate is revoked.
|
|
func (s *Store) IsRevoked(serial string) bool {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
|
|
if info, ok := s.certs[serial]; ok {
|
|
return info.Revoked
|
|
}
|
|
return false
|
|
}
|
|
|
|
// RevokedSerials returns all revoked certificate serial numbers.
|
|
func (s *Store) RevokedSerials() []string {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
|
|
var serials []string
|
|
for serial, cert := range s.certs {
|
|
if cert.Revoked {
|
|
serials = append(serials, serial)
|
|
}
|
|
}
|
|
return serials
|
|
}
|
|
|
|
// writeFile is a helper that creates parent directories.
|
|
func writeFile(path string, data []byte, perm os.FileMode) error {
|
|
dir := filepath.Dir(path)
|
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
|
return err
|
|
}
|
|
return os.WriteFile(path, data, perm)
|
|
}
|
|
|
|
// readFile is a helper for reading files.
|
|
func readFile(path string) ([]byte, error) {
|
|
return os.ReadFile(path)
|
|
}
|