310 lines
8.4 KiB
Go
310 lines
8.4 KiB
Go
package pki
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"encoding/pem"
|
|
"fmt"
|
|
"math/big"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
// Default key size for RSA keys
|
|
DefaultRSAKeySize = 2048
|
|
// Default CA certificate validity period
|
|
DefaultCAValidityDays = 3650 // ~10 years
|
|
// Default certificate validity period
|
|
DefaultCertValidityDays = 365 // 1 year
|
|
// Default PKI directory
|
|
DefaultPKIDir = "~/.kat/pki"
|
|
)
|
|
|
|
// GenerateCA creates a new Certificate Authority key pair and certificate.
|
|
// It saves the private key and certificate to the specified paths.
|
|
func GenerateCA(pkiDir string, keyPath, certPath string) error {
|
|
// Create PKI directory if it doesn't exist
|
|
if err := os.MkdirAll(pkiDir, 0700); err != nil {
|
|
return fmt.Errorf("failed to create PKI directory: %w", err)
|
|
}
|
|
|
|
// Generate RSA key
|
|
key, err := rsa.GenerateKey(rand.Reader, DefaultRSAKeySize)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to generate CA key: %w", err)
|
|
}
|
|
|
|
// Create self-signed certificate
|
|
serialNumber, err := generateSerialNumber()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to generate serial number: %w", err)
|
|
}
|
|
|
|
// Certificate template
|
|
notBefore := time.Now()
|
|
notAfter := notBefore.Add(time.Duration(DefaultCAValidityDays) * 24 * time.Hour)
|
|
|
|
template := x509.Certificate{
|
|
SerialNumber: serialNumber,
|
|
Subject: pkix.Name{
|
|
CommonName: "KAT Root CA",
|
|
Organization: []string{"KAT System"},
|
|
},
|
|
NotBefore: notBefore,
|
|
NotAfter: notAfter,
|
|
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
|
|
BasicConstraintsValid: true,
|
|
IsCA: true,
|
|
MaxPathLen: 1, // Only allow one level of intermediate certs
|
|
}
|
|
|
|
// Create certificate
|
|
derBytes, err := x509.CreateCertificate(
|
|
rand.Reader,
|
|
&template,
|
|
&template, // Self-signed
|
|
&key.PublicKey,
|
|
key,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create CA certificate: %w", err)
|
|
}
|
|
|
|
// Save private key
|
|
keyOut, err := os.OpenFile(keyPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to open CA key file for writing: %w", err)
|
|
}
|
|
defer keyOut.Close()
|
|
|
|
err = pem.Encode(keyOut, &pem.Block{
|
|
Type: "RSA PRIVATE KEY",
|
|
Bytes: x509.MarshalPKCS1PrivateKey(key),
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to write CA key to file: %w", err)
|
|
}
|
|
|
|
// Save certificate
|
|
certOut, err := os.OpenFile(certPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to open CA certificate file for writing: %w", err)
|
|
}
|
|
defer certOut.Close()
|
|
|
|
err = pem.Encode(certOut, &pem.Block{
|
|
Type: "CERTIFICATE",
|
|
Bytes: derBytes,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to write CA certificate to file: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GenerateCertificateRequest creates a new key pair and a Certificate Signing Request (CSR).
|
|
// It saves the private key and CSR to the specified paths.
|
|
func GenerateCertificateRequest(commonName, keyOutPath, csrOutPath string) error {
|
|
// Generate RSA key
|
|
key, err := rsa.GenerateKey(rand.Reader, DefaultRSAKeySize)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to generate key: %w", err)
|
|
}
|
|
|
|
// Create CSR template
|
|
template := x509.CertificateRequest{
|
|
Subject: pkix.Name{
|
|
CommonName: commonName,
|
|
Organization: []string{"KAT System"},
|
|
},
|
|
SignatureAlgorithm: x509.SHA256WithRSA,
|
|
DNSNames: []string{commonName}, // Add the CN as a SAN
|
|
}
|
|
|
|
// Create CSR
|
|
csrBytes, err := x509.CreateCertificateRequest(rand.Reader, &template, key)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create CSR: %w", err)
|
|
}
|
|
|
|
// Save private key
|
|
keyOut, err := os.OpenFile(keyOutPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to open key file for writing: %w", err)
|
|
}
|
|
defer keyOut.Close()
|
|
|
|
err = pem.Encode(keyOut, &pem.Block{
|
|
Type: "RSA PRIVATE KEY",
|
|
Bytes: x509.MarshalPKCS1PrivateKey(key),
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to write key to file: %w", err)
|
|
}
|
|
|
|
// Save CSR
|
|
csrOut, err := os.OpenFile(csrOutPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to open CSR file for writing: %w", err)
|
|
}
|
|
defer csrOut.Close()
|
|
|
|
err = pem.Encode(csrOut, &pem.Block{
|
|
Type: "CERTIFICATE REQUEST",
|
|
Bytes: csrBytes,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to write CSR to file: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// SignCertificateRequest signs a CSR using the CA key and certificate.
|
|
// It reads the CSR from csrPath and saves the signed certificate to certOutPath.
|
|
func SignCertificateRequest(caKeyPath, caCertPath, csrPath, certOutPath string, duration time.Duration) error {
|
|
// Load CA key
|
|
caKey, err := LoadCAPrivateKey(caKeyPath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to load CA key: %w", err)
|
|
}
|
|
|
|
// Load CA certificate
|
|
caCert, err := LoadCACertificate(caCertPath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to load CA certificate: %w", err)
|
|
}
|
|
|
|
// Read CSR
|
|
csrPEM, err := os.ReadFile(csrPath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read CSR file: %w", err)
|
|
}
|
|
|
|
block, _ := pem.Decode(csrPEM)
|
|
if block == nil || block.Type != "CERTIFICATE REQUEST" {
|
|
return fmt.Errorf("failed to decode PEM block containing CSR")
|
|
}
|
|
|
|
csr, err := x509.ParseCertificateRequest(block.Bytes)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse CSR: %w", err)
|
|
}
|
|
|
|
// Verify CSR signature
|
|
if err = csr.CheckSignature(); err != nil {
|
|
return fmt.Errorf("CSR signature verification failed: %w", err)
|
|
}
|
|
|
|
// Create certificate template from CSR
|
|
serialNumber, err := generateSerialNumber()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to generate serial number: %w", err)
|
|
}
|
|
|
|
notBefore := time.Now()
|
|
notAfter := notBefore.Add(duration)
|
|
|
|
template := x509.Certificate{
|
|
SerialNumber: serialNumber,
|
|
Subject: csr.Subject,
|
|
NotBefore: notBefore,
|
|
NotAfter: notAfter,
|
|
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
|
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
|
|
DNSNames: []string{csr.Subject.CommonName}, // Add the CN as a SAN
|
|
}
|
|
|
|
// Create certificate
|
|
derBytes, err := x509.CreateCertificate(
|
|
rand.Reader,
|
|
&template,
|
|
caCert,
|
|
csr.PublicKey,
|
|
caKey,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create certificate: %w", err)
|
|
}
|
|
|
|
// Save certificate
|
|
certOut, err := os.OpenFile(certOutPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to open certificate file for writing: %w", err)
|
|
}
|
|
defer certOut.Close()
|
|
|
|
err = pem.Encode(certOut, &pem.Block{
|
|
Type: "CERTIFICATE",
|
|
Bytes: derBytes,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to write certificate to file: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetPKIPathFromClusterConfig determines the PKI directory from the cluster configuration.
|
|
// If backupPath is provided, it uses the parent directory of backupPath.
|
|
// Otherwise, it uses the default PKI directory.
|
|
func GetPKIPathFromClusterConfig(backupPath string) string {
|
|
if backupPath == "" {
|
|
return DefaultPKIDir
|
|
}
|
|
|
|
// Use the parent directory of backupPath
|
|
return filepath.Dir(backupPath) + "/pki"
|
|
}
|
|
|
|
// generateSerialNumber creates a random serial number for certificates
|
|
func generateSerialNumber() (*big.Int, error) {
|
|
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) // 128 bits
|
|
return rand.Int(rand.Reader, serialNumberLimit)
|
|
}
|
|
|
|
// LoadCACertificate loads a CA certificate from a file
|
|
func LoadCACertificate(certPath string) (*x509.Certificate, error) {
|
|
certPEM, err := os.ReadFile(certPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read CA certificate file: %w", err)
|
|
}
|
|
|
|
block, _ := pem.Decode(certPEM)
|
|
if block == nil || block.Type != "CERTIFICATE" {
|
|
return nil, fmt.Errorf("failed to decode PEM block containing certificate")
|
|
}
|
|
|
|
cert, err := x509.ParseCertificate(block.Bytes)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse CA certificate: %w", err)
|
|
}
|
|
|
|
return cert, nil
|
|
}
|
|
|
|
// LoadCAPrivateKey loads a CA private key from a file
|
|
func LoadCAPrivateKey(keyPath string) (*rsa.PrivateKey, error) {
|
|
keyPEM, err := os.ReadFile(keyPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read CA key file: %w", err)
|
|
}
|
|
|
|
block, _ := pem.Decode(keyPEM)
|
|
if block == nil || block.Type != "RSA PRIVATE KEY" {
|
|
return nil, fmt.Errorf("failed to decode PEM block containing private key")
|
|
}
|
|
|
|
key, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse CA private key: %w", err)
|
|
}
|
|
|
|
return key, nil
|
|
}
|