maddy/internal/target/remote/dane_test.go
2025-09-05 16:54:41 +02:00

228 lines
8.0 KiB
Go

/*
Maddy Mail Server - Composable all-in-one email server.
Copyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package remote
import (
"crypto/sha256"
"crypto/tls"
"crypto/x509"
"encoding/hex"
"encoding/pem"
"testing"
"time"
"github.com/miekg/dns"
)
// These certificates are related like this:
//
// Root A -> Intermediate A -> Leaf A
// Root B -> LeafB
var (
rootA = `-----BEGIN CERTIFICATE-----
MIIBMDCB46ADAgECAhRDwag3n5CG90BEO87zEMAPejn6YTAFBgMrZXAwFjEUMBIG
A1UEAxMLVGVzdCBSb290IEEwHhcNMjAxMTI4MjExODA4WhcNMzAxMTI2MjExODA4
WjAWMRQwEgYDVQQDEwtUZXN0IFJvb3QgQTAqMAUGAytlcAMhADXMzcRec5ocluNR
ExnnNT7I5fmcpjf2P4ik5k0DJNbco0MwQTAPBgNVHRMBAf8EBTADAQH/MA8GA1Ud
DwEB/wQFAwMHBAAwHQYDVR0OBBYEFM5b/b1di1vA+YpMZcsF4K7N1LbaMAUGAytl
cANBAAZ0XTxBDjN9VGPqWjXrYqGPUqbjm4JD3PeHUB4YGH+MNTgeVIlU8qCLIXtM
9kmAkCk7+j5G8p0gMjJMNygeuwE=
-----END CERTIFICATE-----`
intermediateA = `-----BEGIN CERTIFICATE-----
MIIBWjCCAQygAwIBAgIUEOd619/8HC1pWXxaEpQ1vUZOe7wwBQYDK2VwMBYxFDAS
BgNVBAMTC1Rlc3QgUm9vdCBBMB4XDTIwMTEyODIxMTk0M1oXDTMwMTEyNjIxMTk0
M1owHjEcMBoGA1UEAxMTVGVzdCBJbnRlcm1lZGlhdGUgQTAqMAUGAytlcAMhAFgW
aZz5316olEIHn1Q4RTPd2u/EjN2bo+Cn3EmSlFxto2QwYjAPBgNVHRMBAf8EBTAD
AQH/MA8GA1UdDwEB/wQFAwMHBAAwHQYDVR0OBBYEFB0P00Qphygy+KgkI9tjihFD
ELxhMB8GA1UdIwQYMBaAFM5b/b1di1vA+YpMZcsF4K7N1LbaMAUGAytlcANBAJJH
zsS8ahEjdyRCNUlsPalZiKW8N3G0LnwdVKFhVfcCT+RTRcrMP7vjuWsbJyD5e7hu
z2eCI68xreLQlNySdQ0=
-----END CERTIFICATE-----`
leafA = `-----BEGIN CERTIFICATE-----
MIIBjzCCAUGgAwIBAgIUONvbCs6r9zKFM3IAPRMdrNiJpNgwBQYDK2VwMB4xHDAa
BgNVBAMTE1Rlc3QgSW50ZXJtZWRpYXRlIEEwHhcNMjAxMTI4MjEyMTIyWhcNMzAx
MTI2MjEyMTIyWjAWMRQwEgYDVQQDEwtUZXN0IExlYWYgQTAqMAUGAytlcAMhABIj
W7gwY78RCWHs9eSIdy4x4MXjzdhZwgNSNHHCp5pAo4GYMIGVMAwGA1UdEwEB/wQC
MAAwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMBUGA1UdEQQOMAyCCm1h
ZGR5LnRlc3QwDwYDVR0PAQH/BAUDAweAADAdBgNVHQ4EFgQU9PFQCnG5fNpNPXUT
8rCuylS6tVwwHwYDVR0jBBgwFoAUHQ/TRCmHKDL4qCQj22OKEUMQvGEwBQYDK2Vw
A0EAGdvHA4VLxpUeUu1Vjom2YX3MukPJG0a3/dB3HiAWWpxMgWfU+Ftie7noaNcI
oUW+M8my46dqN6oXSHU47/QjDg==
-----END CERTIFICATE-----`
rootB = `-----BEGIN CERTIFICATE-----
MIIBMDCB46ADAgECAhRXD7xuPkipDyxyCtm8pZaxhuulaDAFBgMrZXAwFjEUMBIG
A1UEAxMLVGVzdCBSb290IEIwHhcNMjAxMTI4MjExODMwWhcNMzAxMTI2MjExODMw
WjAWMRQwEgYDVQQDEwtUZXN0IFJvb3QgQjAqMAUGAytlcAMhAPOIGJJh5jK8N/Vc
lLrFpysV+SiZjT1Cmt7hoFtMrlbTo0MwQTAPBgNVHRMBAf8EBTADAQH/MA8GA1Ud
DwEB/wQFAwMHBAAwHQYDVR0OBBYEFOLGYf4mkhKbZPwZKCv952tfz/KDMAUGAytl
cANBAOX2gb6ud8CAvOsCgw6uaRm0+jMDVZfkAkNuCIO6cJ/WYfdvuXYXu3e88SuI
gri++h118PomIzJ5PHAaCYsFPgQ=
-----END CERTIFICATE-----`
leafB = `-----BEGIN CERTIFICATE-----
MIIBhzCCATmgAwIBAgIUR2bVQ/Cu4j7Td5TdbWd6Q0LEpOgwBQYDK2VwMBYxFDAS
BgNVBAMTC1Rlc3QgUm9vdCBCMB4XDTIwMTEyODIxMjE0M1oXDTMwMTEyNjIxMjE0
M1owFjEUMBIGA1UEAxMLVGVzdCBMZWFmIEIwKjAFBgMrZXADIQBiHCTUxF3UxPIV
M/o5OkTtmUrI7AInOvMa0dchU4iJXqOBmDCBlTAMBgNVHRMBAf8EAjAAMB0GA1Ud
JQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDATAVBgNVHREEDjAMggptYWRkeS50ZXN0
MA8GA1UdDwEB/wQFAwMHgAAwHQYDVR0OBBYEFPYZPubaAXyr6kXs3khqpMNfdHKK
MB8GA1UdIwQYMBaAFOLGYf4mkhKbZPwZKCv952tfz/KDMAUGAytlcANBABlOwVxE
h7vYmaMYoyOSF1GQiB0ZLsGUjrTNHDnv0+Xp8xG5Td5mGnBi/4Ehq39PdLrj2T7j
3Xy0aiqdDomvwQY=
-----END CERTIFICATE-----`
)
func parsePEMCert(blob string) *x509.Certificate {
block, _ := pem.Decode([]byte(blob))
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
panic(err)
}
return cert
}
func singleTlsaRecord(usage, matchType, selector uint8, cert string) dns.TLSA {
return dns.TLSA{
Hdr: dns.RR_Header{
Name: "maddy.test.",
Class: dns.ClassINET,
Rrtype: dns.TypeTLSA,
Ttl: 9999,
},
Usage: usage,
MatchingType: matchType,
Selector: selector,
Certificate: cert,
}
}
func keySHA256(blob string) string {
cert := parsePEMCert(blob)
hash := sha256.Sum256(cert.RawSubjectPublicKeyInfo)
return hex.EncodeToString(hash[:])
}
func TestVerifyDANE(t *testing.T) {
verifyDANETime = time.Unix(1606600100, 0)
test := func(name string, recs []dns.TLSA, connState tls.ConnectionState, expectErr bool) {
t.Helper()
t.Run(name, func(t *testing.T) {
t.Helper()
_, err := verifyDANE(recs, connState)
if (err != nil) != expectErr {
t.Error("err:", err, "expectErr:", expectErr)
}
})
}
// RFC 7672, Section 2.2:
// An "insecure" TLSA RRset or DNSSEC-authenticated denial of existence
// of the TLSA records:
// A connection to the MTA SHOULD be made using (pre-DANE)
// opportunistic TLS;
//
// "Insecure" TLSA RRset results in verifyDANE not being called at all,
// but for the latter (authenticated denial of existence) it is still
// called and should be tested for.
//
// More specific tests for TLSA RRset discovery (including CNAME
// shenanigans) are in dane_delivery_test.go.
test("no TLSA, TLS", []dns.TLSA{}, tls.ConnectionState{
HandshakeComplete: true,
}, false)
test("no TLSA, no TLS", []dns.TLSA{}, tls.ConnectionState{
HandshakeComplete: false,
}, false)
// RFC 7272, Section 2.2:
// A "secure" non-empty TLSA RRset where all the records are unusable:
// Any connection to the MTA MUST be made via TLS, but authentication
// is not required.
test("unusable TLSA, TLS", []dns.TLSA{
singleTlsaRecord(4, 1, 2, "whatever"),
singleTlsaRecord(4, 5, 2, "whatever"),
singleTlsaRecord(4, 1, 1, "whatever"),
}, tls.ConnectionState{
HandshakeComplete: true,
PeerCertificates: []*x509.Certificate{parsePEMCert(leafA)},
}, false)
test("unusable TLSA, no TLS", []dns.TLSA{
singleTlsaRecord(4, 1, 2, "whatever"),
}, tls.ConnectionState{
HandshakeComplete: false,
}, true)
// RFC 7672, Section 2.2:
// A "secure" TLSA RRset with at least one usable record: Any
// connection to the MTA MUST employ TLS encryption and MUST
// authenticate the SMTP server using the techniques discussed in the
// rest of this document.
test("DANE-EE, non-self-signed", []dns.TLSA{
singleTlsaRecord(3, 1, 1, keySHA256(leafA)),
}, tls.ConnectionState{
HandshakeComplete: true,
PeerCertificates: []*x509.Certificate{parsePEMCert(leafA)},
}, false)
test("DANE-EE, multiple records", []dns.TLSA{
singleTlsaRecord(3, 1, 1, keySHA256(leafB)),
singleTlsaRecord(3, 1, 1, keySHA256(leafA)),
}, tls.ConnectionState{
HandshakeComplete: true,
PeerCertificates: []*x509.Certificate{parsePEMCert(leafA)},
}, false)
test("DANE-EE, self-signed", []dns.TLSA{
singleTlsaRecord(3, 1, 1, keySHA256(rootA)),
}, tls.ConnectionState{
HandshakeComplete: true,
PeerCertificates: []*x509.Certificate{parsePEMCert(rootA)},
}, false)
test("DANE-TA, intermediate TA", []dns.TLSA{
singleTlsaRecord(2, 1, 1, keySHA256(intermediateA)),
}, tls.ConnectionState{
HandshakeComplete: true,
PeerCertificates: []*x509.Certificate{
parsePEMCert(leafA),
parsePEMCert(intermediateA),
parsePEMCert(rootA),
},
}, false)
test("DANE-TA, intermediate TA, mismatch", []dns.TLSA{
singleTlsaRecord(2, 1, 1, keySHA256(intermediateA)),
}, tls.ConnectionState{
HandshakeComplete: true,
PeerCertificates: []*x509.Certificate{
parsePEMCert(leafB),
parsePEMCert(rootB),
},
}, true)
test("DANE-TA, intermediate TA, multiple records", []dns.TLSA{
singleTlsaRecord(2, 1, 1, keySHA256(rootB)),
singleTlsaRecord(2, 1, 1, keySHA256(intermediateA)),
// Add multiple times to make sure that multiple records matching the
// same cert do not break anything.
singleTlsaRecord(2, 1, 1, keySHA256(intermediateA)),
}, tls.ConnectionState{
HandshakeComplete: true,
PeerCertificates: []*x509.Certificate{
parsePEMCert(leafA),
parsePEMCert(intermediateA),
parsePEMCert(rootA),
},
}, false)
}