diff --git a/backend/config/ssl_storage.go b/backend/config/ssl_storage.go index ba89004d..aac97beb 100644 --- a/backend/config/ssl_storage.go +++ b/backend/config/ssl_storage.go @@ -227,7 +227,7 @@ func validateSSLMaterial(privateKeyPEM, certificatePEM, issuerPEM []byte, domain if issuerErr != nil { return ErrInvalidSSLMaterial } - certificates = append(certificates, issuerCertificates...) + certificates = appendUniqueCertificates(certificates, issuerCertificates...) } if _, err := tls.X509KeyPair(certificatePEM, privateKeyPEM); err != nil { return ErrSSLCertificateMismatch @@ -255,6 +255,28 @@ func validateSSLMaterial(privateKeyPEM, certificatePEM, issuerPEM []byte, domain return nil } +func appendUniqueCertificates(certificates []*x509.Certificate, additions ...*x509.Certificate) []*x509.Certificate { + seen := make(map[string]struct{}, len(certificates)+len(additions)) + for _, certificate := range certificates { + if certificate == nil { + continue + } + seen[string(certificate.Raw)] = struct{}{} + } + for _, certificate := range additions { + if certificate == nil { + continue + } + key := string(certificate.Raw) + if _, exists := seen[key]; exists { + continue + } + seen[key] = struct{}{} + certificates = append(certificates, certificate) + } + return certificates +} + func parseCertificatePEM(data []byte) ([]*x509.Certificate, error) { var certificates []*x509.Certificate remaining := data diff --git a/backend/config/ssl_storage_test.go b/backend/config/ssl_storage_test.go index 03943bb4..23d5fb8e 100644 --- a/backend/config/ssl_storage_test.go +++ b/backend/config/ssl_storage_test.go @@ -203,6 +203,14 @@ func TestValidateSSLMaterialRejectsDomainExpiryAndWeakKeys(t *testing.T) { } } +func TestValidateSSLMaterialAcceptsBundledIssuerWithoutDuplicateChainEntry(t *testing.T) { + privateKeyPEM, certificatePEM, issuerPEM := testChainedCertificate(t) + certificateBundle := append(append([]byte{}, certificatePEM...), issuerPEM...) + if err := ValidateSSLMaterial(privateKeyPEM, certificateBundle, issuerPEM, []string{"mail.example.com"}); err != nil { + t.Fatalf("bundled issuer was rejected as a duplicate chain entry: %v", err) + } +} + func testManagedCertificate(t *testing.T, domain string, notBefore, notAfter time.Time, bits int) ([]byte, []byte) { t.Helper() key, err := rsa.GenerateKey(rand.Reader, bits) @@ -226,3 +234,46 @@ func testManagedCertificate(t *testing.T, domain string, notBefore, notAfter tim } return pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}), pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}) } + +func testChainedCertificate(t *testing.T) ([]byte, []byte, []byte) { + t.Helper() + issuerKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Fatal(err) + } + now := time.Now() + issuerTemplate := &x509.Certificate{ + SerialNumber: big.NewInt(100), + Subject: pkix.Name{CommonName: "Smail Test Issuer"}, + NotBefore: now.Add(-time.Hour), + NotAfter: now.Add(24 * time.Hour), + BasicConstraintsValid: true, + IsCA: true, + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature, + } + issuerDER, err := x509.CreateCertificate(rand.Reader, issuerTemplate, issuerTemplate, &issuerKey.PublicKey, issuerKey) + if err != nil { + t.Fatal(err) + } + leafKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Fatal(err) + } + leafTemplate := &x509.Certificate{ + SerialNumber: big.NewInt(101), + Subject: pkix.Name{CommonName: "mail.example.com"}, + DNSNames: []string{"mail.example.com"}, + NotBefore: now.Add(-time.Hour), + NotAfter: now.Add(24 * time.Hour), + BasicConstraintsValid: true, + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + } + leafDER, err := x509.CreateCertificate(rand.Reader, leafTemplate, issuerTemplate, &leafKey.PublicKey, issuerKey) + if err != nil { + t.Fatal(err) + } + return pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(leafKey)}), + pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: leafDER}), + pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: issuerDER}) +}