feat: support mTLS-only authn/authz with AccessControl and allow combining mTLS with other auth mechanisms (#3624)

* feat: support mTLS-only authn/authz with AccessControl and allow combining mTLS with other auth mechanisms

Signed-off-by: Ivan Arkhipov <me@endevir.ru>

* refactor: improve authentication logic and TLS certificate generation

- Fix mTLS authentication to use only leaf certificate instead of iterating
  through all certificates in the chain
- Reject Authorization headers when corresponding auth method is disabled,
  regardless of mTLS status (security improvement)
- Simplify authentication switch statement ordering and logic
- Move ErrUserDataNotFound error handling into sessionAuthn method
- Refactor TLS certificate generation to use Options pattern with
  CertificateOptions struct for better extensibility
- Consolidate duplicate certificate generation code into helper functions
  (generateCertificate, parseCA, initializeTemplate, applyOptions)
- Rename certificate generation functions for clarity:
  - GenerateCertWithCN -> GenerateClientCert
  - GenerateSelfSignedCertWithCN -> GenerateClientSelfSignedCert
- Add support for SAN settings including email addresses in certificates
- Update tests to reflect new authentication behavior and certificate API

This commit improves both the security posture (rejecting disabled auth
methods) and code maintainability (consolidated certificate generation).

Signed-off-by: Andrei Aaron <andreifdaaron@gmail.com>

* fix: guard against multiple Authorization headers

Signed-off-by: Andrei Aaron <andreifdaaron@gmail.com>

---------

Signed-off-by: Ivan Arkhipov <me@endevir.ru>
Signed-off-by: Andrei Aaron <andreifdaaron@gmail.com>
Co-authored-by: Ivan Arkhipov <me@endevir.ru>
This commit is contained in:
Andrei Aaron
2025-12-11 20:08:32 +02:00
committed by GitHub
parent e7b73b6c2d
commit 08fae9104d
11 changed files with 2066 additions and 536 deletions
+407
View File
@@ -0,0 +1,407 @@
package tls
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"errors"
"fmt"
"net"
"os"
"time"
)
var (
ErrDecodeCAPEM = errors.New("failed to decode CA certificate PEM")
ErrInvalidCertificateType = errors.New("invalid certificate type")
ErrCertificateOptionsRequired = errors.New("CertificateOptions is required")
ErrHostnameRequired = errors.New("Hostname is required in CertificateOptions")
ErrNoCertificatesProvided = errors.New("at least one certificate is required")
)
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
// 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 client certificates.
// For client certificates, this is optional - if not provided, the certificate will not have a CN.
CommonName string
}
// generateCertificate is a helper function that generates a certificate and private key.
// If signerCert and signerKey are nil, the certificate will be self-signed.
func generateCertificate(
certType string,
opts *CertificateOptions,
signerCert *x509.Certificate,
signerKey *rsa.PrivateKey,
) ([]byte, []byte, error) {
// Generate private key
privKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, nil, fmt.Errorf("failed to generate private key: %w", err)
}
// 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)
var issuerCert *x509.Certificate
var issuerKey *rsa.PrivateKey
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, &privKey.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
keyPEM := pem.EncodeToMemory(&pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(privKey),
})
return certPEM, keyPEM, nil
}
// parseCA parses CA certificate and private key from PEM format.
func parseCA(caCertPEM, caKeyPEM []byte) (*x509.Certificate, *rsa.PrivateKey, 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 := x509.ParsePKCS1PrivateKey(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{""},
}
template.NotBefore = time.Now()
template.NotAfter = time.Now().AddDate(10, 0, 0) // 10 years for CA
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{""},
}
template.NotBefore = time.Now()
template.NotAfter = time.Now().AddDate(1, 0, 0) // 1 year for server
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{""},
}
template.NotBefore = time.Now()
template.NotAfter = time.Now().AddDate(1, 0, 0) // 1 year for client
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 if provided in options
if !opts.NotBefore.IsZero() {
template.NotBefore = opts.NotBefore
}
// Apply NotAfter if provided in options
if !opts.NotAfter.IsZero() {
template.NotAfter = opts.NotAfter
}
// 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 CommonName - explicitly set to empty string if not provided to ensure it's empty
if opts.CommonName != "" {
template.Subject.CommonName = opts.CommonName
} else {
template.Subject.CommonName = ""
}
}
// 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)
}
+438
View File
@@ -0,0 +1,438 @@
package tls_test
import (
"crypto/x509"
"encoding/pem"
"net"
"path"
"testing"
"time"
. "github.com/smartystreets/goconvey/convey"
"zotregistry.dev/zot/v2/pkg/test/tls"
)
func TestGenerateCACert(t *testing.T) {
Convey("Generate CA certificate", t, func() {
certPEM, keyPEM, err := tls.GenerateCACert()
So(err, ShouldBeNil)
Convey("Certificate should be valid PEM", func() {
certBlock, _ := pem.Decode(certPEM)
So(certBlock, ShouldNotBeNil)
So(certBlock.Type, ShouldEqual, "CERTIFICATE")
cert, err := x509.ParseCertificate(certBlock.Bytes)
So(err, ShouldBeNil)
So(cert.IsCA, ShouldBeTrue)
So(cert.Subject.Organization[0], ShouldEqual, "Test CA")
})
Convey("Private key should be valid PEM", func() {
keyBlock, _ := pem.Decode(keyPEM)
So(keyBlock, ShouldNotBeNil)
So(keyBlock.Type, ShouldEqual, "RSA PRIVATE KEY")
_, err := x509.ParsePKCS1PrivateKey(keyBlock.Bytes)
So(err, ShouldBeNil)
})
})
}
func TestGenerateServerCert(t *testing.T) {
Convey("Generate server certificate", t, func() {
caCertPEM, caKeyPEM, err := tls.GenerateCACert()
So(err, ShouldBeNil)
Convey("With hostname", func() {
hostname := "localhost"
opts := &tls.CertificateOptions{
Hostname: hostname,
}
certPEM, keyPEM, err := tls.GenerateServerCert(caCertPEM, caKeyPEM, opts)
So(err, ShouldBeNil)
certBlock, _ := pem.Decode(certPEM)
So(certBlock, ShouldNotBeNil)
cert, err := x509.ParseCertificate(certBlock.Bytes)
So(err, ShouldBeNil)
So(cert.DNSNames, ShouldContain, hostname)
So(cert.ExtKeyUsage, ShouldContain, x509.ExtKeyUsageServerAuth)
keyBlock, _ := pem.Decode(keyPEM)
So(keyBlock, ShouldNotBeNil)
})
Convey("With IP address", func() {
ipaddr := "127.0.0.1"
opts := &tls.CertificateOptions{
Hostname: ipaddr,
}
certPEM, _, err := tls.GenerateServerCert(caCertPEM, caKeyPEM, opts)
So(err, ShouldBeNil)
certBlock, _ := pem.Decode(certPEM)
cert, err := x509.ParseCertificate(certBlock.Bytes)
So(err, ShouldBeNil)
So(len(cert.IPAddresses), ShouldBeGreaterThan, 0)
So(cert.IPAddresses[0].String(), ShouldEqual, ipaddr)
})
Convey("With invalid CA PEM", func() {
invalidPEM := []byte("invalid pem")
opts := &tls.CertificateOptions{
Hostname: "localhost",
}
_, _, err := tls.GenerateServerCert(invalidPEM, invalidPEM, opts)
So(err, ShouldEqual, tls.ErrDecodeCAPEM)
})
})
}
func TestGenerateCertWithCN(t *testing.T) {
Convey("Generate client certificate with CN", t, func() {
caCertPEM, caKeyPEM, err := tls.GenerateCACert()
So(err, ShouldBeNil)
commonName := "test-client"
opts := &tls.CertificateOptions{
CommonName: commonName,
}
certPEM, keyPEM, err := tls.GenerateClientCert(caCertPEM, caKeyPEM, opts)
So(err, ShouldBeNil)
Convey("Certificate should have correct properties", func() {
certBlock, _ := pem.Decode(certPEM)
So(certBlock, ShouldNotBeNil)
cert, err := x509.ParseCertificate(certBlock.Bytes)
So(err, ShouldBeNil)
So(cert.Subject.CommonName, ShouldEqual, commonName)
So(cert.ExtKeyUsage, ShouldContain, x509.ExtKeyUsageClientAuth)
})
Convey("Private key should be valid", func() {
keyBlock, _ := pem.Decode(keyPEM)
So(keyBlock, ShouldNotBeNil)
})
})
}
func TestGenerateSelfSignedCertWithCN(t *testing.T) {
Convey("Generate self-signed certificate with CN", t, func() {
commonName := "self-signed-client"
opts := &tls.CertificateOptions{
CommonName: commonName,
}
certPEM, keyPEM, err := tls.GenerateClientSelfSignedCert(opts)
So(err, ShouldBeNil)
Convey("Certificate should be self-signed", func() {
certBlock, _ := pem.Decode(certPEM)
So(certBlock, ShouldNotBeNil)
cert, err := x509.ParseCertificate(certBlock.Bytes)
So(err, ShouldBeNil)
So(cert.Subject.CommonName, ShouldEqual, commonName)
So(cert.Subject.String(), ShouldEqual, cert.Issuer.String())
})
Convey("Certificate should have correct validity period", func() {
certBlock, _ := pem.Decode(certPEM)
cert, err := x509.ParseCertificate(certBlock.Bytes)
So(err, ShouldBeNil)
So(cert.NotAfter.After(time.Now().AddDate(0, 11, 0)), ShouldBeTrue)
})
Convey("Private key should be valid", func() {
keyBlock, _ := pem.Decode(keyPEM)
So(keyBlock, ShouldNotBeNil)
_, err := x509.ParsePKCS1PrivateKey(keyBlock.Bytes)
So(err, ShouldBeNil)
})
})
}
func TestApplyOptionsCoverage(t *testing.T) {
Convey("Test applyOptions with various options", t, func() {
caCertPEM, caKeyPEM, err := tls.GenerateCACert()
So(err, ShouldBeNil)
Convey("Test with custom NotBefore and NotAfter", func() {
customNotBefore := time.Now().Add(-24 * time.Hour)
customNotAfter := time.Now().Add(2 * 365 * 24 * time.Hour)
opts := &tls.CertificateOptions{
Hostname: "localhost",
NotBefore: customNotBefore,
NotAfter: customNotAfter,
}
certPEM, _, err := tls.GenerateServerCert(caCertPEM, caKeyPEM, opts)
So(err, ShouldBeNil)
certBlock, _ := pem.Decode(certPEM)
cert, err := x509.ParseCertificate(certBlock.Bytes)
So(err, ShouldBeNil)
So(cert.NotBefore.Unix(), ShouldEqual, customNotBefore.Unix())
So(cert.NotAfter.Unix(), ShouldEqual, customNotAfter.Unix())
// Verify Hostname is encoded in DNSNames (since "localhost" is a DNS name)
So(cert.DNSNames, ShouldContain, "localhost")
})
Convey("Test with explicit IPAddresses", func() {
customIPs := []net.IP{net.ParseIP("192.168.1.1"), net.ParseIP("10.0.0.1")}
opts := &tls.CertificateOptions{
Hostname: "localhost",
IPAddresses: customIPs,
}
certPEM, _, err := tls.GenerateServerCert(caCertPEM, caKeyPEM, opts)
So(err, ShouldBeNil)
certBlock, _ := pem.Decode(certPEM)
cert, err := x509.ParseCertificate(certBlock.Bytes)
So(err, ShouldBeNil)
So(len(cert.IPAddresses), ShouldEqual, 2)
So(cert.IPAddresses[0].String(), ShouldEqual, "192.168.1.1")
So(cert.IPAddresses[1].String(), ShouldEqual, "10.0.0.1")
// Verify explicit IPAddresses are used (not the Hostname IP)
So(cert.IPAddresses, ShouldNotContain, net.ParseIP("127.0.0.1"))
// Verify Hostname DNS name is still added to DNSNames when no explicit DNSNames provided
So(cert.DNSNames, ShouldContain, "localhost")
})
Convey("Test with explicit DNSNames", func() {
customDNS := []string{"example.com", "test.example.com"}
opts := &tls.CertificateOptions{
Hostname: "localhost",
DNSNames: customDNS,
}
certPEM, _, err := tls.GenerateServerCert(caCertPEM, caKeyPEM, opts)
So(err, ShouldBeNil)
certBlock, _ := pem.Decode(certPEM)
cert, err := x509.ParseCertificate(certBlock.Bytes)
So(err, ShouldBeNil)
So(len(cert.DNSNames), ShouldEqual, 2)
So(cert.DNSNames, ShouldContain, "example.com")
So(cert.DNSNames, ShouldContain, "test.example.com")
// Verify explicit DNSNames take precedence - Hostname should NOT be added
So(cert.DNSNames, ShouldNotContain, "localhost")
})
Convey("Test with EmailAddresses", func() {
customEmails := []string{"user@example.com", "admin@example.com"}
opts := &tls.CertificateOptions{
Hostname: "localhost",
EmailAddresses: customEmails,
}
certPEM, _, err := tls.GenerateServerCert(caCertPEM, caKeyPEM, opts)
So(err, ShouldBeNil)
certBlock, _ := pem.Decode(certPEM)
cert, err := x509.ParseCertificate(certBlock.Bytes)
So(err, ShouldBeNil)
So(len(cert.EmailAddresses), ShouldEqual, 2)
So(cert.EmailAddresses, ShouldContain, "user@example.com")
So(cert.EmailAddresses, ShouldContain, "admin@example.com")
})
Convey("Test with all options combined", func() {
customNotBefore := time.Now().Add(-12 * time.Hour)
customNotAfter := time.Now().Add(365 * 24 * time.Hour)
customIPs := []net.IP{net.ParseIP("192.168.1.100")}
customDNS := []string{"combined.example.com"}
customEmails := []string{"combined@example.com"}
opts := &tls.CertificateOptions{
Hostname: "localhost",
NotBefore: customNotBefore,
NotAfter: customNotAfter,
IPAddresses: customIPs,
DNSNames: customDNS,
EmailAddresses: customEmails,
}
certPEM, _, err := tls.GenerateServerCert(caCertPEM, caKeyPEM, opts)
So(err, ShouldBeNil)
certBlock, _ := pem.Decode(certPEM)
cert, err := x509.ParseCertificate(certBlock.Bytes)
So(err, ShouldBeNil)
So(cert.NotBefore.Unix(), ShouldEqual, customNotBefore.Unix())
So(cert.NotAfter.Unix(), ShouldEqual, customNotAfter.Unix())
So(len(cert.IPAddresses), ShouldEqual, 1)
So(cert.IPAddresses[0].String(), ShouldEqual, "192.168.1.100")
So(len(cert.DNSNames), ShouldEqual, 1)
So(cert.DNSNames[0], ShouldEqual, "combined.example.com")
So(len(cert.EmailAddresses), ShouldEqual, 1)
So(cert.EmailAddresses[0], ShouldEqual, "combined@example.com")
// Verify explicit DNSNames take precedence - Hostname should NOT be added
So(cert.DNSNames, ShouldNotContain, "localhost")
})
Convey("Test Hostname as IP address is encoded in IPAddresses", func() {
ipHostname := "192.168.2.50"
opts := &tls.CertificateOptions{
Hostname: ipHostname,
}
certPEM, _, err := tls.GenerateServerCert(caCertPEM, caKeyPEM, opts)
So(err, ShouldBeNil)
certBlock, _ := pem.Decode(certPEM)
cert, err := x509.ParseCertificate(certBlock.Bytes)
So(err, ShouldBeNil)
// Verify Hostname IP is in IPAddresses
So(len(cert.IPAddresses), ShouldBeGreaterThan, 0)
So(cert.IPAddresses[0].String(), ShouldEqual, ipHostname)
// Verify it's NOT in DNSNames
So(cert.DNSNames, ShouldNotContain, ipHostname)
})
Convey("Test Hostname as DNS name is encoded in DNSNames", func() {
dnsHostname := "example.test"
opts := &tls.CertificateOptions{
Hostname: dnsHostname,
}
certPEM, _, err := tls.GenerateServerCert(caCertPEM, caKeyPEM, opts)
So(err, ShouldBeNil)
certBlock, _ := pem.Decode(certPEM)
cert, err := x509.ParseCertificate(certBlock.Bytes)
So(err, ShouldBeNil)
// Verify Hostname DNS is in DNSNames
So(cert.DNSNames, ShouldContain, dnsHostname)
})
Convey("Test with nil options (CA certificate)", func() {
// This tests the nil check in applyOptions
certPEM, _, err := tls.GenerateCACert()
So(err, ShouldBeNil)
certBlock, _ := pem.Decode(certPEM)
cert, err := x509.ParseCertificate(certBlock.Bytes)
So(err, ShouldBeNil)
So(cert.IsCA, ShouldBeTrue)
})
})
}
func TestErrorPaths(t *testing.T) {
Convey("Test error paths", t, func() {
caCertPEM, caKeyPEM, err := tls.GenerateCACert()
So(err, ShouldBeNil)
Convey("Test parseCA with invalid cert PEM", func() {
invalidCertPEM := []byte("not a valid PEM")
_, _, err := tls.GenerateServerCert(invalidCertPEM, caKeyPEM, &tls.CertificateOptions{
Hostname: "localhost",
})
So(err, ShouldEqual, tls.ErrDecodeCAPEM)
})
Convey("Test parseCA with invalid key PEM", func() {
invalidKeyPEM := []byte("not a valid PEM")
_, _, err := tls.GenerateServerCert(caCertPEM, invalidKeyPEM, &tls.CertificateOptions{
Hostname: "localhost",
})
So(err, ShouldEqual, tls.ErrDecodeCAPEM)
})
Convey("Test GenerateServerCertToFile with nil opts", func() {
tempDir := t.TempDir()
certPath := path.Join(tempDir, "server.crt")
keyPath := path.Join(tempDir, "server.key")
err := tls.GenerateServerCertToFile(caCertPEM, caKeyPEM, certPath, keyPath, nil)
So(err, ShouldEqual, tls.ErrHostnameRequired)
})
Convey("Test GenerateCACert with nil option", func() {
// Test when opts[0] == nil - should still work (uses default options)
certPEM, keyPEM, err := tls.GenerateCACert(nil)
So(err, ShouldBeNil)
So(certPEM, ShouldNotBeNil)
So(keyPEM, ShouldNotBeNil)
})
Convey("Test writeCertAndKeyToFile error when cert file write fails", func() {
tempDir := t.TempDir()
// Create a directory path instead of a file path to cause write error
certPath := tempDir // This is a directory, not a file
keyPath := path.Join(tempDir, "server.key")
opts := &tls.CertificateOptions{
Hostname: "localhost",
}
err := tls.GenerateServerCertToFile(caCertPEM, caKeyPEM, certPath, keyPath, opts)
So(err, ShouldNotBeNil)
})
Convey("Test writeCertAndKeyToFile error when key file write fails", func() {
tempDir := t.TempDir()
certPath := path.Join(tempDir, "server.crt")
// Create a directory path instead of a file path to cause write error
keyPath := tempDir // This is a directory, not a file
opts := &tls.CertificateOptions{
Hostname: "localhost",
}
err := tls.GenerateServerCertToFile(caCertPEM, caKeyPEM, certPath, keyPath, opts)
So(err, ShouldNotBeNil)
})
Convey("Test GenerateServerCertToFile error propagation", func() {
// Test that error from GenerateServerCert is propagated
tempDir := t.TempDir()
certPath := path.Join(tempDir, "server.crt")
keyPath := path.Join(tempDir, "server.key")
// Use invalid CA to trigger error in GenerateServerCert
invalidPEM := []byte("invalid")
err := tls.GenerateServerCertToFile(invalidPEM, invalidPEM, certPath, keyPath, &tls.CertificateOptions{
Hostname: "localhost",
})
So(err, ShouldNotBeNil)
So(err, ShouldEqual, tls.ErrDecodeCAPEM)
})
Convey("Test GenerateClientCert with invalid PEM", func() {
// Test that parseCA error is propagated from GenerateClientCert
invalidCertPEM := []byte("not a valid PEM")
_, _, err := tls.GenerateClientCert(invalidCertPEM, caKeyPEM, nil)
So(err, ShouldEqual, tls.ErrDecodeCAPEM)
})
Convey("Test GenerateClientCertToFile error propagation", func() {
// Test that error from GenerateClientCert is propagated
tempDir := t.TempDir()
certPath := path.Join(tempDir, "client.crt")
keyPath := path.Join(tempDir, "client.key")
// Use invalid CA to trigger error in GenerateClientCert
invalidPEM := []byte("invalid")
err := tls.GenerateClientCertToFile(invalidPEM, invalidPEM, certPath, keyPath, nil)
So(err, ShouldNotBeNil)
So(err, ShouldEqual, tls.ErrDecodeCAPEM)
})
Convey("Test GenerateIntermediateCACert with invalid PEM", func() {
// Test that parseCA error is propagated from GenerateIntermediateCACert
invalidCertPEM := []byte("not a valid PEM")
_, _, err := tls.GenerateIntermediateCACert(invalidCertPEM, caKeyPEM)
So(err, ShouldEqual, tls.ErrDecodeCAPEM)
})
Convey("Test GenerateClientSelfSignedCertToFile error propagation", func() {
// Test writeCertAndKeyToFile error path
tempDir := t.TempDir()
// Create a directory path instead of a file path to cause write error
certPath := tempDir // This is a directory, not a file
keyPath := path.Join(tempDir, "client.key")
err := tls.GenerateClientSelfSignedCertToFile(certPath, keyPath, nil)
So(err, ShouldNotBeNil)
})
})
}