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:
parent
512b8d6730
commit
9617edf0c2
5 changed files with 295 additions and 88 deletions
|
@ -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"`
|
||||
|
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue