From 9617edf0c2b160f1fbd3e2835e2a40855ee5644c Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Thu, 27 Jan 2022 17:18:33 +0100 Subject: [PATCH] Improve internationalized domain name handling This PR improves internationalized domain name handling according to rules of IDNA and based on the description in RFC 5280, section 7: https://datatracker.ietf.org/doc/html/rfc5280#section-7. Support for internationalized URI(s), so-called IRIs, still needs to be done. --- authority/provisioner/nebula.go | 1 - policy/engine.go | 79 ++++++++++++++---- policy/engine_test.go | 137 ++++++++++++++++++++------------ policy/options.go | 60 ++++++++++++-- policy/options_test.go | 106 ++++++++++++++++++++---- 5 files changed, 295 insertions(+), 88 deletions(-) diff --git a/authority/provisioner/nebula.go b/authority/provisioner/nebula.go index 545939ac..6c31dbc5 100644 --- a/authority/provisioner/nebula.go +++ b/authority/provisioner/nebula.go @@ -35,7 +35,6 @@ const ( // https://signal.org/docs/specifications/xeddsa/#xeddsa and implemented by // go.step.sm/crypto/x25519. type Nebula struct { - *base ID string `json:"-"` Type string `json:"type"` Name string `json:"name"` diff --git a/policy/engine.go b/policy/engine.go index 345d6282..42d4f303 100755 --- a/policy/engine.go +++ b/policy/engine.go @@ -10,9 +10,9 @@ import ( "reflect" "strings" - "github.com/pkg/errors" "go.step.sm/crypto/x509util" "golang.org/x/crypto/ssh" + "golang.org/x/net/idna" ) type NamePolicyReason int @@ -23,6 +23,15 @@ const ( // doesn't permit a DNS or another type of SAN to be signed // (or otherwise used). NotAuthorizedForThisName NamePolicyReason = iota + // CannotParseDomain is returned when an error occurs + // when parsing the domain part of SAN or subject. + CannotParseDomain + // CannotParseRFC822Name is returned when an error + // occurs when parsing an email address. + CannotParseRFC822Name + // CannotMatch is the type of error returned when + // an error happens when matching SAN types. + CannotMatchNameToConstraint ) type NamePolicyError struct { @@ -31,16 +40,26 @@ type NamePolicyError struct { } func (e NamePolicyError) Error() string { - if e.Reason == NotAuthorizedForThisName { + switch e.Reason { + case NotAuthorizedForThisName: return "not authorized to sign for this name: " + e.Detail + case CannotParseDomain: + return "cannot parse domain: " + e.Detail + case CannotParseRFC822Name: + return "cannot parse rfc822Name: " + e.Detail + case CannotMatchNameToConstraint: + return "error matching name to constraint: " + e.Detail + default: + return "unknown error: " + e.Detail } - return "unknown error" } // NamePolicyEngine can be used to check that a CSR or Certificate meets all allowed and // denied names before a CA creates and/or signs the Certificate. // TODO(hs): the X509 RFC also defines name checks on directory name; support that? // TODO(hs): implement Stringer interface: describe the contents of the NamePolicyEngine? +// TODO(hs): implement matching URI schemes, paths, etc; not just the domain part of URI domains + type NamePolicyEngine struct { // verifySubjectCommonName is set when Subject Common Name must be verified @@ -275,8 +294,6 @@ func (e *NamePolicyEngine) validateNames(dnsNames []string, ips []net.IP, emailA // this number as a total of all checks and keeps a (pointer to a) counter of the number of checks // executed so far. - // TODO: implement matching URI schemes, paths, etc; not just the domain - // TODO: gather all errors, or return early? Currently we return early on the first wrong name; check might fail for multiple names. // Perhaps make that an option? for _, dns := range dnsNames { @@ -289,10 +306,28 @@ func (e *NamePolicyEngine) validateNames(dnsNames []string, ips []net.IP, emailA Detail: fmt.Sprintf("dns %q is not explicitly permitted by any constraint", dns), } } - if _, ok := domainToReverseLabels(dns); !ok { - return errors.Errorf("cannot parse dns %q", dns) + didCutWildcard := false + if strings.HasPrefix(dns, "*.") { + dns = dns[1:] + didCutWildcard = true + } + parsedDNS, err := idna.Lookup.ToASCII(dns) + if err != nil { + return NamePolicyError{ + Reason: CannotParseDomain, + Detail: fmt.Sprintf("dns %q cannot be converted to ASCII", dns), + } + } + if didCutWildcard { + parsedDNS = "*" + parsedDNS + } + if _, ok := domainToReverseLabels(parsedDNS); !ok { + return NamePolicyError{ + Reason: CannotParseDomain, + Detail: fmt.Sprintf("cannot parse dns %q", dns), + } } - if err := checkNameConstraints("dns", dns, dns, + if err := checkNameConstraints("dns", dns, parsedDNS, func(parsedName, constraint interface{}) (bool, error) { return e.matchDomainConstraint(parsedName.(string), constraint.(string)) }, e.permittedDNSDomains, e.excludedDNSDomains); err != nil { @@ -324,8 +359,22 @@ func (e *NamePolicyEngine) validateNames(dnsNames []string, ips []net.IP, emailA } mailbox, ok := parseRFC2821Mailbox(email) if !ok { - return fmt.Errorf("cannot parse rfc822Name %q", mailbox) + return NamePolicyError{ + Reason: CannotParseRFC822Name, + Detail: fmt.Sprintf("invalid rfc822Name %q", mailbox), + } + } + // According to RFC 5280, section 7.5, emails are considered to match if the local part is + // an exact match and the host (domain) part matches the ASCII representation (case-insensitive): + // https://datatracker.ietf.org/doc/html/rfc5280#section-7.5 + domainASCII, err := idna.ToASCII(mailbox.domain) + if err != nil { + return NamePolicyError{ + Reason: CannotParseDomain, + Detail: fmt.Sprintf("cannot parse email domain %q", email), + } } + mailbox.domain = domainASCII if err := checkNameConstraints("email", email, mailbox, func(parsedName, constraint interface{}) (bool, error) { return e.matchEmailConstraint(parsedName.(rfc2821Mailbox), constraint.(string)) @@ -334,6 +383,8 @@ func (e *NamePolicyEngine) validateNames(dnsNames []string, ips []net.IP, emailA } } + // TODO(hs): fix internationalization for URIs (IRIs) + for _, uri := range uris { if e.numberOfURIDomainConstraints == 0 && e.totalNumberOfPermittedConstraints > 0 { return NamePolicyError{ @@ -365,12 +416,6 @@ func (e *NamePolicyEngine) validateNames(dnsNames []string, ips []net.IP, emailA } } - // TODO(hs): when the error is not nil and returned up in the above, we can add - // additional context to it (i.e. the cert or csr that was inspected). - - // TODO(hs): validate other types of SANs? The Go std library skips those. - // These could be custom checkers. - // if all checks out, all SANs are allowed return nil } @@ -393,7 +438,7 @@ func checkNameConstraints( match, err := match(parsedName, constraint) if err != nil { return NamePolicyError{ - Reason: NotAuthorizedForThisName, + Reason: CannotMatchNameToConstraint, Detail: err.Error(), } } @@ -414,7 +459,7 @@ func checkNameConstraints( var err error if ok, err = match(parsedName, constraint); err != nil { return NamePolicyError{ - Reason: NotAuthorizedForThisName, + Reason: CannotMatchNameToConstraint, Detail: err.Error(), } } diff --git a/policy/engine_test.go b/policy/engine_test.go index 9bc535ea..e42c589d 100755 --- a/policy/engine_test.go +++ b/policy/engine_test.go @@ -17,16 +17,15 @@ import ( func TestNamePolicyEngine_matchDomainConstraint(t *testing.T) { tests := []struct { - name string - engine *NamePolicyEngine - domain string - constraint string - want bool - wantErr bool + name string + allowLiteralWildcardNames bool + domain string + constraint string + want bool + wantErr bool }{ { name: "fail/wildcard", - engine: &NamePolicyEngine{}, domain: "host.local", constraint: ".example.com", // internally we're using the x509 period prefix as the indicator for exactly one subdomain want: false, @@ -34,7 +33,6 @@ func TestNamePolicyEngine_matchDomainConstraint(t *testing.T) { }, { name: "fail/wildcard-literal", - engine: &NamePolicyEngine{}, domain: "*.example.com", constraint: ".example.com", // internally we're using the x509 period prefix as the indicator for exactly one subdomain want: false, @@ -42,7 +40,6 @@ func TestNamePolicyEngine_matchDomainConstraint(t *testing.T) { }, { name: "fail/specific-domain", - engine: &NamePolicyEngine{}, domain: "www.example.com", constraint: "host.example.com", want: false, @@ -50,7 +47,6 @@ func TestNamePolicyEngine_matchDomainConstraint(t *testing.T) { }, { name: "fail/single-whitespace-domain", - engine: &NamePolicyEngine{}, domain: " ", constraint: "host.example.com", want: false, @@ -58,7 +54,6 @@ func TestNamePolicyEngine_matchDomainConstraint(t *testing.T) { }, { name: "fail/period-domain", - engine: &NamePolicyEngine{}, domain: ".host.example.com", constraint: ".example.com", want: false, @@ -66,7 +61,6 @@ func TestNamePolicyEngine_matchDomainConstraint(t *testing.T) { }, { name: "fail/wrong-asterisk-prefix", - engine: &NamePolicyEngine{}, domain: "*Xexample.com", constraint: ".example.com", want: false, @@ -74,7 +68,6 @@ func TestNamePolicyEngine_matchDomainConstraint(t *testing.T) { }, { name: "fail/asterisk-in-domain", - engine: &NamePolicyEngine{}, domain: "e*ample.com", constraint: ".com", want: false, @@ -82,7 +75,6 @@ func TestNamePolicyEngine_matchDomainConstraint(t *testing.T) { }, { name: "fail/asterisk-label", - engine: &NamePolicyEngine{}, domain: "example.*.local", constraint: ".local", want: false, @@ -90,7 +82,6 @@ func TestNamePolicyEngine_matchDomainConstraint(t *testing.T) { }, { name: "fail/multiple-periods", - engine: &NamePolicyEngine{}, domain: "example.local", constraint: "..local", want: false, @@ -98,23 +89,20 @@ func TestNamePolicyEngine_matchDomainConstraint(t *testing.T) { }, { name: "fail/error-parsing-domain", - engine: &NamePolicyEngine{}, - domain: string([]byte{0}), + domain: string(byte(0)), constraint: ".local", want: false, wantErr: true, }, { name: "fail/error-parsing-constraint", - engine: &NamePolicyEngine{}, domain: "example.local", - constraint: string([]byte{0}), + constraint: string(byte(0)), want: false, wantErr: true, }, { name: "fail/no-subdomain", - engine: &NamePolicyEngine{}, domain: "local", constraint: ".local", want: false, @@ -122,7 +110,6 @@ func TestNamePolicyEngine_matchDomainConstraint(t *testing.T) { }, { name: "fail/too-many-subdomains", - engine: &NamePolicyEngine{}, domain: "www.example.local", constraint: ".local", want: false, @@ -130,7 +117,6 @@ func TestNamePolicyEngine_matchDomainConstraint(t *testing.T) { }, { name: "fail/wrong-domain", - engine: &NamePolicyEngine{}, domain: "example.notlocal", constraint: ".local", want: false, @@ -138,7 +124,6 @@ func TestNamePolicyEngine_matchDomainConstraint(t *testing.T) { }, { name: "false/idna-internationalized-domain-name", - engine: &NamePolicyEngine{}, domain: "JP納豆.例.jp", // Example value from https://www.w3.org/International/articles/idn-and-iri/ constraint: ".例.jp", want: false, @@ -146,7 +131,6 @@ func TestNamePolicyEngine_matchDomainConstraint(t *testing.T) { }, { name: "false/idna-internationalized-domain-name-constraint", - engine: &NamePolicyEngine{}, domain: "xn--jp-cd2fp15c.xn--fsq.jp", // Example value from https://www.w3.org/International/articles/idn-and-iri/ constraint: ".例.jp", want: false, @@ -154,7 +138,6 @@ func TestNamePolicyEngine_matchDomainConstraint(t *testing.T) { }, { name: "ok/empty-constraint", - engine: &NamePolicyEngine{}, domain: "www.example.com", constraint: "", want: true, @@ -162,25 +145,21 @@ func TestNamePolicyEngine_matchDomainConstraint(t *testing.T) { }, { name: "ok/wildcard", - engine: &NamePolicyEngine{}, domain: "www.example.com", constraint: ".example.com", // internally we're using the x509 period prefix as the indicator for exactly one subdomain want: true, wantErr: false, }, { - name: "ok/wildcard-literal", - engine: &NamePolicyEngine{ - allowLiteralWildcardNames: true, - }, - domain: "*.example.com", // specifically allowed using an option on the NamePolicyEngine - constraint: ".example.com", // internally we're using the x509 period prefix as the indicator for exactly one subdomain - want: true, - wantErr: false, + name: "ok/wildcard-literal", + allowLiteralWildcardNames: true, + domain: "*.example.com", // specifically allowed using an option on the NamePolicyEngine + constraint: ".example.com", // internally we're using the x509 period prefix as the indicator for exactly one subdomain + want: true, + wantErr: false, }, { name: "ok/specific-domain", - engine: &NamePolicyEngine{}, domain: "www.example.com", constraint: "www.example.com", want: true, @@ -188,7 +167,6 @@ func TestNamePolicyEngine_matchDomainConstraint(t *testing.T) { }, { name: "ok/different-case", - engine: &NamePolicyEngine{}, domain: "WWW.EXAMPLE.com", constraint: "www.example.com", want: true, @@ -196,7 +174,6 @@ func TestNamePolicyEngine_matchDomainConstraint(t *testing.T) { }, { name: "ok/idna-internationalized-domain-name-punycode", - engine: &NamePolicyEngine{}, domain: "xn--jp-cd2fp15c.xn--fsq.jp", // Example value from https://www.w3.org/International/articles/idn-and-iri/ constraint: ".xn--fsq.jp", want: true, @@ -205,7 +182,10 @@ func TestNamePolicyEngine_matchDomainConstraint(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := tt.engine.matchDomainConstraint(tt.domain, tt.constraint) + engine := NamePolicyEngine{ + allowLiteralWildcardNames: tt.allowLiteralWildcardNames, + } + got, err := engine.matchDomainConstraint(tt.domain, tt.constraint) if (err != nil) != tt.wantErr { t.Errorf("NamePolicyEngine.matchDomainConstraint() error = %v, wantErr %v", err, tt.wantErr) return @@ -749,6 +729,19 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { want: false, wantErr: true, }, + { + name: "fail/dns-permitted-idna-internationalized-domain", + options: []NamePolicyOption{ + AddPermittedDNSDomain("*.豆.jp"), + }, + cert: &x509.Certificate{ + DNSNames: []string{ + string(byte(0)) + ".例.jp", + }, + }, + want: false, + wantErr: true, + }, { name: "fail/ipv4-permitted", options: []NamePolicyOption{ @@ -837,6 +830,39 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { want: false, wantErr: true, }, + { + name: "fail/mail-permitted-idna-internationalized-domain", + options: []NamePolicyOption{ + AddPermittedEmailAddress("@例.jp"), + }, + cert: &x509.Certificate{ + EmailAddresses: []string{"bücher@例.jp"}, + }, + want: false, + wantErr: true, + }, + { + name: "fail/mail-permitted-idna-internationalized-domain-rfc822", + options: []NamePolicyOption{ + AddPermittedEmailAddress("@例.jp"), + }, + cert: &x509.Certificate{ + EmailAddresses: []string{"bücher@例.jp" + string(byte(0))}, + }, + want: false, + wantErr: true, + }, + { + name: "fail/mail-permitted-idna-internationalized-domain-ascii", + options: []NamePolicyOption{ + AddPermittedEmailAddress("@例.jp"), + }, + cert: &x509.Certificate{ + EmailAddresses: []string{"mail@xn---bla.jp"}, + }, + want: false, + wantErr: true, + }, { name: "fail/permitted-uri-domain-wildcard", options: []NamePolicyOption{ @@ -1453,17 +1479,6 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { want: true, wantErr: false, }, - { - name: "ok/empty-dns-constraint", - options: []NamePolicyOption{ - AddPermittedDNSDomain(""), - }, - cert: &x509.Certificate{ - DNSNames: []string{"example.local"}, - }, - want: true, - wantErr: false, - }, { name: "ok/dns-permitted-wildcard-literal", options: []NamePolicyOption{ @@ -1497,6 +1512,19 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { want: true, wantErr: false, }, + { + name: "ok/dns-permitted-idna-internationalized-domain", + options: []NamePolicyOption{ + AddPermittedDNSDomain("*.例.jp"), + }, + cert: &x509.Certificate{ + DNSNames: []string{ + "JP納豆.例.jp", // Example value from https://www.w3.org/International/articles/idn-and-iri/ + }, + }, + want: true, + wantErr: false, + }, { name: "ok/ipv4-permitted", options: []NamePolicyOption{ @@ -1558,6 +1586,17 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { want: true, wantErr: false, }, + { + name: "ok/mail-permitted-idna-internationalized-domain", + options: []NamePolicyOption{ + AddPermittedEmailAddress("@例.jp"), + }, + cert: &x509.Certificate{ + EmailAddresses: []string{}, + }, + want: true, + wantErr: false, + }, { name: "ok/uri-permitted-domain-wildcard", options: []NamePolicyOption{ diff --git a/policy/options.go b/policy/options.go index 60bf2f72..d37b206f 100755 --- a/policy/options.go +++ b/policy/options.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/pkg/errors" + "golang.org/x/net/idna" ) type NamePolicyOption func(e *NamePolicyEngine) error @@ -592,14 +593,24 @@ func isIPv4(ip net.IP) bool { func normalizeAndValidateDNSDomainConstraint(constraint string) (string, error) { normalizedConstraint := strings.ToLower(strings.TrimSpace(constraint)) + if normalizedConstraint == "" { + return "", errors.Errorf("contraint %q can not be empty or white space string", constraint) + } if strings.Contains(normalizedConstraint, "..") { return "", errors.Errorf("domain constraint %q cannot have empty labels", constraint) } + if normalizedConstraint[0] == '*' && normalizedConstraint[1] != '.' { + return "", errors.Errorf("wildcard character in domain constraint %q can only be used to match (full) labels", constraint) + } + if strings.LastIndex(normalizedConstraint, "*") > 0 { + return "", errors.Errorf("domain constraint %q can only have wildcard as starting character", constraint) + } if strings.HasPrefix(normalizedConstraint, "*.") { normalizedConstraint = normalizedConstraint[1:] // cut off wildcard character; keep the period } - if strings.Contains(normalizedConstraint, "*") { - return "", errors.Errorf("domain constraint %q can only have wildcard as starting character", constraint) + normalizedConstraint, err := idna.Lookup.ToASCII(normalizedConstraint) + if err != nil { + return "", errors.Wrapf(err, "domain constraint %q can not be converted to ASCII", constraint) } if _, ok := domainToReverseLabels(normalizedConstraint); !ok { return "", errors.Errorf("cannot parse domain constraint %q", constraint) @@ -609,8 +620,11 @@ func normalizeAndValidateDNSDomainConstraint(constraint string) (string, error) func normalizeAndValidateEmailConstraint(constraint string) (string, error) { normalizedConstraint := strings.ToLower(strings.TrimSpace(constraint)) + if normalizedConstraint == "" { + return "", errors.Errorf("email contraint %q can not be empty or white space string", constraint) + } if strings.Contains(normalizedConstraint, "*") { - return "", fmt.Errorf("email constraint %q cannot contain asterisk", constraint) + return "", fmt.Errorf("email constraint %q cannot contain asterisk wildcard", constraint) } if strings.Count(normalizedConstraint, "@") > 1 { return "", fmt.Errorf("email constraint %q contains too many @ characters", constraint) @@ -622,8 +636,23 @@ func normalizeAndValidateEmailConstraint(constraint string) (string, error) { return "", fmt.Errorf("email constraint %q cannot start with period", constraint) } if strings.Contains(normalizedConstraint, "@") { - if _, ok := parseRFC2821Mailbox(normalizedConstraint); !ok { - return "", fmt.Errorf("cannot parse email constraint %q", constraint) + mailbox, ok := parseRFC2821Mailbox(normalizedConstraint) + if !ok { + return "", fmt.Errorf("cannot parse email constraint %q as RFC 2821 mailbox", constraint) + } + // According to RFC 5280, section 7.5, emails are considered to match if the local part is + // an exact match and the host (domain) part matches the ASCII representation (case-insensitive): + // https://datatracker.ietf.org/doc/html/rfc5280#section-7.5 + domainASCII, err := idna.Lookup.ToASCII(mailbox.domain) + if err != nil { + return "", errors.Wrapf(err, "email constraint %q domain part %q cannot be converted to ASCII", constraint, mailbox.domain) + } + normalizedConstraint = mailbox.local + "@" + domainASCII + } else { + var err error + normalizedConstraint, err = idna.Lookup.ToASCII(normalizedConstraint) + if err != nil { + return "", errors.Wrapf(err, "email constraint %q cannot be converted to ASCII", constraint) } } if _, ok := domainToReverseLabels(normalizedConstraint); !ok { @@ -634,6 +663,9 @@ func normalizeAndValidateEmailConstraint(constraint string) (string, error) { func normalizeAndValidateURIDomainConstraint(constraint string) (string, error) { normalizedConstraint := strings.ToLower(strings.TrimSpace(constraint)) + if normalizedConstraint == "" { + return "", errors.Errorf("URI domain contraint %q cannot be empty or white space string", constraint) + } if strings.Contains(normalizedConstraint, "..") { return "", errors.Errorf("URI domain constraint %q cannot have empty labels", constraint) } @@ -643,7 +675,23 @@ func normalizeAndValidateURIDomainConstraint(constraint string) (string, error) if strings.Contains(normalizedConstraint, "*") { return "", errors.Errorf("URI domain constraint %q can only have wildcard as starting character", constraint) } - // TODO(hs): block constraints that look like IPs too? Because hosts can't be matched to those. + // we're being strict with square brackets in domains; we don't allow them, no matter what + if strings.Contains(normalizedConstraint, "[") || strings.Contains(normalizedConstraint, "]") { + return "", errors.Errorf("URI domain constraint %q contains invalid square brackets", constraint) + } + if _, _, err := net.SplitHostPort(normalizedConstraint); err == nil { + // a successful split (likely) with host and port; we don't currently allow ports in the config + return "", errors.Errorf("URI domain constraint %q cannot contain port", constraint) + } + // check if the host part of the URI domain constraint is an IP + if net.ParseIP(normalizedConstraint) != nil { + return "", errors.Errorf("URI domain constraint %q cannot be an IP", constraint) + } + // TODO(hs): verify that this is OK for URI (IRI) domains too + normalizedConstraint, err := idna.Lookup.ToASCII(normalizedConstraint) + if err != nil { + return "", errors.Wrapf(err, "URI domain constraint %q cannot be converted to ASCII", constraint) + } _, ok := domainToReverseLabels(normalizedConstraint) if !ok { return "", fmt.Errorf("cannot parse URI domain constraint %q", constraint) diff --git a/policy/options_test.go b/policy/options_test.go index 7f417887..af4aeb3a 100644 --- a/policy/options_test.go +++ b/policy/options_test.go @@ -16,32 +16,38 @@ func Test_normalizeAndValidateDNSDomainConstraint(t *testing.T) { wantErr bool }{ { - name: "fail/too-many-asterisks", - constraint: "**.local", + name: "fail/empty-constraint", + constraint: "", want: "", wantErr: true, }, { - name: "fail/empty-label", - constraint: "..local", + name: "fail/wildcard-partial-label", + constraint: "*xxxx.local", want: "", wantErr: true, }, { - name: "fail/empty-reverse", - constraint: ".", + name: "fail/wildcard-in-the-middle", + constraint: "x.*.local", want: "", wantErr: true, }, { - name: "false/idna-internationalized-domain-name", - constraint: ".例.jp", // Example value from https://www.w3.org/International/articles/idn-and-iri/ + name: "fail/empty-label", + constraint: "..local", want: "", wantErr: true, }, { - name: "false/idna-internationalized-domain-name-constraint", - constraint: ".例.jp", // Example value from https://www.w3.org/International/articles/idn-and-iri/ + name: "fail/empty-reverse", + constraint: ".", + want: "", + wantErr: true, + }, + { + name: "fail/idna-internationalized-domain-name-lookup", + constraint: `\00.local`, // invalid IDNA ASCII character want: "", wantErr: true, }, @@ -63,13 +69,18 @@ func Test_normalizeAndValidateDNSDomainConstraint(t *testing.T) { want: ".xn--fsq.jp", wantErr: false, }, + { + name: "ok/idna-internationalized-domain-name-lookup-transformed", + constraint: ".例.jp", // Example value from https://www.w3.org/International/articles/idn-and-iri/ + want: ".xn--fsq.jp", + wantErr: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := normalizeAndValidateDNSDomainConstraint(tt.constraint) if (err != nil) != tt.wantErr { t.Errorf("normalizeAndValidateDNSDomainConstraint() error = %v, wantErr %v", err, tt.wantErr) - return } if got != tt.want { t.Errorf("normalizeAndValidateDNSDomainConstraint() = %v, want %v", got, tt.want) @@ -85,6 +96,12 @@ func Test_normalizeAndValidateEmailConstraint(t *testing.T) { want string wantErr bool }{ + { + name: "fail/empty-constraint", + constraint: "", + want: "", + wantErr: true, + }, { name: "fail/asterisk", constraint: "*.local", @@ -111,13 +128,25 @@ func Test_normalizeAndValidateEmailConstraint(t *testing.T) { }, { name: "fail/parse-mailbox", - constraint: "mail@example.com" + string([]byte{0}), + constraint: "mail@example.com" + string(byte(0)), + want: "", + wantErr: true, + }, + { + name: "fail/idna-internationalized-domain", + constraint: `mail@xn--bla.local`, + want: "", + wantErr: true, + }, + { + name: "fail/idna-internationalized-domain-name-lookup", + constraint: `\00local`, want: "", wantErr: true, }, { name: "fail/parse-domain", - constraint: "example.com" + string([]byte{0}), + constraint: "x..example.com", want: "", wantErr: true, }, @@ -133,13 +162,19 @@ func Test_normalizeAndValidateEmailConstraint(t *testing.T) { want: "mail@local", wantErr: false, }, + // TODO(hs): fix the below; doesn't get past parseRFC2821Mailbox; I think it should be allowed. + // { + // name: "ok/idna-internationalized-local", + // constraint: `bücher@local`, + // want: "bücher@local", + // wantErr: false, + // }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := normalizeAndValidateEmailConstraint(tt.constraint) if (err != nil) != tt.wantErr { t.Errorf("normalizeAndValidateEmailConstraint() error = %v, wantErr %v", err, tt.wantErr) - return } if got != tt.want { t.Errorf("normalizeAndValidateEmailConstraint() = %v, want %v", got, tt.want) @@ -155,6 +190,12 @@ func Test_normalizeAndValidateURIDomainConstraint(t *testing.T) { want string wantErr bool }{ + { + name: "fail/empty-constraint", + constraint: "", + want: "", + wantErr: true, + }, { name: "fail/too-many-asterisks", constraint: "**.local", @@ -173,6 +214,42 @@ func Test_normalizeAndValidateURIDomainConstraint(t *testing.T) { want: "", wantErr: true, }, + { + name: "fail/domain-with-port", + constraint: "host.local:8443", + want: "", + wantErr: true, + }, + { + name: "fail/ipv4", + constraint: "127.0.0.1", + want: "", + wantErr: true, + }, + { + name: "fail/ipv6-brackets", + constraint: "[::1]", + want: "", + wantErr: true, + }, + { + name: "fail/ipv6-no-brackets", + constraint: "::1", + want: "", + wantErr: true, + }, + { + name: "fail/ipv6-no-brackets", + constraint: "[::1", + want: "", + wantErr: true, + }, + { + name: "fail/idna-internationalized-domain-name-lookup", + constraint: `\00local`, + want: "", + wantErr: true, + }, { name: "ok/wildcard", constraint: "*.local", @@ -191,7 +268,6 @@ func Test_normalizeAndValidateURIDomainConstraint(t *testing.T) { got, err := normalizeAndValidateURIDomainConstraint(tt.constraint) if (err != nil) != tt.wantErr { t.Errorf("normalizeAndValidateURIDomainConstraint() error = %v, wantErr %v", err, tt.wantErr) - return } if got != tt.want { t.Errorf("normalizeAndValidateURIDomainConstraint() = %v, want %v", got, tt.want)