diff --git a/v3/lints/cabf_br/lint_ext_tor_service_descriptor_hash_invalid.go b/v3/lints/cabf_br/lint_ext_tor_service_descriptor_hash_invalid.go index c67aee186..196d6f60f 100644 --- a/v3/lints/cabf_br/lint_ext_tor_service_descriptor_hash_invalid.go +++ b/v3/lints/cabf_br/lint_ext_tor_service_descriptor_hash_invalid.go @@ -53,7 +53,8 @@ func (l *torServiceDescHashInvalid) CheckApplies(c *x509.Certificate) bool { ext := util.GetExtFromCert(c, util.BRTorServiceDescriptor) return ext != nil || (util.IsSubscriberCert(c) && util.CertificateSubjInTLD(c, util.OnionTLD) && - util.IsEV(c.PolicyIdentifiers)) + util.IsEV(c.PolicyIdentifiers)) && + !util.IsOnionV3Cert(c) } // failResult is a small utility function for creating a failed lint result. diff --git a/v3/lints/cabf_br/lint_ext_tor_service_descriptor_hash_invalid_test.go b/v3/lints/cabf_br/lint_ext_tor_service_descriptor_hash_invalid_test.go index 6545c8711..d19598f5e 100644 --- a/v3/lints/cabf_br/lint_ext_tor_service_descriptor_hash_invalid_test.go +++ b/v3/lints/cabf_br/lint_ext_tor_service_descriptor_hash_invalid_test.go @@ -60,6 +60,11 @@ func TestTorDescHashInvalid(t *testing.T) { InputFilename: "onionSANGoodServDesc.pem", ExpectedResult: lint.Pass, }, + { + Name: "V3 address does not require TorServiceDescriptorHash", + InputFilename: "facebookOnionV3Address.pem", + ExpectedResult: lint.NA, + }, } for _, tc := range testCases { diff --git a/v3/testdata/facebookOnionV3Address.pem b/v3/testdata/facebookOnionV3Address.pem new file mode 100644 index 000000000..075db4af6 --- /dev/null +++ b/v3/testdata/facebookOnionV3Address.pem @@ -0,0 +1,146 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: + 05:c8:f6:08:3e:f0:0e:ee:97:f9:dc:0d:14:ca:fe:25 + Signature Algorithm: ecdsa-with-SHA384 + Issuer: C = US, O = DigiCert Inc, OU = www.digicert.com, CN = DigiCert ECC Extended Validation Server CA + Validity + Not Before: Mar 10 00:00:00 2022 GMT + Not After : May 21 23:59:59 2022 GMT + Subject: jurisdictionC = US, jurisdictionST = Delaware, businessCategory = Private Organization, serialNumber = 3835815, C = US, ST = California, L = Menlo Park, O = "Facebook, Inc.", CN = *.facebookwkhpilnemxj7asaniu7vnjjbiltxjqhye3mhbshg7kx5tfyd.onion + Subject Public Key Info: + Public Key Algorithm: id-ecPublicKey + Public-Key: (256 bit) + pub: + 04:0d:a8:c5:66:cd:32:9d:e2:96:e3:ed:07:e4:bf: + 54:cf:51:cc:09:07:88:a0:95:82:4d:52:28:7f:05: + ee:3d:93:06:65:29:99:8d:e1:e1:ae:d5:4b:c2:3e: + 3a:40:6b:5a:57:e9:a3:0f:df:44:e9:1d:18:d1:b9: + 4a:47:0c:c4:94 + ASN1 OID: prime256v1 + NIST CURVE: P-256 + X509v3 extensions: + X509v3 Authority Key Identifier: + keyid:F8:25:D9:A6:39:C7:C3:81:87:25:3E:30:54:91:18:21:40:9B:17:9D + + X509v3 Subject Key Identifier: + 34:B9:92:66:05:94:E0:82:1B:58:47:6F:29:2C:05:EA:7E:78:CE:5C + X509v3 Subject Alternative Name: + DNS:*.facebookwkhpilnemxj7asaniu7vnjjbiltxjqhye3mhbshg7kx5tfyd.onion, DNS:*.facebookcooa4ldbat4g7iacswl3p2zrf5nuylvnhxn6kqolvojixwid.onion, DNS:*.facebooksg4bc7ddneq44pf4miux7o7oqdn2agstg5v3d45odhyu4sqd.onion, DNS:*.m.facebookwkhpilnemxj7asaniu7vnjjbiltxjqhye3mhbshg7kx5tfyd.onion, DNS:*.xx.facebookcooa4ldbat4g7iacswl3p2zrf5nuylvnhxn6kqolvojixwid.onion, DNS:*.xy.facebookcooa4ldbat4g7iacswl3p2zrf5nuylvnhxn6kqolvojixwid.onion, DNS:*.xz.facebookcooa4ldbat4g7iacswl3p2zrf5nuylvnhxn6kqolvojixwid.onion, DNS:facebookcooa4ldbat4g7iacswl3p2zrf5nuylvnhxn6kqolvojixwid.onion, DNS:facebooksg4bc7ddneq44pf4miux7o7oqdn2agstg5v3d45odhyu4sqd.onion, DNS:facebookwkhpilnemxj7asaniu7vnjjbiltxjqhye3mhbshg7kx5tfyd.onion + X509v3 Key Usage: critical + Digital Signature + X509v3 Extended Key Usage: + TLS Web Server Authentication, TLS Web Client Authentication + X509v3 CRL Distribution Points: + + Full Name: + URI:http://crl3.digicert.com/DigiCertECCExtendedValidationServerCA.crl + + Full Name: + URI:http://crl4.digicert.com/DigiCertECCExtendedValidationServerCA.crl + + X509v3 Certificate Policies: + Policy: 2.16.840.1.114412.2.1 + Policy: 2.23.140.1.1 + CPS: http://www.digicert.com/CPS + + Authority Information Access: + OCSP - URI:http://ocsp.digicert.com + CA Issuers - URI:http://cacerts.digicert.com/DigiCertECCExtendedValidationServerCA.crt + + X509v3 Basic Constraints: + CA:FALSE + CT Precertificate SCTs: + Signed Certificate Timestamp: + Version : v1 (0x0) + Log ID : 29:79:BE:F0:9E:39:39:21:F0:56:73:9F:63:A5:77:E5: + BE:57:7D:9C:60:0A:F8:F9:4D:5D:26:5C:25:5D:C7:84 + Timestamp : Mar 10 22:34:29.487 2022 GMT + Extensions: none + Signature : ecdsa-with-SHA256 + 30:45:02:21:00:F1:00:D1:80:2B:E1:BE:F5:CB:9B:A9: + 45:23:A1:CC:66:D7:F3:F9:AE:D0:83:F3:2C:61:0D:0C: + F5:32:DC:40:6D:02:20:53:7F:78:2B:3A:B6:9B:6C:A2: + 87:A1:E8:BE:25:38:B0:3A:95:24:11:F0:A3:B3:86:9F: + 23:23:B8:E3:C4:BF:60 + Signed Certificate Timestamp: + Version : v1 (0x0) + Log ID : 51:A3:B0:F5:FD:01:79:9C:56:6D:B8:37:78:8F:0C:A4: + 7A:CC:1B:27:CB:F7:9E:88:42:9A:0D:FE:D4:8B:05:E5 + Timestamp : Mar 10 22:34:29.564 2022 GMT + Extensions: none + Signature : ecdsa-with-SHA256 + 30:45:02:21:00:95:60:F6:64:E8:98:FA:C3:C2:EE:49: + A1:F7:37:CB:D7:CA:19:7F:C5:F6:44:79:36:A4:E3:C4: + 18:C1:B1:0A:C0:02:20:0A:A8:02:53:12:FB:03:B2:5E: + AA:4B:B8:49:91:43:A7:11:9E:8C:33:EB:C2:DA:F6:39: + EA:E4:90:4B:E6:E8:D7 + Signed Certificate Timestamp: + Version : v1 (0x0) + Log ID : 41:C8:CA:B1:DF:22:46:4A:10:C6:A1:3A:09:42:87:5E: + 4E:31:8B:1B:03:EB:EB:4B:C7:68:F0:90:62:96:06:F6 + Timestamp : Mar 10 22:34:29.550 2022 GMT + Extensions: none + Signature : ecdsa-with-SHA256 + 30:46:02:21:00:F5:AC:90:E3:83:9C:A8:E2:9B:5C:D5: + 25:26:0D:FD:5A:40:D1:9E:9A:DB:93:D8:9F:35:DB:BB: + 41:E9:86:E4:CC:02:21:00:F6:EB:D8:A0:87:C4:80:74: + 8D:3D:92:6D:EF:B1:1B:FC:CC:CB:78:61:B2:3B:26:E6: + CB:45:49:53:EF:DB:8C:6C + Signature Algorithm: ecdsa-with-SHA384 + 30:65:02:30:02:3c:bd:85:48:9d:8c:fa:56:4d:90:d6:a9:b9: + 1c:8e:84:2c:8b:e0:44:63:be:ff:fc:89:d4:34:88:8c:64:d8: + 40:ec:3c:26:05:c5:14:ad:f2:28:41:a2:53:1d:0d:1a:02:31: + 00:c3:75:d4:d4:47:c9:cd:88:95:44:ed:28:bc:40:fa:d3:6b: + 38:80:c4:e5:c8:ed:7e:64:6e:c3:1a:5b:7f:0d:c2:54:25:bd: + 1b:7a:47:1b:a2:33:57:12:dc:af:36:6c:89 +-----BEGIN CERTIFICATE----- +MIIItDCCCDqgAwIBAgIQBcj2CD7wDu6X+dwNFMr+JTAKBggqhkjOPQQDAzB0MQsw +CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu +ZGlnaWNlcnQuY29tMTMwMQYDVQQDEypEaWdpQ2VydCBFQ0MgRXh0ZW5kZWQgVmFs +aWRhdGlvbiBTZXJ2ZXIgQ0EwHhcNMjIwMzEwMDAwMDAwWhcNMjIwNTIxMjM1OTU5 +WjCB/DETMBEGCysGAQQBgjc8AgEDEwJVUzEZMBcGCysGAQQBgjc8AgECEwhEZWxh +d2FyZTEdMBsGA1UEDwwUUHJpdmF0ZSBPcmdhbml6YXRpb24xEDAOBgNVBAUTBzM4 +MzU4MTUxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRMwEQYDVQQH +EwpNZW5sbyBQYXJrMRcwFQYDVQQKEw5GYWNlYm9vaywgSW5jLjFJMEcGA1UEAwxA +Ki5mYWNlYm9va3draHBpbG5lbXhqN2FzYW5pdTd2bmpqYmlsdHhqcWh5ZTNtaGJz +aGc3a3g1dGZ5ZC5vbmlvbjBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABA2oxWbN +Mp3iluPtB+S/VM9RzAkHiKCVgk1SKH8F7j2TBmUpmY3h4a7VS8I+OkBrWlfpow/f +ROkdGNG5SkcMxJSjggYjMIIGHzAfBgNVHSMEGDAWgBT4JdmmOcfDgYclPjBUkRgh +QJsXnTAdBgNVHQ4EFgQUNLmSZgWU4IIbWEdvKSwF6n54zlwwggKmBgNVHREEggKd +MIICmYJAKi5mYWNlYm9va3draHBpbG5lbXhqN2FzYW5pdTd2bmpqYmlsdHhqcWh5 +ZTNtaGJzaGc3a3g1dGZ5ZC5vbmlvboJAKi5mYWNlYm9va2Nvb2E0bGRiYXQ0Zzdp +YWNzd2wzcDJ6cmY1bnV5bHZuaHhuNmtxb2x2b2ppeHdpZC5vbmlvboJAKi5mYWNl +Ym9va3NnNGJjN2RkbmVxNDRwZjRtaXV4N283b3FkbjJhZ3N0ZzV2M2Q0NW9kaHl1 +NHNxZC5vbmlvboJCKi5tLmZhY2Vib29rd2tocGlsbmVteGo3YXNhbml1N3Zuampi +aWx0eGpxaHllM21oYnNoZzdreDV0ZnlkLm9uaW9ugkMqLnh4LmZhY2Vib29rY29v +YTRsZGJhdDRnN2lhY3N3bDNwMnpyZjVudXlsdm5oeG42a3FvbHZvaml4d2lkLm9u +aW9ugkMqLnh5LmZhY2Vib29rY29vYTRsZGJhdDRnN2lhY3N3bDNwMnpyZjVudXls +dm5oeG42a3FvbHZvaml4d2lkLm9uaW9ugkMqLnh6LmZhY2Vib29rY29vYTRsZGJh +dDRnN2lhY3N3bDNwMnpyZjVudXlsdm5oeG42a3FvbHZvaml4d2lkLm9uaW9ugj5m +YWNlYm9va2Nvb2E0bGRiYXQ0ZzdpYWNzd2wzcDJ6cmY1bnV5bHZuaHhuNmtxb2x2 +b2ppeHdpZC5vbmlvboI+ZmFjZWJvb2tzZzRiYzdkZG5lcTQ0cGY0bWl1eDdvN29x +ZG4yYWdzdGc1djNkNDVvZGh5dTRzcWQub25pb26CPmZhY2Vib29rd2tocGlsbmVt +eGo3YXNhbml1N3ZuampiaWx0eGpxaHllM21oYnNoZzdreDV0ZnlkLm9uaW9uMA4G +A1UdDwEB/wQEAwIHgDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwgZ8G +A1UdHwSBlzCBlDBIoEagRIZCaHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lD +ZXJ0RUNDRXh0ZW5kZWRWYWxpZGF0aW9uU2VydmVyQ0EuY3JsMEigRqBEhkJodHRw +Oi8vY3JsNC5kaWdpY2VydC5jb20vRGlnaUNlcnRFQ0NFeHRlbmRlZFZhbGlkYXRp +b25TZXJ2ZXJDQS5jcmwwSgYDVR0gBEMwQTALBglghkgBhv1sAgEwMgYFZ4EMAQEw +KTAnBggrBgEFBQcCARYbaHR0cDovL3d3dy5kaWdpY2VydC5jb20vQ1BTMIGHBggr +BgEFBQcBAQR7MHkwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNv +bTBRBggrBgEFBQcwAoZFaHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lD +ZXJ0RUNDRXh0ZW5kZWRWYWxpZGF0aW9uU2VydmVyQ0EuY3J0MAkGA1UdEwQCMAAw +ggF/BgorBgEEAdZ5AgQCBIIBbwSCAWsBaQB2ACl5vvCeOTkh8FZzn2Old+W+V32c +YAr4+U1dJlwlXceEAAABf3X4Hu8AAAQDAEcwRQIhAPEA0YAr4b71y5upRSOhzGbX +8/mu0IPzLGENDPUy3EBtAiBTf3grOrabbKKHoei+JTiwOpUkEfCjs4afIyO448S/ +YAB2AFGjsPX9AXmcVm24N3iPDKR6zBsny/eeiEKaDf7UiwXlAAABf3X4HzwAAAQD +AEcwRQIhAJVg9mTomPrDwu5Jofc3y9fKGX/F9kR5NqTjxBjBsQrAAiAKqAJTEvsD +sl6qS7hJkUOnEZ6MM+vC2vY56uSQS+bo1wB3AEHIyrHfIkZKEMahOglCh15OMYsb +A+vrS8do8JBilgb2AAABf3X4Hy4AAAQDAEgwRgIhAPWskOODnKjim1zVJSYN/VpA +0Z6a25PYnzXbu0HphuTMAiEA9uvYoIfEgHSNPZJt77Eb/MzLeGGyOybmy0VJU+/b +jGwwCgYIKoZIzj0EAwMDaAAwZQIwAjy9hUidjPpWTZDWqbkcjoQsi+BEY77//InU +NIiMZNhA7DwmBcUUrfIoQaJTHQ0aAjEAw3XU1EfJzYiVRO0ovED602s4gMTlyO1+ +ZG7DGlt/DcJUJb0bekcbojNXEtyvNmyJ +-----END CERTIFICATE----- diff --git a/v3/util/onion.go b/v3/util/onion.go new file mode 100644 index 000000000..d9ec15b1c --- /dev/null +++ b/v3/util/onion.go @@ -0,0 +1,54 @@ +package util + +import ( + "encoding/base32" + "strings" + + "github.com/zmap/zcrypto/x509" +) + +// An onion V3 address is base32 encoded, however Tor believes that the standard base32 encoding +// is lowercase while the Go standard library believes that the standard base32 encoding is uppercase. +// +// onionV3Base32Encoding is simply base32.StdEncoding but lowercase instead of uppercase in order +// to work with the above mismatch. +var onionV3Base32Encoding = base32.NewEncoding("abcdefghijklmnopqrstuvwxyz234567") + +// IsOnionV3 returns whether or not the provided DNS name is an Onion V3 encoded address. +// +// In order to be an Onion V3 encoded address, the DNS name must satisfy the following: +// 1. Contain at least two labels. +// 2. The right most label MUST be "onion". +// 3. The second to the right most label MUST be exactly 56 characters long. +// 4. The second to the right most label MUST be base32 encoded against the lowercase standard encoding. +// 5. The final byte of the decoded result from #4 MUST be equal to 0x03. +func IsOnionV3(dnsName string) bool { + labels := strings.Split(dnsName, ".") + if len(labels) < 2 || labels[len(labels)-1] != "onion" { + return false + } + address := labels[len(labels)-2] + if len(address) != 56 { + return false + } + raw, err := onionV3Base32Encoding.DecodeString(address) + if err != nil { + return false + } + return raw[len(raw)-1] == 0x03 +} + +// AllAreOnionV3 returns whether-or-not EVERY name provided conforms to IsOnionV3 +func AllAreOnionV3(names []string) bool { + isV3 := !(len(names) == 0) + for _, name := range names { + isV3 = isV3 && IsOnionV3(name) + } + return isV3 +} + +// IsOnionV3Cert returns whether-or-not the provided certificates' subject common name and +// ALL subject alternative DNS names are version 3 Onion addresses. +func IsOnionV3Cert(c *x509.Certificate) bool { + return AllAreOnionV3(append(c.DNSNames, c.Subject.CommonName)) +} diff --git a/v3/util/onion_test.go b/v3/util/onion_test.go new file mode 100644 index 000000000..20845e110 --- /dev/null +++ b/v3/util/onion_test.go @@ -0,0 +1,119 @@ +package util + +import "testing" + +func TestIsOnionV3(t *testing.T) { + data := []struct { + in string + want bool + }{ + { + "*.facebookwkhpilnemxj7asaniu7vnjjbiltxjqhye3mhbshg7kx5tfyd.onion", + true, + }, + { + "*.facebookwkhpilnemxj7asaniu7vnjjbiltxjqhye3mhbshg7kx5tfyd.com", + false, + }, + { + // Tricky to spot, but different final byte (e instead of d) + "*.facebookwkhpilnemxj7asaniu7vnjjbiltxjqhye3mhbshg7kx5tfye.onion", + false, + }, + { + "pg6mmjiyjmcrsslvykfwnntlaru7p5svn6y2ymmju6nubxndf4pscryd.onion", + true, + }, + + { + "sp3k262uwy4r2k3ycr5awluarykdpag6a7y33jxop4cs2lu5uz5sseqd.onion", + true, + }, + + { + "xa4r2iadxm55fbnqgwwi5mymqdcofiu3w6rpbtqn7b2dyn7mgwj64jyd.onion", + true, + }, + { + "facebook.onion", + false, + }, + { + // Trigger bad base32 decoding with the leading # + "#a4r2iadxm55fbnqgwwi5mymqdcofiu3w6rpbtqn7b2dyn7mgwj64jyd.onion", + false, + }, + } + for _, test := range data { + test := test + t.Run(test.in, func(t *testing.T) { + got := IsOnionV3(test.in) + if got != test.want { + t.Errorf("expected %v got %v", test.want, got) + } + }) + } +} + +func TestAllAreOnionV3(t *testing.T) { + data := []struct { + in []string + want bool + }{ + { + []string{"*.facebookwkhpilnemxj7asaniu7vnjjbiltxjqhye3mhbshg7kx5tfyd.onion"}, + true, + }, + { + []string{}, + false, + }, + { + []string{ + "pg6mmjiyjmcrsslvykfwnntlaru7p5svn6y2ymmju6nubxndf4pscryd.onion", + "sp3k262uwy4r2k3ycr5awluarykdpag6a7y33jxop4cs2lu5uz5sseqd.onion", + "xa4r2iadxm55fbnqgwwi5mymqdcofiu3w6rpbtqn7b2dyn7mgwj64jyd.onion", + }, + true, + }, + { + []string{ + "pg6mmjiyjmcrsslvykfwnntlaru7p5svn6y2ymmju6nubxndf4pscryd.onion", + "facebook.com", + "xa4r2iadxm55fbnqgwwi5mymqdcofiu3w6rpbtqn7b2dyn7mgwj64jyd.onion", + }, + false, + }, + { + []string{ + "facebook.com", + "pg6mmjiyjmcrsslvykfwnntlaru7p5svn6y2ymmju6nubxndf4pscryd.onion", + "xa4r2iadxm55fbnqgwwi5mymqdcofiu3w6rpbtqn7b2dyn7mgwj64jyd.onion", + }, + false, + }, + { + []string{ + "pg6mmjiyjmcrsslvykfwnntlaru7p5svn6y2ymmju6nubxndf4pscryd.onion", + "xa4r2iadxm55fbnqgwwi5mymqdcofiu3w6rpbtqn7b2dyn7mgwj64jyd.onion", + "facebook.com", + }, + false, + }, + } + for _, test := range data { + test := test + var name string + if len(test.in) == 0 { + name = "empty" + } else { + name = test.in[0] + } + t.Run(name, func(t *testing.T) { + got := AllAreOnionV3(test.in) + if got != test.want { + t.Errorf("expected %v got %v", test.want, got) + } + }) + } +}