1/*2Maddy Mail Server - Composable all-in-one email server.3Copyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors45This program is free software: you can redistribute it and/or modify6it under the terms of the GNU General Public License as published by7the Free Software Foundation, either version 3 of the License, or8(at your option) any later version.910This program is distributed in the hope that it will be useful,11but WITHOUT ANY WARRANTY; without even the implied warranty of12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the13GNU General Public License for more details.1415You should have received a copy of the GNU General Public License16along with this program. If not, see <https://www.gnu.org/licenses/>.17*/1819package remote2021import (22 "crypto/sha256"23 "crypto/tls"24 "crypto/x509"25 "encoding/hex"26 "encoding/pem"27 "testing"28 "time"2930 "github.com/miekg/dns"31)3233// These certificates are related like this:34//35// Root A -> Intermediate A -> Leaf A36// Root B -> LeafB37var (38 rootA = `-----BEGIN CERTIFICATE-----39MIIBMDCB46ADAgECAhRDwag3n5CG90BEO87zEMAPejn6YTAFBgMrZXAwFjEUMBIG40A1UEAxMLVGVzdCBSb290IEEwHhcNMjAxMTI4MjExODA4WhcNMzAxMTI2MjExODA441WjAWMRQwEgYDVQQDEwtUZXN0IFJvb3QgQTAqMAUGAytlcAMhADXMzcRec5ocluNR42ExnnNT7I5fmcpjf2P4ik5k0DJNbco0MwQTAPBgNVHRMBAf8EBTADAQH/MA8GA1Ud43DwEB/wQFAwMHBAAwHQYDVR0OBBYEFM5b/b1di1vA+YpMZcsF4K7N1LbaMAUGAytl44cANBAAZ0XTxBDjN9VGPqWjXrYqGPUqbjm4JD3PeHUB4YGH+MNTgeVIlU8qCLIXtM459kmAkCk7+j5G8p0gMjJMNygeuwE=46-----END CERTIFICATE-----`47 intermediateA = `-----BEGIN CERTIFICATE-----48MIIBWjCCAQygAwIBAgIUEOd619/8HC1pWXxaEpQ1vUZOe7wwBQYDK2VwMBYxFDAS49BgNVBAMTC1Rlc3QgUm9vdCBBMB4XDTIwMTEyODIxMTk0M1oXDTMwMTEyNjIxMTk050M1owHjEcMBoGA1UEAxMTVGVzdCBJbnRlcm1lZGlhdGUgQTAqMAUGAytlcAMhAFgW51aZz5316olEIHn1Q4RTPd2u/EjN2bo+Cn3EmSlFxto2QwYjAPBgNVHRMBAf8EBTAD52AQH/MA8GA1UdDwEB/wQFAwMHBAAwHQYDVR0OBBYEFB0P00Qphygy+KgkI9tjihFD53ELxhMB8GA1UdIwQYMBaAFM5b/b1di1vA+YpMZcsF4K7N1LbaMAUGAytlcANBAJJH54zsS8ahEjdyRCNUlsPalZiKW8N3G0LnwdVKFhVfcCT+RTRcrMP7vjuWsbJyD5e7hu55z2eCI68xreLQlNySdQ0=56-----END CERTIFICATE-----`57 leafA = `-----BEGIN CERTIFICATE-----58MIIBjzCCAUGgAwIBAgIUONvbCs6r9zKFM3IAPRMdrNiJpNgwBQYDK2VwMB4xHDAa59BgNVBAMTE1Rlc3QgSW50ZXJtZWRpYXRlIEEwHhcNMjAxMTI4MjEyMTIyWhcNMzAx60MTI2MjEyMTIyWjAWMRQwEgYDVQQDEwtUZXN0IExlYWYgQTAqMAUGAytlcAMhABIj61W7gwY78RCWHs9eSIdy4x4MXjzdhZwgNSNHHCp5pAo4GYMIGVMAwGA1UdEwEB/wQC62MAAwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMBUGA1UdEQQOMAyCCm1h63ZGR5LnRlc3QwDwYDVR0PAQH/BAUDAweAADAdBgNVHQ4EFgQU9PFQCnG5fNpNPXUT648rCuylS6tVwwHwYDVR0jBBgwFoAUHQ/TRCmHKDL4qCQj22OKEUMQvGEwBQYDK2Vw65A0EAGdvHA4VLxpUeUu1Vjom2YX3MukPJG0a3/dB3HiAWWpxMgWfU+Ftie7noaNcI66oUW+M8my46dqN6oXSHU47/QjDg==67-----END CERTIFICATE-----`68 rootB = `-----BEGIN CERTIFICATE-----69MIIBMDCB46ADAgECAhRXD7xuPkipDyxyCtm8pZaxhuulaDAFBgMrZXAwFjEUMBIG70A1UEAxMLVGVzdCBSb290IEIwHhcNMjAxMTI4MjExODMwWhcNMzAxMTI2MjExODMw71WjAWMRQwEgYDVQQDEwtUZXN0IFJvb3QgQjAqMAUGAytlcAMhAPOIGJJh5jK8N/Vc72lLrFpysV+SiZjT1Cmt7hoFtMrlbTo0MwQTAPBgNVHRMBAf8EBTADAQH/MA8GA1Ud73DwEB/wQFAwMHBAAwHQYDVR0OBBYEFOLGYf4mkhKbZPwZKCv952tfz/KDMAUGAytl74cANBAOX2gb6ud8CAvOsCgw6uaRm0+jMDVZfkAkNuCIO6cJ/WYfdvuXYXu3e88SuI75gri++h118PomIzJ5PHAaCYsFPgQ=76-----END CERTIFICATE-----`77 leafB = `-----BEGIN CERTIFICATE-----78MIIBhzCCATmgAwIBAgIUR2bVQ/Cu4j7Td5TdbWd6Q0LEpOgwBQYDK2VwMBYxFDAS79BgNVBAMTC1Rlc3QgUm9vdCBCMB4XDTIwMTEyODIxMjE0M1oXDTMwMTEyNjIxMjE080M1owFjEUMBIGA1UEAxMLVGVzdCBMZWFmIEIwKjAFBgMrZXADIQBiHCTUxF3UxPIV81M/o5OkTtmUrI7AInOvMa0dchU4iJXqOBmDCBlTAMBgNVHRMBAf8EAjAAMB0GA1Ud82JQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDATAVBgNVHREEDjAMggptYWRkeS50ZXN083MA8GA1UdDwEB/wQFAwMHgAAwHQYDVR0OBBYEFPYZPubaAXyr6kXs3khqpMNfdHKK84MB8GA1UdIwQYMBaAFOLGYf4mkhKbZPwZKCv952tfz/KDMAUGAytlcANBABlOwVxE85h7vYmaMYoyOSF1GQiB0ZLsGUjrTNHDnv0+Xp8xG5Td5mGnBi/4Ehq39PdLrj2T7j863Xy0aiqdDomvwQY=87-----END CERTIFICATE-----`88)8990func parsePEMCert(blob string) *x509.Certificate {91 block, _ := pem.Decode([]byte(blob))92 cert, err := x509.ParseCertificate(block.Bytes)93 if err != nil {94 panic(err)95 }96 return cert97}9899func singleTlsaRecord(usage, matchType, selector uint8, cert string) dns.TLSA {100 return dns.TLSA{101 Hdr: dns.RR_Header{102 Name: "maddy.test.",103 Class: dns.ClassINET,104 Rrtype: dns.TypeTLSA,105 Ttl: 9999,106 },107 Usage: usage,108 MatchingType: matchType,109 Selector: selector,110 Certificate: cert,111 }112}113114func keySHA256(blob string) string {115 cert := parsePEMCert(blob)116 hash := sha256.Sum256(cert.RawSubjectPublicKeyInfo)117 return hex.EncodeToString(hash[:])118}119120func TestVerifyDANE(t *testing.T) {121 verifyDANETime = time.Unix(1606600100, 0)122 test := func(name string, recs []dns.TLSA, connState tls.ConnectionState, expectErr bool) {123 t.Helper()124 t.Run(name, func(t *testing.T) {125 t.Helper()126 _, err := verifyDANE(recs, connState)127 if (err != nil) != expectErr {128 t.Error("err:", err, "expectErr:", expectErr)129 }130 })131 }132133 // RFC 7672, Section 2.2:134 // An "insecure" TLSA RRset or DNSSEC-authenticated denial of existence135 // of the TLSA records:136 // A connection to the MTA SHOULD be made using (pre-DANE)137 // opportunistic TLS;138 //139 // "Insecure" TLSA RRset results in verifyDANE not being called at all,140 // but for the latter (authenticated denial of existence) it is still141 // called and should be tested for.142 //143 // More specific tests for TLSA RRset discovery (including CNAME144 // shenanigans) are in dane_delivery_test.go.145 test("no TLSA, TLS", []dns.TLSA{}, tls.ConnectionState{146 HandshakeComplete: true,147 }, false)148 test("no TLSA, no TLS", []dns.TLSA{}, tls.ConnectionState{149 HandshakeComplete: false,150 }, false)151152 // RFC 7272, Section 2.2:153 // A "secure" non-empty TLSA RRset where all the records are unusable:154 // Any connection to the MTA MUST be made via TLS, but authentication155 // is not required.156 test("unusable TLSA, TLS", []dns.TLSA{157 singleTlsaRecord(4, 1, 2, "whatever"),158 singleTlsaRecord(4, 5, 2, "whatever"),159 singleTlsaRecord(4, 1, 1, "whatever"),160 }, tls.ConnectionState{161 HandshakeComplete: true,162 PeerCertificates: []*x509.Certificate{parsePEMCert(leafA)},163 }, false)164 test("unusable TLSA, no TLS", []dns.TLSA{165 singleTlsaRecord(4, 1, 2, "whatever"),166 }, tls.ConnectionState{167 HandshakeComplete: false,168 }, true)169170 // RFC 7672, Section 2.2:171 // A "secure" TLSA RRset with at least one usable record: Any172 // connection to the MTA MUST employ TLS encryption and MUST173 // authenticate the SMTP server using the techniques discussed in the174 // rest of this document.175 test("DANE-EE, non-self-signed", []dns.TLSA{176 singleTlsaRecord(3, 1, 1, keySHA256(leafA)),177 }, tls.ConnectionState{178 HandshakeComplete: true,179 PeerCertificates: []*x509.Certificate{parsePEMCert(leafA)},180 }, false)181 test("DANE-EE, multiple records", []dns.TLSA{182 singleTlsaRecord(3, 1, 1, keySHA256(leafB)),183 singleTlsaRecord(3, 1, 1, keySHA256(leafA)),184 }, tls.ConnectionState{185 HandshakeComplete: true,186 PeerCertificates: []*x509.Certificate{parsePEMCert(leafA)},187 }, false)188 test("DANE-EE, self-signed", []dns.TLSA{189 singleTlsaRecord(3, 1, 1, keySHA256(rootA)),190 }, tls.ConnectionState{191 HandshakeComplete: true,192 PeerCertificates: []*x509.Certificate{parsePEMCert(rootA)},193 }, false)194 test("DANE-TA, intermediate TA", []dns.TLSA{195 singleTlsaRecord(2, 1, 1, keySHA256(intermediateA)),196 }, tls.ConnectionState{197 HandshakeComplete: true,198 PeerCertificates: []*x509.Certificate{199 parsePEMCert(leafA),200 parsePEMCert(intermediateA),201 parsePEMCert(rootA),202 },203 }, false)204 test("DANE-TA, intermediate TA, mismatch", []dns.TLSA{205 singleTlsaRecord(2, 1, 1, keySHA256(intermediateA)),206 }, tls.ConnectionState{207 HandshakeComplete: true,208 PeerCertificates: []*x509.Certificate{209 parsePEMCert(leafB),210 parsePEMCert(rootB),211 },212 }, true)213 test("DANE-TA, intermediate TA, multiple records", []dns.TLSA{214 singleTlsaRecord(2, 1, 1, keySHA256(rootB)),215 singleTlsaRecord(2, 1, 1, keySHA256(intermediateA)),216 // Add multiple times to make sure that multiple records matching the217 // same cert do not break anything.218 singleTlsaRecord(2, 1, 1, keySHA256(intermediateA)),219 }, tls.ConnectionState{220 HandshakeComplete: true,221 PeerCertificates: []*x509.Certificate{222 parsePEMCert(leafA),223 parsePEMCert(intermediateA),224 parsePEMCert(rootA),225 },226 }, false)227}