package ca import ( "crypto" "crypto/ed25519" "crypto/rand" "crypto/x509" "crypto/x509/pkix" "encoding/pem" "fmt" "math/big" "os" "path/filepath" "time" ) type CA struct { key crypto.Signer cert *x509.Certificate } func NewOrLoad(dataDir string, keyType string, validity time.Duration) (*CA, error) { keyPath := filepath.Join(dataDir, "ca.key") certPath := filepath.Join(dataDir, "ca.crt") caKey, err := loadOrGenKey(keyPath, keyType) if err != nil { return nil, fmt.Errorf("ca key: %w", err) } caCert, err := loadOrGenCert(certPath, caKey, validity) if err != nil { return nil, fmt.Errorf("ca cert: %w", err) } return &CA{key: caKey, cert: caCert}, nil } func loadOrGenKey(path, keyType string) (crypto.Signer, error) { if data, err := os.ReadFile(path); err == nil { block, _ := pem.Decode(data) if block == nil { return nil, fmt.Errorf("invalid CA key PEM") } key, err := x509.ParsePKCS8PrivateKey(block.Bytes) if err != nil { return nil, err } return key.(crypto.Signer), nil } var signer crypto.Signer switch keyType { case "ed25519": _, priv, err := ed25519.GenerateKey(rand.Reader) if err != nil { return nil, err } signer = priv default: return nil, fmt.Errorf("unsupported key type: %s", keyType) } b, err := x509.MarshalPKCS8PrivateKey(signer) if err != nil { return nil, err } pemData := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: b}) if err := os.WriteFile(path, pemData, 0600); err != nil { return nil, err } return signer, nil } func loadOrGenCert(path string, key crypto.Signer, validity time.Duration) (*x509.Certificate, error) { if data, err := os.ReadFile(path); err == nil { block, _ := pem.Decode(data) if block == nil { return nil, fmt.Errorf("invalid CA cert PEM") } return x509.ParseCertificate(block.Bytes) } pub := key.Public() serial, _ := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) now := time.Now() template := &x509.Certificate{ SerialNumber: serial, Subject: pkix.Name{ CommonName: "QuiC Call Root CA", }, NotBefore: now, NotAfter: now.Add(validity), KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, BasicConstraintsValid: true, IsCA: true, MaxPathLenZero: true, } certDER, err := x509.CreateCertificate(rand.Reader, template, template, pub, key) if err != nil { return nil, err } pemData := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}) if err := os.WriteFile(path, pemData, 0644); err != nil { return nil, err } return x509.ParseCertificate(certDER) } func (ca *CA) IssueCert(commonName string, pub crypto.PublicKey, validity time.Duration) ([]byte, error) { serial, _ := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) now := time.Now() template := &x509.Certificate{ SerialNumber: serial, Subject: pkix.Name{ CommonName: commonName, Organization: []string{"QuiC Call"}, }, NotBefore: now, NotAfter: now.Add(validity), KeyUsage: x509.KeyUsageDigitalSignature, ExtKeyUsage: []x509.ExtKeyUsage{ x509.ExtKeyUsageClientAuth, }, } certDER, err := x509.CreateCertificate(rand.Reader, template, ca.cert, pub, ca.key) if err != nil { return nil, fmt.Errorf("create cert: %w", err) } return certDER, nil } func (ca *CA) CAKey() crypto.Signer { return ca.key } func (ca *CA) CACert() *x509.Certificate { return ca.cert } func (ca *CA) CACertPEM() []byte { return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: ca.cert.Raw}) } func (ca *CA) VerifyCert(cert *x509.Certificate) error { roots := x509.NewCertPool() roots.AddCert(ca.cert) opts := x509.VerifyOptions{ Roots: roots, KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, } if _, err := cert.Verify(opts); err != nil { return fmt.Errorf("cert verify: %w", err) } return nil }