Files
tyto/backend/internal/pki/ca.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

232 lines
5.5 KiB
Go

package pki
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"math/big"
"os"
"path/filepath"
"time"
)
// CA represents a Certificate Authority for signing certificates.
type CA struct {
cert *x509.Certificate
key *rsa.PrivateKey
certPEM []byte
keyPEM []byte
store *Store
}
// CAConfig contains options for creating a new CA.
type CAConfig struct {
CommonName string
Organization string
Country string
Validity time.Duration
KeySize int
}
// DefaultCAConfig returns a CAConfig with sensible defaults.
func DefaultCAConfig() CAConfig {
return CAConfig{
CommonName: "Tyto CA",
Organization: "Tyto",
Country: "US",
Validity: DefaultCAValidity,
KeySize: DefaultKeySize,
}
}
// InitCA creates a new Certificate Authority with the given configuration.
func InitCA(cfg CAConfig) (*CA, error) {
if cfg.KeySize == 0 {
cfg.KeySize = DefaultKeySize
}
if cfg.Validity == 0 {
cfg.Validity = DefaultCAValidity
}
// Generate private key
key, err := rsa.GenerateKey(rand.Reader, cfg.KeySize)
if err != nil {
return nil, fmt.Errorf("failed to generate CA key: %w", err)
}
// Generate serial number
serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
if err != nil {
return nil, fmt.Errorf("failed to generate serial number: %w", err)
}
now := time.Now()
template := &x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{
CommonName: cfg.CommonName,
Organization: []string{cfg.Organization},
Country: []string{cfg.Country},
},
NotBefore: now,
NotAfter: now.Add(cfg.Validity),
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign | x509.KeyUsageDigitalSignature,
BasicConstraintsValid: true,
IsCA: true,
MaxPathLen: 1,
}
// Self-sign the certificate
certDER, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key)
if err != nil {
return nil, fmt.Errorf("failed to create CA certificate: %w", err)
}
cert, err := x509.ParseCertificate(certDER)
if err != nil {
return nil, fmt.Errorf("failed to parse CA certificate: %w", err)
}
// Encode to PEM
certPEM := pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: certDER,
})
keyPEM := pem.EncodeToMemory(&pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(key),
})
return &CA{
cert: cert,
key: key,
certPEM: certPEM,
keyPEM: keyPEM,
}, nil
}
// LoadCA loads an existing CA from PEM-encoded certificate and key.
func LoadCA(certPEM, keyPEM []byte) (*CA, error) {
// Parse certificate
certBlock, _ := pem.Decode(certPEM)
if certBlock == nil {
return nil, fmt.Errorf("failed to decode CA certificate PEM")
}
cert, err := x509.ParseCertificate(certBlock.Bytes)
if err != nil {
return nil, fmt.Errorf("failed to parse CA certificate: %w", err)
}
if !cert.IsCA {
return nil, fmt.Errorf("certificate is not a CA")
}
// Parse private key
keyBlock, _ := pem.Decode(keyPEM)
if keyBlock == nil {
return nil, fmt.Errorf("failed to decode CA key PEM")
}
var key *rsa.PrivateKey
switch keyBlock.Type {
case "RSA PRIVATE KEY":
key, err = x509.ParsePKCS1PrivateKey(keyBlock.Bytes)
case "PRIVATE KEY":
parsedKey, parseErr := x509.ParsePKCS8PrivateKey(keyBlock.Bytes)
if parseErr != nil {
return nil, fmt.Errorf("failed to parse CA key: %w", parseErr)
}
var ok bool
key, ok = parsedKey.(*rsa.PrivateKey)
if !ok {
return nil, fmt.Errorf("CA key is not RSA")
}
default:
return nil, fmt.Errorf("unsupported key type: %s", keyBlock.Type)
}
if err != nil {
return nil, fmt.Errorf("failed to parse CA key: %w", err)
}
return &CA{
cert: cert,
key: key,
certPEM: certPEM,
keyPEM: keyPEM,
}, nil
}
// LoadCAFromDir loads a CA from a directory containing ca.crt and ca.key files.
func LoadCAFromDir(dir string) (*CA, error) {
certPath := filepath.Join(dir, "ca.crt")
keyPath := filepath.Join(dir, "ca.key")
certPEM, err := os.ReadFile(certPath)
if err != nil {
return nil, fmt.Errorf("failed to read CA certificate: %w", err)
}
keyPEM, err := os.ReadFile(keyPath)
if err != nil {
return nil, fmt.Errorf("failed to read CA key: %w", err)
}
ca, err := LoadCA(certPEM, keyPEM)
if err != nil {
return nil, err
}
// Initialize store for this directory
ca.store = NewStore(dir)
return ca, nil
}
// SaveToDir saves the CA certificate and key to a directory.
func (ca *CA) SaveToDir(dir string) error {
if err := os.MkdirAll(dir, 0700); err != nil {
return fmt.Errorf("failed to create CA directory: %w", err)
}
certPath := filepath.Join(dir, "ca.crt")
keyPath := filepath.Join(dir, "ca.key")
if err := os.WriteFile(certPath, ca.certPEM, 0644); err != nil {
return fmt.Errorf("failed to write CA certificate: %w", err)
}
if err := os.WriteFile(keyPath, ca.keyPEM, 0600); err != nil {
return fmt.Errorf("failed to write CA key: %w", err)
}
// Initialize store
ca.store = NewStore(dir)
return nil
}
// Certificate returns the CA certificate.
func (ca *CA) Certificate() *x509.Certificate {
return ca.cert
}
// CertificatePEM returns the PEM-encoded CA certificate.
func (ca *CA) CertificatePEM() []byte {
return ca.certPEM
}
// Info returns metadata about the CA certificate.
func (ca *CA) Info() CertInfo {
return CertInfoFromX509(ca.cert, CertTypeCA)
}
// Store returns the certificate store associated with this CA.
func (ca *CA) Store() *Store {
return ca.store
}