Files
tyto/backend/internal/pki/store.go
vikingowl c8fbade575 feat: add PKI infrastructure for mTLS authentication
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>
2025-12-28 07:29:42 +01:00

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)
}