diff --git a/v3/lints/cabf_br/lint_dnsname_contains_prohibited_reserved_label.go b/v3/lints/cabf_br/lint_dnsname_contains_prohibited_reserved_label.go new file mode 100644 index 000000000..2c3983eff --- /dev/null +++ b/v3/lints/cabf_br/lint_dnsname_contains_prohibited_reserved_label.go @@ -0,0 +1,58 @@ +/* + * ZLint Copyright 2021 Regents of the University of Michigan + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package cabf_br + +import ( + "strings" + + "github.com/zmap/zcrypto/x509" + "github.com/zmap/zlint/v3/lint" + "github.com/zmap/zlint/v3/util" +) + +func init() { + lint.RegisterLint(&lint.Lint{ + Name: "e_dnsname_contains_prohibited_reserved_label", + Description: "FQDNs MUST consist solely of Domain Labels that are P‐Labels or Non‐Reserved LDH Labels", + Citation: "BRs: 7.1.4.2.1", + Source: lint.CABFBaselineRequirements, + EffectiveDate: util.NoReservedDomainLabelsDate, + Lint: NewDNSNameContainsProhibitedReservedLabel, + }) +} + +type DNSNameContainsProhibitedReservedLabel struct{} + +func NewDNSNameContainsProhibitedReservedLabel() lint.LintInterface { + return &DNSNameContainsProhibitedReservedLabel{} +} + +func (l *DNSNameContainsProhibitedReservedLabel) CheckApplies(c *x509.Certificate) bool { + return util.IsSubscriberCert(c) && util.DNSNamesExist(c) +} + +func (l *DNSNameContainsProhibitedReservedLabel) Execute(c *x509.Certificate) *lint.LintResult { + for _, dns := range c.DNSNames { + labels := strings.Split(dns, ".") + + for _, label := range labels { + if util.HasReservedLabelPrefix(label) && !util.HasXNLabelPrefix(label) { + return &lint.LintResult{Status: lint.Error} + } + } + } + + return &lint.LintResult{Status: lint.Pass} +} diff --git a/v3/lints/cabf_br/lint_dnsname_contains_prohibited_reserved_label_test.go b/v3/lints/cabf_br/lint_dnsname_contains_prohibited_reserved_label_test.go new file mode 100644 index 000000000..c5cfc2747 --- /dev/null +++ b/v3/lints/cabf_br/lint_dnsname_contains_prohibited_reserved_label_test.go @@ -0,0 +1,40 @@ +package cabf_br + +/* + * ZLint Copyright 2021 Regents of the University of Michigan + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import ( + "testing" + + "github.com/zmap/zlint/v3/lint" + "github.com/zmap/zlint/v3/test" +) + +func TestDNSNameHasProhibitedReservedLabel(t *testing.T) { + inputPath := "dnsNameProhibitedReservedLabel.pem" + expected := lint.Error + out := test.TestLint("e_dnsname_contains_prohibited_reserved_label", inputPath) + if out.Status != expected { + t.Errorf("%s: expected %s, got %s", inputPath, expected, out.Status) + } +} + +func TestDNSNameHasXNLabel(t *testing.T) { + inputPath := "dnsNameXNLabel.pem" + expected := lint.Pass + out := test.TestLint("e_dnsname_contains_prohibited_reserved_label", inputPath) + if out.Status != expected { + t.Errorf("%s: expected %s, got %s", inputPath, expected, out.Status) + } +} diff --git a/v3/testdata/dnsNameProhibitedReservedLabel.pem b/v3/testdata/dnsNameProhibitedReservedLabel.pem new file mode 100644 index 000000000..298bf2aea --- /dev/null +++ b/v3/testdata/dnsNameProhibitedReservedLabel.pem @@ -0,0 +1,38 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: + 78:a9:8c:5d:a5:fd:28:95:fa:61:28:08:69:87:76:f5:b1:9e:67:fd + Signature Algorithm: sha256WithRSAEncryption + Issuer: CN = Bar + Validity + Not Before: Oct 1 00:00:00 2021 GMT + Not After : Oct 1 00:00:00 2022 GMT + Subject: CN = Foo + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + RSA Public-Key: (512 bit) + Modulus: + 00:e8:a0:a2:22:4e:8d:a1:62:63:ca:d2:4e:c8:10: + 97:97:d7:ad:c5:cc:27:f7:fd:5c:78:fc:dc:87:b1: + cf:b7:15:44:4a:1b:42:5b:7d:08:93:54:80:7a:bf: + af:d1:cd:4a:9a:9b:ad:f5:36:9e:5f:69:20:98:d1: + 9a:7e:9c:67:73 + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Subject Alternative Name: + DNS:xr--rahrah + Signature Algorithm: sha256WithRSAEncryption + 2c:e5:0b:6e:d7:51:ff:f2:07:6a:4f:91:0e:8d:8c:84:6f:ea: + ba:11:85:b0:f2:1a:18:92:90:a0:93:d5:dd:70:3b:50:7a:47: + 9b:2e:d1:2c:4a:c3:34:63:fa:33:c7:f1:76:2c:95:23:91:5d: + c4:45:ea:db:54:07:6e:0c:cb:18 +-----BEGIN CERTIFICATE----- +MIIBODCB46ADAgECAhR4qYxdpf0olfphKAhph3b1sZ5n/TANBgkqhkiG9w0BAQsF +ADAOMQwwCgYDVQQDDANCYXIwHhcNMjExMDAxMDAwMDAwWhcNMjIxMDAxMDAwMDAw +WjAOMQwwCgYDVQQDDANGb28wXDANBgkqhkiG9w0BAQEFAANLADBIAkEA6KCiIk6N +oWJjytJOyBCXl9etxcwn9/1cePzch7HPtxVEShtCW30Ik1SAer+v0c1Kmput9Tae +X2kgmNGafpxncwIDAQABoxkwFzAVBgNVHREEDjAMggp4ci0tcmFocmFoMA0GCSqG +SIb3DQEBCwUAA0EALOULbtdR//IHak+RDo2MhG/quhGFsPIaGJKQoJPV3XA7UHpH +my7RLErDNGP6M8fxdiyVI5FdxEXq21QHbgzLGA== +-----END CERTIFICATE----- diff --git a/v3/testdata/dnsNameXNLabel.pem b/v3/testdata/dnsNameXNLabel.pem new file mode 100644 index 000000000..a90479c75 --- /dev/null +++ b/v3/testdata/dnsNameXNLabel.pem @@ -0,0 +1,38 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: + 72:97:35:23:08:57:73:30:eb:cf:f5:47:18:81:0b:4f:25:e2:6a:ef + Signature Algorithm: sha256WithRSAEncryption + Issuer: CN = Bar + Validity + Not Before: Oct 1 00:00:00 2021 GMT + Not After : Oct 1 00:00:00 2022 GMT + Subject: CN = Foo + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + RSA Public-Key: (512 bit) + Modulus: + 00:aa:71:4b:ae:d4:0c:ee:da:6c:b8:f0:1e:a0:e8: + dc:1e:98:91:7d:64:b3:26:0a:77:70:f7:6f:6f:e3: + f2:ed:05:7f:4a:0e:45:07:98:32:3b:66:0c:01:9f: + 7d:6f:75:c1:ed:08:c0:dd:73:bf:a9:80:9b:31:1a: + e7:db:40:41:4b + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Subject Alternative Name: + DNS:xN--foo + Signature Algorithm: sha256WithRSAEncryption + 9f:14:e2:58:4e:28:a2:0e:bb:53:68:63:07:ba:ba:3c:ce:72: + 52:b2:22:66:2d:8a:e8:7e:fc:83:fd:83:8f:96:b7:96:81:9e: + 4b:e0:6f:c1:86:bf:99:de:c5:fd:b6:f1:dd:f6:86:2c:b9:3f: + 3f:93:31:a1:5c:20:a7:2d:46:08 +-----BEGIN CERTIFICATE----- +MIIBNTCB4KADAgECAhRylzUjCFdzMOvP9UcYgQtPJeJq7zANBgkqhkiG9w0BAQsF +ADAOMQwwCgYDVQQDDANCYXIwHhcNMjExMDAxMDAwMDAwWhcNMjIxMDAxMDAwMDAw +WjAOMQwwCgYDVQQDDANGb28wXDANBgkqhkiG9w0BAQEFAANLADBIAkEAqnFLrtQM +7tpsuPAeoOjcHpiRfWSzJgp3cPdvb+Py7QV/Sg5FB5gyO2YMAZ99b3XB7QjA3XO/ +qYCbMRrn20BBSwIDAQABoxYwFDASBgNVHREECzAJggd4Ti0tZm9vMA0GCSqGSIb3 +DQEBCwUAA0EAnxTiWE4oog67U2hjB7q6PM5yUrIiZi2K6H78g/2Dj5a3loGeS+Bv +wYa/md7F/bbx3faGLLk/P5MxoVwgpy1GCA== +-----END CERTIFICATE----- diff --git a/v3/util/idna.go b/v3/util/idna.go index 762cab8bb..5e2543efb 100644 --- a/v3/util/idna.go +++ b/v3/util/idna.go @@ -20,9 +20,20 @@ import ( "golang.org/x/net/idna" ) +var reservedLabelPrefix = regexp.MustCompile(`^..--`) + var xnLabelPrefix = regexp.MustCompile(`(?i)^xn--`) -// HasXNLabelPrefix checks whether-or-not the given string (presumably a +// HasReservedLabelPrefix checks whether the given string (presumably +// a domain label) has hyphens ("-") as the third and fourth characters. Domain +// labels with hyphens in these positions are considered to be "Reserved Labels" +// per RFC 5890, section 2.3.1. +// (https://datatracker.ietf.org/doc/html/rfc5890#section-2.3.1) +func HasReservedLabelPrefix(s string) bool { + return reservedLabelPrefix.MatchString(s) +} + +// HasXNLabelPrefix checks whether the given string (presumably a // domain label) is prefixed with the case-insensitive string "xn--" (the // IDNA ACE prefix). // diff --git a/v3/util/idna_test.go b/v3/util/idna_test.go index c0a9d21cd..4f264ca64 100644 --- a/v3/util/idna_test.go +++ b/v3/util/idna_test.go @@ -20,7 +20,28 @@ import ( "golang.org/x/net/idna" ) -func TestIsIdnaACEPrefixed(t *testing.T) { +func TestHasReservedLabelPrefix(t *testing.T) { + input := map[string]bool{ + "ab--": true, + "ab--foo": true, + "a---foo": true, + "A---foo": true, + "XN--foo": true, + "": false, + "a-b": false, + "a--": false, + "foobar--aa": false, + "XNA--foo": false, + } + for input, want := range input { + got := HasReservedLabelPrefix(input) + if got != want { + t.Errorf("got %v want %v for input '%s'", got, want, input) + } + } +} + +func TestHasXNLabelPrefix(t *testing.T) { input := map[string]bool{ "xn--zlint.org": true, "Xn--zlint.org": true, diff --git a/v3/util/time.go b/v3/util/time.go index eda128c59..d840b835c 100644 --- a/v3/util/time.go +++ b/v3/util/time.go @@ -64,6 +64,7 @@ var ( CABFBRs_1_7_1_Date = time.Date(2020, time.August, 20, 0, 0, 0, 0, time.UTC) AppleReducedLifetimeDate = time.Date(2020, time.September, 1, 0, 0, 0, 0, time.UTC) CABFBRs_1_8_0_Date = time.Date(2021, time.August, 21, 0, 0, 0, 0, time.UTC) + NoReservedDomainLabelsDate = time.Date(2021, time.October, 1, 0, 0, 0, 0, time.UTC) ) var (