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.
This commit is contained in:
Herman Slatman 2022-01-27 17:18:33 +01:00
parent 512b8d6730
commit 9617edf0c2
No known key found for this signature in database
GPG key ID: F4D8A44EA0A75A4F
5 changed files with 295 additions and 88 deletions

View file

@ -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"`

View file

@ -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
}
if err := checkNameConstraints("dns", dns, dns,
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, 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(),
}
}

View file

@ -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{

View file

@ -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)

View file

@ -16,8 +16,20 @@ 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/wildcard-partial-label",
constraint: "*xxxx.local",
want: "",
wantErr: true,
},
{
name: "fail/wildcard-in-the-middle",
constraint: "x.*.local",
want: "",
wantErr: true,
},
@ -34,14 +46,8 @@ func Test_normalizeAndValidateDNSDomainConstraint(t *testing.T) {
wantErr: true,
},
{
name: "false/idna-internationalized-domain-name",
constraint: ".例.jp", // Example value from https://www.w3.org/International/articles/idn-and-iri/
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/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)