diff --git a/authority/provisioner/policy.go b/authority/provisioner/policy.go index 282eabdc..1adfd115 100644 --- a/authority/provisioner/policy.go +++ b/authority/provisioner/policy.go @@ -13,14 +13,14 @@ func newX509PolicyEngine(x509Opts *X509Options) (*x509policy.NamePolicyEngine, e } options := []x509policy.NamePolicyOption{ - x509policy.WithEnableSubjectCommonNameVerification(), // enable x509 Subject Common Name validation by default + x509policy.WithSubjectCommonNameVerification(), // enable x509 Subject Common Name validation by default } allowed := x509Opts.GetAllowedNameOptions() if allowed != nil && allowed.HasNames() { options = append(options, - x509policy.WithPermittedDNSDomains(allowed.DNSDomains), // TODO(hs): be a bit more lenient w.r.t. the format of domains? I.e. allow "*.localhost" instead of the ".localhost", which is what Name Constraints do. - x509policy.WithPermittedCIDRs(allowed.IPRanges), // TODO(hs): support IPs in addition to ranges + x509policy.WithPermittedDNSDomains(allowed.DNSDomains), + x509policy.WithPermittedCIDRs(allowed.IPRanges), // TODO(hs): support IPs in addition to ranges x509policy.WithPermittedEmailAddresses(allowed.EmailAddresses), x509policy.WithPermittedURIDomains(allowed.URIDomains), ) @@ -29,8 +29,8 @@ func newX509PolicyEngine(x509Opts *X509Options) (*x509policy.NamePolicyEngine, e denied := x509Opts.GetDeniedNameOptions() if denied != nil && denied.HasNames() { options = append(options, - x509policy.WithExcludedDNSDomains(denied.DNSDomains), // TODO(hs): be a bit more lenient w.r.t. the format of domains? I.e. allow "*.localhost" instead of the ".localhost", which is what Name Constraints do. - x509policy.WithExcludedCIDRs(denied.IPRanges), // TODO(hs): support IPs in addition to ranges + x509policy.WithExcludedDNSDomains(denied.DNSDomains), + x509policy.WithExcludedCIDRs(denied.IPRanges), // TODO(hs): support IPs in addition to ranges x509policy.WithExcludedEmailAddresses(denied.EmailAddresses), x509policy.WithExcludedURIDomains(denied.URIDomains), ) diff --git a/policy/x509/options.go b/policy/x509/options.go index d3557876..ecd793a7 100755 --- a/policy/x509/options.go +++ b/policy/x509/options.go @@ -12,97 +12,120 @@ type NamePolicyOption func(e *NamePolicyEngine) error // TODO: wrap (more) errors; and prove a set of known (exported) errors -func WithEnableSubjectCommonNameVerification() NamePolicyOption { +func WithSubjectCommonNameVerification() NamePolicyOption { return func(e *NamePolicyEngine) error { e.verifySubjectCommonName = true return nil } } +func WithAllowLiteralWildcardNames() NamePolicyOption { + return func(e *NamePolicyEngine) error { + e.allowLiteralWildcardNames = true + return nil + } +} + func WithPermittedDNSDomains(domains []string) NamePolicyOption { return func(e *NamePolicyEngine) error { - for _, domain := range domains { - if err := validateDNSDomainConstraint(domain); err != nil { + normalizedDomains := make([]string, len(domains)) + for i, domain := range domains { + normalizedDomain, err := normalizeAndValidateDNSDomainConstraint(domain) + if err != nil { return errors.Errorf("cannot parse permitted domain constraint %q", domain) } + normalizedDomains[i] = normalizedDomain } - e.permittedDNSDomains = domains + e.permittedDNSDomains = normalizedDomains return nil } } func AddPermittedDNSDomains(domains []string) NamePolicyOption { return func(e *NamePolicyEngine) error { - for _, domain := range domains { - if err := validateDNSDomainConstraint(domain); err != nil { + normalizedDomains := make([]string, len(domains)) + for i, domain := range domains { + normalizedDomain, err := normalizeAndValidateDNSDomainConstraint(domain) + if err != nil { return errors.Errorf("cannot parse permitted domain constraint %q", domain) } + normalizedDomains[i] = normalizedDomain } - e.permittedDNSDomains = append(e.permittedDNSDomains, domains...) + e.permittedDNSDomains = append(e.permittedDNSDomains, normalizedDomains...) return nil } } func WithExcludedDNSDomains(domains []string) NamePolicyOption { return func(e *NamePolicyEngine) error { - for _, domain := range domains { - if err := validateDNSDomainConstraint(domain); err != nil { - return errors.Errorf("cannot parse excluded domain constraint %q", domain) + normalizedDomains := make([]string, len(domains)) + for i, domain := range domains { + normalizedDomain, err := normalizeAndValidateDNSDomainConstraint(domain) + if err != nil { + return errors.Errorf("cannot parse permitted domain constraint %q", domain) } + normalizedDomains[i] = normalizedDomain } - e.excludedDNSDomains = domains + e.excludedDNSDomains = normalizedDomains return nil } } func AddExcludedDNSDomains(domains []string) NamePolicyOption { return func(e *NamePolicyEngine) error { - for _, domain := range domains { - if err := validateDNSDomainConstraint(domain); err != nil { - return errors.Errorf("cannot parse excluded domain constraint %q", domain) + normalizedDomains := make([]string, len(domains)) + for i, domain := range domains { + normalizedDomain, err := normalizeAndValidateDNSDomainConstraint(domain) + if err != nil { + return errors.Errorf("cannot parse permitted domain constraint %q", domain) } + normalizedDomains[i] = normalizedDomain } - e.excludedDNSDomains = append(e.excludedDNSDomains, domains...) + e.excludedDNSDomains = append(e.excludedDNSDomains, normalizedDomains...) return nil } } func WithPermittedDNSDomain(domain string) NamePolicyOption { return func(e *NamePolicyEngine) error { - if err := validateDNSDomainConstraint(domain); err != nil { + normalizedDomain, err := normalizeAndValidateDNSDomainConstraint(domain) + if err != nil { return errors.Errorf("cannot parse permitted domain constraint %q", domain) } - e.permittedDNSDomains = []string{domain} + e.permittedDNSDomains = []string{normalizedDomain} return nil } } func AddPermittedDNSDomain(domain string) NamePolicyOption { return func(e *NamePolicyEngine) error { - if err := validateDNSDomainConstraint(domain); err != nil { + normalizedDomain, err := normalizeAndValidateDNSDomainConstraint(domain) + if err != nil { return errors.Errorf("cannot parse permitted domain constraint %q", domain) } - e.permittedDNSDomains = append(e.permittedDNSDomains, domain) + e.permittedDNSDomains = append(e.permittedDNSDomains, normalizedDomain) return nil } } func WithExcludedDNSDomain(domain string) NamePolicyOption { return func(e *NamePolicyEngine) error { - if err := validateDNSDomainConstraint(domain); err != nil { - return errors.Errorf("cannot parse excluded domain constraint %q", domain) + normalizedDomain, err := normalizeAndValidateDNSDomainConstraint(domain) + if err != nil { + return errors.Errorf("cannot parse permitted domain constraint %q", domain) } - e.excludedDNSDomains = []string{domain} + e.excludedDNSDomains = []string{normalizedDomain} return nil } } func AddExcludedDNSDomain(domain string) NamePolicyOption { return func(e *NamePolicyEngine) error { - if err := validateDNSDomainConstraint(domain); err != nil { - return errors.Errorf("cannot parse excluded domain constraint %q", domain) + normalizedDomain, err := normalizeAndValidateDNSDomainConstraint(domain) + if err != nil { + return errors.Errorf("cannot parse permitted domain constraint %q", domain) } - e.excludedDNSDomains = append(e.excludedDNSDomains, domain) + e.excludedDNSDomains = append(e.excludedDNSDomains, normalizedDomain) return nil } } @@ -123,13 +146,13 @@ func AddPermittedIPRanges(ipRanges []*net.IPNet) NamePolicyOption { func WithPermittedCIDRs(cidrs []string) NamePolicyOption { return func(e *NamePolicyEngine) error { - networks := []*net.IPNet{} - for _, cidr := range cidrs { + networks := make([]*net.IPNet, len(cidrs)) + for i, cidr := range cidrs { _, nw, err := net.ParseCIDR(cidr) if err != nil { return errors.Errorf("cannot parse permitted CIDR constraint %q", cidr) } - networks = append(networks, nw) + networks[i] = nw } e.permittedIPRanges = networks return nil @@ -138,13 +161,13 @@ func WithPermittedCIDRs(cidrs []string) NamePolicyOption { func AddPermittedCIDRs(cidrs []string) NamePolicyOption { return func(e *NamePolicyEngine) error { - networks := []*net.IPNet{} - for _, cidr := range cidrs { + networks := make([]*net.IPNet, len(cidrs)) + for i, cidr := range cidrs { _, nw, err := net.ParseCIDR(cidr) if err != nil { return errors.Errorf("cannot parse permitted CIDR constraint %q", cidr) } - networks = append(networks, nw) + networks[i] = nw } e.permittedIPRanges = append(e.permittedIPRanges, networks...) return nil @@ -153,13 +176,13 @@ func AddPermittedCIDRs(cidrs []string) NamePolicyOption { func WithExcludedCIDRs(cidrs []string) NamePolicyOption { return func(e *NamePolicyEngine) error { - networks := []*net.IPNet{} - for _, cidr := range cidrs { + networks := make([]*net.IPNet, len(cidrs)) + for i, cidr := range cidrs { _, nw, err := net.ParseCIDR(cidr) if err != nil { return errors.Errorf("cannot parse excluded CIDR constraint %q", cidr) } - networks = append(networks, nw) + networks[i] = nw } e.excludedIPRanges = networks return nil @@ -168,13 +191,13 @@ func WithExcludedCIDRs(cidrs []string) NamePolicyOption { func AddExcludedCIDRs(cidrs []string) NamePolicyOption { return func(e *NamePolicyEngine) error { - networks := []*net.IPNet{} - for _, cidr := range cidrs { + networks := make([]*net.IPNet, len(cidrs)) + for i, cidr := range cidrs { _, nw, err := net.ParseCIDR(cidr) if err != nil { return errors.Errorf("cannot parse excluded CIDR constraint %q", cidr) } - networks = append(networks, nw) + networks[i] = nw } e.excludedIPRanges = append(e.excludedIPRanges, networks...) return nil @@ -309,205 +332,269 @@ func AddExcludedIP(ip net.IP) NamePolicyOption { func WithPermittedEmailAddresses(emailAddresses []string) NamePolicyOption { return func(e *NamePolicyEngine) error { - for _, email := range emailAddresses { - if err := validateEmailConstraint(email); err != nil { + normalizedEmailAddresses := make([]string, len(emailAddresses)) + for i, email := range emailAddresses { + normalizedEmailAddress, err := normalizeAndValidateEmailConstraint(email) + if err != nil { return err } + normalizedEmailAddresses[i] = normalizedEmailAddress } - e.permittedEmailAddresses = emailAddresses + e.permittedEmailAddresses = normalizedEmailAddresses return nil } } func AddPermittedEmailAddresses(emailAddresses []string) NamePolicyOption { return func(e *NamePolicyEngine) error { - for _, email := range emailAddresses { - if err := validateEmailConstraint(email); err != nil { + normalizedEmailAddresses := make([]string, len(emailAddresses)) + for i, email := range emailAddresses { + normalizedEmailAddress, err := normalizeAndValidateEmailConstraint(email) + if err != nil { return err } + normalizedEmailAddresses[i] = normalizedEmailAddress } - e.permittedEmailAddresses = append(e.permittedEmailAddresses, emailAddresses...) + e.permittedEmailAddresses = append(e.permittedEmailAddresses, normalizedEmailAddresses...) return nil } } func WithExcludedEmailAddresses(emailAddresses []string) NamePolicyOption { return func(e *NamePolicyEngine) error { - for _, email := range emailAddresses { - if err := validateEmailConstraint(email); err != nil { + normalizedEmailAddresses := make([]string, len(emailAddresses)) + for i, email := range emailAddresses { + normalizedEmailAddress, err := normalizeAndValidateEmailConstraint(email) + if err != nil { return err } + normalizedEmailAddresses[i] = normalizedEmailAddress } - e.excludedEmailAddresses = emailAddresses + e.excludedEmailAddresses = normalizedEmailAddresses return nil } } func AddExcludedEmailAddresses(emailAddresses []string) NamePolicyOption { return func(e *NamePolicyEngine) error { - for _, email := range emailAddresses { - if err := validateEmailConstraint(email); err != nil { + normalizedEmailAddresses := make([]string, len(emailAddresses)) + for i, email := range emailAddresses { + normalizedEmailAddress, err := normalizeAndValidateEmailConstraint(email) + if err != nil { return err } + normalizedEmailAddresses[i] = normalizedEmailAddress } - e.excludedEmailAddresses = append(e.excludedEmailAddresses, emailAddresses...) + e.excludedEmailAddresses = append(e.excludedEmailAddresses, normalizedEmailAddresses...) return nil } } func WithPermittedEmailAddress(emailAddress string) NamePolicyOption { return func(e *NamePolicyEngine) error { - if err := validateEmailConstraint(emailAddress); err != nil { + normalizedEmailAddress, err := normalizeAndValidateEmailConstraint(emailAddress) + if err != nil { return err } - e.permittedEmailAddresses = []string{emailAddress} + e.permittedEmailAddresses = []string{normalizedEmailAddress} return nil } } func AddPermittedEmailAddress(emailAddress string) NamePolicyOption { return func(e *NamePolicyEngine) error { - if err := validateEmailConstraint(emailAddress); err != nil { + normalizedEmailAddress, err := normalizeAndValidateEmailConstraint(emailAddress) + if err != nil { return err } - e.permittedEmailAddresses = append(e.permittedEmailAddresses, emailAddress) + e.permittedEmailAddresses = append(e.permittedEmailAddresses, normalizedEmailAddress) return nil } } func WithExcludedEmailAddress(emailAddress string) NamePolicyOption { return func(e *NamePolicyEngine) error { - if err := validateEmailConstraint(emailAddress); err != nil { + normalizedEmailAddress, err := normalizeAndValidateEmailConstraint(emailAddress) + if err != nil { return err } - e.excludedEmailAddresses = []string{emailAddress} + e.excludedEmailAddresses = []string{normalizedEmailAddress} return nil } } func AddExcludedEmailAddress(emailAddress string) NamePolicyOption { return func(e *NamePolicyEngine) error { - if err := validateEmailConstraint(emailAddress); err != nil { + normalizedEmailAddress, err := normalizeAndValidateEmailConstraint(emailAddress) + if err != nil { return err } - e.excludedEmailAddresses = append(e.excludedEmailAddresses, emailAddress) + e.excludedEmailAddresses = append(e.excludedEmailAddresses, normalizedEmailAddress) return nil } } func WithPermittedURIDomains(uriDomains []string) NamePolicyOption { return func(e *NamePolicyEngine) error { - for _, domain := range uriDomains { - if err := validateURIDomainConstraint(domain); err != nil { + normalizedURIDomains := make([]string, len(uriDomains)) + for i, domain := range uriDomains { + normalizedURIDomain, err := normalizeAndValidateURIDomainConstraint(domain) + if err != nil { return err } + normalizedURIDomains[i] = normalizedURIDomain } - e.permittedURIDomains = uriDomains + e.permittedURIDomains = normalizedURIDomains return nil } } func AddPermittedURIDomains(uriDomains []string) NamePolicyOption { return func(e *NamePolicyEngine) error { - for _, domain := range uriDomains { - if err := validateURIDomainConstraint(domain); err != nil { + normalizedURIDomains := make([]string, len(uriDomains)) + for i, domain := range uriDomains { + normalizedURIDomain, err := normalizeAndValidateURIDomainConstraint(domain) + if err != nil { return err } + normalizedURIDomains[i] = normalizedURIDomain } - e.permittedURIDomains = append(e.permittedURIDomains, uriDomains...) + e.permittedURIDomains = append(e.permittedURIDomains, normalizedURIDomains...) return nil } } func WithPermittedURIDomain(uriDomain string) NamePolicyOption { return func(e *NamePolicyEngine) error { - if err := validateURIDomainConstraint(uriDomain); err != nil { + normalizedURIDomain, err := normalizeAndValidateURIDomainConstraint(uriDomain) + if err != nil { return err } - e.permittedURIDomains = []string{uriDomain} + e.permittedURIDomains = []string{normalizedURIDomain} return nil } } func AddPermittedURIDomain(uriDomain string) NamePolicyOption { return func(e *NamePolicyEngine) error { - if err := validateURIDomainConstraint(uriDomain); err != nil { + normalizedURIDomain, err := normalizeAndValidateURIDomainConstraint(uriDomain) + if err != nil { return err } - e.permittedURIDomains = append(e.permittedURIDomains, uriDomain) + e.permittedURIDomains = append(e.permittedURIDomains, normalizedURIDomain) return nil } } func WithExcludedURIDomains(uriDomains []string) NamePolicyOption { return func(e *NamePolicyEngine) error { - for _, domain := range uriDomains { - if err := validateURIDomainConstraint(domain); err != nil { + normalizedURIDomains := make([]string, len(uriDomains)) + for i, domain := range uriDomains { + normalizedURIDomain, err := normalizeAndValidateURIDomainConstraint(domain) + if err != nil { return err } + normalizedURIDomains[i] = normalizedURIDomain } - e.excludedURIDomains = uriDomains + e.excludedURIDomains = normalizedURIDomains return nil } } func AddExcludedURIDomains(uriDomains []string) NamePolicyOption { return func(e *NamePolicyEngine) error { - for _, domain := range uriDomains { - if err := validateURIDomainConstraint(domain); err != nil { + normalizedURIDomains := make([]string, len(uriDomains)) + for i, domain := range uriDomains { + normalizedURIDomain, err := normalizeAndValidateURIDomainConstraint(domain) + if err != nil { return err } + normalizedURIDomains[i] = normalizedURIDomain } - e.excludedURIDomains = append(e.excludedURIDomains, uriDomains...) + e.excludedURIDomains = append(e.excludedURIDomains, normalizedURIDomains...) return nil } } func WithExcludedURIDomain(uriDomain string) NamePolicyOption { return func(e *NamePolicyEngine) error { - if err := validateURIDomainConstraint(uriDomain); err != nil { + normalizedURIDomain, err := normalizeAndValidateURIDomainConstraint(uriDomain) + if err != nil { return err } - e.excludedURIDomains = []string{uriDomain} + e.excludedURIDomains = []string{normalizedURIDomain} return nil } } func AddExcludedURIDomain(uriDomain string) NamePolicyOption { return func(e *NamePolicyEngine) error { - if err := validateURIDomainConstraint(uriDomain); err != nil { + normalizedURIDomain, err := normalizeAndValidateURIDomainConstraint(uriDomain) + if err != nil { return err } - e.excludedURIDomains = append(e.excludedURIDomains, uriDomain) + e.excludedURIDomains = append(e.excludedURIDomains, normalizedURIDomain) return nil } } -func validateDNSDomainConstraint(domain string) error { - if _, ok := domainToReverseLabels(domain); !ok { - return errors.Errorf("cannot parse permitted domain constraint %q", domain) +func normalizeAndValidateDNSDomainConstraint(constraint string) (string, error) { + normalizedConstraint := strings.TrimSpace(constraint) + if strings.Contains(normalizedConstraint, "..") { + return "", errors.Errorf("domain constraint %q cannot have empty labels", constraint) } - return nil + 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) + } + if _, ok := domainToReverseLabels(normalizedConstraint); !ok { + return "", errors.Errorf("cannot parse permitted domain constraint %q", constraint) + } + return normalizedConstraint, nil } -func validateEmailConstraint(constraint string) error { - if strings.Contains(constraint, "@") { - _, ok := parseRFC2821Mailbox(constraint) - if !ok { - return fmt.Errorf("cannot parse email constraint %q", constraint) +func normalizeAndValidateEmailConstraint(constraint string) (string, error) { + normalizedConstraint := strings.TrimSpace(constraint) + if strings.Contains(normalizedConstraint, "*") { + return "", fmt.Errorf("email constraint %q cannot contain asterisk", constraint) + } + if strings.Count(normalizedConstraint, "@") > 1 { + return "", fmt.Errorf("email constraint %q contains too many @ characters", constraint) + } + if normalizedConstraint[0] == '@' { + normalizedConstraint = normalizedConstraint[1:] // remove the leading @ as wildcard for emails + } + if normalizedConstraint[0] == '.' { + 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) } } - _, ok := domainToReverseLabels(constraint) - if !ok { - return fmt.Errorf("cannot parse email domain constraint %q", constraint) + if _, ok := domainToReverseLabels(normalizedConstraint); !ok { + return "", fmt.Errorf("cannot parse email domain constraint %q", constraint) } - return nil + return normalizedConstraint, nil } -func validateURIDomainConstraint(constraint string) error { - _, ok := domainToReverseLabels(constraint) +func normalizeAndValidateURIDomainConstraint(constraint string) (string, error) { + normalizedConstraint := strings.TrimSpace(constraint) + if strings.Contains(normalizedConstraint, "..") { + return "", errors.Errorf("URI domain constraint %q cannot have empty labels", constraint) + } + if strings.HasPrefix(normalizedConstraint, "*.") { + normalizedConstraint = normalizedConstraint[1:] // cut off wildcard character; keep the period + } + 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. + _, ok := domainToReverseLabels(normalizedConstraint) if !ok { - return fmt.Errorf("cannot parse URI domain constraint %q", constraint) + return "", fmt.Errorf("cannot parse URI domain constraint %q", constraint) } - return nil + return normalizedConstraint, nil } diff --git a/policy/x509/options_test.go b/policy/x509/options_test.go new file mode 100644 index 00000000..304e208f --- /dev/null +++ b/policy/x509/options_test.go @@ -0,0 +1,1339 @@ +package x509policy + +import ( + "net" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/smallstep/assert" +) + +func Test_normalizeAndValidateDNSDomainConstraint(t *testing.T) { + tests := []struct { + name string + constraint string + want string + wantErr bool + }{ + { + name: "fail/too-many-asterisks", + constraint: "**.local", + want: "", + wantErr: true, + }, + { + name: "fail/empty-label", + constraint: "..local", + want: "", + wantErr: true, + }, + { + name: "fail/empty-reverse", + constraint: ".", + want: "", + wantErr: true, + }, + { + name: "ok/wildcard", + constraint: "*.local", + want: ".local", + wantErr: false, + }, + { + name: "ok/specific-domain", + constraint: "example.local", + want: "example.local", + 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) + } + }) + } +} + +func Test_normalizeAndValidateEmailConstraint(t *testing.T) { + tests := []struct { + name string + constraint string + want string + wantErr bool + }{ + { + name: "fail/asterisk", + constraint: "*.local", + want: "", + wantErr: true, + }, + { + name: "fail/period", + constraint: ".local", + want: "", + wantErr: true, + }, + { + name: "fail/@period", + constraint: "@.local", + want: "", + wantErr: true, + }, + { + name: "fail/too-many-@s", + constraint: "@local@example.com", + want: "", + wantErr: true, + }, + { + name: "fail/parse-mailbox", + constraint: "mail@example.com" + string([]byte{0}), + want: "", + wantErr: true, + }, + { + name: "fail/parse-domain", + constraint: "example.com" + string([]byte{0}), + want: "", + wantErr: true, + }, + { + name: "ok/wildcard", + constraint: "@local", + want: "local", + wantErr: false, + }, + { + name: "ok/specific-mail", + constraint: "mail@local", + want: "mail@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) + } + }) + } +} + +func Test_normalizeAndValidateURIDomainConstraint(t *testing.T) { + tests := []struct { + name string + constraint string + want string + wantErr bool + }{ + { + name: "fail/too-many-asterisks", + constraint: "**.local", + want: "", + wantErr: true, + }, + { + name: "fail/empty-label", + constraint: "..local", + want: "", + wantErr: true, + }, + { + name: "fail/empty-reverse", + constraint: ".", + want: "", + wantErr: true, + }, + { + name: "ok/wildcard", + constraint: "*.local", + want: ".local", + wantErr: false, + }, + { + name: "ok/specific-domain", + constraint: "example.local", + want: "example.local", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(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) + } + }) + } +} + +func TestNew(t *testing.T) { + type test struct { + options []NamePolicyOption + want *NamePolicyEngine + wantErr bool + } + var tests = map[string]func(t *testing.T) test{ + "fail/with-permitted-dns-domains": func(t *testing.T) test { + return test{ + options: []NamePolicyOption{ + WithPermittedDNSDomains([]string{"**.local"}), + }, + want: nil, + wantErr: true, + } + }, + "fail/add-permitted-dns-domains": func(t *testing.T) test { + return test{ + options: []NamePolicyOption{ + AddPermittedDNSDomains([]string{"**.local"}), + }, + want: nil, + wantErr: true, + } + }, + "fail/with-excluded-dns-domains": func(t *testing.T) test { + return test{ + options: []NamePolicyOption{ + WithExcludedDNSDomains([]string{"**.local"}), + }, + want: nil, + wantErr: true, + } + }, + "fail/add-excluded-dns-domains": func(t *testing.T) test { + return test{ + options: []NamePolicyOption{ + AddExcludedDNSDomains([]string{"**.local"}), + }, + want: nil, + wantErr: true, + } + }, + "fail/with-permitted-dns-domain": func(t *testing.T) test { + return test{ + options: []NamePolicyOption{ + WithPermittedDNSDomain("**.local"), + }, + want: nil, + wantErr: true, + } + }, + "fail/add-permitted-dns-domain": func(t *testing.T) test { + return test{ + options: []NamePolicyOption{ + AddPermittedDNSDomain("**.local"), + }, + want: nil, + wantErr: true, + } + }, + "fail/with-excluded-dns-domain": func(t *testing.T) test { + return test{ + options: []NamePolicyOption{ + WithExcludedDNSDomain("**.local"), + }, + want: nil, + wantErr: true, + } + }, + "fail/add-excluded-dns-domain": func(t *testing.T) test { + return test{ + options: []NamePolicyOption{ + AddExcludedDNSDomain("**.local"), + }, + want: nil, + wantErr: true, + } + }, + "fail/with-permitted-cidrs": func(t *testing.T) test { + return test{ + options: []NamePolicyOption{ + WithPermittedCIDRs([]string{"127.0.0.1//24"}), + }, + want: nil, + wantErr: true, + } + }, + "fail/add-permitted-cidrs": func(t *testing.T) test { + return test{ + options: []NamePolicyOption{ + AddPermittedCIDRs([]string{"127.0.0.1//24"}), + }, + want: nil, + wantErr: true, + } + }, + "fail/with-excluded-cidrs": func(t *testing.T) test { + return test{ + options: []NamePolicyOption{ + WithExcludedCIDRs([]string{"127.0.0.1//24"}), + }, + want: nil, + wantErr: true, + } + }, + "fail/add-excluded-cidrs": func(t *testing.T) test { + return test{ + options: []NamePolicyOption{ + AddExcludedCIDRs([]string{"127.0.0.1//24"}), + }, + want: nil, + wantErr: true, + } + }, + "fail/with-permitted-cidr": func(t *testing.T) test { + return test{ + options: []NamePolicyOption{ + WithPermittedCIDR("127.0.0.1//24"), + }, + want: nil, + wantErr: true, + } + }, + "fail/add-permitted-cidr": func(t *testing.T) test { + return test{ + options: []NamePolicyOption{ + AddPermittedCIDR("127.0.0.1//24"), + }, + want: nil, + wantErr: true, + } + }, + "fail/with-excluded-cidr": func(t *testing.T) test { + return test{ + options: []NamePolicyOption{ + WithExcludedCIDR("127.0.0.1//24"), + }, + want: nil, + wantErr: true, + } + }, + "fail/add-excluded-cidr": func(t *testing.T) test { + return test{ + options: []NamePolicyOption{ + AddExcludedCIDR("127.0.0.1//24"), + }, + want: nil, + wantErr: true, + } + }, + "fail/with-permitted-emails": func(t *testing.T) test { + return test{ + options: []NamePolicyOption{ + WithPermittedEmailAddresses([]string{"*.local"}), + }, + want: nil, + wantErr: true, + } + }, + "fail/add-permitted-emails": func(t *testing.T) test { + return test{ + options: []NamePolicyOption{ + AddPermittedEmailAddresses([]string{"*.local"}), + }, + want: nil, + wantErr: true, + } + }, + "fail/with-excluded-emails": func(t *testing.T) test { + return test{ + options: []NamePolicyOption{ + WithExcludedEmailAddresses([]string{"*.local"}), + }, + want: nil, + wantErr: true, + } + }, + "fail/add-excluded-emails": func(t *testing.T) test { + return test{ + options: []NamePolicyOption{ + AddExcludedEmailAddresses([]string{"*.local"}), + }, + want: nil, + wantErr: true, + } + }, + "fail/with-permitted-email": func(t *testing.T) test { + return test{ + options: []NamePolicyOption{ + WithPermittedEmailAddress("*.local"), + }, + want: nil, + wantErr: true, + } + }, + "fail/add-permitted-email": func(t *testing.T) test { + return test{ + options: []NamePolicyOption{ + AddPermittedEmailAddress("*.local"), + }, + want: nil, + wantErr: true, + } + }, + "fail/with-excluded-email": func(t *testing.T) test { + return test{ + options: []NamePolicyOption{ + WithExcludedEmailAddress("*.local"), + }, + want: nil, + wantErr: true, + } + }, + "fail/add-excluded-email": func(t *testing.T) test { + return test{ + options: []NamePolicyOption{ + AddExcludedEmailAddress("*.local"), + }, + want: nil, + wantErr: true, + } + }, + "fail/with-permitted-uris": func(t *testing.T) test { + return test{ + options: []NamePolicyOption{ + WithPermittedURIDomains([]string{"**.local"}), + }, + want: nil, + wantErr: true, + } + }, + "fail/add-permitted-uris": func(t *testing.T) test { + return test{ + options: []NamePolicyOption{ + AddPermittedURIDomains([]string{"**.local"}), + }, + want: nil, + wantErr: true, + } + }, + "fail/with-excluded-uris": func(t *testing.T) test { + return test{ + options: []NamePolicyOption{ + WithExcludedURIDomains([]string{"**.local"}), + }, + want: nil, + wantErr: true, + } + }, + "fail/add-excluded-uris": func(t *testing.T) test { + return test{ + options: []NamePolicyOption{ + AddExcludedURIDomains([]string{"**.local"}), + }, + want: nil, + wantErr: true, + } + }, + "fail/with-permitted-uri": func(t *testing.T) test { + return test{ + options: []NamePolicyOption{ + WithPermittedURIDomain("**.local"), + }, + want: nil, + wantErr: true, + } + }, + "fail/add-permitted-uri": func(t *testing.T) test { + return test{ + options: []NamePolicyOption{ + AddPermittedURIDomain("**.local"), + }, + want: nil, + wantErr: true, + } + }, + "fail/with-excluded-uri": func(t *testing.T) test { + return test{ + options: []NamePolicyOption{ + WithExcludedURIDomain("**.local"), + }, + want: nil, + wantErr: true, + } + }, + "fail/add-excluded-uri": func(t *testing.T) test { + return test{ + options: []NamePolicyOption{ + AddExcludedURIDomain("**.local"), + }, + want: nil, + wantErr: true, + } + }, + "ok/default": func(t *testing.T) test { + return test{ + options: []NamePolicyOption{}, + want: &NamePolicyEngine{}, + wantErr: false, + } + }, + "ok/subject-verification": func(t *testing.T) test { + options := []NamePolicyOption{ + WithSubjectCommonNameVerification(), + } + return test{ + options: options, + want: &NamePolicyEngine{ + verifySubjectCommonName: true, + }, + wantErr: false, + } + }, + "ok/literal-wildcards": func(t *testing.T) test { + options := []NamePolicyOption{ + WithAllowLiteralWildcardNames(), + } + return test{ + options: options, + want: &NamePolicyEngine{ + allowLiteralWildcardNames: true, + }, + wantErr: false, + } + }, + "ok/with-permitted-dns-wildcard-domains": func(t *testing.T) test { + options := []NamePolicyOption{ + WithPermittedDNSDomains([]string{"*.local", "*.example.com"}), + } + return test{ + options: options, + want: &NamePolicyEngine{ + permittedDNSDomains: []string{".local", ".example.com"}, + numberOfDNSDomainConstraints: 2, + totalNumberOfPermittedConstraints: 2, + totalNumberOfConstraints: 2, + }, + wantErr: false, + } + }, + "ok/add-permitted-dns-wildcard-domains": func(t *testing.T) test { + options := []NamePolicyOption{ + WithPermittedDNSDomains([]string{"*.local"}), + AddPermittedDNSDomains([]string{"*.example.com", "*.local"}), + } + return test{ + options: options, + want: &NamePolicyEngine{ + permittedDNSDomains: []string{".local", ".example.com"}, + numberOfDNSDomainConstraints: 2, + totalNumberOfPermittedConstraints: 2, + totalNumberOfConstraints: 2, + }, + wantErr: false, + } + }, + "ok/with-excluded-dns-domains": func(t *testing.T) test { + options := []NamePolicyOption{ + WithExcludedDNSDomains([]string{"*.local", "*.example.com"}), + } + return test{ + options: options, + want: &NamePolicyEngine{ + excludedDNSDomains: []string{".local", ".example.com"}, + numberOfDNSDomainConstraints: 2, + totalNumberOfExcludedConstraints: 2, + totalNumberOfConstraints: 2, + }, + wantErr: false, + } + }, + "ok/add-excluded-dns-domains": func(t *testing.T) test { + options := []NamePolicyOption{ + WithExcludedDNSDomains([]string{"*.local"}), + AddExcludedDNSDomains([]string{"*.local", "*.example.com"}), + } + return test{ + options: options, + want: &NamePolicyEngine{ + excludedDNSDomains: []string{".local", ".example.com"}, + numberOfDNSDomainConstraints: 2, + totalNumberOfExcludedConstraints: 2, + totalNumberOfConstraints: 2, + }, + wantErr: false, + } + }, + "ok/with-permitted-dns-wildcard-domain": func(t *testing.T) test { + options := []NamePolicyOption{ + WithPermittedDNSDomain("*.example.com"), + } + return test{ + options: options, + want: &NamePolicyEngine{ + permittedDNSDomains: []string{".example.com"}, + numberOfDNSDomainConstraints: 1, + totalNumberOfPermittedConstraints: 1, + totalNumberOfConstraints: 1, + }, + wantErr: false, + } + }, + "ok/add-permitted-dns-wildcard-domain": func(t *testing.T) test { + options := []NamePolicyOption{ + WithPermittedDNSDomain("*.example.com"), + AddPermittedDNSDomain("*.local"), + } + return test{ + options: options, + want: &NamePolicyEngine{ + permittedDNSDomains: []string{".example.com", ".local"}, + numberOfDNSDomainConstraints: 2, + totalNumberOfPermittedConstraints: 2, + totalNumberOfConstraints: 2, + }, + wantErr: false, + } + }, + "ok/with-permitted-dns-domain": func(t *testing.T) test { + options := []NamePolicyOption{ + WithPermittedDNSDomain("www.example.com"), + } + return test{ + options: options, + want: &NamePolicyEngine{ + permittedDNSDomains: []string{"www.example.com"}, + numberOfDNSDomainConstraints: 1, + totalNumberOfPermittedConstraints: 1, + totalNumberOfConstraints: 1, + }, + wantErr: false, + } + }, + "ok/add-permitted-dns-domain": func(t *testing.T) test { + options := []NamePolicyOption{ + WithPermittedDNSDomain("www.example.com"), + AddPermittedDNSDomain("host.local"), + } + return test{ + options: options, + want: &NamePolicyEngine{ + permittedDNSDomains: []string{"www.example.com", "host.local"}, + numberOfDNSDomainConstraints: 2, + totalNumberOfPermittedConstraints: 2, + totalNumberOfConstraints: 2, + }, + wantErr: false, + } + }, + "ok/with-permitted-ip-ranges": func(t *testing.T) test { + _, nw1, err := net.ParseCIDR("127.0.0.1/24") + assert.FatalError(t, err) + _, nw2, err := net.ParseCIDR("192.168.0.1/24") + assert.FatalError(t, err) + options := []NamePolicyOption{ + WithPermittedIPRanges( + []*net.IPNet{ + nw1, nw2, + }, + ), + } + return test{ + options: options, + want: &NamePolicyEngine{ + permittedIPRanges: []*net.IPNet{ + nw1, nw2, + }, + numberOfIPRangeConstraints: 2, + totalNumberOfPermittedConstraints: 2, + totalNumberOfConstraints: 2, + }, + wantErr: false, + } + }, + "ok/add-permitted-ip-ranges": func(t *testing.T) test { + _, nw1, err := net.ParseCIDR("127.0.0.1/24") + assert.FatalError(t, err) + _, nw2, err := net.ParseCIDR("192.168.0.1/24") + assert.FatalError(t, err) + options := []NamePolicyOption{ + WithPermittedIPRanges( + []*net.IPNet{ + nw1, + }, + ), + AddPermittedIPRanges( + []*net.IPNet{ + nw1, nw2, + }, + ), + } + return test{ + options: options, + want: &NamePolicyEngine{ + permittedIPRanges: []*net.IPNet{ + nw1, nw2, + }, + numberOfIPRangeConstraints: 2, + totalNumberOfPermittedConstraints: 2, + totalNumberOfConstraints: 2, + }, + wantErr: false, + } + }, + "ok/with-excluded-ip-ranges": func(t *testing.T) test { + _, nw1, err := net.ParseCIDR("127.0.0.1/24") + assert.FatalError(t, err) + _, nw2, err := net.ParseCIDR("192.168.0.1/24") + assert.FatalError(t, err) + options := []NamePolicyOption{ + WithExcludedIPRanges( + []*net.IPNet{ + nw1, nw2, + }, + ), + } + return test{ + options: options, + want: &NamePolicyEngine{ + excludedIPRanges: []*net.IPNet{ + nw1, nw2, + }, + numberOfIPRangeConstraints: 2, + totalNumberOfExcludedConstraints: 2, + totalNumberOfConstraints: 2, + }, + wantErr: false, + } + }, + "ok/add-excluded-ip-ranges": func(t *testing.T) test { + _, nw1, err := net.ParseCIDR("127.0.0.1/24") + assert.FatalError(t, err) + _, nw2, err := net.ParseCIDR("192.168.0.1/24") + assert.FatalError(t, err) + options := []NamePolicyOption{ + WithExcludedIPRanges( + []*net.IPNet{ + nw1, + }, + ), + AddExcludedIPRanges( + []*net.IPNet{ + nw1, nw2, + }, + ), + } + return test{ + options: options, + want: &NamePolicyEngine{ + excludedIPRanges: []*net.IPNet{ + nw1, nw2, + }, + numberOfIPRangeConstraints: 2, + totalNumberOfExcludedConstraints: 2, + totalNumberOfConstraints: 2, + }, + wantErr: false, + } + }, + "ok/with-permitted-cidrs": func(t *testing.T) test { + _, nw1, err := net.ParseCIDR("127.0.0.1/24") + assert.FatalError(t, err) + _, nw2, err := net.ParseCIDR("192.168.0.1/24") + assert.FatalError(t, err) + options := []NamePolicyOption{ + WithPermittedCIDRs([]string{"127.0.0.1/24", "192.168.0.1/24"}), + } + return test{ + options: options, + want: &NamePolicyEngine{ + permittedIPRanges: []*net.IPNet{ + nw1, nw2, + }, + numberOfIPRangeConstraints: 2, + totalNumberOfPermittedConstraints: 2, + totalNumberOfConstraints: 2, + }, + wantErr: false, + } + }, + "ok/add-permitted-cidrs": func(t *testing.T) test { + _, nw1, err := net.ParseCIDR("127.0.0.1/24") + assert.FatalError(t, err) + _, nw2, err := net.ParseCIDR("192.168.0.1/24") + assert.FatalError(t, err) + options := []NamePolicyOption{ + WithPermittedCIDRs([]string{"127.0.0.1/24"}), + AddPermittedCIDRs([]string{"127.0.0.1/24", "192.168.0.1/24"}), + } + return test{ + options: options, + want: &NamePolicyEngine{ + permittedIPRanges: []*net.IPNet{ + nw1, nw2, + }, + numberOfIPRangeConstraints: 2, + totalNumberOfPermittedConstraints: 2, + totalNumberOfConstraints: 2, + }, + wantErr: false, + } + }, + "ok/with-excluded-cidrs": func(t *testing.T) test { + _, nw1, err := net.ParseCIDR("127.0.0.1/24") + assert.FatalError(t, err) + _, nw2, err := net.ParseCIDR("192.168.0.1/24") + assert.FatalError(t, err) + options := []NamePolicyOption{ + WithExcludedCIDRs([]string{"127.0.0.1/24", "192.168.0.1/24"}), + } + return test{ + options: options, + want: &NamePolicyEngine{ + excludedIPRanges: []*net.IPNet{ + nw1, nw2, + }, + numberOfIPRangeConstraints: 2, + totalNumberOfExcludedConstraints: 2, + totalNumberOfConstraints: 2, + }, + wantErr: false, + } + }, + "ok/add-excluded-cidrs": func(t *testing.T) test { + _, nw1, err := net.ParseCIDR("127.0.0.1/24") + assert.FatalError(t, err) + _, nw2, err := net.ParseCIDR("192.168.0.1/24") + assert.FatalError(t, err) + options := []NamePolicyOption{ + WithExcludedCIDRs([]string{"127.0.0.1/24"}), + AddExcludedCIDRs([]string{"127.0.0.1/24", "192.168.0.1/24"}), + } + return test{ + options: options, + want: &NamePolicyEngine{ + excludedIPRanges: []*net.IPNet{ + nw1, nw2, + }, + numberOfIPRangeConstraints: 2, + totalNumberOfExcludedConstraints: 2, + totalNumberOfConstraints: 2, + }, + wantErr: false, + } + }, + "ok/with-permitted-cidr": func(t *testing.T) test { + _, nw1, err := net.ParseCIDR("127.0.0.1/24") + assert.FatalError(t, err) + options := []NamePolicyOption{ + WithPermittedCIDR("127.0.0.1/24"), + } + return test{ + options: options, + want: &NamePolicyEngine{ + permittedIPRanges: []*net.IPNet{ + nw1, + }, + numberOfIPRangeConstraints: 1, + totalNumberOfPermittedConstraints: 1, + totalNumberOfConstraints: 1, + }, + wantErr: false, + } + }, + "ok/add-permitted-cidr": func(t *testing.T) test { + _, nw1, err := net.ParseCIDR("127.0.0.1/24") + assert.FatalError(t, err) + _, nw2, err := net.ParseCIDR("192.168.0.1/24") + assert.FatalError(t, err) + options := []NamePolicyOption{ + WithPermittedCIDR("127.0.0.1/24"), + AddPermittedCIDR("192.168.0.1/24"), + } + return test{ + options: options, + want: &NamePolicyEngine{ + permittedIPRanges: []*net.IPNet{ + nw1, nw2, + }, + numberOfIPRangeConstraints: 2, + totalNumberOfPermittedConstraints: 2, + totalNumberOfConstraints: 2, + }, + wantErr: false, + } + }, + "ok/with-excluded-cidr": func(t *testing.T) test { + _, nw1, err := net.ParseCIDR("127.0.0.1/24") + assert.FatalError(t, err) + options := []NamePolicyOption{ + WithExcludedCIDR("127.0.0.1/24"), + } + return test{ + options: options, + want: &NamePolicyEngine{ + excludedIPRanges: []*net.IPNet{ + nw1, + }, + numberOfIPRangeConstraints: 1, + totalNumberOfExcludedConstraints: 1, + totalNumberOfConstraints: 1, + }, + wantErr: false, + } + }, + "ok/add-excluded-cidr": func(t *testing.T) test { + _, nw1, err := net.ParseCIDR("127.0.0.1/24") + assert.FatalError(t, err) + _, nw2, err := net.ParseCIDR("192.168.0.1/24") + assert.FatalError(t, err) + options := []NamePolicyOption{ + WithExcludedCIDR("127.0.0.1/24"), + AddExcludedCIDR("192.168.0.1/24"), + } + return test{ + options: options, + want: &NamePolicyEngine{ + excludedIPRanges: []*net.IPNet{ + nw1, nw2, + }, + numberOfIPRangeConstraints: 2, + totalNumberOfExcludedConstraints: 2, + totalNumberOfConstraints: 2, + }, + wantErr: false, + } + }, + "ok/with-permitted-ipv4": func(t *testing.T) test { + ip1, nw1, err := net.ParseCIDR("127.0.0.15/32") + assert.FatalError(t, err) + options := []NamePolicyOption{ + WithPermittedIP(ip1), + } + return test{ + options: options, + want: &NamePolicyEngine{ + permittedIPRanges: []*net.IPNet{ + nw1, + }, + numberOfIPRangeConstraints: 1, + totalNumberOfPermittedConstraints: 1, + totalNumberOfConstraints: 1, + }, + wantErr: false, + } + }, + "ok/add-permitted-ipv4": func(t *testing.T) test { + ip1, nw1, err := net.ParseCIDR("127.0.0.45/32") + assert.FatalError(t, err) + ip2, nw2, err := net.ParseCIDR("192.168.0.55/32") + assert.FatalError(t, err) + options := []NamePolicyOption{ + WithPermittedIP(ip1), + AddPermittedIP(ip2), + } + return test{ + options: options, + want: &NamePolicyEngine{ + permittedIPRanges: []*net.IPNet{ + nw1, nw2, + }, + numberOfIPRangeConstraints: 2, + totalNumberOfPermittedConstraints: 2, + totalNumberOfConstraints: 2, + }, + wantErr: false, + } + }, + "ok/with-excluded-ipv4": func(t *testing.T) test { + ip1, nw1, err := net.ParseCIDR("127.0.0.15/32") + assert.FatalError(t, err) + options := []NamePolicyOption{ + WithExcludedIP(ip1), + } + return test{ + options: options, + want: &NamePolicyEngine{ + excludedIPRanges: []*net.IPNet{ + nw1, + }, + numberOfIPRangeConstraints: 1, + totalNumberOfExcludedConstraints: 1, + totalNumberOfConstraints: 1, + }, + wantErr: false, + } + }, + "ok/add-excluded-ipv4": func(t *testing.T) test { + ip1, nw1, err := net.ParseCIDR("127.0.0.45/32") + assert.FatalError(t, err) + ip2, nw2, err := net.ParseCIDR("192.168.0.55/32") + assert.FatalError(t, err) + options := []NamePolicyOption{ + WithExcludedIP(ip1), + AddExcludedIP(ip2), + } + return test{ + options: options, + want: &NamePolicyEngine{ + excludedIPRanges: []*net.IPNet{ + nw1, nw2, + }, + numberOfIPRangeConstraints: 2, + totalNumberOfExcludedConstraints: 2, + totalNumberOfConstraints: 2, + }, + wantErr: false, + } + }, + "ok/with-permitted-ipv6": func(t *testing.T) test { + ip1, nw1, err := net.ParseCIDR("2001:0db8:85a3:0000:0000:8a2e:0370:7334/128") + assert.FatalError(t, err) + options := []NamePolicyOption{ + WithPermittedIP(ip1), + } + return test{ + options: options, + want: &NamePolicyEngine{ + permittedIPRanges: []*net.IPNet{ + nw1, + }, + numberOfIPRangeConstraints: 1, + totalNumberOfPermittedConstraints: 1, + totalNumberOfConstraints: 1, + }, + wantErr: false, + } + }, + "ok/add-permitted-ipv6": func(t *testing.T) test { + ip1, nw1, err := net.ParseCIDR("127.0.0.10/32") + assert.FatalError(t, err) + ip2, nw2, err := net.ParseCIDR("2001:0db8:85a3:0000:0000:8a2e:0370:7334/128") + assert.FatalError(t, err) + options := []NamePolicyOption{ + WithPermittedIP(ip1), + AddPermittedIP(ip2), + } + return test{ + options: options, + want: &NamePolicyEngine{ + permittedIPRanges: []*net.IPNet{ + nw1, nw2, + }, + numberOfIPRangeConstraints: 2, + totalNumberOfPermittedConstraints: 2, + totalNumberOfConstraints: 2, + }, + wantErr: false, + } + }, + "ok/with-excluded-ipv6": func(t *testing.T) test { + ip1, nw1, err := net.ParseCIDR("2001:0db8:85a3:0000:0000:8a2e:0370:7334/128") + assert.FatalError(t, err) + options := []NamePolicyOption{ + WithExcludedIP(ip1), + } + return test{ + options: options, + want: &NamePolicyEngine{ + excludedIPRanges: []*net.IPNet{ + nw1, + }, + numberOfIPRangeConstraints: 1, + totalNumberOfExcludedConstraints: 1, + totalNumberOfConstraints: 1, + }, + wantErr: false, + } + }, + "ok/add-excluded-ipv6": func(t *testing.T) test { + ip1, nw1, err := net.ParseCIDR("127.0.0.10/32") + assert.FatalError(t, err) + ip2, nw2, err := net.ParseCIDR("2001:0db8:85a3:0000:0000:8a2e:0370:7334/128") + assert.FatalError(t, err) + options := []NamePolicyOption{ + WithExcludedIP(ip1), + AddExcludedIP(ip2), + } + return test{ + options: options, + want: &NamePolicyEngine{ + excludedIPRanges: []*net.IPNet{ + nw1, nw2, + }, + numberOfIPRangeConstraints: 2, + totalNumberOfExcludedConstraints: 2, + totalNumberOfConstraints: 2, + }, + wantErr: false, + } + }, + "ok/with-permitted-emails": func(t *testing.T) test { + options := []NamePolicyOption{ + WithPermittedEmailAddresses([]string{"mail@local", "@example.com"}), + } + return test{ + options: options, + want: &NamePolicyEngine{ + permittedEmailAddresses: []string{"mail@local", "example.com"}, + numberOfEmailAddressConstraints: 2, + totalNumberOfPermittedConstraints: 2, + totalNumberOfConstraints: 2, + }, + wantErr: false, + } + }, + "ok/add-permitted-emails": func(t *testing.T) test { + options := []NamePolicyOption{ + WithPermittedEmailAddresses([]string{"mail@local"}), + AddPermittedEmailAddresses([]string{"@example.com", "mail@local"}), + } + return test{ + options: options, + want: &NamePolicyEngine{ + permittedEmailAddresses: []string{"mail@local", "example.com"}, + numberOfEmailAddressConstraints: 2, + totalNumberOfPermittedConstraints: 2, + totalNumberOfConstraints: 2, + }, + wantErr: false, + } + }, + "ok/with-excluded-emails": func(t *testing.T) test { + options := []NamePolicyOption{ + WithExcludedEmailAddresses([]string{"mail@local", "@example.com"}), + } + return test{ + options: options, + want: &NamePolicyEngine{ + excludedEmailAddresses: []string{"mail@local", "example.com"}, + numberOfEmailAddressConstraints: 2, + totalNumberOfExcludedConstraints: 2, + totalNumberOfConstraints: 2, + }, + wantErr: false, + } + }, + "ok/add-excluded-emails": func(t *testing.T) test { + options := []NamePolicyOption{ + WithExcludedEmailAddresses([]string{"mail@local"}), + AddExcludedEmailAddresses([]string{"@example.com", "mail@local"}), + } + return test{ + options: options, + want: &NamePolicyEngine{ + excludedEmailAddresses: []string{"mail@local", "example.com"}, + numberOfEmailAddressConstraints: 2, + totalNumberOfExcludedConstraints: 2, + totalNumberOfConstraints: 2, + }, + wantErr: false, + } + }, + "ok/with-permitted-email": func(t *testing.T) test { + options := []NamePolicyOption{ + WithPermittedEmailAddress("mail@local"), + } + return test{ + options: options, + want: &NamePolicyEngine{ + permittedEmailAddresses: []string{"mail@local"}, + numberOfEmailAddressConstraints: 1, + totalNumberOfPermittedConstraints: 1, + totalNumberOfConstraints: 1, + }, + wantErr: false, + } + }, + "ok/add-permitted-email": func(t *testing.T) test { + options := []NamePolicyOption{ + WithPermittedEmailAddress("mail@local"), + AddPermittedEmailAddress("@example.com"), + } + return test{ + options: options, + want: &NamePolicyEngine{ + permittedEmailAddresses: []string{"mail@local", "example.com"}, + numberOfEmailAddressConstraints: 2, + totalNumberOfPermittedConstraints: 2, + totalNumberOfConstraints: 2, + }, + wantErr: false, + } + }, + "ok/with-excluded-email": func(t *testing.T) test { + options := []NamePolicyOption{ + WithExcludedEmailAddress("mail@local"), + } + return test{ + options: options, + want: &NamePolicyEngine{ + excludedEmailAddresses: []string{"mail@local"}, + numberOfEmailAddressConstraints: 1, + totalNumberOfExcludedConstraints: 1, + totalNumberOfConstraints: 1, + }, + wantErr: false, + } + }, + "ok/add-excluded-email": func(t *testing.T) test { + options := []NamePolicyOption{ + WithExcludedEmailAddress("mail@local"), + AddExcludedEmailAddress("@example.com"), + } + return test{ + options: options, + want: &NamePolicyEngine{ + excludedEmailAddresses: []string{"mail@local", "example.com"}, + numberOfEmailAddressConstraints: 2, + totalNumberOfExcludedConstraints: 2, + totalNumberOfConstraints: 2, + }, + wantErr: false, + } + }, + "ok/with-permitted-uris": func(t *testing.T) test { + options := []NamePolicyOption{ + WithPermittedURIDomains([]string{"host.local", "*.example.com"}), + } + return test{ + options: options, + want: &NamePolicyEngine{ + permittedURIDomains: []string{"host.local", ".example.com"}, + numberOfURIDomainConstraints: 2, + totalNumberOfPermittedConstraints: 2, + totalNumberOfConstraints: 2, + }, + wantErr: false, + } + }, + "ok/add-permitted-uris": func(t *testing.T) test { + options := []NamePolicyOption{ + WithPermittedURIDomains([]string{"host.local"}), + AddPermittedURIDomains([]string{"*.example.com", "host.local"}), + } + return test{ + options: options, + want: &NamePolicyEngine{ + permittedURIDomains: []string{"host.local", ".example.com"}, + numberOfURIDomainConstraints: 2, + totalNumberOfPermittedConstraints: 2, + totalNumberOfConstraints: 2, + }, + wantErr: false, + } + }, + "ok/with-excluded-uris": func(t *testing.T) test { + options := []NamePolicyOption{ + WithExcludedURIDomains([]string{"host.local", "*.example.com"}), + } + return test{ + options: options, + want: &NamePolicyEngine{ + excludedURIDomains: []string{"host.local", ".example.com"}, + numberOfURIDomainConstraints: 2, + totalNumberOfExcludedConstraints: 2, + totalNumberOfConstraints: 2, + }, + wantErr: false, + } + }, + "ok/add-excluded-uris": func(t *testing.T) test { + options := []NamePolicyOption{ + WithExcludedURIDomains([]string{"host.local"}), + AddExcludedURIDomains([]string{"*.example.com", "host.local"}), + } + return test{ + options: options, + want: &NamePolicyEngine{ + excludedURIDomains: []string{"host.local", ".example.com"}, + numberOfURIDomainConstraints: 2, + totalNumberOfExcludedConstraints: 2, + totalNumberOfConstraints: 2, + }, + wantErr: false, + } + }, + "ok/with-permitted-uri": func(t *testing.T) test { + options := []NamePolicyOption{ + WithPermittedURIDomain("host.local"), + } + return test{ + options: options, + want: &NamePolicyEngine{ + permittedURIDomains: []string{"host.local"}, + numberOfURIDomainConstraints: 1, + totalNumberOfPermittedConstraints: 1, + totalNumberOfConstraints: 1, + }, + wantErr: false, + } + }, + "ok/add-permitted-uri": func(t *testing.T) test { + options := []NamePolicyOption{ + WithPermittedURIDomain("host.local"), + AddPermittedURIDomain("*.example.com"), + } + return test{ + options: options, + want: &NamePolicyEngine{ + permittedURIDomains: []string{"host.local", ".example.com"}, + numberOfURIDomainConstraints: 2, + totalNumberOfPermittedConstraints: 2, + totalNumberOfConstraints: 2, + }, + wantErr: false, + } + }, + "ok/with-excluded-uri": func(t *testing.T) test { + options := []NamePolicyOption{ + WithExcludedURIDomain("host.local"), + } + return test{ + options: options, + want: &NamePolicyEngine{ + excludedURIDomains: []string{"host.local"}, + numberOfURIDomainConstraints: 1, + totalNumberOfExcludedConstraints: 1, + totalNumberOfConstraints: 1, + }, + wantErr: false, + } + }, + "ok/add-excluded-uri": func(t *testing.T) test { + options := []NamePolicyOption{ + WithExcludedURIDomain("host.local"), + AddExcludedURIDomain("*.example.com"), + } + return test{ + options: options, + want: &NamePolicyEngine{ + excludedURIDomains: []string{"host.local", ".example.com"}, + numberOfURIDomainConstraints: 2, + totalNumberOfExcludedConstraints: 2, + totalNumberOfConstraints: 2, + }, + wantErr: false, + } + }, + } + for name, prep := range tests { + tc := prep(t) + t.Run(name, func(t *testing.T) { + got, err := New(tc.options...) + if (err != nil) != tc.wantErr { + t.Errorf("New() error = %v, wantErr %v", err, tc.wantErr) + return + } + if !cmp.Equal(tc.want, got, cmp.AllowUnexported(NamePolicyEngine{})) { + t.Errorf("New() diff =\n %s", cmp.Diff(tc.want, got, cmp.AllowUnexported(NamePolicyEngine{}))) + } + }) + } +} diff --git a/policy/x509/x509.go b/policy/x509/x509.go index 408251cd..5a8337b9 100755 --- a/policy/x509/x509.go +++ b/policy/x509/x509.go @@ -50,8 +50,13 @@ func (e CertificateInvalidError) Error() string { // 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? type NamePolicyEngine struct { - options []NamePolicyOption + + // verifySubjectCommonName is set when Subject Common Name must be verified verifySubjectCommonName bool + // allowLiteralWildcardNames allows literal wildcard DNS domains + allowLiteralWildcardNames bool + + // permitted and exluded constraints similar to x509 Name Constraints permittedDNSDomains []string excludedDNSDomains []string permittedIPRanges []*net.IPNet @@ -60,22 +65,84 @@ type NamePolicyEngine struct { excludedEmailAddresses []string permittedURIDomains []string excludedURIDomains []string + + // some internal counts for housekeeping + numberOfDNSDomainConstraints int + numberOfIPRangeConstraints int + numberOfEmailAddressConstraints int + numberOfURIDomainConstraints int + totalNumberOfPermittedConstraints int + totalNumberOfExcludedConstraints int + totalNumberOfConstraints int } // NewNamePolicyEngine creates a new NamePolicyEngine with NamePolicyOptions func New(opts ...NamePolicyOption) (*NamePolicyEngine, error) { e := &NamePolicyEngine{} - e.options = append(e.options, opts...) - for _, option := range e.options { + for _, option := range opts { if err := option(e); err != nil { return nil, err } } + e.permittedDNSDomains = removeDuplicates(e.permittedDNSDomains) + e.permittedIPRanges = removeDuplicateIPRanges(e.permittedIPRanges) + e.permittedEmailAddresses = removeDuplicates(e.permittedEmailAddresses) + e.permittedURIDomains = removeDuplicates(e.permittedURIDomains) + + e.excludedDNSDomains = removeDuplicates(e.excludedDNSDomains) + e.excludedIPRanges = removeDuplicateIPRanges(e.excludedIPRanges) + e.excludedEmailAddresses = removeDuplicates(e.excludedEmailAddresses) + e.excludedURIDomains = removeDuplicates(e.excludedURIDomains) + + e.numberOfDNSDomainConstraints = len(e.permittedDNSDomains) + len(e.excludedDNSDomains) + e.numberOfIPRangeConstraints = len(e.permittedIPRanges) + len(e.excludedIPRanges) + e.numberOfEmailAddressConstraints = len(e.permittedEmailAddresses) + len(e.excludedEmailAddresses) + e.numberOfURIDomainConstraints = len(e.permittedURIDomains) + len(e.excludedURIDomains) + + e.totalNumberOfPermittedConstraints = len(e.permittedDNSDomains) + len(e.permittedIPRanges) + + len(e.permittedEmailAddresses) + len(e.permittedURIDomains) + + e.totalNumberOfExcludedConstraints = len(e.excludedDNSDomains) + len(e.excludedIPRanges) + + len(e.excludedEmailAddresses) + len(e.excludedURIDomains) + + e.totalNumberOfConstraints = e.totalNumberOfPermittedConstraints + e.totalNumberOfExcludedConstraints + return e, nil } +func removeDuplicates(strSlice []string) []string { + if len(strSlice) == 0 { + return nil + } + keys := make(map[string]bool) + result := []string{} + for _, item := range strSlice { + if _, value := keys[item]; !value { + keys[item] = true + result = append(result, item) + } + } + return result +} + +func removeDuplicateIPRanges(ipRanges []*net.IPNet) []*net.IPNet { + if len(ipRanges) == 0 { + return nil + } + keys := make(map[string]bool) + result := []*net.IPNet{} + for _, item := range ipRanges { + key := item.String() + if _, value := keys[key]; !value { + keys[key] = true + result = append(result, item) + } + } + return result +} + // AreCertificateNamesAllowed verifies that all SANs in a Certificate are allowed. func (e *NamePolicyEngine) AreCertificateNamesAllowed(cert *x509.Certificate) (bool, error) { dnsNames, ips, emails, uris := cert.DNSNames, cert.IPAddresses, cert.EmailAddresses, cert.URIs @@ -155,28 +222,51 @@ func appendSubjectCommonName(subject pkix.Name, dnsNames *[]string, ips *[]net.I // in https://cs.opensource.google/go/go/+/refs/tags/go1.17.5:src/crypto/x509/verify.go func (e *NamePolicyEngine) validateNames(dnsNames []string, ips []net.IP, emailAddresses []string, uris []*url.URL) error { - // TODO: return our own type of error? + // nothing to compare against; return early + if e.totalNumberOfConstraints == 0 { + return nil + } + + // TODO: return our own type(s) of error? + // TODO: implement check that requires at least a single name in all of the SANs + subject? - // TODO: set limit on total of all names? In x509 there's a limit on the number of comparisons + // TODO: set limit on total of all names validated? In x509 there's a limit on the number of comparisons // that protects the CA from a DoS (i.e. many heavy comparisons). The x509 implementation takes // 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 { + // if there are DNS names to check, no DNS constraints set, but there are other permitted constraints, + // then return error, because DNS should be explicitly configured to be allowed in that case. In case there are + // (other) excluded constraints, we'll allow a DNS (implicit allow; currently). + if e.numberOfDNSDomainConstraints == 0 && e.totalNumberOfPermittedConstraints > 0 { + return CertificateInvalidError{ + Reason: x509.CANotAuthorizedForThisName, + Detail: fmt.Sprintf("dns %q is not permitted by any constraint", dns), // TODO(hs): change this error (message) + } + } if _, ok := domainToReverseLabels(dns); !ok { return errors.Errorf("cannot parse dns %q", dns) } if err := checkNameConstraints("dns", dns, dns, func(parsedName, constraint interface{}) (bool, error) { - return matchDomainConstraint(parsedName.(string), constraint.(string)) + return e.matchDomainConstraint(parsedName.(string), constraint.(string)) }, e.permittedDNSDomains, e.excludedDNSDomains); err != nil { return err } } for _, ip := range ips { + if e.numberOfIPRangeConstraints == 0 && e.totalNumberOfPermittedConstraints > 0 { + return CertificateInvalidError{ + Reason: x509.CANotAuthorizedForThisName, + Detail: fmt.Sprintf("ip %q is not permitted by any constraint", ip.String()), + } + } if err := checkNameConstraints("ip", ip.String(), ip, func(parsedName, constraint interface{}) (bool, error) { return matchIPConstraint(parsedName.(net.IP), constraint.(*net.IPNet)) @@ -186,22 +276,34 @@ func (e *NamePolicyEngine) validateNames(dnsNames []string, ips []net.IP, emailA } for _, email := range emailAddresses { + if e.numberOfEmailAddressConstraints == 0 && e.totalNumberOfPermittedConstraints > 0 { + return CertificateInvalidError{ + Reason: x509.CANotAuthorizedForThisName, + Detail: fmt.Sprintf("email %q is not permitted by any constraint", email), + } + } mailbox, ok := parseRFC2821Mailbox(email) if !ok { return fmt.Errorf("cannot parse rfc822Name %q", mailbox) } if err := checkNameConstraints("email", email, mailbox, func(parsedName, constraint interface{}) (bool, error) { - return matchEmailConstraint(parsedName.(rfc2821Mailbox), constraint.(string)) + return e.matchEmailConstraint(parsedName.(rfc2821Mailbox), constraint.(string)) }, e.permittedEmailAddresses, e.excludedEmailAddresses); err != nil { return err } } for _, uri := range uris { + if e.numberOfURIDomainConstraints == 0 && e.totalNumberOfPermittedConstraints > 0 { + return CertificateInvalidError{ + Reason: x509.CANotAuthorizedForThisName, + Detail: fmt.Sprintf("uri %q is not permitted by any constraint", uri.String()), + } + } if err := checkNameConstraints("uri", uri.String(), uri, func(parsedName, constraint interface{}) (bool, error) { - return matchURIConstraint(parsedName.(*url.URL), constraint.(string)) + return e.matchURIConstraint(parsedName.(*url.URL), constraint.(string)) }, e.permittedURIDomains, e.excludedURIDomains); err != nil { return err } @@ -217,11 +319,10 @@ func (e *NamePolicyEngine) validateNames(dnsNames []string, ips []net.IP, emailA return nil } -// checkNameConstraints checks that c permits a child certificate to claim the -// given name, of type nameType. The argument parsedName contains the parsed -// form of name, suitable for passing to the match function. The total number -// of comparisons is tracked in the given count and should not exceed the given -// limit. +// checkNameConstraints checks that a name, of type nameType is permitted. +// The argument parsedName contains the parsed form of name, suitable for passing +// to the match function. The total number of comparisons is tracked in the given +// count and should not exceed the given limit. // SOURCE: https://cs.opensource.google/go/go/+/refs/tags/go1.17.5:src/crypto/x509/verify.go func checkNameConstraints( nameType string, @@ -476,13 +577,44 @@ func parseRFC2821Mailbox(in string) (mailbox rfc2821Mailbox, ok bool) { } // SOURCE: https://cs.opensource.google/go/go/+/refs/tags/go1.17.5:src/crypto/x509/verify.go -func matchDomainConstraint(domain, constraint string) (bool, error) { +func (e *NamePolicyEngine) matchDomainConstraint(domain, constraint string) (bool, error) { // The meaning of zero length constraints is not specified, but this // code follows NSS and accepts them as matching everything. if constraint == "" { return true, nil } + // A single whitespace seems to be considered a valid domain, but we don't allow it. + if domain == " " { + return false, nil + } + + // Block domains that start with just a period + // TODO(hs): check if we should allow domains starting with "." at all; not sure if this is allowed in x509 names and certs. + if domain[0] == '.' { + return false, nil + } + + // Block wildcard domains that don't start with exactly "*." (i.e. double wildcards and such) + if domain[0] == '*' && domain[1] != '.' { + return false, nil + } + + // Check if the domain starts with a wildcard and return early if not allowed + if strings.HasPrefix(domain, "*.") && !e.allowLiteralWildcardNames { + return false, nil + } + + // Only allow asterisk at the start of the domain; we don't allow them as part of a domain label or as a (sub)domain label (currently) + if strings.LastIndex(domain, "*") > 0 { + return false, nil + } + + // Don't allow constraints with empty labels in any position + if strings.Contains(constraint, "..") { + return false, nil + } + domainLabels, ok := domainToReverseLabels(domain) if !ok { return false, fmt.Errorf("cannot parse domain %q", domain) @@ -491,7 +623,9 @@ func matchDomainConstraint(domain, constraint string) (bool, error) { // RFC 5280 says that a leading period in a domain name means that at // least one label must be prepended, but only for URI and email // constraints, not DNS constraints. The code also supports that - // behavior for DNS constraints. + // behavior for DNS constraints. In our adaptation of the original + // Go stdlib x509 Name Constraint implementation we look for exactly + // one subdomain, currently. mustHaveSubdomains := false if constraint[0] == '.' { @@ -501,11 +635,22 @@ func matchDomainConstraint(domain, constraint string) (bool, error) { constraintLabels, ok := domainToReverseLabels(constraint) if !ok { - return false, fmt.Errorf("cannot parse domain %q", constraint) + return false, fmt.Errorf("cannot parse domain constraint %q", constraint) } - if len(domainLabels) < len(constraintLabels) || - (mustHaveSubdomains && len(domainLabels) == len(constraintLabels)) { + // fmt.Println(mustHaveSubdomains) + // fmt.Println(constraintLabels) + // fmt.Println(domainLabels) + + expectedNumberOfLabels := len(constraintLabels) + if mustHaveSubdomains { + // we expect exactly one more label if it starts with the "canonical" x509 "wildcard": "." + // in the future we could extend this to support multiple additional labels and/or more + // complex matching. + expectedNumberOfLabels++ + } + + if len(domainLabels) != expectedNumberOfLabels { return false, nil } @@ -552,8 +697,12 @@ func isIPv4(ip net.IP) bool { } // SOURCE: https://cs.opensource.google/go/go/+/refs/tags/go1.17.5:src/crypto/x509/verify.go -func matchEmailConstraint(mailbox rfc2821Mailbox, constraint string) (bool, error) { - // If the constraint contains an @, then it specifies an exact mailbox name. +func (e *NamePolicyEngine) matchEmailConstraint(mailbox rfc2821Mailbox, constraint string) (bool, error) { + // TODO(hs): handle literal wildcard case for emails? Does that even make sense? + // If the constraint contains an @, then it specifies an exact mailbox name (currently) + if strings.Contains(constraint, "*") { + return false, fmt.Errorf("email constraint %q cannot contain asterisk", constraint) + } if strings.Contains(constraint, "@") { constraintMailbox, ok := parseRFC2821Mailbox(constraint) if !ok { @@ -564,11 +713,11 @@ func matchEmailConstraint(mailbox rfc2821Mailbox, constraint string) (bool, erro // Otherwise the constraint is like a DNS constraint of the domain part // of the mailbox. - return matchDomainConstraint(mailbox.domain, constraint) + return e.matchDomainConstraint(mailbox.domain, constraint) } // SOURCE: https://cs.opensource.google/go/go/+/refs/tags/go1.17.5:src/crypto/x509/verify.go -func matchURIConstraint(uri *url.URL, constraint string) (bool, error) { +func (e *NamePolicyEngine) matchURIConstraint(uri *url.URL, constraint string) (bool, error) { // From RFC 5280, Section 4.2.1.10: // “a uniformResourceIdentifier that does not include an authority // component with a host name specified as a fully qualified domain @@ -582,6 +731,11 @@ func matchURIConstraint(uri *url.URL, constraint string) (bool, error) { return false, fmt.Errorf("URI with empty host (%q) cannot be matched against constraints", uri.String()) } + // Block hosts with the wildcard character; no exceptions, also not when wildcards allowed. + if strings.Contains(host, "*") { + return false, fmt.Errorf("URI host %q cannot contain asterisk", uri.String()) + } + if strings.Contains(host, ":") && !strings.HasSuffix(host, "]") { var err error host, _, err = net.SplitHostPort(uri.Host) @@ -592,8 +746,10 @@ func matchURIConstraint(uri *url.URL, constraint string) (bool, error) { if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") || net.ParseIP(host) != nil { - return false, fmt.Errorf("URI with IP (%q) cannot be matched against constraints", uri.String()) + return false, fmt.Errorf("URI with IP %q cannot be matched against constraints", uri.String()) } - return matchDomainConstraint(host, constraint) + // TODO(hs): add checks for scheme, path, etc.; either here, or in a different constraint matcher (to keep this one simple) + + return e.matchDomainConstraint(host, constraint) } diff --git a/policy/x509/x509_test.go b/policy/x509/x509_test.go index 103c6009..a2977214 100755 --- a/policy/x509/x509_test.go +++ b/policy/x509/x509_test.go @@ -10,31 +10,582 @@ import ( "github.com/smallstep/assert" ) -func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { - // TODO(hs): refactor these tests into using validateNames instead of AreCertificateNamesAllowed - // TODO(hs): the functionality in the policy engine is a nice candidate for trying fuzzing on - type fields struct { - verifySubjectCommonName bool - permittedDNSDomains []string - excludedDNSDomains []string - permittedIPRanges []*net.IPNet - excludedIPRanges []*net.IPNet - permittedEmailAddresses []string - excludedEmailAddresses []string - permittedURIDomains []string - excludedURIDomains []string +// TODO(hs): the functionality in the policy engine is a nice candidate for trying fuzzing on +// TODO(hs): more complex uses cases that combine multiple names and permitted/excluded entries +// TODO(hs): check errors (reasons) are as expected + +func TestNamePolicyEngine_matchDomainConstraint(t *testing.T) { + tests := []struct { + name string + engine *NamePolicyEngine + 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, + wantErr: false, + }, + { + 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, + wantErr: false, + }, + { + name: "fail/specific-domain", + engine: &NamePolicyEngine{}, + domain: "www.example.com", + constraint: "host.example.com", + want: false, + wantErr: false, + }, + { + name: "fail/single-whitespace-domain", + engine: &NamePolicyEngine{}, + domain: " ", + constraint: "host.example.com", + want: false, + wantErr: false, + }, + { + name: "fail/period-domain", + engine: &NamePolicyEngine{}, + domain: ".host.example.com", + constraint: ".example.com", + want: false, + wantErr: false, + }, + { + name: "fail/wrong-asterisk-prefix", + engine: &NamePolicyEngine{}, + domain: "*Xexample.com", + constraint: ".example.com", + want: false, + wantErr: false, + }, + { + name: "fail/asterisk-in-domain", + engine: &NamePolicyEngine{}, + domain: "e*ample.com", + constraint: ".com", + want: false, + wantErr: false, + }, + { + name: "fail/asterisk-label", + engine: &NamePolicyEngine{}, + domain: "example.*.local", + constraint: ".local", + want: false, + wantErr: false, + }, + { + name: "fail/multiple-periods", + engine: &NamePolicyEngine{}, + domain: "example.local", + constraint: "..local", + want: false, + wantErr: false, + }, + { + name: "fail/error-parsing-domain", + engine: &NamePolicyEngine{}, + domain: string([]byte{0}), + constraint: ".local", + want: false, + wantErr: true, + }, + { + name: "fail/error-parsing-constraint", + engine: &NamePolicyEngine{}, + domain: "example.local", + constraint: string([]byte{0}), + want: false, + wantErr: true, + }, + { + name: "fail/no-subdomain", + engine: &NamePolicyEngine{}, + domain: "local", + constraint: ".local", + want: false, + wantErr: false, + }, + { + name: "fail/too-many-subdomains", + engine: &NamePolicyEngine{}, + domain: "www.example.local", + constraint: ".local", + want: false, + wantErr: false, + }, + { + name: "fail/wrong-domain", + engine: &NamePolicyEngine{}, + domain: "example.notlocal", + constraint: ".local", + want: false, + wantErr: false, + }, + { + name: "ok/empty-constraint", + engine: &NamePolicyEngine{}, + domain: "www.example.com", + constraint: "", + want: true, + wantErr: false, + }, + { + 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/specific-domain", + engine: &NamePolicyEngine{}, + domain: "www.example.com", + constraint: "www.example.com", + want: true, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.engine.matchDomainConstraint(tt.domain, tt.constraint) + if (err != nil) != tt.wantErr { + t.Errorf("NamePolicyEngine.matchDomainConstraint() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("NamePolicyEngine.matchDomainConstraint() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_matchIPConstraint(t *testing.T) { + nat64IP, nat64Net, err := net.ParseCIDR("64:ff9b::/96") + assert.FatalError(t, err) + tests := []struct { + name string + ip net.IP + constraint *net.IPNet + want bool + wantErr bool + }{ + { + name: "false/ipv4-in-ipv6-nat64", + ip: net.ParseIP("192.0.2.128"), + constraint: nat64Net, + want: false, + wantErr: false, + }, + { + name: "ok/ipv4", + ip: net.ParseIP("127.0.0.1"), + constraint: &net.IPNet{ + IP: net.ParseIP("127.0.0.0"), + Mask: net.IPv4Mask(255, 255, 255, 0), + }, + want: true, + wantErr: false, + }, + { + name: "ok/ipv6", + ip: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7335"), + constraint: &net.IPNet{ + IP: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), + Mask: net.CIDRMask(120, 128), + }, + want: true, + wantErr: false, + }, + { + name: "ok/ipv4-in-ipv6", // ipv4 in ipv6 addresses are considered the same in the current implementation, because Go parses them as IPv4 + ip: net.ParseIP("::ffff:192.0.2.128"), + constraint: &net.IPNet{ + IP: net.ParseIP("192.0.2.0"), + Mask: net.IPv4Mask(255, 255, 255, 0), + }, + want: true, + wantErr: false, + }, + { + name: "ok/ipv4-in-ipv6-nat64-fixed-ip", + ip: nat64IP, + constraint: nat64Net, + want: true, + wantErr: false, + }, + { + name: "ok/ipv4-in-ipv6-nat64", + ip: net.ParseIP("64:ff9b::192.0.2.129"), + constraint: nat64Net, + want: true, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := matchIPConstraint(tt.ip, tt.constraint) + if (err != nil) != tt.wantErr { + t.Errorf("matchIPConstraint() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("matchIPConstraint() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNamePolicyEngine_matchEmailConstraint(t *testing.T) { + + tests := []struct { + name string + engine *NamePolicyEngine + mailbox rfc2821Mailbox + constraint string + want bool + wantErr bool + }{ + { + name: "fail/asterisk-prefix", + engine: &NamePolicyEngine{}, + mailbox: rfc2821Mailbox{ + local: "mail", + domain: "local", + }, + constraint: "*@example.com", + want: false, + wantErr: true, + }, + { + name: "fail/asterisk-label", + engine: &NamePolicyEngine{}, + mailbox: rfc2821Mailbox{ + local: "mail", + domain: "local", + }, + constraint: "@host.*.example.com", + want: false, + wantErr: true, + }, + { + name: "fail/asterisk-inside-local", + engine: &NamePolicyEngine{}, + mailbox: rfc2821Mailbox{ + local: "mail", + domain: "local", + }, + constraint: "m*il@local", + want: false, + wantErr: true, + }, + { + name: "fail/asterisk-inside-domain", + engine: &NamePolicyEngine{}, + mailbox: rfc2821Mailbox{ + local: "mail", + domain: "local", + }, + constraint: "@h*st.example.com", + want: false, + wantErr: true, + }, + { + name: "fail/parse-email", + engine: &NamePolicyEngine{}, + mailbox: rfc2821Mailbox{ + local: "mail", + domain: "local", + }, + constraint: "@example.com", + want: false, + wantErr: true, + }, + { + name: "fail/wildcard", + engine: &NamePolicyEngine{}, + mailbox: rfc2821Mailbox{ + local: "mail", + domain: "local", + }, + constraint: "example.com", + want: false, + wantErr: false, + }, + { + name: "fail/wildcard-x509-period", + engine: &NamePolicyEngine{}, + mailbox: rfc2821Mailbox{ + local: "mail", + domain: "local", + }, + constraint: ".local", // "wildcard" for the local domain; requires exactly 1 subdomain + want: false, + wantErr: false, + }, + { + name: "fail/specific-mail-wrong-domain", + engine: &NamePolicyEngine{}, + mailbox: rfc2821Mailbox{ + local: "mail", + domain: "local", + }, + constraint: "mail@example.com", + want: false, + wantErr: false, + }, + { + name: "fail/specific-mail-wrong-local", + engine: &NamePolicyEngine{}, + mailbox: rfc2821Mailbox{ + local: "root", + domain: "example.com", + }, + constraint: "mail@example.com", + want: false, + wantErr: false, + }, + { + name: "ok/wildcard", + engine: &NamePolicyEngine{}, + mailbox: rfc2821Mailbox{ + local: "mail", + domain: "local", + }, + constraint: "local", // "wildcard" for the local domain + want: true, + wantErr: false, + }, + { + name: "ok/wildcard-x509-period", + engine: &NamePolicyEngine{}, + mailbox: rfc2821Mailbox{ + local: "mail", + domain: "example.local", + }, + constraint: ".local", // "wildcard" for the local domain; requires exactly 1 subdomain + want: true, + wantErr: false, + }, + { + name: "ok/specific-mail", + engine: &NamePolicyEngine{}, + mailbox: rfc2821Mailbox{ + local: "mail", + domain: "local", + }, + constraint: "mail@local", + want: true, + wantErr: false, + }, + { + name: "ok/wildcard-tld", + engine: &NamePolicyEngine{}, + mailbox: rfc2821Mailbox{ + local: "mail", + domain: "example.com", + }, + constraint: "example.com", // "wildcard" for 'example.com' + want: true, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.engine.matchEmailConstraint(tt.mailbox, tt.constraint) + if (err != nil) != tt.wantErr { + t.Errorf("NamePolicyEngine.matchEmailConstraint() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("NamePolicyEngine.matchEmailConstraint() = %v, want %v", got, tt.want) + } + }) } +} + +func TestNamePolicyEngine_matchURIConstraint(t *testing.T) { + tests := []struct { + name string + engine *NamePolicyEngine + uri *url.URL + constraint string + want bool + wantErr bool + }{ + { + name: "fail/empty-host", + engine: &NamePolicyEngine{}, + uri: &url.URL{ + Scheme: "https", + Host: "", + }, + constraint: ".local", + want: false, + wantErr: true, + }, + { + name: "fail/host-with-asterisk-prefix", + engine: &NamePolicyEngine{}, + uri: &url.URL{ + Scheme: "https", + Host: "*.local", + }, + constraint: ".local", + want: false, + wantErr: true, + }, + { + name: "fail/host-with-asterisk-label", + engine: &NamePolicyEngine{}, + uri: &url.URL{ + Scheme: "https", + Host: "host.*.local", + }, + constraint: ".local", + want: false, + wantErr: true, + }, + { + name: "fail/host-with-asterisk-inside", + engine: &NamePolicyEngine{}, + uri: &url.URL{ + Scheme: "https", + Host: "h*st.local", + }, + constraint: ".local", + want: false, + wantErr: true, + }, + { + name: "fail/wildcard", + engine: &NamePolicyEngine{}, + uri: &url.URL{ + Scheme: "https", + Host: "www.example.notlocal", + }, + constraint: ".example.local", // using x509 period as the "wildcard"; expects a single subdomain + want: false, + wantErr: false, + }, + { + name: "fail/wildcard-subdomains-too-deep", + engine: &NamePolicyEngine{}, + uri: &url.URL{ + Scheme: "https", + Host: "www.sub.example.local", + }, + constraint: ".example.local", // using x509 period as the "wildcard"; expects a single subdomain + want: false, + wantErr: false, + }, + { + name: "fail/host-with-port-split-error", + engine: &NamePolicyEngine{}, + uri: &url.URL{ + Scheme: "https", + Host: "www.example.local::8080", + }, + constraint: ".example.local", // using x509 period as the "wildcard"; expects a single subdomain + want: false, + wantErr: true, + }, + { + name: "fail/host-with-ipv4", + engine: &NamePolicyEngine{}, + uri: &url.URL{ + Scheme: "https", + Host: "127.0.0.1", + }, + constraint: ".example.local", // using x509 period as the "wildcard"; expects a single subdomain + want: false, + wantErr: true, + }, + { + name: "fail/host-with-ipv6", + engine: &NamePolicyEngine{}, + uri: &url.URL{ + Scheme: "https", + Host: "[2001:0db8:85a3:0000:0000:8a2e:0370:7334]", + }, + constraint: ".example.local", // using x509 period as the "wildcard"; expects a single subdomain + want: false, + wantErr: true, + }, + { + name: "ok/wildcard", + engine: &NamePolicyEngine{}, + uri: &url.URL{ + Scheme: "https", + Host: "www.example.local", + }, + constraint: ".example.local", // using x509 period as the "wildcard"; expects a single subdomain + want: true, + wantErr: false, + }, + { + name: "ok/host-with-port", + engine: &NamePolicyEngine{}, + uri: &url.URL{ + Scheme: "https", + Host: "www.example.local:8080", + }, + constraint: ".example.local", // using x509 period as the "wildcard"; expects a single subdomain + want: true, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.engine.matchURIConstraint(tt.uri, tt.constraint) + if (err != nil) != tt.wantErr { + t.Errorf("NamePolicyEngine.matchURIConstraint() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("NamePolicyEngine.matchURIConstraint() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { tests := []struct { name string - fields fields + options []NamePolicyOption cert *x509.Certificate want bool wantErr bool }{ + // SINGLE SAN TYPE PERMITTED FAILURE TESTS { name: "fail/dns-permitted", - fields: fields{ - permittedDNSDomains: []string{".local"}, + options: []NamePolicyOption{ + AddPermittedDNSDomain("*.local"), }, cert: &x509.Certificate{ DNSNames: []string{"www.example.com"}, @@ -42,10 +593,23 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { want: false, wantErr: true, }, + { + name: "fail/dns-permitted-wildcard-literal-x509", + options: []NamePolicyOption{ + AddPermittedDNSDomain("*.x509local"), + }, + cert: &x509.Certificate{ + DNSNames: []string{ + "*.x509local", + }, + }, + want: false, + wantErr: true, + }, { name: "fail/dns-permitted-single-host", - fields: fields{ - permittedDNSDomains: []string{"host.local"}, + options: []NamePolicyOption{ + AddPermittedDNSDomain("host.local"), }, cert: &x509.Certificate{ DNSNames: []string{"differenthost.local"}, @@ -55,8 +619,8 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { }, { name: "fail/dns-permitted-no-label", - fields: fields{ - permittedDNSDomains: []string{".local"}, + options: []NamePolicyOption{ + AddPermittedDNSDomain("*.local"), }, cert: &x509.Certificate{ DNSNames: []string{"local"}, @@ -66,8 +630,8 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { }, { name: "fail/dns-permitted-empty-label", - fields: fields{ - permittedDNSDomains: []string{".local"}, + options: []NamePolicyOption{ + AddPermittedDNSDomain("*.local"), }, cert: &x509.Certificate{ DNSNames: []string{"www..local"}, @@ -76,68 +640,73 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { wantErr: true, }, { - name: "fail/dns-excluded", - fields: fields{ - excludedDNSDomains: []string{"example.com"}, + name: "fail/dns-permitted-dot-domain", + options: []NamePolicyOption{ + AddPermittedDNSDomain("*.local"), }, cert: &x509.Certificate{ - DNSNames: []string{"www.example.com"}, + DNSNames: []string{ + ".local", + }, }, want: false, wantErr: true, }, { - name: "fail/dns-excluded-single-host", - fields: fields{ - excludedDNSDomains: []string{"example.com"}, + name: "fail/dns-permitted-wildcard-multiple-subdomains", + options: []NamePolicyOption{ + AddPermittedDNSDomain("*.local"), }, cert: &x509.Certificate{ - DNSNames: []string{"example.com"}, + DNSNames: []string{ + "sub.example.local", + }, }, want: false, wantErr: true, }, { - name: "fail/ipv4-permitted", - fields: fields{ - permittedIPRanges: []*net.IPNet{ - { - IP: net.ParseIP("127.0.0.1"), - Mask: net.IPv4Mask(255, 255, 255, 0), - }, - }, + name: "fail/dns-permitted-wildcard-literal", + options: []NamePolicyOption{ + AddPermittedDNSDomain("*.local"), }, cert: &x509.Certificate{ - IPAddresses: []net.IP{net.ParseIP("1.1.1.1")}, + DNSNames: []string{ + "*.local", + }, }, want: false, wantErr: true, }, { - name: "fail/ipv4-excluded", - fields: fields{ - excludedIPRanges: []*net.IPNet{ - { - IP: net.ParseIP("127.0.0.1"), - Mask: net.IPv4Mask(255, 255, 255, 0), + name: "fail/ipv4-permitted", + options: []NamePolicyOption{ + AddPermittedIPRanges( + []*net.IPNet{ + { + IP: net.ParseIP("127.0.0.1"), + Mask: net.IPv4Mask(255, 255, 255, 0), + }, }, - }, + ), }, cert: &x509.Certificate{ - IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, + IPAddresses: []net.IP{net.ParseIP("1.1.1.1")}, }, want: false, wantErr: true, }, { name: "fail/ipv6-permitted", - fields: fields{ - permittedIPRanges: []*net.IPNet{ - { - IP: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), - Mask: net.CIDRMask(120, 128), + options: []NamePolicyOption{ + AddPermittedIPRanges( + []*net.IPNet{ + { + IP: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), + Mask: net.CIDRMask(120, 128), + }, }, - }, + ), }, cert: &x509.Certificate{ IPAddresses: []net.IP{net.ParseIP("3001:0db8:85a3:0000:0000:8a2e:0370:7334")}, @@ -146,64 +715,67 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { wantErr: true, }, { - name: "fail/ipv6-excluded", - fields: fields{ - excludedIPRanges: []*net.IPNet{ - { - IP: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), - Mask: net.CIDRMask(120, 128), - }, - }, + name: "fail/mail-permitted-wildcard", + options: []NamePolicyOption{ + AddPermittedEmailAddress("@example.com"), }, cert: &x509.Certificate{ - IPAddresses: []net.IP{net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334")}, + EmailAddresses: []string{ + "test@local.com", + }, }, want: false, wantErr: true, }, { - name: "fail/mail-permitted", - fields: fields{ - permittedEmailAddresses: []string{"example.local"}, + name: "fail/mail-permitted-wildcard-x509", + options: []NamePolicyOption{ + AddPermittedEmailAddress("example.com"), }, cert: &x509.Certificate{ - EmailAddresses: []string{"mail@example.com"}, + EmailAddresses: []string{ + "test@local.com", + }, }, want: false, wantErr: true, }, { - name: "fail/mail-permitted-period-domain", - fields: fields{ - permittedEmailAddresses: []string{".example.local"}, // any address in a domain, but not on the host example.local + name: "fail/mail-permitted-specific-mailbox", + options: []NamePolicyOption{ + AddPermittedEmailAddress("test@local.com"), }, cert: &x509.Certificate{ - EmailAddresses: []string{"mail@example.local"}, + EmailAddresses: []string{ + "root@local.com", + }, }, want: false, wantErr: true, }, { - name: "fail/mail-excluded", - fields: fields{ - excludedEmailAddresses: []string{"example.local"}, + name: "fail/mail-permitted-wildcard-subdomain", + options: []NamePolicyOption{ + AddPermittedEmailAddress("@example.com"), }, cert: &x509.Certificate{ - EmailAddresses: []string{"mail@example.local"}, + EmailAddresses: []string{ + "test@sub.example.com", + }, }, want: false, wantErr: true, }, { - name: "fail/uri-permitted", - fields: fields{ - permittedURIDomains: []string{".example.com"}, + name: "fail/permitted-uri-domain-wildcard", + options: []NamePolicyOption{ + AddPermittedURIDomain("*.local"), }, cert: &x509.Certificate{ URIs: []*url.URL{ { Scheme: "https", - Host: "www.example.local", + Host: "example.com", }, }, }, @@ -211,15 +783,15 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { wantErr: true, }, { - name: "fail/uri-permitted-period-host", - fields: fields{ - permittedURIDomains: []string{".example.local"}, + name: "fail/permitted-uri", + options: []NamePolicyOption{ + AddPermittedURIDomain("test.local"), }, cert: &x509.Certificate{ URIs: []*url.URL{ { Scheme: "https", - Host: "example.local", + Host: "bad.local", }, }, }, @@ -227,90 +799,113 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { wantErr: true, }, { - name: "fail/uri-permitted-period-host-certificate", - fields: fields{ - permittedURIDomains: []string{".example.local"}, + name: "fail/permitted-uri-with-literal-wildcard", // don't allow literal wildcard in URI, e.g. xxxx://*.domain.tld + options: []NamePolicyOption{ + AddPermittedURIDomain("*.local"), }, cert: &x509.Certificate{ URIs: []*url.URL{ { Scheme: "https", - Host: ".example.local", + Host: "*.local", }, }, }, want: false, wantErr: true, }, + // SINGLE SAN TYPE EXCLUDED FAILURE TESTS { - name: "fail/uri-permitted-empty-host", - fields: fields{ - permittedURIDomains: []string{".example.com"}, + name: "fail/dns-excluded", + options: []NamePolicyOption{ + AddExcludedDNSDomain("*.example.com"), }, cert: &x509.Certificate{ - URIs: []*url.URL{ - { - Scheme: "https", - Host: "", - }, - }, + DNSNames: []string{"www.example.com"}, }, want: false, wantErr: true, }, { - name: "fail/uri-permitted-port-missing", - fields: fields{ - permittedURIDomains: []string{".example.com"}, + name: "fail/dns-excluded-single-host", + options: []NamePolicyOption{ + AddExcludedDNSDomain("host.example.com"), }, cert: &x509.Certificate{ - URIs: []*url.URL{ - { - Scheme: "https", - Host: "example.local::", - }, - }, + DNSNames: []string{"host.example.com"}, }, want: false, wantErr: true, }, { - name: "fail/uri-permitted-ip", - fields: fields{ - permittedURIDomains: []string{".example.com"}, + name: "fail/ipv4-excluded", + options: []NamePolicyOption{ + AddExcludedIPRanges( + []*net.IPNet{ + { + IP: net.ParseIP("127.0.0.1"), + Mask: net.IPv4Mask(255, 255, 255, 0), + }, + }, + ), }, cert: &x509.Certificate{ - URIs: []*url.URL{ - { - Scheme: "https", - Host: "127.0.0.1", + IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, + }, + want: false, + wantErr: true, + }, + { + name: "fail/ipv6-excluded", + options: []NamePolicyOption{ + AddExcludedIPRanges( + []*net.IPNet{ + { + IP: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), + Mask: net.CIDRMask(120, 128), + }, }, - }, + ), + }, + cert: &x509.Certificate{ + IPAddresses: []net.IP{net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334")}, + }, + want: false, + wantErr: true, + }, + { + name: "fail/mail-excluded", + options: []NamePolicyOption{ + AddExcludedEmailAddress("@example.com"), + }, + cert: &x509.Certificate{ + EmailAddresses: []string{"mail@example.com"}, }, want: false, wantErr: true, }, { name: "fail/uri-excluded", - fields: fields{ - excludedURIDomains: []string{".example.local"}, + options: []NamePolicyOption{ + AddExcludedURIDomain("*.example.com"), }, cert: &x509.Certificate{ URIs: []*url.URL{ { Scheme: "https", - Host: "www.example.local", + Host: "www.example.com", }, }, }, want: false, wantErr: true, }, + // SUBJECT FAILURE TESTS { name: "fail/subject-dns-permitted", - fields: fields{ - verifySubjectCommonName: true, - permittedDNSDomains: []string{".local"}, + options: []NamePolicyOption{ + WithSubjectCommonNameVerification(), + AddPermittedDNSDomain("*.local"), }, cert: &x509.Certificate{ Subject: pkix.Name{ @@ -322,9 +917,9 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { }, { name: "fail/subject-dns-excluded", - fields: fields{ - verifySubjectCommonName: true, - excludedDNSDomains: []string{".local"}, + options: []NamePolicyOption{ + WithSubjectCommonNameVerification(), + AddExcludedDNSDomain("*.local"), }, cert: &x509.Certificate{ Subject: pkix.Name{ @@ -336,14 +931,16 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { }, { name: "fail/subject-ipv4-permitted", - fields: fields{ - verifySubjectCommonName: true, - permittedIPRanges: []*net.IPNet{ - { - IP: net.ParseIP("127.0.0.1"), - Mask: net.IPv4Mask(255, 255, 255, 0), + options: []NamePolicyOption{ + WithSubjectCommonNameVerification(), + AddPermittedIPRanges( + []*net.IPNet{ + { + IP: net.ParseIP("127.0.0.1"), + Mask: net.IPv4Mask(255, 255, 255, 0), + }, }, - }, + ), }, cert: &x509.Certificate{ Subject: pkix.Name{ @@ -355,18 +952,20 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { }, { name: "fail/subject-ipv4-excluded", - fields: fields{ - verifySubjectCommonName: true, - excludedIPRanges: []*net.IPNet{ - { - IP: net.ParseIP("127.0.0.1"), - Mask: net.IPv4Mask(255, 255, 255, 0), + options: []NamePolicyOption{ + WithSubjectCommonNameVerification(), + AddExcludedIPRanges( + []*net.IPNet{ + { + IP: net.ParseIP("127.0.0.1"), + Mask: net.IPv4Mask(255, 255, 255, 0), + }, }, - }, + ), }, cert: &x509.Certificate{ Subject: pkix.Name{ - CommonName: "127.0.0.1", + CommonName: "127.0.0.30", }, }, want: false, @@ -374,14 +973,16 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { }, { name: "fail/subject-ipv6-permitted", - fields: fields{ - verifySubjectCommonName: true, - permittedIPRanges: []*net.IPNet{ - { - IP: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), - Mask: net.CIDRMask(120, 128), + options: []NamePolicyOption{ + WithSubjectCommonNameVerification(), + AddPermittedIPRanges( + []*net.IPNet{ + { + IP: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), + Mask: net.CIDRMask(120, 128), + }, }, - }, + ), }, cert: &x509.Certificate{ Subject: pkix.Name{ @@ -393,14 +994,16 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { }, { name: "fail/subject-ipv6-excluded", - fields: fields{ - verifySubjectCommonName: true, - excludedIPRanges: []*net.IPNet{ - { - IP: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), - Mask: net.CIDRMask(120, 128), + options: []NamePolicyOption{ + WithSubjectCommonNameVerification(), + AddExcludedIPRanges( + []*net.IPNet{ + { + IP: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), + Mask: net.CIDRMask(120, 128), + }, }, - }, + ), }, cert: &x509.Certificate{ Subject: pkix.Name{ @@ -412,9 +1015,9 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { }, { name: "fail/subject-email-permitted", - fields: fields{ - verifySubjectCommonName: true, - permittedEmailAddresses: []string{"example.local"}, + options: []NamePolicyOption{ + WithSubjectCommonNameVerification(), + AddPermittedEmailAddress("@example.local"), }, cert: &x509.Certificate{ Subject: pkix.Name{ @@ -426,9 +1029,9 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { }, { name: "fail/subject-email-excluded", - fields: fields{ - verifySubjectCommonName: true, - excludedEmailAddresses: []string{"example.local"}, + options: []NamePolicyOption{ + WithSubjectCommonNameVerification(), + AddExcludedEmailAddress("@example.local"), }, cert: &x509.Certificate{ Subject: pkix.Name{ @@ -440,9 +1043,9 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { }, { name: "fail/subject-uri-permitted", - fields: fields{ - verifySubjectCommonName: true, - permittedURIDomains: []string{".example.com"}, + options: []NamePolicyOption{ + WithSubjectCommonNameVerification(), + AddPermittedURIDomain("*.example.com"), }, cert: &x509.Certificate{ Subject: pkix.Name{ @@ -454,9 +1057,9 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { }, { name: "fail/subject-uri-excluded", - fields: fields{ - verifySubjectCommonName: true, - excludedURIDomains: []string{".example.com"}, + options: []NamePolicyOption{ + WithSubjectCommonNameVerification(), + AddExcludedURIDomain("*.example.com"), }, cert: &x509.Certificate{ Subject: pkix.Name{ @@ -466,25 +1069,199 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { want: false, wantErr: true, }, + // DIFFERENT SAN PERMITTED FAILURE TESTS + { + name: "fail/dns-permitted-with-ip-name", // when only DNS is permitted, IPs are not allowed. + options: []NamePolicyOption{ + AddPermittedDNSDomain("*.local"), + }, + cert: &x509.Certificate{ + IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, + }, + want: false, + wantErr: true, + }, + { + name: "fail/dns-permitted-with-mail", // when only DNS is permitted, mails are not allowed. + options: []NamePolicyOption{ + AddPermittedDNSDomain("*.local"), + }, + cert: &x509.Certificate{ + EmailAddresses: []string{"mail@smallstep.com"}, + }, + want: false, + wantErr: true, + }, + { + name: "fail/dns-permitted-with-uri", // when only DNS is permitted, URIs are not allowed. + options: []NamePolicyOption{ + AddPermittedDNSDomain("*.local"), + }, + cert: &x509.Certificate{ + URIs: []*url.URL{ + { + Scheme: "https", + Host: "www.example.com", + }, + }, + }, + want: false, + wantErr: true, + }, + { + name: "fail/ip-permitted-with-dns-name", // when only IP is permitted, DNS names are not allowed. + options: []NamePolicyOption{ + AddPermittedIPRanges( + []*net.IPNet{ + { + IP: net.ParseIP("127.0.0.1"), + Mask: net.IPv4Mask(255, 255, 255, 0), + }, + }, + ), + }, + cert: &x509.Certificate{ + DNSNames: []string{"www.example.com"}, + }, + want: false, + wantErr: true, + }, + { + name: "fail/ip-permitted-with-mail", // when only IP is permitted, mails are not allowed. + options: []NamePolicyOption{ + AddPermittedIPRanges( + []*net.IPNet{ + { + IP: net.ParseIP("127.0.0.1"), + Mask: net.IPv4Mask(255, 255, 255, 0), + }, + }, + ), + }, + cert: &x509.Certificate{ + EmailAddresses: []string{"mail@smallstep.com"}, + }, + want: false, + wantErr: true, + }, + { + name: "fail/ip-permitted-with-uri", // when only IP is permitted, URIs are not allowed. + options: []NamePolicyOption{ + AddPermittedIPRanges( + []*net.IPNet{ + { + IP: net.ParseIP("127.0.0.1"), + Mask: net.IPv4Mask(255, 255, 255, 0), + }, + }, + ), + }, + cert: &x509.Certificate{ + URIs: []*url.URL{ + { + Scheme: "https", + Host: "www.example.com", + }, + }, + }, + want: false, + wantErr: true, + }, + { + name: "fail/mail-permitted-with-dns-name", // when only mail is permitted, DNS names are not allowed. + options: []NamePolicyOption{ + AddPermittedEmailAddress("@example.com"), + }, + cert: &x509.Certificate{ + DNSNames: []string{"www.example.com"}, + }, + want: false, + wantErr: true, + }, + { + name: "fail/mail-permitted-with-ip", // when only mail is permitted, IPs are not allowed. + options: []NamePolicyOption{ + AddPermittedEmailAddress("@example.com"), + }, + cert: &x509.Certificate{ + IPAddresses: []net.IP{ + net.ParseIP("127.0.0.1"), + }, + }, + want: false, + wantErr: true, + }, + { + name: "fail/mail-permitted-with-uri", // when only mail is permitted, URIs are not allowed. + options: []NamePolicyOption{ + AddPermittedEmailAddress("@example.com"), + }, + cert: &x509.Certificate{ + URIs: []*url.URL{ + { + Scheme: "https", + Host: "www.example.com", + }, + }, + }, + want: false, + wantErr: true, + }, + { + name: "fail/uri-permitted-with-dns-name", // when only URI is permitted, DNS names are not allowed. + options: []NamePolicyOption{ + AddPermittedURIDomain("*.local"), + }, + cert: &x509.Certificate{ + DNSNames: []string{"host.local"}, + }, + want: false, + wantErr: true, + }, + { + name: "fail/uri-permitted-with-ip-name", // when only URI is permitted, IPs are not allowed. + options: []NamePolicyOption{ + AddPermittedURIDomain("*.local"), + }, + cert: &x509.Certificate{ + IPAddresses: []net.IP{ + net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), + }, + }, + want: false, + wantErr: true, + }, + { + name: "fail/uri-permitted-with-ip-name", // when only URI is permitted, mails are not allowed. + options: []NamePolicyOption{ + AddPermittedURIDomain("*.local"), + }, + cert: &x509.Certificate{ + EmailAddresses: []string{"mail@smallstep.com"}, + }, + want: false, + wantErr: true, + }, + // COMBINED FAILURE TESTS { name: "fail/combined-simple-all-badhost.local", - fields: fields{ - verifySubjectCommonName: true, - permittedDNSDomains: []string{".local"}, - permittedIPRanges: []*net.IPNet{{IP: net.ParseIP("127.0.0.1"), Mask: net.IPv4Mask(255, 255, 255, 0)}}, - permittedEmailAddresses: []string{"example.local"}, - permittedURIDomains: []string{".example.local"}, - excludedDNSDomains: []string{"badhost.local"}, - excludedIPRanges: []*net.IPNet{{IP: net.ParseIP("1.1.1.1"), Mask: net.IPv4Mask(255, 255, 255, 0)}}, - excludedEmailAddresses: []string{"badmail@example.local"}, - excludedURIDomains: []string{"https://badwww.example.local"}, + options: []NamePolicyOption{ + WithSubjectCommonNameVerification(), + WithPermittedDNSDomain("*.local"), + WithPermittedCIDR("127.0.0.1/24"), + WithPermittedEmailAddress("@example.local"), + WithPermittedURIDomain("*.example.local"), + WithExcludedDNSDomain("badhost.local"), + WithExcludedCIDR("127.0.0.128/25"), + WithExcludedEmailAddress("badmail@example.local"), + WithExcludedURIDomain("badwww.example.local"), }, cert: &x509.Certificate{ Subject: pkix.Name{ CommonName: "badhost.local", }, DNSNames: []string{"example.local"}, - IPAddresses: []net.IP{net.ParseIP("127.0.0.130")}, + IPAddresses: []net.IP{net.ParseIP("127.0.0.40")}, EmailAddresses: []string{"mail@example.local"}, URIs: []*url.URL{ { @@ -496,9 +1273,10 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { want: false, wantErr: true, }, + // NO CONSTRAINT SUCCESS TESTS { - name: "ok/no-constraints", - fields: fields{}, + name: "ok/dns-no-constraints", + options: []NamePolicyOption{}, cert: &x509.Certificate{ DNSNames: []string{"www.example.com"}, }, @@ -506,167 +1284,219 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { wantErr: false, }, { - name: "ok/empty-dns-constraint", - fields: fields{ - permittedDNSDomains: []string{""}, - }, + name: "ok/ipv4-no-constraints", + options: []NamePolicyOption{}, cert: &x509.Certificate{ - DNSNames: []string{"example.local"}, + IPAddresses: []net.IP{ + net.ParseIP("127.0.0.1"), + }, }, want: true, wantErr: false, }, { - name: "ok/dns-permitted", - fields: fields{ - permittedDNSDomains: []string{".local"}, - }, + name: "ok/ipv6-no-constraints", + options: []NamePolicyOption{}, cert: &x509.Certificate{ - DNSNames: []string{"example.local"}, + IPAddresses: []net.IP{ + net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), + }, }, want: true, wantErr: false, }, { - name: "ok/dns-excluded", - fields: fields{ - excludedDNSDomains: []string{".notlocal"}, - }, + name: "ok/mail-no-constraints", + options: []NamePolicyOption{}, cert: &x509.Certificate{ - DNSNames: []string{"example.local"}, + EmailAddresses: []string{"mail@smallstep.com"}, }, want: true, wantErr: false, }, { - name: "ok/ipv4-permitted", - fields: fields{ - permittedIPRanges: []*net.IPNet{ + name: "ok/uri-no-constraints", + options: []NamePolicyOption{}, + cert: &x509.Certificate{ + URIs: []*url.URL{ { - IP: net.ParseIP("127.0.0.1"), - Mask: net.IPv4Mask(255, 255, 255, 0), + Scheme: "https", + Host: "www.example.com", }, }, }, + want: true, + wantErr: false, + }, + { + name: "ok/subject-no-constraints", + options: []NamePolicyOption{ + WithSubjectCommonNameVerification(), + }, cert: &x509.Certificate{ - IPAddresses: []net.IP{net.ParseIP("127.0.0.20")}, + Subject: pkix.Name{ + CommonName: "www.example.com", + }, }, want: true, wantErr: false, }, { - name: "ok/ipv4-excluded", - fields: fields{ - excludedIPRanges: []*net.IPNet{ - { - IP: net.ParseIP("127.0.0.1"), - Mask: net.IPv4Mask(255, 255, 255, 0), - }, + name: "ok/subject-empty-no-constraints", + options: []NamePolicyOption{ + WithSubjectCommonNameVerification(), + }, + cert: &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "", }, }, + want: true, + wantErr: false, + }, + // SINGLE SAN TYPE PERMITTED SUCCESS TESTS + { + name: "ok/dns-permitted", + options: []NamePolicyOption{ + AddPermittedDNSDomain("*.local"), + }, cert: &x509.Certificate{ - IPAddresses: []net.IP{net.ParseIP("10.10.10.10")}, + DNSNames: []string{"example.local"}, }, want: true, wantErr: false, }, { - name: "ok/ipv6-permitted", - fields: fields{ - permittedIPRanges: []*net.IPNet{ - { - IP: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), - Mask: net.CIDRMask(120, 128), - }, + name: "ok/dns-permitted-wildcard", + options: []NamePolicyOption{ + AddPermittedDNSDomain("*.local"), + AddPermittedDNSDomain(".x509local"), + WithAllowLiteralWildcardNames(), + }, + cert: &x509.Certificate{ + DNSNames: []string{ + "host.local", + "test.x509local", }, }, + want: true, + wantErr: false, + }, + { + name: "ok/empty-dns-constraint", + options: []NamePolicyOption{ + AddPermittedDNSDomain(""), + }, cert: &x509.Certificate{ - IPAddresses: []net.IP{net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7339")}, + DNSNames: []string{"example.local"}, }, want: true, wantErr: false, }, { - name: "ok/ipv6-excluded", - fields: fields{ - excludedIPRanges: []*net.IPNet{ - { - IP: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), - Mask: net.CIDRMask(120, 128), - }, + name: "ok/dns-permitted-wildcard-literal", + options: []NamePolicyOption{ + AddPermittedDNSDomain("*.local"), + AddPermittedDNSDomain("*.x509local"), + WithAllowLiteralWildcardNames(), + }, + cert: &x509.Certificate{ + DNSNames: []string{ + "*.local", + "*.x509local", }, }, + want: true, + wantErr: false, + }, + { + name: "ok/dns-permitted-combined", + options: []NamePolicyOption{ + AddPermittedDNSDomain("*.local"), + AddPermittedDNSDomain("*.x509local"), + AddPermittedDNSDomain("host.example.com"), + }, cert: &x509.Certificate{ - IPAddresses: []net.IP{net.ParseIP("2003:0db8:85a3:0000:0000:8a2e:0370:7334")}, + DNSNames: []string{ + "example.local", + "example.x509local", + "host.example.com", + }, }, want: true, wantErr: false, }, { - name: "ok/mail-permitted", - fields: fields{ - permittedEmailAddresses: []string{"example.local"}, + name: "ok/ipv4-permitted", + options: []NamePolicyOption{ + AddPermittedCIDR("127.0.0.1/24"), }, cert: &x509.Certificate{ - EmailAddresses: []string{"mail@example.local"}, + IPAddresses: []net.IP{net.ParseIP("127.0.0.20")}, }, want: true, wantErr: false, }, { - name: "ok/mail-permitted-with-period-domain", - fields: fields{ - permittedEmailAddresses: []string{".example.local"}, + name: "ok/ipv6-permitted", + options: []NamePolicyOption{ + AddPermittedCIDR("2001:0db8:85a3:0000:0000:8a2e:0370:7334/120"), }, cert: &x509.Certificate{ - EmailAddresses: []string{"mail@somehost.example.local"}, + IPAddresses: []net.IP{net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7339")}, }, want: true, wantErr: false, }, { - name: "ok/mail-permitted-with-multiple-labels", - fields: fields{ - permittedEmailAddresses: []string{".example.local"}, + name: "ok/mail-permitted-wildcard", + options: []NamePolicyOption{ + AddPermittedEmailAddress("@example.com"), }, cert: &x509.Certificate{ - EmailAddresses: []string{"mail@sub.www.example.local"}, + EmailAddresses: []string{ + "test@example.com", + }, }, want: true, wantErr: false, }, { - name: "ok/mail-excluded", - fields: fields{ - excludedEmailAddresses: []string{"example.notlocal"}, + name: "ok/mail-permitted-plain-domain", + options: []NamePolicyOption{ + AddPermittedEmailAddress("example.com"), }, cert: &x509.Certificate{ - EmailAddresses: []string{"mail@example.local"}, + EmailAddresses: []string{ + "test@example.com", + }, }, want: true, wantErr: false, }, { - name: "ok/mail-excluded-with-period-domain", - fields: fields{ - excludedEmailAddresses: []string{".example.notlocal"}, + name: "ok/mail-permitted-specific-mailbox", + options: []NamePolicyOption{ + AddPermittedEmailAddress("test@local.com"), }, cert: &x509.Certificate{ - EmailAddresses: []string{"mail@somehost.example.local"}, + EmailAddresses: []string{ + "test@local.com", + }, }, want: true, wantErr: false, }, { - name: "ok/uri-permitted", - fields: fields{ - permittedURIDomains: []string{".example.com"}, + name: "ok/uri-permitted-domain-wildcard", + options: []NamePolicyOption{ + AddPermittedURIDomain("*.local"), }, cert: &x509.Certificate{ URIs: []*url.URL{ { Scheme: "https", - Host: "www.example.com", + Host: "example.local", }, }, }, @@ -674,15 +1504,15 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { wantErr: false, }, { - name: "ok/uri-permitted-with-port", - fields: fields{ - permittedURIDomains: []string{".example.com"}, + name: "ok/uri-permitted-specific-uri", + options: []NamePolicyOption{ + AddPermittedURIDomain("test.local"), }, cert: &x509.Certificate{ URIs: []*url.URL{ { Scheme: "https", - Host: "www.example.com:8080", + Host: "test.local", }, }, }, @@ -690,25 +1520,88 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { wantErr: false, }, { - name: "ok/uri-sub-permitted", - fields: fields{ - permittedURIDomains: []string{"example.com"}, + name: "ok/uri-permitted-with-port", + options: []NamePolicyOption{ + AddPermittedURIDomain(".example.com"), }, cert: &x509.Certificate{ URIs: []*url.URL{ { Scheme: "https", - Host: "sub.host.example.com", + Host: "www.example.com:8080", }, }, }, want: true, wantErr: false, }, + // SINGLE SAN TYPE EXCLUDED SUCCESS TESTS + { + name: "ok/dns-excluded", + options: []NamePolicyOption{ + WithExcludedDNSDomain("*.notlocal"), + }, + cert: &x509.Certificate{ + DNSNames: []string{"example.local"}, + }, + want: true, + wantErr: false, + }, + { + name: "ok/ipv4-excluded", + options: []NamePolicyOption{ + AddExcludedIPRanges( + []*net.IPNet{ + { + IP: net.ParseIP("127.0.0.1"), + Mask: net.IPv4Mask(255, 255, 255, 0), + }, + }, + ), + }, + cert: &x509.Certificate{ + IPAddresses: []net.IP{net.ParseIP("10.10.10.10")}, + }, + want: true, + wantErr: false, + }, + { + name: "ok/ipv6-excluded", + options: []NamePolicyOption{ + AddExcludedCIDR("2001:0db8:85a3:0000:0000:8a2e:0370:7334/120"), + }, + cert: &x509.Certificate{ + IPAddresses: []net.IP{net.ParseIP("2003:0db8:85a3:0000:0000:8a2e:0370:7334")}, + }, + want: true, + wantErr: false, + }, + { + name: "ok/mail-excluded", + options: []NamePolicyOption{ + WithExcludedEmailAddress("@notlocal"), + }, + cert: &x509.Certificate{ + EmailAddresses: []string{"mail@local"}, + }, + want: true, + wantErr: false, + }, + { + name: "ok/mail-excluded-with-subdomain", + options: []NamePolicyOption{ + WithExcludedEmailAddress("@local"), + }, + cert: &x509.Certificate{ + EmailAddresses: []string{"mail@example.local"}, + }, + want: true, + wantErr: false, + }, { name: "ok/uri-excluded", - fields: fields{ - excludedURIDomains: []string{".google.com"}, + options: []NamePolicyOption{ + WithExcludedURIDomain("*.google.com"), }, cert: &x509.Certificate{ URIs: []*url.URL{ @@ -721,11 +1614,12 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { want: true, wantErr: false, }, + // SUBJECT SUCCESS TESTS { name: "ok/subject-empty", - fields: fields{ - verifySubjectCommonName: true, - permittedDNSDomains: []string{".local"}, + options: []NamePolicyOption{ + WithSubjectCommonNameVerification(), + AddPermittedDNSDomain("*.local"), }, cert: &x509.Certificate{ Subject: pkix.Name{ @@ -738,9 +1632,9 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { }, { name: "ok/subject-dns-permitted", - fields: fields{ - verifySubjectCommonName: true, - permittedDNSDomains: []string{".local"}, + options: []NamePolicyOption{ + WithSubjectCommonNameVerification(), + AddPermittedDNSDomain("*.local"), }, cert: &x509.Certificate{ Subject: pkix.Name{ @@ -752,9 +1646,9 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { }, { name: "ok/subject-dns-excluded", - fields: fields{ - verifySubjectCommonName: true, - excludedDNSDomains: []string{".notlocal"}, + options: []NamePolicyOption{ + WithSubjectCommonNameVerification(), + AddExcludedDNSDomain("*.notlocal"), }, cert: &x509.Certificate{ Subject: pkix.Name{ @@ -766,14 +1660,16 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { }, { name: "ok/subject-ipv4-permitted", - fields: fields{ - verifySubjectCommonName: true, - permittedIPRanges: []*net.IPNet{ - { - IP: net.ParseIP("127.0.0.1"), - Mask: net.IPv4Mask(255, 255, 255, 0), + options: []NamePolicyOption{ + WithSubjectCommonNameVerification(), + AddPermittedIPRanges( + []*net.IPNet{ + { + IP: net.ParseIP("127.0.0.1"), + Mask: net.IPv4Mask(255, 255, 255, 0), + }, }, - }, + ), }, cert: &x509.Certificate{ Subject: pkix.Name{ @@ -785,14 +1681,16 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { }, { name: "ok/subject-ipv4-excluded", - fields: fields{ - verifySubjectCommonName: true, - excludedIPRanges: []*net.IPNet{ - { - IP: net.ParseIP("128.0.0.1"), - Mask: net.IPv4Mask(255, 255, 255, 0), + options: []NamePolicyOption{ + WithSubjectCommonNameVerification(), + AddExcludedIPRanges( + []*net.IPNet{ + { + IP: net.ParseIP("128.0.0.1"), + Mask: net.IPv4Mask(255, 255, 255, 0), + }, }, - }, + ), }, cert: &x509.Certificate{ Subject: pkix.Name{ @@ -804,14 +1702,16 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { }, { name: "ok/subject-ipv6-permitted", - fields: fields{ - verifySubjectCommonName: true, - permittedIPRanges: []*net.IPNet{ - { - IP: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), - Mask: net.CIDRMask(120, 128), + options: []NamePolicyOption{ + WithSubjectCommonNameVerification(), + AddPermittedIPRanges( + []*net.IPNet{ + { + IP: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), + Mask: net.CIDRMask(120, 128), + }, }, - }, + ), }, cert: &x509.Certificate{ Subject: pkix.Name{ @@ -823,14 +1723,16 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { }, { name: "ok/subject-ipv6-excluded", - fields: fields{ - verifySubjectCommonName: true, - excludedIPRanges: []*net.IPNet{ - { - IP: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), - Mask: net.CIDRMask(120, 128), + options: []NamePolicyOption{ + WithSubjectCommonNameVerification(), + AddExcludedIPRanges( + []*net.IPNet{ + { + IP: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), + Mask: net.CIDRMask(120, 128), + }, }, - }, + ), }, cert: &x509.Certificate{ Subject: pkix.Name{ @@ -842,9 +1744,9 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { }, { name: "ok/subject-email-permitted", - fields: fields{ - verifySubjectCommonName: true, - permittedEmailAddresses: []string{"example.local"}, + options: []NamePolicyOption{ + WithSubjectCommonNameVerification(), + AddPermittedEmailAddress("@example.local"), }, cert: &x509.Certificate{ Subject: pkix.Name{ @@ -856,9 +1758,9 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { }, { name: "ok/subject-email-excluded", - fields: fields{ - verifySubjectCommonName: true, - excludedEmailAddresses: []string{"example.notlocal"}, + options: []NamePolicyOption{ + WithSubjectCommonNameVerification(), + AddExcludedEmailAddress("@example.notlocal"), }, cert: &x509.Certificate{ Subject: pkix.Name{ @@ -870,9 +1772,9 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { }, { name: "ok/subject-uri-permitted", - fields: fields{ - verifySubjectCommonName: true, - permittedURIDomains: []string{".example.com"}, + options: []NamePolicyOption{ + WithSubjectCommonNameVerification(), + AddPermittedURIDomain("*.example.com"), }, cert: &x509.Certificate{ Subject: pkix.Name{ @@ -884,9 +1786,9 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { }, { name: "ok/subject-uri-excluded", - fields: fields{ - verifySubjectCommonName: true, - excludedURIDomains: []string{".google.com"}, + options: []NamePolicyOption{ + WithSubjectCommonNameVerification(), + AddExcludedURIDomain("*.smallstep.com"), }, cert: &x509.Certificate{ Subject: pkix.Name{ @@ -896,21 +1798,185 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { want: true, wantErr: false, }, + // DIFFERENT SAN TYPE EXCLUDED SUCCESS TESTS + { + name: "ok/dns-excluded-with-ip-name", // when only DNS is exluded, we allow anything else + options: []NamePolicyOption{ + AddExcludedDNSDomain("*.local"), + }, + cert: &x509.Certificate{ + IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, + }, + want: true, + wantErr: false, + }, + { + name: "ok/dns-excluded-with-mail", // when only DNS is exluded, we allow anything else + options: []NamePolicyOption{ + AddExcludedDNSDomain("*.local"), + }, + cert: &x509.Certificate{ + EmailAddresses: []string{"mail@example.com"}, + }, + want: true, + wantErr: false, + }, + { + name: "ok/dns-excluded-with-mail", // when only DNS is exluded, we allow anything else + options: []NamePolicyOption{ + AddExcludedDNSDomain("*.local"), + }, + cert: &x509.Certificate{ + URIs: []*url.URL{ + { + Scheme: "https", + Host: "www.example.com", + }, + }, + }, + want: true, + wantErr: false, + }, + { + name: "ok/ip-excluded-with-dns", // when only IP is exluded, we allow anything else + options: []NamePolicyOption{ + WithExcludedCIDR("127.0.0.1/24"), + }, + cert: &x509.Certificate{ + DNSNames: []string{"test.local"}, + }, + want: true, + wantErr: false, + }, + { + name: "ok/ip-excluded-with-mail", // when only IP is exluded, we allow anything else + options: []NamePolicyOption{ + WithExcludedCIDR("127.0.0.1/24"), + }, + cert: &x509.Certificate{ + EmailAddresses: []string{"mail@example.com"}, + }, + want: true, + wantErr: false, + }, + { + name: "ok/ip-excluded-with-mail", // when only IP is exluded, we allow anything else + options: []NamePolicyOption{ + WithExcludedCIDR("127.0.0.1/24"), + }, + cert: &x509.Certificate{ + URIs: []*url.URL{ + { + Scheme: "https", + Host: "www.example.com", + }, + }, + }, + want: true, + wantErr: false, + }, + { + name: "ok/mail-excluded-with-dns", // when only mail is exluded, we allow anything else + options: []NamePolicyOption{ + WithExcludedEmailAddress("@example.com"), + }, + cert: &x509.Certificate{ + DNSNames: []string{"test.local"}, + }, + want: true, + wantErr: false, + }, + { + name: "ok/mail-excluded-with-ip", // when only mail is exluded, we allow anything else + options: []NamePolicyOption{ + WithExcludedEmailAddress("@example.com"), + }, + cert: &x509.Certificate{ + IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, + }, + want: true, + wantErr: false, + }, + { + name: "ok/mail-excluded-with-uri", // when only mail is exluded, we allow anything else + options: []NamePolicyOption{ + WithExcludedEmailAddress("@example.com"), + }, + cert: &x509.Certificate{ + URIs: []*url.URL{ + { + Scheme: "https", + Host: "www.example.com", + }, + }, + }, + want: true, + wantErr: false, + }, + { + name: "ok/uri-excluded-with-dns", // when only URI is exluded, we allow anything else + options: []NamePolicyOption{ + WithExcludedURIDomain("*.example.local"), + }, + cert: &x509.Certificate{ + DNSNames: []string{"test.example.local"}, + }, + want: true, + wantErr: false, + }, + { + name: "ok/uri-excluded-with-dns", // when only URI is exluded, we allow anything else + options: []NamePolicyOption{ + WithExcludedURIDomain("*.example.local"), + }, + cert: &x509.Certificate{ + IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, + }, + want: true, + wantErr: false, + }, + { + name: "ok/uri-excluded-with-mail", // when only URI is exluded, we allow anything else + options: []NamePolicyOption{ + WithExcludedURIDomain("*.example.local"), + }, + cert: &x509.Certificate{ + EmailAddresses: []string{"mail@example.local"}, + }, + want: true, + wantErr: false, + }, + { + name: "ok/dns-excluded-with-subject-ip-name", // when only DNS is exluded, we allow anything else + options: []NamePolicyOption{ + WithSubjectCommonNameVerification(), + AddExcludedDNSDomain("*.local"), + }, + cert: &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "127.0.0.1", + }, + IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, + }, + want: true, + wantErr: false, + }, + // COMBINED SUCCESS TESTS { name: "ok/combined-simple-permitted", - fields: fields{ - verifySubjectCommonName: true, - permittedDNSDomains: []string{".local"}, - permittedIPRanges: []*net.IPNet{{IP: net.ParseIP("127.0.0.1"), Mask: net.IPv4Mask(255, 255, 255, 0)}}, - permittedEmailAddresses: []string{"example.local"}, - permittedURIDomains: []string{".example.local"}, + options: []NamePolicyOption{ + WithSubjectCommonNameVerification(), + WithPermittedDNSDomain("*.local"), + WithPermittedCIDR("127.0.0.1/24"), + WithPermittedEmailAddress("@example.local"), + WithPermittedURIDomain("*.example.local"), }, cert: &x509.Certificate{ Subject: pkix.Name{ CommonName: "somehost.local", }, DNSNames: []string{"example.local"}, - IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, + IPAddresses: []net.IP{net.ParseIP("127.0.0.15")}, EmailAddresses: []string{"mail@example.local"}, URIs: []*url.URL{ { @@ -924,12 +1990,11 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { }, { name: "ok/combined-simple-permitted-without-subject-verification", - fields: fields{ - verifySubjectCommonName: false, - permittedDNSDomains: []string{".local"}, - permittedIPRanges: []*net.IPNet{{IP: net.ParseIP("127.0.0.1"), Mask: net.IPv4Mask(255, 255, 255, 0)}}, - permittedEmailAddresses: []string{"example.local"}, - permittedURIDomains: []string{".example.local"}, + options: []NamePolicyOption{ + WithPermittedDNSDomain("*.local"), + WithPermittedCIDR("127.0.0.1/24"), + WithPermittedEmailAddress("@example.local"), + WithPermittedURIDomain("*.example.local"), }, cert: &x509.Certificate{ Subject: pkix.Name{ @@ -950,16 +2015,16 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { }, { name: "ok/combined-simple-all", - fields: fields{ - verifySubjectCommonName: true, - permittedDNSDomains: []string{".local"}, - permittedIPRanges: []*net.IPNet{{IP: net.ParseIP("127.0.0.1"), Mask: net.IPv4Mask(255, 255, 255, 0)}}, - permittedEmailAddresses: []string{"example.local"}, - permittedURIDomains: []string{".example.local"}, - excludedDNSDomains: []string{"badhost.local"}, - excludedIPRanges: []*net.IPNet{{IP: net.ParseIP("127.0.0.128"), Mask: net.IPv4Mask(255, 255, 255, 128)}}, - excludedEmailAddresses: []string{"badmail@example.local"}, - excludedURIDomains: []string{"https://badwww.example.local"}, + options: []NamePolicyOption{ + WithSubjectCommonNameVerification(), + WithPermittedDNSDomain("*.local"), + WithPermittedCIDR("127.0.0.1/24"), + WithPermittedEmailAddress("@example.local"), + WithPermittedURIDomain("*.example.local"), + WithExcludedDNSDomain("badhost.local"), + WithExcludedCIDR("127.0.0.128/25"), + WithExcludedEmailAddress("badmail@example.local"), + WithExcludedURIDomain("badwww.example.local"), }, cert: &x509.Certificate{ Subject: pkix.Name{ @@ -978,30 +2043,16 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { want: true, wantErr: false, }, - // TODO: more complex uses cases that combine multiple names and permitted/excluded entries - // TODO: check errors (reasons) are as expected } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - g := &NamePolicyEngine{ - verifySubjectCommonName: tt.fields.verifySubjectCommonName, - permittedDNSDomains: tt.fields.permittedDNSDomains, - excludedDNSDomains: tt.fields.excludedDNSDomains, - permittedIPRanges: tt.fields.permittedIPRanges, - excludedIPRanges: tt.fields.excludedIPRanges, - permittedEmailAddresses: tt.fields.permittedEmailAddresses, - excludedEmailAddresses: tt.fields.excludedEmailAddresses, - permittedURIDomains: tt.fields.permittedURIDomains, - excludedURIDomains: tt.fields.excludedURIDomains, - } - got, err := g.AreCertificateNamesAllowed(tt.cert) + engine, err := New(tt.options...) + assert.FatalError(t, err) + got, err := engine.AreCertificateNamesAllowed(tt.cert) // TODO: perform tests on CSR, sans, etc. too if (err != nil) != tt.wantErr { t.Errorf("NamePolicyEngine.AreCertificateNamesAllowed() error = %v, wantErr %v", err, tt.wantErr) return } - if err != nil { - assert.NotEquals(t, "", err.Error()) // TODO(hs): make this a complete equality check - } if got != tt.want { t.Errorf("NamePolicyEngine.AreCertificateNamesAllowed() = %v, want %v", got, tt.want) }