mirror of
https://github.com/project-zot/zot.git
synced 2026-06-17 21:17:58 +08:00
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:
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user