mirror of
https://github.com/project-zot/zot.git
synced 2026-06-17 21:17:58 +08:00
79439bbf63
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>
729 lines
23 KiB
Go
729 lines
23 KiB
Go
package tls_test
|
|
|
|
import (
|
|
"crypto/x509"
|
|
"encoding/pem"
|
|
"net"
|
|
"os"
|
|
"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 URIs including invalid URI", func() {
|
|
// Mix of valid and invalid URIs - invalid ones should be skipped
|
|
customURIs := []string{
|
|
"spiffe://example.org/workload/test",
|
|
"not a valid uri", // Invalid URI - should be skipped
|
|
"https://example.com",
|
|
"://invalid", // Invalid URI - should be skipped
|
|
}
|
|
opts := &tls.CertificateOptions{
|
|
Hostname: "localhost",
|
|
URIs: customURIs,
|
|
}
|
|
certPEM, _, err := tls.GenerateServerCert(caCertPEM, caKeyPEM, opts)
|
|
So(err, ShouldBeNil)
|
|
|
|
certBlock, _ := pem.Decode(certPEM)
|
|
cert, err := x509.ParseCertificate(certBlock.Bytes)
|
|
So(err, ShouldBeNil)
|
|
// Should only contain valid URIs (2 out of 4)
|
|
So(len(cert.URIs), ShouldEqual, 2)
|
|
So(cert.URIs[0].String(), ShouldEqual, "spiffe://example.org/workload/test")
|
|
So(cert.URIs[1].String(), ShouldEqual, "https://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)
|
|
})
|
|
|
|
Convey("Test generateCertificate with invalid key type", func() {
|
|
// This tests the default case in generateCertificate switch
|
|
invalidKeyType := tls.KeyType("INVALID")
|
|
opts := &tls.CertificateOptions{
|
|
KeyType: invalidKeyType,
|
|
}
|
|
_, _, err := tls.GenerateCACert(opts)
|
|
So(err, ShouldNotBeNil)
|
|
})
|
|
|
|
Convey("Test generateCertificate with ECDSA key type", func() {
|
|
// Test that ECDSA key generation works correctly
|
|
caCertPEM, caKeyPEM, err := tls.GenerateCACert()
|
|
So(err, ShouldBeNil)
|
|
|
|
opts := &tls.CertificateOptions{
|
|
Hostname: "localhost",
|
|
KeyType: tls.KeyTypeECDSA,
|
|
}
|
|
certPEM, keyPEM, err := tls.GenerateServerCert(caCertPEM, caKeyPEM, opts)
|
|
So(err, ShouldBeNil)
|
|
So(certPEM, ShouldNotBeNil)
|
|
So(keyPEM, ShouldNotBeNil)
|
|
|
|
// Verify ECDSA key was generated
|
|
keyBlock, _ := pem.Decode(keyPEM)
|
|
So(keyBlock, ShouldNotBeNil)
|
|
So(keyBlock.Type, ShouldEqual, "EC PRIVATE KEY")
|
|
})
|
|
|
|
Convey("Test generateCertificate with ED25519 key type", func() {
|
|
caCertPEM, caKeyPEM, err := tls.GenerateCACert()
|
|
So(err, ShouldBeNil)
|
|
|
|
opts := &tls.CertificateOptions{
|
|
Hostname: "localhost",
|
|
KeyType: tls.KeyTypeED25519,
|
|
}
|
|
certPEM, keyPEM, err := tls.GenerateServerCert(caCertPEM, caKeyPEM, opts)
|
|
So(err, ShouldBeNil)
|
|
So(certPEM, ShouldNotBeNil)
|
|
So(keyPEM, ShouldNotBeNil)
|
|
|
|
// Verify ED25519 key was generated
|
|
keyBlock, _ := pem.Decode(keyPEM)
|
|
So(keyBlock, ShouldNotBeNil)
|
|
So(keyBlock.Type, ShouldEqual, "PRIVATE KEY")
|
|
})
|
|
|
|
Convey("Test parsePrivateKeyFromPEM with PKCS8 format", func() {
|
|
// Generate a certificate with ED25519 (uses PKCS8)
|
|
opts := &tls.CertificateOptions{
|
|
KeyType: tls.KeyTypeED25519,
|
|
}
|
|
_, keyPEM, err := tls.GenerateCACert(opts)
|
|
So(err, ShouldBeNil)
|
|
|
|
// Parse it back - should work with PKCS8
|
|
keyBlock, _ := pem.Decode(keyPEM)
|
|
So(keyBlock, ShouldNotBeNil)
|
|
|
|
// This tests the PKCS8 path in parsePrivateKeyFromPEM
|
|
_, err = x509.ParsePKCS8PrivateKey(keyBlock.Bytes)
|
|
So(err, ShouldBeNil)
|
|
})
|
|
|
|
Convey("Test parsePrivateKeyFromPEM with EC SEC1 format", func() {
|
|
// Generate a certificate with ECDSA (uses SEC1)
|
|
opts := &tls.CertificateOptions{
|
|
KeyType: tls.KeyTypeECDSA,
|
|
}
|
|
_, keyPEM, err := tls.GenerateCACert(opts)
|
|
So(err, ShouldBeNil)
|
|
|
|
// Parse it back - should work
|
|
keyBlock, _ := pem.Decode(keyPEM)
|
|
So(keyBlock, ShouldNotBeNil)
|
|
|
|
// This tests the EC SEC1 path in parsePrivateKeyFromPEM
|
|
_, err = x509.ParseECPrivateKey(keyBlock.Bytes)
|
|
So(err, ShouldBeNil)
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestExtractPublicKeyFromCert(t *testing.T) {
|
|
Convey("Test ExtractPublicKeyFromCert", t, func() {
|
|
caCertPEM, _, err := tls.GenerateCACert()
|
|
So(err, ShouldBeNil)
|
|
|
|
Convey("Extract public key from valid certificate", func() {
|
|
publicKeyPEM, err := tls.ExtractPublicKeyFromCert(caCertPEM)
|
|
So(err, ShouldBeNil)
|
|
So(publicKeyPEM, ShouldNotBeNil)
|
|
|
|
// Verify it's valid PEM
|
|
block, _ := pem.Decode(publicKeyPEM)
|
|
So(block, ShouldNotBeNil)
|
|
So(block.Type, ShouldEqual, "PUBLIC KEY")
|
|
})
|
|
|
|
Convey("Extract public key from invalid PEM", func() {
|
|
invalidPEM := []byte("not a valid PEM")
|
|
_, err := tls.ExtractPublicKeyFromCert(invalidPEM)
|
|
So(err, ShouldEqual, tls.ErrFailedDecodeCertPEM)
|
|
})
|
|
|
|
Convey("Extract public key from server certificate", func() {
|
|
caCertPEM, caKeyPEM, err := tls.GenerateCACert()
|
|
So(err, ShouldBeNil)
|
|
|
|
opts := &tls.CertificateOptions{
|
|
Hostname: "localhost",
|
|
}
|
|
serverCertPEM, _, err := tls.GenerateServerCert(caCertPEM, caKeyPEM, opts)
|
|
So(err, ShouldBeNil)
|
|
|
|
publicKeyPEM, err := tls.ExtractPublicKeyFromCert(serverCertPEM)
|
|
So(err, ShouldBeNil)
|
|
So(publicKeyPEM, ShouldNotBeNil)
|
|
})
|
|
|
|
Convey("Extract public key from ECDSA certificate", func() {
|
|
caOpts := &tls.CertificateOptions{
|
|
KeyType: tls.KeyTypeECDSA,
|
|
}
|
|
caCertPEM, caKeyPEM, err := tls.GenerateCACert(caOpts)
|
|
So(err, ShouldBeNil)
|
|
|
|
opts := &tls.CertificateOptions{
|
|
Hostname: "localhost",
|
|
KeyType: tls.KeyTypeECDSA,
|
|
}
|
|
serverCertPEM, _, err := tls.GenerateServerCert(caCertPEM, caKeyPEM, opts)
|
|
So(err, ShouldBeNil)
|
|
|
|
publicKeyPEM, err := tls.ExtractPublicKeyFromCert(serverCertPEM)
|
|
So(err, ShouldBeNil)
|
|
So(publicKeyPEM, ShouldNotBeNil)
|
|
})
|
|
|
|
Convey("Extract public key from ED25519 certificate", func() {
|
|
caOpts := &tls.CertificateOptions{
|
|
KeyType: tls.KeyTypeED25519,
|
|
}
|
|
caCertPEM, caKeyPEM, err := tls.GenerateCACert(caOpts)
|
|
So(err, ShouldBeNil)
|
|
|
|
opts := &tls.CertificateOptions{
|
|
Hostname: "localhost",
|
|
KeyType: tls.KeyTypeED25519,
|
|
}
|
|
serverCertPEM, _, err := tls.GenerateServerCert(caCertPEM, caKeyPEM, opts)
|
|
So(err, ShouldBeNil)
|
|
|
|
publicKeyPEM, err := tls.ExtractPublicKeyFromCert(serverCertPEM)
|
|
So(err, ShouldBeNil)
|
|
So(publicKeyPEM, ShouldNotBeNil)
|
|
})
|
|
|
|
Convey("Extract public key from certificate with invalid certificate data", func() {
|
|
// Create a PEM block with invalid certificate data
|
|
invalidCertPEM := pem.EncodeToMemory(&pem.Block{
|
|
Type: "CERTIFICATE",
|
|
Bytes: []byte("invalid certificate data"),
|
|
})
|
|
|
|
_, err := tls.ExtractPublicKeyFromCert(invalidCertPEM)
|
|
So(err, ShouldNotBeNil)
|
|
So(err.Error(), ShouldContainSubstring, "failed to parse certificate")
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestExtractRSAPublicKeyPKCS1(t *testing.T) {
|
|
Convey("Test ExtractRSAPublicKeyPKCS1", t, func() {
|
|
_, keyPEM, err := tls.GenerateCACert()
|
|
So(err, ShouldBeNil)
|
|
|
|
Convey("Extract RSA public key in PKCS1 format", func() {
|
|
publicKeyPEM, err := tls.ExtractRSAPublicKeyPKCS1(keyPEM)
|
|
So(err, ShouldBeNil)
|
|
So(publicKeyPEM, ShouldNotBeNil)
|
|
|
|
// Verify it's valid PEM
|
|
block, _ := pem.Decode(publicKeyPEM)
|
|
So(block, ShouldNotBeNil)
|
|
So(block.Type, ShouldEqual, "RSA PUBLIC KEY")
|
|
})
|
|
|
|
Convey("Extract RSA public key from invalid PEM", func() {
|
|
invalidPEM := []byte("not a valid PEM")
|
|
_, err := tls.ExtractRSAPublicKeyPKCS1(invalidPEM)
|
|
So(err, ShouldEqual, tls.ErrFailedDecodeKeyPEM)
|
|
})
|
|
|
|
Convey("Extract RSA public key from non-RSA key", func() {
|
|
opts := &tls.CertificateOptions{
|
|
KeyType: tls.KeyTypeECDSA,
|
|
}
|
|
_, ecdsaKeyPEM, err := tls.GenerateCACert(opts)
|
|
So(err, ShouldBeNil)
|
|
|
|
_, err = tls.ExtractRSAPublicKeyPKCS1(ecdsaKeyPEM)
|
|
So(err, ShouldNotBeNil)
|
|
So(err.Error(), ShouldContainSubstring, "private key is not RSA")
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestWriteCertificateChainToFile(t *testing.T) {
|
|
Convey("Test WriteCertificateChainToFile", t, func() {
|
|
Convey("Write certificate chain with multiple certificates", func() {
|
|
tempDir := t.TempDir()
|
|
chainPath := path.Join(tempDir, "chain.crt")
|
|
|
|
// Generate root CA
|
|
rootCACert, rootCAKey, err := tls.GenerateCACert()
|
|
So(err, ShouldBeNil)
|
|
|
|
// Generate intermediate CA
|
|
intermediateCACert, _, err := tls.GenerateIntermediateCACert(rootCACert, rootCAKey)
|
|
So(err, ShouldBeNil)
|
|
|
|
// Generate leaf certificate
|
|
leafCert, _, err := tls.GenerateClientCert(rootCACert, rootCAKey, nil)
|
|
So(err, ShouldBeNil)
|
|
|
|
// Write chain (leaf first, then intermediate)
|
|
err = tls.WriteCertificateChainToFile(chainPath, leafCert, intermediateCACert, rootCACert)
|
|
So(err, ShouldBeNil)
|
|
|
|
// Verify file was created
|
|
chainData, err := os.ReadFile(chainPath)
|
|
So(err, ShouldBeNil)
|
|
So(len(chainData), ShouldBeGreaterThan, 0)
|
|
|
|
// Verify it contains all certificates
|
|
So(string(chainData), ShouldContainSubstring, "BEGIN CERTIFICATE")
|
|
})
|
|
|
|
Convey("Write certificate chain with no certificates", func() {
|
|
tempDir := t.TempDir()
|
|
chainPath := path.Join(tempDir, "chain.crt")
|
|
|
|
err := tls.WriteCertificateChainToFile(chainPath)
|
|
So(err, ShouldEqual, tls.ErrNoCertificatesProvided)
|
|
})
|
|
|
|
Convey("Write certificate chain with single certificate", func() {
|
|
tempDir := t.TempDir()
|
|
chainPath := path.Join(tempDir, "chain.crt")
|
|
|
|
cert, _, err := tls.GenerateCACert()
|
|
So(err, ShouldBeNil)
|
|
|
|
err = tls.WriteCertificateChainToFile(chainPath, cert)
|
|
So(err, ShouldBeNil)
|
|
|
|
// Verify file was created
|
|
chainData, err := os.ReadFile(chainPath)
|
|
So(err, ShouldBeNil)
|
|
So(len(chainData), ShouldBeGreaterThan, 0)
|
|
})
|
|
})
|
|
}
|