forked from TrueCloudLab/certificates
Clean up, improve test cases and coverage
This commit is contained in:
parent
1e808b61e5
commit
6440870a80
10 changed files with 723 additions and 123 deletions
0
acme/api/order.go
Executable file → Normal file
0
acme/api/order.go
Executable file → Normal file
0
authority/provisioner/acme.go
Executable file → Normal file
0
authority/provisioner/acme.go
Executable file → Normal file
0
authority/provisioner/jwk.go
Executable file → Normal file
0
authority/provisioner/jwk.go
Executable file → Normal file
4
authority/provisioner/options.go
Executable file → Normal file
4
authority/provisioner/options.go
Executable file → Normal file
|
@ -90,7 +90,7 @@ func (o *X509Options) GetDeniedNameOptions() *DeniedX509NameOptions {
|
||||||
// AllowedX509NameOptions models the allowed names
|
// AllowedX509NameOptions models the allowed names
|
||||||
type AllowedX509NameOptions struct {
|
type AllowedX509NameOptions struct {
|
||||||
DNSDomains []string `json:"dns,omitempty"`
|
DNSDomains []string `json:"dns,omitempty"`
|
||||||
IPRanges []string `json:"ip,omitempty"` // TODO(hs): support IPs as well as ranges
|
IPRanges []string `json:"ip,omitempty"`
|
||||||
EmailAddresses []string `json:"email,omitempty"`
|
EmailAddresses []string `json:"email,omitempty"`
|
||||||
URIDomains []string `json:"uri,omitempty"`
|
URIDomains []string `json:"uri,omitempty"`
|
||||||
}
|
}
|
||||||
|
@ -98,7 +98,7 @@ type AllowedX509NameOptions struct {
|
||||||
// DeniedX509NameOptions models the denied names
|
// DeniedX509NameOptions models the denied names
|
||||||
type DeniedX509NameOptions struct {
|
type DeniedX509NameOptions struct {
|
||||||
DNSDomains []string `json:"dns,omitempty"`
|
DNSDomains []string `json:"dns,omitempty"`
|
||||||
IPRanges []string `json:"ip,omitempty"` // TODO(hs): support IPs as well as ranges
|
IPRanges []string `json:"ip,omitempty"`
|
||||||
EmailAddresses []string `json:"email,omitempty"`
|
EmailAddresses []string `json:"email,omitempty"`
|
||||||
URIDomains []string `json:"uri,omitempty"`
|
URIDomains []string `json:"uri,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,7 @@ func newX509PolicyEngine(x509Opts *X509Options) (policy.X509NamePolicyEngine, er
|
||||||
if allowed != nil && allowed.HasNames() {
|
if allowed != nil && allowed.HasNames() {
|
||||||
options = append(options,
|
options = append(options,
|
||||||
policy.WithPermittedDNSDomains(allowed.DNSDomains),
|
policy.WithPermittedDNSDomains(allowed.DNSDomains),
|
||||||
policy.WithPermittedCIDRs(allowed.IPRanges), // TODO(hs): support IPs in addition to ranges
|
policy.WithPermittedIPsOrCIDRs(allowed.IPRanges),
|
||||||
policy.WithPermittedEmailAddresses(allowed.EmailAddresses),
|
policy.WithPermittedEmailAddresses(allowed.EmailAddresses),
|
||||||
policy.WithPermittedURIDomains(allowed.URIDomains),
|
policy.WithPermittedURIDomains(allowed.URIDomains),
|
||||||
)
|
)
|
||||||
|
@ -29,7 +29,7 @@ func newX509PolicyEngine(x509Opts *X509Options) (policy.X509NamePolicyEngine, er
|
||||||
if denied != nil && denied.HasNames() {
|
if denied != nil && denied.HasNames() {
|
||||||
options = append(options,
|
options = append(options,
|
||||||
policy.WithExcludedDNSDomains(denied.DNSDomains),
|
policy.WithExcludedDNSDomains(denied.DNSDomains),
|
||||||
policy.WithExcludedCIDRs(denied.IPRanges), // TODO(hs): support IPs in addition to ranges
|
policy.WithExcludedIPsOrCIDRs(denied.IPRanges),
|
||||||
policy.WithExcludedEmailAddresses(denied.EmailAddresses),
|
policy.WithExcludedEmailAddresses(denied.EmailAddresses),
|
||||||
policy.WithExcludedURIDomains(denied.URIDomains),
|
policy.WithExcludedURIDomains(denied.URIDomains),
|
||||||
)
|
)
|
||||||
|
|
0
authority/provisioner/sign_options.go
Executable file → Normal file
0
authority/provisioner/sign_options.go
Executable file → Normal file
136
policy/engine.go
136
policy/engine.go
|
@ -15,33 +15,25 @@ import (
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
)
|
)
|
||||||
|
|
||||||
type CertificateInvalidError struct {
|
type NamePolicyReason int
|
||||||
Reason x509.InvalidReason
|
|
||||||
|
const (
|
||||||
|
// NotAuthorizedForThisName results when an instance of
|
||||||
|
// NamePolicyEngine determines that there's a constraint which
|
||||||
|
// doesn't permit a DNS or another type of SAN to be signed
|
||||||
|
// (or otherwise used).
|
||||||
|
NotAuthorizedForThisName NamePolicyReason = iota
|
||||||
|
)
|
||||||
|
|
||||||
|
type NamePolicyError struct {
|
||||||
|
Reason NamePolicyReason
|
||||||
Detail string
|
Detail string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e CertificateInvalidError) Error() string {
|
func (e NamePolicyError) Error() string {
|
||||||
switch e.Reason {
|
switch e.Reason {
|
||||||
// TODO: include logical errors for this package; exlude ones that don't make sense for its current use case?
|
case NotAuthorizedForThisName:
|
||||||
// TODO: currently only CANotAuthorizedForThisName is used by this package; we're not checking the other things in CSRs in this package.
|
|
||||||
case x509.NotAuthorizedToSign:
|
|
||||||
return "not authorized to sign other certificates" // TODO: this one doesn't make sense for this pkg
|
|
||||||
case x509.Expired:
|
|
||||||
return "csr has expired or is not yet valid: " + e.Detail
|
|
||||||
case x509.CANotAuthorizedForThisName:
|
|
||||||
return "not authorized to sign for this name: " + e.Detail
|
return "not authorized to sign for this name: " + e.Detail
|
||||||
case x509.CANotAuthorizedForExtKeyUsage:
|
|
||||||
return "not authorized for an extended key usage: " + e.Detail
|
|
||||||
case x509.TooManyIntermediates:
|
|
||||||
return "too many intermediates for path length constraint"
|
|
||||||
case x509.IncompatibleUsage:
|
|
||||||
return "csr specifies an incompatible key usage"
|
|
||||||
case x509.NameMismatch:
|
|
||||||
return "issuer name does not match subject from issuing certificate"
|
|
||||||
case x509.NameConstraintsWithoutSANs:
|
|
||||||
return "issuer has name constraints but csr doesn't have a SAN extension"
|
|
||||||
case x509.UnconstrainedName:
|
|
||||||
return "issuer has name constraints but csr contains unknown or unconstrained name: " + e.Detail
|
|
||||||
}
|
}
|
||||||
return "unknown error"
|
return "unknown error"
|
||||||
}
|
}
|
||||||
|
@ -126,7 +118,7 @@ func removeDuplicates(strSlice []string) []string {
|
||||||
keys := make(map[string]bool)
|
keys := make(map[string]bool)
|
||||||
result := []string{}
|
result := []string{}
|
||||||
for _, item := range strSlice {
|
for _, item := range strSlice {
|
||||||
if _, value := keys[item]; !value {
|
if _, value := keys[item]; !value && item != "" { // skip empty constraints
|
||||||
keys[item] = true
|
keys[item] = true
|
||||||
result = append(result, item)
|
result = append(result, item)
|
||||||
}
|
}
|
||||||
|
@ -206,8 +198,8 @@ func (e *NamePolicyEngine) IsIPAllowed(ip net.IP) (bool, error) {
|
||||||
|
|
||||||
// ArePrincipalsAllowed verifies that all principals in an SSH certificate are allowed.
|
// ArePrincipalsAllowed verifies that all principals in an SSH certificate are allowed.
|
||||||
func (e *NamePolicyEngine) ArePrincipalsAllowed(cert *ssh.Certificate) (bool, error) {
|
func (e *NamePolicyEngine) ArePrincipalsAllowed(cert *ssh.Certificate) (bool, error) {
|
||||||
dnsNames, emails, usernames := splitPrincipals(cert.ValidPrincipals)
|
dnsNames, ips, emails, usernames := splitPrincipals(cert.ValidPrincipals)
|
||||||
if err := e.validateNames(dnsNames, []net.IP{}, emails, []*url.URL{}, usernames); err != nil {
|
if err := e.validateNames(dnsNames, ips, emails, []*url.URL{}, usernames); err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
return true, nil
|
return true, nil
|
||||||
|
@ -234,13 +226,16 @@ func appendSubjectCommonName(subject pkix.Name, dnsNames *[]string, ips *[]net.I
|
||||||
}
|
}
|
||||||
|
|
||||||
// splitPrincipals splits SSH certificate principals into DNS names, emails and usernames.
|
// splitPrincipals splits SSH certificate principals into DNS names, emails and usernames.
|
||||||
func splitPrincipals(principals []string) (dnsNames, emails, usernames []string) {
|
func splitPrincipals(principals []string) (dnsNames []string, ips []net.IP, emails, usernames []string) {
|
||||||
dnsNames = []string{}
|
dnsNames = []string{}
|
||||||
|
ips = []net.IP{}
|
||||||
emails = []string{}
|
emails = []string{}
|
||||||
usernames = []string{}
|
usernames = []string{}
|
||||||
for _, principal := range principals {
|
for _, principal := range principals {
|
||||||
if strings.Contains(principal, "@") {
|
if strings.Contains(principal, "@") {
|
||||||
emails = append(emails, principal)
|
emails = append(emails, principal)
|
||||||
|
} else if ip := net.ParseIP(principal); ip != nil {
|
||||||
|
ips = append(ips, ip)
|
||||||
} else if len(strings.Split(principal, ".")) > 1 {
|
} else if len(strings.Split(principal, ".")) > 1 {
|
||||||
dnsNames = append(dnsNames, principal)
|
dnsNames = append(dnsNames, principal)
|
||||||
} else {
|
} else {
|
||||||
|
@ -260,7 +255,6 @@ func (e *NamePolicyEngine) validateNames(dnsNames []string, ips []net.IP, emailA
|
||||||
return nil
|
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: implement check that requires at least a single name in all of the SANs + subject?
|
||||||
|
|
||||||
// TODO: set limit on total of all names validated? 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
|
||||||
|
@ -277,9 +271,9 @@ func (e *NamePolicyEngine) validateNames(dnsNames []string, ips []net.IP, emailA
|
||||||
// then return error, because DNS should be explicitly configured to be allowed in that case. In case there are
|
// 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).
|
// (other) excluded constraints, we'll allow a DNS (implicit allow; currently).
|
||||||
if e.numberOfDNSDomainConstraints == 0 && e.totalNumberOfPermittedConstraints > 0 {
|
if e.numberOfDNSDomainConstraints == 0 && e.totalNumberOfPermittedConstraints > 0 {
|
||||||
return CertificateInvalidError{
|
return NamePolicyError{
|
||||||
Reason: x509.CANotAuthorizedForThisName,
|
Reason: NotAuthorizedForThisName,
|
||||||
Detail: fmt.Sprintf("dns %q is not permitted by any constraint", dns), // TODO(hs): change this error (message)
|
Detail: fmt.Sprintf("dns %q is not explicitly permitted by any constraint", dns),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if _, ok := domainToReverseLabels(dns); !ok {
|
if _, ok := domainToReverseLabels(dns); !ok {
|
||||||
|
@ -295,9 +289,9 @@ func (e *NamePolicyEngine) validateNames(dnsNames []string, ips []net.IP, emailA
|
||||||
|
|
||||||
for _, ip := range ips {
|
for _, ip := range ips {
|
||||||
if e.numberOfIPRangeConstraints == 0 && e.totalNumberOfPermittedConstraints > 0 {
|
if e.numberOfIPRangeConstraints == 0 && e.totalNumberOfPermittedConstraints > 0 {
|
||||||
return CertificateInvalidError{
|
return NamePolicyError{
|
||||||
Reason: x509.CANotAuthorizedForThisName,
|
Reason: NotAuthorizedForThisName,
|
||||||
Detail: fmt.Sprintf("ip %q is not permitted by any constraint", ip.String()),
|
Detail: fmt.Sprintf("ip %q is not explicitly permitted by any constraint", ip.String()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := checkNameConstraints("ip", ip.String(), ip,
|
if err := checkNameConstraints("ip", ip.String(), ip,
|
||||||
|
@ -310,9 +304,9 @@ func (e *NamePolicyEngine) validateNames(dnsNames []string, ips []net.IP, emailA
|
||||||
|
|
||||||
for _, email := range emailAddresses {
|
for _, email := range emailAddresses {
|
||||||
if e.numberOfEmailAddressConstraints == 0 && e.totalNumberOfPermittedConstraints > 0 {
|
if e.numberOfEmailAddressConstraints == 0 && e.totalNumberOfPermittedConstraints > 0 {
|
||||||
return CertificateInvalidError{
|
return NamePolicyError{
|
||||||
Reason: x509.CANotAuthorizedForThisName,
|
Reason: NotAuthorizedForThisName,
|
||||||
Detail: fmt.Sprintf("email %q is not permitted by any constraint", email),
|
Detail: fmt.Sprintf("email %q is not explicitly permitted by any constraint", email),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mailbox, ok := parseRFC2821Mailbox(email)
|
mailbox, ok := parseRFC2821Mailbox(email)
|
||||||
|
@ -329,9 +323,9 @@ func (e *NamePolicyEngine) validateNames(dnsNames []string, ips []net.IP, emailA
|
||||||
|
|
||||||
for _, uri := range uris {
|
for _, uri := range uris {
|
||||||
if e.numberOfURIDomainConstraints == 0 && e.totalNumberOfPermittedConstraints > 0 {
|
if e.numberOfURIDomainConstraints == 0 && e.totalNumberOfPermittedConstraints > 0 {
|
||||||
return CertificateInvalidError{
|
return NamePolicyError{
|
||||||
Reason: x509.CANotAuthorizedForThisName,
|
Reason: NotAuthorizedForThisName,
|
||||||
Detail: fmt.Sprintf("uri %q is not permitted by any constraint", uri.String()),
|
Detail: fmt.Sprintf("uri %q is not explicitly permitted by any constraint", uri.String()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := checkNameConstraints("uri", uri.String(), uri,
|
if err := checkNameConstraints("uri", uri.String(), uri,
|
||||||
|
@ -342,23 +336,11 @@ func (e *NamePolicyEngine) validateNames(dnsNames []string, ips []net.IP, emailA
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//"dns": ["*.smallstep.com"],
|
|
||||||
//"email": ["@smallstep.com", "@google.com"],
|
|
||||||
//"principal": ["max", "mariano", "mike"]
|
|
||||||
/* No regexes for now. But if we ever implement them, they'd probably look like this */
|
|
||||||
/*"principal": ["foo.smallstep.com", "/^*\.smallstep\.com$/"]*/
|
|
||||||
|
|
||||||
// Principals can be single user names (mariano, max, mike, ...), hostnames/domains (*.smallstep.com, host.smallstep.com, ...) and "emails" (max@smallstep.com, @smallstep.com, ...)
|
|
||||||
// All ValidPrincipals can thus be any one of those, and they can be mixed (mike@smallstep.com, mike, ...); we need to split this?
|
|
||||||
// Should we assume a generic engine, or can we do it host vs. user based? If host vs. user based, then it becomes easier w.r.t. dns; hosts will only be DNS, right?
|
|
||||||
// If we assume generic, we _may_ have a harder time distinguishing host vs. user certs. We propose to use host + user specific provisioners, though...
|
|
||||||
// Perhaps we can do some heuristics on the principal names vs. hostnames (i.e. when only a single label and no dot, then it's a user principal)
|
|
||||||
|
|
||||||
for _, username := range usernames {
|
for _, username := range usernames {
|
||||||
if e.numberOfPrincipalConstraints == 0 && e.totalNumberOfPermittedConstraints > 0 {
|
if e.numberOfPrincipalConstraints == 0 && e.totalNumberOfPermittedConstraints > 0 {
|
||||||
return CertificateInvalidError{
|
return NamePolicyError{
|
||||||
Reason: x509.CANotAuthorizedForThisName,
|
Reason: NotAuthorizedForThisName,
|
||||||
Detail: fmt.Sprintf("username principal %q is not permitted by any constraint", username),
|
Detail: fmt.Sprintf("username principal %q is not explicity permitted by any constraint", username),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// TODO: some validation? I.e. allowed characters?
|
// TODO: some validation? I.e. allowed characters?
|
||||||
|
@ -370,7 +352,7 @@ func (e *NamePolicyEngine) validateNames(dnsNames []string, ips []net.IP, emailA
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: when the error is not nil and returned up in the above, we can add
|
// TODO(hs): when the error is not nil and returned up in the above, we can add
|
||||||
// additional context to it (i.e. the cert or csr that was inspected).
|
// additional context to it (i.e. the cert or csr that was inspected).
|
||||||
|
|
||||||
// TODO(hs): validate other types of SANs? The Go std library skips those.
|
// TODO(hs): validate other types of SANs? The Go std library skips those.
|
||||||
|
@ -382,8 +364,7 @@ func (e *NamePolicyEngine) validateNames(dnsNames []string, ips []net.IP, emailA
|
||||||
|
|
||||||
// checkNameConstraints checks that a name, of type nameType is permitted.
|
// checkNameConstraints checks that a name, of type nameType is permitted.
|
||||||
// The argument parsedName contains the parsed form of name, suitable for passing
|
// 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
|
// to the match function.
|
||||||
// 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
|
// SOURCE: https://cs.opensource.google/go/go/+/refs/tags/go1.17.5:src/crypto/x509/verify.go
|
||||||
func checkNameConstraints(
|
func checkNameConstraints(
|
||||||
nameType string,
|
nameType string,
|
||||||
|
@ -394,26 +375,19 @@ func checkNameConstraints(
|
||||||
|
|
||||||
excludedValue := reflect.ValueOf(excluded)
|
excludedValue := reflect.ValueOf(excluded)
|
||||||
|
|
||||||
// *count += excludedValue.Len()
|
|
||||||
// if *count > maxConstraintComparisons {
|
|
||||||
// return x509.CertificateInvalidError{c, x509.TooManyConstraints, ""}
|
|
||||||
// }
|
|
||||||
|
|
||||||
// TODO: fix the errors; return our own, because we don't have cert ...
|
|
||||||
|
|
||||||
for i := 0; i < excludedValue.Len(); i++ {
|
for i := 0; i < excludedValue.Len(); i++ {
|
||||||
constraint := excludedValue.Index(i).Interface()
|
constraint := excludedValue.Index(i).Interface()
|
||||||
match, err := match(parsedName, constraint)
|
match, err := match(parsedName, constraint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return CertificateInvalidError{
|
return NamePolicyError{
|
||||||
Reason: x509.CANotAuthorizedForThisName,
|
Reason: NotAuthorizedForThisName,
|
||||||
Detail: err.Error(),
|
Detail: err.Error(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if match {
|
if match {
|
||||||
return CertificateInvalidError{
|
return NamePolicyError{
|
||||||
Reason: x509.CANotAuthorizedForThisName,
|
Reason: NotAuthorizedForThisName,
|
||||||
Detail: fmt.Sprintf("%s %q is excluded by constraint %q", nameType, name, constraint),
|
Detail: fmt.Sprintf("%s %q is excluded by constraint %q", nameType, name, constraint),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -421,18 +395,13 @@ func checkNameConstraints(
|
||||||
|
|
||||||
permittedValue := reflect.ValueOf(permitted)
|
permittedValue := reflect.ValueOf(permitted)
|
||||||
|
|
||||||
// *count += permittedValue.Len()
|
|
||||||
// if *count > maxConstraintComparisons {
|
|
||||||
// return x509.CertificateInvalidError{c, x509.TooManyConstraints, ""}
|
|
||||||
// }
|
|
||||||
|
|
||||||
ok := true
|
ok := true
|
||||||
for i := 0; i < permittedValue.Len(); i++ {
|
for i := 0; i < permittedValue.Len(); i++ {
|
||||||
constraint := permittedValue.Index(i).Interface()
|
constraint := permittedValue.Index(i).Interface()
|
||||||
var err error
|
var err error
|
||||||
if ok, err = match(parsedName, constraint); err != nil {
|
if ok, err = match(parsedName, constraint); err != nil {
|
||||||
return CertificateInvalidError{
|
return NamePolicyError{
|
||||||
Reason: x509.CANotAuthorizedForThisName,
|
Reason: NotAuthorizedForThisName,
|
||||||
Detail: err.Error(),
|
Detail: err.Error(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -443,8 +412,8 @@ func checkNameConstraints(
|
||||||
}
|
}
|
||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
return CertificateInvalidError{
|
return NamePolicyError{
|
||||||
Reason: x509.CANotAuthorizedForThisName,
|
Reason: NotAuthorizedForThisName,
|
||||||
Detail: fmt.Sprintf("%s %q is not permitted by any constraint", nameType, name),
|
Detail: fmt.Sprintf("%s %q is not permitted by any constraint", nameType, name),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -651,7 +620,6 @@ func (e *NamePolicyEngine) matchDomainConstraint(domain, constraint string) (boo
|
||||||
}
|
}
|
||||||
|
|
||||||
// Block domains that start with just a period
|
// 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] == '.' {
|
if domain[0] == '.' {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
@ -744,19 +712,11 @@ func matchIPConstraint(ip net.IP, constraint *net.IPNet) (bool, error) {
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// if isIPv4(ip) != isIPv4(constraint.IP) { // TODO(hs): this check seems to do what the above intended to do?
|
|
||||||
// return false, nil
|
|
||||||
// }
|
|
||||||
|
|
||||||
contained := constraint.Contains(ip) // TODO(hs): validate that this is the correct behavior; also check IPv4-in-IPv6 (again)
|
contained := constraint.Contains(ip) // TODO(hs): validate that this is the correct behavior; also check IPv4-in-IPv6 (again)
|
||||||
|
|
||||||
return contained, nil
|
return contained, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func isIPv4(ip net.IP) bool {
|
|
||||||
return ip.To4() != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SOURCE: https://cs.opensource.google/go/go/+/refs/tags/go1.17.5:src/crypto/x509/verify.go
|
// SOURCE: https://cs.opensource.google/go/go/+/refs/tags/go1.17.5:src/crypto/x509/verify.go
|
||||||
func (e *NamePolicyEngine) matchEmailConstraint(mailbox rfc2821Mailbox, constraint string) (bool, error) {
|
func (e *NamePolicyEngine) matchEmailConstraint(mailbox rfc2821Mailbox, constraint string) (bool, error) {
|
||||||
// TODO(hs): handle literal wildcard case for emails? Does that even make sense?
|
// TODO(hs): handle literal wildcard case for emails? Does that even make sense?
|
||||||
|
@ -817,5 +777,9 @@ func (e *NamePolicyEngine) matchURIConstraint(uri *url.URL, constraint string) (
|
||||||
|
|
||||||
// matchUsernameConstraint performs a string literal match against a constraint.
|
// matchUsernameConstraint performs a string literal match against a constraint.
|
||||||
func matchUsernameConstraint(username, constraint string) (bool, error) {
|
func matchUsernameConstraint(username, constraint string) (bool, error) {
|
||||||
|
// allow any plain principal username
|
||||||
|
if constraint == "*" {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
return strings.EqualFold(username, constraint), nil
|
return strings.EqualFold(username, constraint), nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,11 +8,11 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/smallstep/assert"
|
"github.com/smallstep/assert"
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO(hs): the functionality in the policy engine is a nice candidate for trying fuzzing on
|
// 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): 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) {
|
func TestNamePolicyEngine_matchDomainConstraint(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
|
@ -135,6 +135,22 @@ func TestNamePolicyEngine_matchDomainConstraint(t *testing.T) {
|
||||||
want: false,
|
want: false,
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "false/idna-internationalized-domain-name",
|
||||||
|
engine: &NamePolicyEngine{},
|
||||||
|
domain: "JP納豆.例.jp", // Example value from https://www.w3.org/International/articles/idn-and-iri/
|
||||||
|
constraint: ".例.jp",
|
||||||
|
want: false,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "false/idna-internationalized-domain-name-constraint",
|
||||||
|
engine: &NamePolicyEngine{},
|
||||||
|
domain: "xn--jp-cd2fp15c.xn--fsq.jp", // Example value from https://www.w3.org/International/articles/idn-and-iri/
|
||||||
|
constraint: ".例.jp",
|
||||||
|
want: false,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "ok/empty-constraint",
|
name: "ok/empty-constraint",
|
||||||
engine: &NamePolicyEngine{},
|
engine: &NamePolicyEngine{},
|
||||||
|
@ -169,6 +185,22 @@ func TestNamePolicyEngine_matchDomainConstraint(t *testing.T) {
|
||||||
want: true,
|
want: true,
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "ok/different-case",
|
||||||
|
engine: &NamePolicyEngine{},
|
||||||
|
domain: "WWW.EXAMPLE.com",
|
||||||
|
constraint: "www.example.com",
|
||||||
|
want: true,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ok/idna-internationalized-domain-name-punycode",
|
||||||
|
engine: &NamePolicyEngine{},
|
||||||
|
domain: "xn--jp-cd2fp15c.xn--fsq.jp", // Example value from https://www.w3.org/International/articles/idn-and-iri/
|
||||||
|
constraint: ".xn--fsq.jp",
|
||||||
|
want: true,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
@ -413,6 +445,17 @@ func TestNamePolicyEngine_matchEmailConstraint(t *testing.T) {
|
||||||
want: true,
|
want: true,
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "ok/different-case",
|
||||||
|
engine: &NamePolicyEngine{},
|
||||||
|
mailbox: rfc2821Mailbox{
|
||||||
|
local: "mail",
|
||||||
|
domain: "EXAMPLE.com",
|
||||||
|
},
|
||||||
|
constraint: "example.com", // "wildcard" for 'example.com'
|
||||||
|
want: true,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
@ -558,6 +601,17 @@ func TestNamePolicyEngine_matchURIConstraint(t *testing.T) {
|
||||||
want: true,
|
want: true,
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "ok/different-case",
|
||||||
|
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,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
@ -573,7 +627,23 @@ func TestNamePolicyEngine_matchURIConstraint(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) {
|
func extractSANs(cert *x509.Certificate, includeSubject bool) []string {
|
||||||
|
sans := []string{}
|
||||||
|
sans = append(sans, cert.DNSNames...)
|
||||||
|
for _, ip := range cert.IPAddresses {
|
||||||
|
sans = append(sans, ip.String())
|
||||||
|
}
|
||||||
|
sans = append(sans, cert.EmailAddresses...)
|
||||||
|
for _, uri := range cert.URIs {
|
||||||
|
sans = append(sans, uri.String())
|
||||||
|
}
|
||||||
|
if includeSubject && cert.Subject.CommonName != "" {
|
||||||
|
sans = append(sans, cert.Subject.CommonName)
|
||||||
|
}
|
||||||
|
return sans
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
options []NamePolicyOption
|
options []NamePolicyOption
|
||||||
|
@ -2048,14 +2118,422 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
engine, err := New(tt.options...)
|
engine, err := New(tt.options...)
|
||||||
assert.FatalError(t, err)
|
assert.FatalError(t, err)
|
||||||
got, err := engine.AreCertificateNamesAllowed(tt.cert) // TODO: perform tests on CSR, sans, etc. too
|
got, err := engine.AreCertificateNamesAllowed(tt.cert)
|
||||||
if (err != nil) != tt.wantErr {
|
if (err != nil) != tt.wantErr {
|
||||||
t.Errorf("NamePolicyEngine.AreCertificateNamesAllowed() error = %v, wantErr %v", err, tt.wantErr)
|
t.Errorf("NamePolicyEngine.AreCertificateNamesAllowed() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if err != nil {
|
||||||
|
assert.NotEquals(t, "", err.Error()) // TODO(hs): implement a more specific error comparison?
|
||||||
|
}
|
||||||
if got != tt.want {
|
if got != tt.want {
|
||||||
t.Errorf("NamePolicyEngine.AreCertificateNamesAllowed() = %v, want %v", got, tt.want)
|
t.Errorf("NamePolicyEngine.AreCertificateNamesAllowed() = %v, want %v", got, tt.want)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Perform the same tests for a CSR, which are similar to Certificates
|
||||||
|
csr := &x509.CertificateRequest{
|
||||||
|
Subject: tt.cert.Subject,
|
||||||
|
DNSNames: tt.cert.DNSNames,
|
||||||
|
EmailAddresses: tt.cert.EmailAddresses,
|
||||||
|
IPAddresses: tt.cert.IPAddresses,
|
||||||
|
URIs: tt.cert.URIs,
|
||||||
|
}
|
||||||
|
got, err = engine.AreCSRNamesAllowed(csr)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("NamePolicyEngine.AreCSRNamesAllowed() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
assert.NotEquals(t, "", err.Error())
|
||||||
|
}
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("NamePolicyEngine.AreCSRNamesAllowed() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform the same tests for a slice of SANs
|
||||||
|
includeSubject := engine.verifySubjectCommonName // copy behavior of the engine when Subject has to be included as a SAN
|
||||||
|
sans := extractSANs(tt.cert, includeSubject)
|
||||||
|
got, err = engine.AreSANsAllowed(sans)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("NamePolicyEngine.AreSANsAllowed() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
assert.NotEquals(t, "", err.Error())
|
||||||
|
}
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("NamePolicyEngine.AreSANsAllowed() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
options []NamePolicyOption
|
||||||
|
cert *ssh.Certificate
|
||||||
|
want bool
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "fail/with-permitted-dns-domain",
|
||||||
|
options: []NamePolicyOption{
|
||||||
|
WithPermittedDNSDomain("*.local"),
|
||||||
|
},
|
||||||
|
cert: &ssh.Certificate{
|
||||||
|
ValidPrincipals: []string{
|
||||||
|
"host.example.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fail/with-excluded-dns-domain",
|
||||||
|
options: []NamePolicyOption{
|
||||||
|
WithExcludedDNSDomain("*.local"),
|
||||||
|
},
|
||||||
|
cert: &ssh.Certificate{
|
||||||
|
ValidPrincipals: []string{
|
||||||
|
"host.local",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fail/with-permitted-ip",
|
||||||
|
options: []NamePolicyOption{
|
||||||
|
WithPermittedCIDR("127.0.0.1/24"),
|
||||||
|
},
|
||||||
|
cert: &ssh.Certificate{
|
||||||
|
ValidPrincipals: []string{
|
||||||
|
"192.168.0.22",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fail/with-excluded-ip",
|
||||||
|
options: []NamePolicyOption{
|
||||||
|
WithExcludedCIDR("127.0.0.1/24"),
|
||||||
|
},
|
||||||
|
cert: &ssh.Certificate{
|
||||||
|
ValidPrincipals: []string{
|
||||||
|
"127.0.0.0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fail/with-permitted-email",
|
||||||
|
options: []NamePolicyOption{
|
||||||
|
WithPermittedEmailAddress("@example.com"),
|
||||||
|
},
|
||||||
|
cert: &ssh.Certificate{
|
||||||
|
ValidPrincipals: []string{
|
||||||
|
"mail@local",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fail/with-excluded-email",
|
||||||
|
options: []NamePolicyOption{
|
||||||
|
WithExcludedEmailAddress("@example.com"),
|
||||||
|
},
|
||||||
|
cert: &ssh.Certificate{
|
||||||
|
ValidPrincipals: []string{
|
||||||
|
"mail@example.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fail/with-permitted-principals",
|
||||||
|
options: []NamePolicyOption{
|
||||||
|
WithPermittedPrincipals([]string{"user"}),
|
||||||
|
},
|
||||||
|
cert: &ssh.Certificate{
|
||||||
|
ValidPrincipals: []string{
|
||||||
|
"root",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fail/with-excluded-principals",
|
||||||
|
options: []NamePolicyOption{
|
||||||
|
WithExcludedPrincipals([]string{"user"}),
|
||||||
|
},
|
||||||
|
cert: &ssh.Certificate{
|
||||||
|
ValidPrincipals: []string{
|
||||||
|
"user",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fail/with-permitted-principal-as-mail",
|
||||||
|
options: []NamePolicyOption{
|
||||||
|
WithPermittedPrincipals([]string{"ops"}),
|
||||||
|
},
|
||||||
|
cert: &ssh.Certificate{
|
||||||
|
ValidPrincipals: []string{
|
||||||
|
"ops@work", // this is (currently) parsed as an email-like principal; not allowed with just "ops" as the permitted principal
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fail/principal-with-permitted-dns-domain", // when only DNS is permitted, username principals are not allowed.
|
||||||
|
options: []NamePolicyOption{
|
||||||
|
WithPermittedDNSDomain("*.local"),
|
||||||
|
},
|
||||||
|
cert: &ssh.Certificate{
|
||||||
|
ValidPrincipals: []string{
|
||||||
|
"user",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fail/principal-with-permitted-ip-range", // when only IPs are permitted, username principals are not allowed.
|
||||||
|
options: []NamePolicyOption{
|
||||||
|
WithPermittedCIDR("127.0.0.1/24"),
|
||||||
|
},
|
||||||
|
cert: &ssh.Certificate{
|
||||||
|
ValidPrincipals: []string{
|
||||||
|
"user",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fail/principal-with-permitted-email", // when only emails are permitted, username principals are not allowed.
|
||||||
|
options: []NamePolicyOption{
|
||||||
|
WithPermittedEmailAddress("@example.com"),
|
||||||
|
},
|
||||||
|
cert: &ssh.Certificate{
|
||||||
|
ValidPrincipals: []string{
|
||||||
|
"user",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fail/combined-user",
|
||||||
|
options: []NamePolicyOption{
|
||||||
|
WithPermittedEmailAddress("@smallstep.com"),
|
||||||
|
WithExcludedEmailAddress("root@smallstep.com"),
|
||||||
|
},
|
||||||
|
cert: &ssh.Certificate{
|
||||||
|
ValidPrincipals: []string{
|
||||||
|
"someone@smallstep.com",
|
||||||
|
"someone",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fail/combined-user-with-excluded-user-principal",
|
||||||
|
options: []NamePolicyOption{
|
||||||
|
WithPermittedEmailAddress("@smallstep.com"),
|
||||||
|
WithExcludedPrincipals([]string{"root"}),
|
||||||
|
},
|
||||||
|
cert: &ssh.Certificate{
|
||||||
|
ValidPrincipals: []string{
|
||||||
|
"someone@smallstep.com",
|
||||||
|
"root",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ok/with-permitted-dns-domain",
|
||||||
|
options: []NamePolicyOption{
|
||||||
|
WithPermittedDNSDomain("*.local"),
|
||||||
|
},
|
||||||
|
cert: &ssh.Certificate{
|
||||||
|
ValidPrincipals: []string{
|
||||||
|
"host.local",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ok/with-excluded-dns-domain",
|
||||||
|
options: []NamePolicyOption{
|
||||||
|
WithExcludedDNSDomain("*.example.com"),
|
||||||
|
},
|
||||||
|
cert: &ssh.Certificate{
|
||||||
|
ValidPrincipals: []string{
|
||||||
|
"host.local",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ok/with-permitted-ip",
|
||||||
|
options: []NamePolicyOption{
|
||||||
|
WithPermittedCIDR("127.0.0.1/24"),
|
||||||
|
},
|
||||||
|
cert: &ssh.Certificate{
|
||||||
|
ValidPrincipals: []string{
|
||||||
|
"127.0.0.33",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ok/with-excluded-ip",
|
||||||
|
options: []NamePolicyOption{
|
||||||
|
WithExcludedCIDR("127.0.0.1/24"),
|
||||||
|
},
|
||||||
|
cert: &ssh.Certificate{
|
||||||
|
ValidPrincipals: []string{
|
||||||
|
"192.168.0.35",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ok/with-permitted-email",
|
||||||
|
options: []NamePolicyOption{
|
||||||
|
WithPermittedEmailAddress("@example.com"),
|
||||||
|
},
|
||||||
|
cert: &ssh.Certificate{
|
||||||
|
ValidPrincipals: []string{
|
||||||
|
"mail@example.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ok/with-excluded-email",
|
||||||
|
options: []NamePolicyOption{
|
||||||
|
WithExcludedEmailAddress("@example.com"),
|
||||||
|
},
|
||||||
|
cert: &ssh.Certificate{
|
||||||
|
ValidPrincipals: []string{
|
||||||
|
"mail@local",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ok/with-permitted-principals",
|
||||||
|
options: []NamePolicyOption{
|
||||||
|
WithPermittedPrincipals([]string{"*"}),
|
||||||
|
},
|
||||||
|
cert: &ssh.Certificate{
|
||||||
|
ValidPrincipals: []string{
|
||||||
|
"user",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ok/with-excluded-principals",
|
||||||
|
options: []NamePolicyOption{
|
||||||
|
WithExcludedPrincipals([]string{"user"}),
|
||||||
|
},
|
||||||
|
cert: &ssh.Certificate{
|
||||||
|
ValidPrincipals: []string{
|
||||||
|
"root",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ok/combined-user",
|
||||||
|
options: []NamePolicyOption{
|
||||||
|
WithPermittedEmailAddress("@smallstep.com"),
|
||||||
|
WithPermittedPrincipals([]string{"*"}), // without specifying the wildcard, "someone" would not be allowed.
|
||||||
|
WithExcludedEmailAddress("root@smallstep.com"),
|
||||||
|
},
|
||||||
|
cert: &ssh.Certificate{
|
||||||
|
ValidPrincipals: []string{
|
||||||
|
"someone@smallstep.com",
|
||||||
|
"someone",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ok/combined-user-with-excluded-user-principal",
|
||||||
|
options: []NamePolicyOption{
|
||||||
|
WithPermittedEmailAddress("@smallstep.com"),
|
||||||
|
WithExcludedEmailAddress("root@smallstep.com"),
|
||||||
|
WithExcludedPrincipals([]string{"root"}), // unlike the previous test, this implicitly allows any other username principal
|
||||||
|
},
|
||||||
|
cert: &ssh.Certificate{
|
||||||
|
ValidPrincipals: []string{
|
||||||
|
"someone@smallstep.com",
|
||||||
|
"someone",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ok/combined-simple-all",
|
||||||
|
options: []NamePolicyOption{
|
||||||
|
WithPermittedDNSDomain("*.local"),
|
||||||
|
WithPermittedCIDR("127.0.0.1/24"),
|
||||||
|
WithPermittedEmailAddress("@example.local"),
|
||||||
|
WithPermittedPrincipals([]string{"user"}),
|
||||||
|
WithExcludedDNSDomain("badhost.local"),
|
||||||
|
WithExcludedCIDR("127.0.0.128/25"),
|
||||||
|
WithExcludedEmailAddress("badmail@example.local"),
|
||||||
|
WithExcludedPrincipals([]string{"root"}),
|
||||||
|
},
|
||||||
|
cert: &ssh.Certificate{
|
||||||
|
ValidPrincipals: []string{
|
||||||
|
"example.local",
|
||||||
|
"127.0.0.1",
|
||||||
|
"user@example.local",
|
||||||
|
"user",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
engine, err := New(tt.options...)
|
||||||
|
assert.FatalError(t, err)
|
||||||
|
got, err := engine.ArePrincipalsAllowed(tt.cert)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("NamePolicyEngine.ArePrincipalsAllowed() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("NamePolicyEngine.ArePrincipalsAllowed() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -204,6 +204,42 @@ func AddExcludedCIDRs(cidrs []string) NamePolicyOption {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func WithPermittedIPsOrCIDRs(ipsOrCIDRs []string) NamePolicyOption {
|
||||||
|
return func(e *NamePolicyEngine) error {
|
||||||
|
networks := make([]*net.IPNet, len(ipsOrCIDRs))
|
||||||
|
for i, ipOrCIDR := range ipsOrCIDRs {
|
||||||
|
_, nw, err := net.ParseCIDR(ipOrCIDR)
|
||||||
|
if err == nil {
|
||||||
|
networks[i] = nw
|
||||||
|
} else if ip := net.ParseIP(ipOrCIDR); ip != nil {
|
||||||
|
networks[i] = networkFor(ip)
|
||||||
|
} else {
|
||||||
|
return errors.Errorf("cannot parse permitted constraint %q as IP nor CIDR", ipOrCIDR)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e.permittedIPRanges = networks
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithExcludedIPsOrCIDRs(ipsOrCIDRs []string) NamePolicyOption {
|
||||||
|
return func(e *NamePolicyEngine) error {
|
||||||
|
networks := make([]*net.IPNet, len(ipsOrCIDRs))
|
||||||
|
for i, ipOrCIDR := range ipsOrCIDRs {
|
||||||
|
_, nw, err := net.ParseCIDR(ipOrCIDR)
|
||||||
|
if err == nil {
|
||||||
|
networks[i] = nw
|
||||||
|
} else if ip := net.ParseIP(ipOrCIDR); ip != nil {
|
||||||
|
networks[i] = networkFor(ip)
|
||||||
|
} else {
|
||||||
|
return errors.Errorf("cannot parse excluded constraint %q as IP nor CIDR", ipOrCIDR)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e.excludedIPRanges = networks
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func WithPermittedCIDR(cidr string) NamePolicyOption {
|
func WithPermittedCIDR(cidr string) NamePolicyOption {
|
||||||
return func(e *NamePolicyEngine) error {
|
return func(e *NamePolicyEngine) error {
|
||||||
_, nw, err := net.ParseCIDR(cidr)
|
_, nw, err := net.ParseCIDR(cidr)
|
||||||
|
@ -228,16 +264,7 @@ func AddPermittedCIDR(cidr string) NamePolicyOption {
|
||||||
|
|
||||||
func WithPermittedIP(ip net.IP) NamePolicyOption {
|
func WithPermittedIP(ip net.IP) NamePolicyOption {
|
||||||
return func(e *NamePolicyEngine) error {
|
return func(e *NamePolicyEngine) error {
|
||||||
var mask net.IPMask
|
nw := networkFor(ip)
|
||||||
if !isIPv4(ip) {
|
|
||||||
mask = net.CIDRMask(128, 128)
|
|
||||||
} else {
|
|
||||||
mask = net.CIDRMask(32, 32)
|
|
||||||
}
|
|
||||||
nw := &net.IPNet{
|
|
||||||
IP: ip,
|
|
||||||
Mask: mask,
|
|
||||||
}
|
|
||||||
e.permittedIPRanges = []*net.IPNet{nw}
|
e.permittedIPRanges = []*net.IPNet{nw}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -245,16 +272,7 @@ func WithPermittedIP(ip net.IP) NamePolicyOption {
|
||||||
|
|
||||||
func AddPermittedIP(ip net.IP) NamePolicyOption {
|
func AddPermittedIP(ip net.IP) NamePolicyOption {
|
||||||
return func(e *NamePolicyEngine) error {
|
return func(e *NamePolicyEngine) error {
|
||||||
var mask net.IPMask
|
nw := networkFor(ip)
|
||||||
if !isIPv4(ip) {
|
|
||||||
mask = net.CIDRMask(128, 128)
|
|
||||||
} else {
|
|
||||||
mask = net.CIDRMask(32, 32)
|
|
||||||
}
|
|
||||||
nw := &net.IPNet{
|
|
||||||
IP: ip,
|
|
||||||
Mask: mask,
|
|
||||||
}
|
|
||||||
e.permittedIPRanges = append(e.permittedIPRanges, nw)
|
e.permittedIPRanges = append(e.permittedIPRanges, nw)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -540,9 +558,7 @@ func AddExcludedURIDomain(uriDomain string) NamePolicyOption {
|
||||||
|
|
||||||
func WithPermittedPrincipals(principals []string) NamePolicyOption {
|
func WithPermittedPrincipals(principals []string) NamePolicyOption {
|
||||||
return func(g *NamePolicyEngine) error {
|
return func(g *NamePolicyEngine) error {
|
||||||
// for _, principal := range principals {
|
// TODO(hs): normalize and parse principal into the right type? Seems the safe thing to do.
|
||||||
// // TODO: validation?
|
|
||||||
// }
|
|
||||||
g.permittedPrincipals = principals
|
g.permittedPrincipals = principals
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -550,16 +566,32 @@ func WithPermittedPrincipals(principals []string) NamePolicyOption {
|
||||||
|
|
||||||
func WithExcludedPrincipals(principals []string) NamePolicyOption {
|
func WithExcludedPrincipals(principals []string) NamePolicyOption {
|
||||||
return func(g *NamePolicyEngine) error {
|
return func(g *NamePolicyEngine) error {
|
||||||
// for _, principal := range principals {
|
// TODO(hs): normalize and parse principal into the right type? Seems the safe thing to do.
|
||||||
// // TODO: validation?
|
|
||||||
// }
|
|
||||||
g.excludedPrincipals = principals
|
g.excludedPrincipals = principals
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func networkFor(ip net.IP) *net.IPNet {
|
||||||
|
var mask net.IPMask
|
||||||
|
if !isIPv4(ip) {
|
||||||
|
mask = net.CIDRMask(128, 128)
|
||||||
|
} else {
|
||||||
|
mask = net.CIDRMask(32, 32)
|
||||||
|
}
|
||||||
|
nw := &net.IPNet{
|
||||||
|
IP: ip,
|
||||||
|
Mask: mask,
|
||||||
|
}
|
||||||
|
return nw
|
||||||
|
}
|
||||||
|
|
||||||
|
func isIPv4(ip net.IP) bool {
|
||||||
|
return ip.To4() != nil
|
||||||
|
}
|
||||||
|
|
||||||
func normalizeAndValidateDNSDomainConstraint(constraint string) (string, error) {
|
func normalizeAndValidateDNSDomainConstraint(constraint string) (string, error) {
|
||||||
normalizedConstraint := strings.TrimSpace(constraint)
|
normalizedConstraint := strings.ToLower(strings.TrimSpace(constraint))
|
||||||
if strings.Contains(normalizedConstraint, "..") {
|
if strings.Contains(normalizedConstraint, "..") {
|
||||||
return "", errors.Errorf("domain constraint %q cannot have empty labels", constraint)
|
return "", errors.Errorf("domain constraint %q cannot have empty labels", constraint)
|
||||||
}
|
}
|
||||||
|
@ -576,7 +608,7 @@ func normalizeAndValidateDNSDomainConstraint(constraint string) (string, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizeAndValidateEmailConstraint(constraint string) (string, error) {
|
func normalizeAndValidateEmailConstraint(constraint string) (string, error) {
|
||||||
normalizedConstraint := strings.TrimSpace(constraint)
|
normalizedConstraint := strings.ToLower(strings.TrimSpace(constraint))
|
||||||
if strings.Contains(normalizedConstraint, "*") {
|
if strings.Contains(normalizedConstraint, "*") {
|
||||||
return "", fmt.Errorf("email constraint %q cannot contain asterisk", constraint)
|
return "", fmt.Errorf("email constraint %q cannot contain asterisk", constraint)
|
||||||
}
|
}
|
||||||
|
@ -601,7 +633,7 @@ func normalizeAndValidateEmailConstraint(constraint string) (string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizeAndValidateURIDomainConstraint(constraint string) (string, error) {
|
func normalizeAndValidateURIDomainConstraint(constraint string) (string, error) {
|
||||||
normalizedConstraint := strings.TrimSpace(constraint)
|
normalizedConstraint := strings.ToLower(strings.TrimSpace(constraint))
|
||||||
if strings.Contains(normalizedConstraint, "..") {
|
if strings.Contains(normalizedConstraint, "..") {
|
||||||
return "", errors.Errorf("URI domain constraint %q cannot have empty labels", constraint)
|
return "", errors.Errorf("URI domain constraint %q cannot have empty labels", constraint)
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,18 @@ func Test_normalizeAndValidateDNSDomainConstraint(t *testing.T) {
|
||||||
want: "",
|
want: "",
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "false/idna-internationalized-domain-name",
|
||||||
|
constraint: ".例.jp", // Example value from https://www.w3.org/International/articles/idn-and-iri/
|
||||||
|
want: "",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "false/idna-internationalized-domain-name-constraint",
|
||||||
|
constraint: ".例.jp", // Example value from https://www.w3.org/International/articles/idn-and-iri/
|
||||||
|
want: "",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "ok/wildcard",
|
name: "ok/wildcard",
|
||||||
constraint: "*.local",
|
constraint: "*.local",
|
||||||
|
@ -45,6 +57,12 @@ func Test_normalizeAndValidateDNSDomainConstraint(t *testing.T) {
|
||||||
want: "example.local",
|
want: "example.local",
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "ok/idna-internationalized-domain-name-punycode",
|
||||||
|
constraint: ".xn--fsq.jp", // Example value from https://www.w3.org/International/articles/idn-and-iri/
|
||||||
|
want: ".xn--fsq.jp",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
@ -297,6 +315,42 @@ func TestNew(t *testing.T) {
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"fail/with-permitted-ipsOrCIDRs-cidr": func(t *testing.T) test {
|
||||||
|
return test{
|
||||||
|
options: []NamePolicyOption{
|
||||||
|
WithPermittedIPsOrCIDRs([]string{"127.0.0.1//24"}),
|
||||||
|
},
|
||||||
|
want: nil,
|
||||||
|
wantErr: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fail/with-permitted-ipsOrCIDRs-ip": func(t *testing.T) test {
|
||||||
|
return test{
|
||||||
|
options: []NamePolicyOption{
|
||||||
|
WithPermittedIPsOrCIDRs([]string{"127.0.0:1"}),
|
||||||
|
},
|
||||||
|
want: nil,
|
||||||
|
wantErr: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fail/with-excluded-ipsOrCIDRs-cidr": func(t *testing.T) test {
|
||||||
|
return test{
|
||||||
|
options: []NamePolicyOption{
|
||||||
|
WithExcludedIPsOrCIDRs([]string{"127.0.0.1//24"}),
|
||||||
|
},
|
||||||
|
want: nil,
|
||||||
|
wantErr: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fail/with-excluded-ipsOrCIDRs-ip": func(t *testing.T) test {
|
||||||
|
return test{
|
||||||
|
options: []NamePolicyOption{
|
||||||
|
WithExcludedIPsOrCIDRs([]string{"127.0.0:1"}),
|
||||||
|
},
|
||||||
|
want: nil,
|
||||||
|
wantErr: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
"fail/with-permitted-cidr": func(t *testing.T) test {
|
"fail/with-permitted-cidr": func(t *testing.T) test {
|
||||||
return test{
|
return test{
|
||||||
options: []NamePolicyOption{
|
options: []NamePolicyOption{
|
||||||
|
@ -828,6 +882,48 @@ func TestNew(t *testing.T) {
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"ok/with-permitted-ipsOrCIDRs-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.31/32")
|
||||||
|
assert.FatalError(t, err)
|
||||||
|
options := []NamePolicyOption{
|
||||||
|
WithPermittedIPsOrCIDRs([]string{"127.0.0.1/24", "192.168.0.31"}),
|
||||||
|
}
|
||||||
|
return test{
|
||||||
|
options: options,
|
||||||
|
want: &NamePolicyEngine{
|
||||||
|
permittedIPRanges: []*net.IPNet{
|
||||||
|
nw1, nw2,
|
||||||
|
},
|
||||||
|
numberOfIPRangeConstraints: 2,
|
||||||
|
totalNumberOfPermittedConstraints: 2,
|
||||||
|
totalNumberOfConstraints: 2,
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ok/with-excluded-ipsOrCIDRs-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.31/32")
|
||||||
|
assert.FatalError(t, err)
|
||||||
|
options := []NamePolicyOption{
|
||||||
|
WithExcludedIPsOrCIDRs([]string{"127.0.0.1/24", "192.168.0.31"}),
|
||||||
|
}
|
||||||
|
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 {
|
"ok/with-permitted-cidr": func(t *testing.T) test {
|
||||||
_, nw1, err := net.ParseCIDR("127.0.0.1/24")
|
_, nw1, err := net.ParseCIDR("127.0.0.1/24")
|
||||||
assert.FatalError(t, err)
|
assert.FatalError(t, err)
|
||||||
|
@ -1322,6 +1418,36 @@ func TestNew(t *testing.T) {
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"ok/with-permitted-principals": func(t *testing.T) test {
|
||||||
|
options := []NamePolicyOption{
|
||||||
|
WithPermittedPrincipals([]string{"root", "ops"}),
|
||||||
|
}
|
||||||
|
return test{
|
||||||
|
options: options,
|
||||||
|
want: &NamePolicyEngine{
|
||||||
|
permittedPrincipals: []string{"root", "ops"},
|
||||||
|
numberOfPrincipalConstraints: 2,
|
||||||
|
totalNumberOfPermittedConstraints: 2,
|
||||||
|
totalNumberOfConstraints: 2,
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ok/with-excluded-principals": func(t *testing.T) test {
|
||||||
|
options := []NamePolicyOption{
|
||||||
|
WithExcludedPrincipals([]string{"root", "ops"}),
|
||||||
|
}
|
||||||
|
return test{
|
||||||
|
options: options,
|
||||||
|
want: &NamePolicyEngine{
|
||||||
|
excludedPrincipals: []string{"root", "ops"},
|
||||||
|
numberOfPrincipalConstraints: 2,
|
||||||
|
totalNumberOfExcludedConstraints: 2,
|
||||||
|
totalNumberOfConstraints: 2,
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for name, prep := range tests {
|
for name, prep := range tests {
|
||||||
tc := prep(t)
|
tc := prep(t)
|
||||||
|
|
Loading…
Reference in a new issue