Files
zot/pkg/test/tls/tls.go
Andrei Aaron 79439bbf63 feat: add configurable mTLS identity extraction with fallback chain (#3640)
Add support for configurable identity attributes in mTLS authentication,
allowing identity extraction from CommonName, Subject DN, Email SAN,
URI SAN, or DNSName SAN with fallback chain support. Includes regex
pattern matching for URI SANs (e.g., SPIFFE workload IDs).

- Add MTLSConfig with identity attributes, URISANPattern, and index fields
- Implement extractMTLSIdentity with fallback chain logic
- Move the mtls tests in the api package to pkg/api/mtls_test.go

Signed-off-by: Andrei Aaron <andreifdaaron@gmail.com>
2025-12-18 09:10:47 -08:00

599 lines
19 KiB
Go

package tls
import (
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"errors"
"fmt"
"net"
"net/url"
"os"
"time"
)
var (
ErrDecodeCAPEM = errors.New("failed to decode CA certificate PEM")
ErrInvalidCertificateType = errors.New("invalid certificate type")
ErrHostnameRequired = errors.New("Hostname is required in CertificateOptions")
ErrNoCertificatesProvided = errors.New("at least one certificate is required")
ErrInvalidKeyType = errors.New("invalid key type")
ErrUnsupportedPrivateKeyType = errors.New("unsupported private key type")
ErrFailedParsePrivateKey = errors.New("failed to parse private key: unsupported key format")
ErrFailedDecodeCertPEM = errors.New("failed to decode certificate PEM")
ErrFailedDecodeKeyPEM = errors.New("failed to decode private key PEM")
ErrPrivateKeyNotRSA = errors.New("private key is not RSA")
)
// KeyType represents the type of cryptographic key to use for certificate generation.
type KeyType string
const (
KeyTypeRSA KeyType = "RSA"
KeyTypeECDSA KeyType = "ECDSA"
KeyTypeED25519 KeyType = "ED25519"
)
const (
certTypeCA = "CA"
certTypeServer = "Server"
certTypeClient = "Client"
)
// CertificateOptions contains optional settings for certificate generation.
// If a field is nil or zero, default values will be used.
type CertificateOptions struct {
// NotBefore is the certificate validity start time.
// If zero, defaults to time.Now().
NotBefore time.Time
// NotAfter is the certificate validity end time.
// If zero, defaults will be used based on certificate type.
NotAfter time.Time
// DNSNames contains the DNS names for the Subject Alternative Name extension.
// If nil, default values may be used based on certificate type.
DNSNames []string
// IPAddresses contains the IP addresses for the Subject Alternative Name extension.
// If nil, default values may be used based on certificate type.
IPAddresses []net.IP
// EmailAddresses contains the email addresses for the Subject Alternative Name extension.
// If nil, no email addresses will be included.
EmailAddresses []string
// URIs contains the URIs for the Subject Alternative Name extension.
// If nil, no URIs will be included.
URIs []string
// Hostname is the hostname or IP address for server certificates.
// For server certificates, this is required and will be added to DNSNames or IPAddresses
// based on whether it's a valid IP address or a DNS name.
Hostname string
// CommonName is the CommonName (CN) for certificates.
// For client certificates, this is optional - if not provided, the certificate will not have a CN.
CommonName string
// OrganizationalUnit is the OrganizationalUnit (OU) for certificates.
// If not provided, the certificate will not have an OU.
OrganizationalUnit string
// KeyType specifies the type of cryptographic key to use.
// Valid values: "RSA" (default), "ECDSA", "ED25519".
// If empty or "RSA", RSA keys will be generated.
KeyType KeyType
}
// generateCertificate is a helper function that generates a certificate and private key.
// If signerCert and signerKey are nil, the certificate will be self-signed.
// signerKey can be *rsa.PrivateKey, *ecdsa.PrivateKey, or ed25519.PrivateKey.
func generateCertificate(
certType string,
opts *CertificateOptions,
signerCert *x509.Certificate,
signerKey any, // Can be *rsa.PrivateKey, *ecdsa.PrivateKey, or ed25519.PrivateKey
) ([]byte, []byte, error) {
var (
issuerCert *x509.Certificate
issuerKey any
privKey any
publicKey any
err error
)
// Determine key type
keyType := KeyTypeRSA
if opts != nil && opts.KeyType != "" {
keyType = opts.KeyType
}
// Generate private key based on key type
switch keyType {
case KeyTypeRSA:
rsaKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, nil, fmt.Errorf("failed to generate RSA private key: %w", err)
}
privKey = rsaKey
publicKey = &rsaKey.PublicKey
case KeyTypeECDSA:
ecKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, nil, fmt.Errorf("failed to generate ECDSA private key: %w", err)
}
privKey = ecKey
publicKey = &ecKey.PublicKey
case KeyTypeED25519:
edPublicKey, edKey, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
return nil, nil, fmt.Errorf("failed to generate ED25519 private key: %w", err)
}
privKey = edKey
publicKey = edPublicKey
default:
return nil, nil, fmt.Errorf("%w: %s", ErrInvalidKeyType, keyType)
}
// Initialize certificate template
template, err := initializeTemplate(certType)
if err != nil {
return nil, nil, err
}
// Apply options
applyOptions(template, opts, certType)
// Determine signer (self-signed if signerCert is nil)
if signerCert == nil {
// Self-signed
issuerCert = template
issuerKey = privKey
} else {
// Signed by CA
issuerCert = signerCert
issuerKey = signerKey
}
// Create the certificate
certDER, err := x509.CreateCertificate(rand.Reader, template, issuerCert, publicKey, issuerKey)
if err != nil {
return nil, nil, fmt.Errorf("failed to create certificate: %w", err)
}
// Encode certificate to PEM
certPEM := pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: certDER,
})
// Encode private key to PEM based on key type
var keyPEM []byte
switch privKeyType := privKey.(type) {
case *rsa.PrivateKey:
keyPEM = pem.EncodeToMemory(&pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(privKeyType),
})
case *ecdsa.PrivateKey:
keyBytes, err := x509.MarshalECPrivateKey(privKeyType)
if err != nil {
return nil, nil, fmt.Errorf("failed to marshal ECDSA private key: %w", err)
}
keyPEM = pem.EncodeToMemory(&pem.Block{
Type: "EC PRIVATE KEY",
Bytes: keyBytes,
})
case ed25519.PrivateKey:
keyBytes, err := x509.MarshalPKCS8PrivateKey(privKeyType)
if err != nil {
return nil, nil, fmt.Errorf("failed to marshal ED25519 private key: %w", err)
}
keyPEM = pem.EncodeToMemory(&pem.Block{
Type: "PRIVATE KEY",
Bytes: keyBytes,
})
default:
return nil, nil, fmt.Errorf("%w: %T", ErrUnsupportedPrivateKeyType, privKey)
}
return certPEM, keyPEM, nil
}
// parsePrivateKeyFromPEM parses a private key from PEM-encoded bytes.
// Tries PKCS8 first (handles RSA, ECDSA, and ED25519), then falls back to PKCS1 (RSA) and EC SEC1 (ECDSA).
func parsePrivateKeyFromPEM(keyBytes []byte) (any, error) {
// Try PKCS8 first (handles RSA, ECDSA, and ED25519)
if privKey, err := x509.ParsePKCS8PrivateKey(keyBytes); err == nil {
return privKey, nil
}
// Fall back to PKCS1 (RSA only)
if rsaKey, err := x509.ParsePKCS1PrivateKey(keyBytes); err == nil {
return rsaKey, nil
}
// Fall back to EC SEC1 format
if ecKey, err := x509.ParseECPrivateKey(keyBytes); err == nil {
return ecKey, nil
}
return nil, ErrFailedParsePrivateKey
}
// ExtractPublicKeyFromCert extracts the public key from a certificate in PEM format.
// Returns the public key in PKIX format (suitable for ECDSA and ED25519).
func ExtractPublicKeyFromCert(certPEM []byte) ([]byte, error) {
block, _ := pem.Decode(certPEM)
if block == nil {
return nil, ErrFailedDecodeCertPEM
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, fmt.Errorf("failed to parse certificate: %w", err)
}
publicKeyBytes, err := x509.MarshalPKIXPublicKey(cert.PublicKey)
if err != nil {
return nil, fmt.Errorf("failed to marshal public key: %w", err)
}
return pem.EncodeToMemory(&pem.Block{
Type: "PUBLIC KEY",
Bytes: publicKeyBytes,
}), nil
}
// ExtractRSAPublicKeyPKCS1 extracts the RSA public key from a private key in PEM format.
// Returns the public key in PKCS1 format (RSA-specific).
func ExtractRSAPublicKeyPKCS1(keyPEM []byte) ([]byte, error) {
block, _ := pem.Decode(keyPEM)
if block == nil {
return nil, ErrFailedDecodeKeyPEM
}
privKey, err := parsePrivateKeyFromPEM(block.Bytes)
if err != nil {
return nil, fmt.Errorf("failed to parse RSA private key: %w", err)
}
rsaKey, ok := privKey.(*rsa.PrivateKey)
if !ok {
return nil, fmt.Errorf("%w, got %T", ErrPrivateKeyNotRSA, privKey)
}
publicKeyBytes := x509.MarshalPKCS1PublicKey(&rsaKey.PublicKey)
return pem.EncodeToMemory(&pem.Block{
Type: "RSA PUBLIC KEY",
Bytes: publicKeyBytes,
}), nil
}
// parseCA parses CA certificate and private key from PEM format.
// Returns the certificate and the private key (which can be *rsa.PrivateKey, *ecdsa.PrivateKey, or ed25519.PrivateKey).
func parseCA(caCertPEM, caKeyPEM []byte) (*x509.Certificate, any, error) {
// Parse CA certificate
caCertBlock, _ := pem.Decode(caCertPEM)
if caCertBlock == nil {
return nil, nil, ErrDecodeCAPEM
}
caCert, err := x509.ParseCertificate(caCertBlock.Bytes)
if err != nil {
return nil, nil, fmt.Errorf("failed to parse CA certificate: %w", err)
}
// Parse CA private key
caKeyBlock, _ := pem.Decode(caKeyPEM)
if caKeyBlock == nil {
return nil, nil, ErrDecodeCAPEM
}
caPrivKey, err := parsePrivateKeyFromPEM(caKeyBlock.Bytes)
if err != nil {
return nil, nil, fmt.Errorf("failed to parse CA private key: %w", err)
}
return caCert, caPrivKey, nil
}
// initializeTemplate creates and initializes a certificate template based on the certificate type.
// certType can be "CA", "Server", or "Client".
func initializeTemplate(certType string) (*x509.Certificate, error) {
template := &x509.Certificate{}
// Initialize certificate type-specific fields and defaults
switch certType {
case certTypeCA:
template.IsCA = true
template.ExtKeyUsage = []x509.ExtKeyUsage{}
template.KeyUsage = x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign
template.BasicConstraintsValid = true
template.Subject = pkix.Name{
Organization: []string{"Test CA"},
Country: []string{"US"},
Province: []string{""},
Locality: []string{"San Francisco"},
StreetAddress: []string{""},
PostalCode: []string{""},
}
// NotBefore and NotAfter are set via CertificateOptions in test logic
case certTypeServer:
template.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}
template.KeyUsage = x509.KeyUsageDigitalSignature
template.Subject = pkix.Name{
Organization: []string{"Test Server"},
Country: []string{"US"},
Province: []string{""},
Locality: []string{"San Francisco"},
StreetAddress: []string{""},
PostalCode: []string{""},
}
// NotBefore and NotAfter are set via CertificateOptions in test logic
template.IPAddresses = []net.IP{net.ParseIP("127.0.0.1")} // Default IP for Server
case certTypeClient:
template.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}
template.KeyUsage = x509.KeyUsageDigitalSignature
template.Subject = pkix.Name{
Organization: []string{"Test Client"},
Country: []string{"US"},
Province: []string{""},
Locality: []string{"San Francisco"},
StreetAddress: []string{""},
PostalCode: []string{""},
}
// NotBefore and NotAfter are set via CertificateOptions in test logic
default:
return nil, fmt.Errorf("%w: %s", ErrInvalidCertificateType, certType)
}
return template, nil
}
// applyOptions applies options to the certificate template, using defaults when options are not provided.
// certType can be "CA", "Server", or "Client".
func applyOptions(template *x509.Certificate, opts *CertificateOptions, certType string) {
if opts == nil {
opts = &CertificateOptions{}
}
// Apply NotBefore - default to time.Now() if not provided
if !opts.NotBefore.IsZero() {
template.NotBefore = opts.NotBefore
} else {
template.NotBefore = time.Now()
}
// Apply NotAfter - default to 1 year if not provided, matching gen_certs.sh
if !opts.NotAfter.IsZero() {
template.NotAfter = opts.NotAfter
} else {
template.NotAfter = time.Now().AddDate(1, 0, 0)
}
// Apply SAN (Subject Alternative Name) - handle IPAddresses
// Priority: 1) opts.IPAddresses, 2) hostname if IP, 3) keep default from initializeTemplate
if opts.IPAddresses != nil {
template.IPAddresses = opts.IPAddresses
} else if certType == certTypeServer && opts.Hostname != "" {
if ip := net.ParseIP(opts.Hostname); ip != nil {
// Hostname is an IP address, use it
template.IPAddresses = []net.IP{ip}
}
// If hostname is DNS name, keep default IP from initializeTemplate
}
// Apply SAN (Subject Alternative Name) - handle DNSNames
// Priority: 1) opts.DNSNames, 2) hostname if DNS name
if opts.DNSNames != nil {
template.DNSNames = opts.DNSNames
} else if certType == certTypeServer && opts.Hostname != "" {
if ip := net.ParseIP(opts.Hostname); ip == nil {
// Hostname is a DNS name, use it
template.DNSNames = []string{opts.Hostname}
}
}
// Apply email addresses
if opts.EmailAddresses != nil {
template.EmailAddresses = opts.EmailAddresses
}
// Apply URIs
if opts.URIs != nil {
templateURIs := make([]*url.URL, 0, len(opts.URIs))
for _, uriStr := range opts.URIs {
uri, err := url.Parse(uriStr)
if err != nil {
// Skip invalid URIs - could log error in production
continue
}
// Validate that the URI has a valid scheme (url.Parse accepts URIs without schemes)
if uri.Scheme == "" {
// Skip URIs without a scheme - could log error in production
continue
}
templateURIs = append(templateURIs, uri)
}
template.URIs = templateURIs
}
// Apply CommonName - if provided, override the default; otherwise keep default from initializeTemplate
if opts.CommonName != "" {
template.Subject.CommonName = opts.CommonName
} else if opts != nil && opts.CommonName == "" && certType == certTypeClient {
// Special case: For client certs, if opts is provided and CommonName is explicitly set to empty,
// use empty CN (for noidentity-style certs)
template.Subject.CommonName = ""
}
// Apply OrganizationalUnit - if provided, set it
if opts.OrganizationalUnit != "" {
template.Subject.OrganizationalUnit = []string{opts.OrganizationalUnit}
}
}
// GenerateCACert generates a CA certificate and private key.
// opts is optional and can be used to customize certificate settings.
func GenerateCACert(opts ...*CertificateOptions) ([]byte, []byte, error) {
var options *CertificateOptions
if len(opts) > 0 && opts[0] != nil {
options = opts[0]
}
// Self-signed certificate (signerCert and signerKey are nil)
return generateCertificate(certTypeCA, options, nil, nil)
}
// GenerateIntermediateCACert generates an intermediate CA certificate signed by the provided parent CA.
// opts is optional and can be used to customize certificate settings, including CommonName.
func GenerateIntermediateCACert(
parentCACertPEM,
parentCAKeyPEM []byte,
opts ...*CertificateOptions,
) ([]byte, []byte, error) {
var options *CertificateOptions
if len(opts) > 0 && opts[0] != nil {
options = opts[0]
} else {
options = &CertificateOptions{}
}
// Parse parent CA certificate and key
parentCACert, parentCAPrivKey, err := parseCA(parentCACertPEM, parentCAKeyPEM)
if err != nil {
return nil, nil, err
}
// Generate intermediate CA certificate signed by parent CA
return generateCertificate(certTypeCA, options, parentCACert, parentCAPrivKey)
}
// writeCertAndKeyToFile writes certificate and key bytes to their respective files.
func writeCertAndKeyToFile(certPath, keyPath string, certBytes, keyBytes []byte) error {
err := os.WriteFile(certPath, certBytes, 0o600)
if err != nil {
return err
}
return os.WriteFile(keyPath, keyBytes, 0o600)
}
// WriteCertificateChainToFile writes a certificate chain to a file.
// The certificates should be provided in order: leaf certificate first, followed by intermediate CAs.
// All certificates should be in PEM format.
func WriteCertificateChainToFile(certChainPath string, certs ...[]byte) error {
if len(certs) == 0 {
return ErrNoCertificatesProvided
}
// Calculate total size for pre-allocation
totalSize := 0
for _, cert := range certs {
totalSize += len(cert)
}
// Concatenate all certificates
chainPEM := make([]byte, 0, totalSize)
for _, cert := range certs {
chainPEM = append(chainPEM, cert...)
}
// Write to file
err := os.WriteFile(certChainPath, chainPEM, 0o600)
if err != nil {
return err
}
return nil
}
// GenerateServerCert generates a server certificate signed by the provided CA.
// opts is required and must contain a Hostname field.
func GenerateServerCert(caCertPEM, caKeyPEM []byte, opts *CertificateOptions) ([]byte, []byte, error) {
if opts == nil || opts.Hostname == "" {
return nil, nil, ErrHostnameRequired
}
// Parse CA certificate and key
caCert, caPrivKey, err := parseCA(caCertPEM, caKeyPEM)
if err != nil {
return nil, nil, err
}
// Generate certificate signed by CA
return generateCertificate(certTypeServer, opts, caCert, caPrivKey)
}
// GenerateServerCertToFile generates a server certificate signed by the provided CA
// and writes generated key and cert to files.
// opts is required and must contain a Hostname field.
func GenerateServerCertToFile(
caCertPEM, caKeyPEM []byte,
certOutputPath, keyOutputPath string,
opts *CertificateOptions,
) error {
serverCertBytes, serverKeyBytes, err := GenerateServerCert(caCertPEM, caKeyPEM, opts)
if err != nil {
return err
}
return writeCertAndKeyToFile(certOutputPath, keyOutputPath, serverCertBytes, serverKeyBytes)
}
// GenerateClientCert generates a client certificate signed by the provided CA.
// opts is optional. CommonName is optional - if not provided, the certificate will not have a CN.
func GenerateClientCert(caCertPEM, caKeyPEM []byte, opts *CertificateOptions) ([]byte, []byte, error) {
// Parse CA certificate and key
caCert, caPrivKey, err := parseCA(caCertPEM, caKeyPEM)
if err != nil {
return nil, nil, err
}
// Generate certificate signed by CA
return generateCertificate(certTypeClient, opts, caCert, caPrivKey)
}
// GenerateClientCertToFile generates a client certificate signed by the provided CA
// and writes generated key and cert to files.
// opts is optional. CommonName is optional - if not provided, the certificate will not have a CN.
func GenerateClientCertToFile(caCertPEM, caKeyPEM []byte, certPath, keyPath string, opts *CertificateOptions) error {
clientCertBytes, clientKeyBytes, err := GenerateClientCert(caCertPEM, caKeyPEM, opts)
if err != nil {
return err
}
return writeCertAndKeyToFile(certPath, keyPath, clientCertBytes, clientKeyBytes)
}
// GenerateClientSelfSignedCert generates a client certificate not signed by any CA.
// opts is optional. CommonName is optional - if not provided, the certificate will not have a CN.
func GenerateClientSelfSignedCert(opts *CertificateOptions) ([]byte, []byte, error) {
// Self-signed certificate (signerCert and signerKey are nil)
return generateCertificate(certTypeClient, opts, nil, nil)
}
// GenerateClientSelfSignedCertToFile generates a client certificate not signed by any CA
// and writes generated key and cert to files.
// opts is optional. CommonName is optional - if not provided, the certificate will not have a CN.
func GenerateClientSelfSignedCertToFile(certOutputPath, keyOutputPath string, opts *CertificateOptions) error {
clientCertBytes, clientKeyBytes, err := GenerateClientSelfSignedCert(opts)
if err != nil {
return err
}
return writeCertAndKeyToFile(certOutputPath, keyOutputPath, clientCertBytes, clientKeyBytes)
}