mirror of
https://github.com/golang/go
synced 2024-11-19 17:44:43 -07:00
crypto/x509: enforce all name constraints and support IP, email and URI constraints
This change makes crypto/x509 enforce name constraints for all names in a leaf certificate, not just the name being validated. Thus, after this change, if a certificate validates then all the names in it can be trusted – one doesn't have a validate again for each interesting name. Making extended key usage work in this fashion still remains to be done. Updates #15196 Change-Id: I72ed5ff2f7284082d5bf3e1e86faf76cef62f9b5 Reviewed-on: https://go-review.googlesource.com/62693 Run-TryBot: Adam Langley <agl@golang.org> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: Russ Cox <rsc@golang.org>
This commit is contained in:
parent
a4aa5c3181
commit
9e76ce7070
1569
src/crypto/x509/name_constraints_test.go
Normal file
1569
src/crypto/x509/name_constraints_test.go
Normal file
File diff suppressed because it is too large
Load Diff
@ -87,7 +87,7 @@ func checkChainTrustStatus(c *Certificate, chainCtx *syscall.CertChainContext) e
|
||||
status := chainCtx.TrustStatus.ErrorStatus
|
||||
switch status {
|
||||
case syscall.CERT_TRUST_IS_NOT_TIME_VALID:
|
||||
return CertificateInvalidError{c, Expired}
|
||||
return CertificateInvalidError{c, Expired, ""}
|
||||
default:
|
||||
return UnknownAuthorityError{c, nil, nil}
|
||||
}
|
||||
@ -125,7 +125,7 @@ func checkChainSSLServerPolicy(c *Certificate, chainCtx *syscall.CertChainContex
|
||||
if status.Error != 0 {
|
||||
switch status.Error {
|
||||
case syscall.CERT_E_EXPIRED:
|
||||
return CertificateInvalidError{c, Expired}
|
||||
return CertificateInvalidError{c, Expired, ""}
|
||||
case syscall.CERT_E_CN_NO_MATCH:
|
||||
return HostnameError{c, opts.DNSName}
|
||||
case syscall.CERT_E_UNTRUSTEDROOT:
|
||||
|
@ -9,6 +9,8 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
@ -25,8 +27,8 @@ const (
|
||||
// given in the VerifyOptions.
|
||||
Expired
|
||||
// CANotAuthorizedForThisName results when an intermediate or root
|
||||
// certificate has a name constraint which doesn't include the name
|
||||
// being checked.
|
||||
// certificate has a name constraint which doesn't permit a DNS or
|
||||
// other name (including IP address) in the leaf certificate.
|
||||
CANotAuthorizedForThisName
|
||||
// TooManyIntermediates results when a path length constraint is
|
||||
// violated.
|
||||
@ -37,6 +39,20 @@ const (
|
||||
// NameMismatch results when the subject name of a parent certificate
|
||||
// does not match the issuer name in the child.
|
||||
NameMismatch
|
||||
// NameConstraintsWithoutSANs results when a leaf certificate doesn't
|
||||
// contain a Subject Alternative Name extension, but a CA certificate
|
||||
// contains name constraints.
|
||||
NameConstraintsWithoutSANs
|
||||
// UnconstrainedName results when a CA certificate contains permitted
|
||||
// name constraints, but leaf certificate contains a name of an
|
||||
// unsupported or unconstrained type.
|
||||
UnconstrainedName
|
||||
// TooManyConstraints results when the number of comparision operations
|
||||
// needed to check a certificate exceeds the limit set by
|
||||
// VerifyOptions.MaxConstraintComparisions. This limit exists to
|
||||
// prevent pathological certificates can consuming excessive amounts of
|
||||
// CPU time to verify.
|
||||
TooManyConstraints
|
||||
)
|
||||
|
||||
// CertificateInvalidError results when an odd error occurs. Users of this
|
||||
@ -44,6 +60,7 @@ const (
|
||||
type CertificateInvalidError struct {
|
||||
Cert *Certificate
|
||||
Reason InvalidReason
|
||||
Detail string
|
||||
}
|
||||
|
||||
func (e CertificateInvalidError) Error() string {
|
||||
@ -53,13 +70,17 @@ func (e CertificateInvalidError) Error() string {
|
||||
case Expired:
|
||||
return "x509: certificate has expired or is not yet valid"
|
||||
case CANotAuthorizedForThisName:
|
||||
return "x509: a root or intermediate certificate is not authorized to sign in this domain"
|
||||
return "x509: a root or intermediate certificate is not authorized to sign for this name: " + e.Detail
|
||||
case TooManyIntermediates:
|
||||
return "x509: too many intermediates for path length constraint"
|
||||
case IncompatibleUsage:
|
||||
return "x509: certificate specifies an incompatible key usage"
|
||||
case NameMismatch:
|
||||
return "x509: issuer name does not match subject from issuing certificate"
|
||||
case NameConstraintsWithoutSANs:
|
||||
return "x509: issuer has name constraints but leaf doesn't have a SAN extension"
|
||||
case UnconstrainedName:
|
||||
return "x509: issuer has name constraints but leaf contains unknown or unconstrained name: " + e.Detail
|
||||
}
|
||||
return "x509: unknown error"
|
||||
}
|
||||
@ -156,6 +177,12 @@ type VerifyOptions struct {
|
||||
// constraint down the chain which mirrors Windows CryptoAPI behavior,
|
||||
// but not the spec. To accept any key usage, include ExtKeyUsageAny.
|
||||
KeyUsages []ExtKeyUsage
|
||||
// MaxConstraintComparisions is the maximum number of comparisons to
|
||||
// perform when checking a given certificate's name constraints. If
|
||||
// zero, a sensible default is used. This limit prevents pathalogical
|
||||
// certificates from consuming excessive amounts of CPU time when
|
||||
// validating.
|
||||
MaxConstraintComparisions int
|
||||
}
|
||||
|
||||
const (
|
||||
@ -164,32 +191,354 @@ const (
|
||||
rootCertificate
|
||||
)
|
||||
|
||||
func matchNameConstraint(domain, constraint string) bool {
|
||||
// rfc2821Mailbox represents a “mailbox” (which is an email address to most
|
||||
// people) by breaking it into the “local” (i.e. before the '@') and “domain”
|
||||
// parts.
|
||||
type rfc2821Mailbox struct {
|
||||
local, domain string
|
||||
}
|
||||
|
||||
// parseRFC2821Mailbox parses an email address into local and domain parts,
|
||||
// based on the ABNF for a “Mailbox” from RFC 2821. According to
|
||||
// https://tools.ietf.org/html/rfc5280#section-4.2.1.6 that's correct for an
|
||||
// rfc822Name from a certificate: “The format of an rfc822Name is a "Mailbox"
|
||||
// as defined in https://tools.ietf.org/html/rfc2821#section-4.1.2”.
|
||||
func parseRFC2821Mailbox(in string) (mailbox rfc2821Mailbox, ok bool) {
|
||||
if len(in) == 0 {
|
||||
return mailbox, false
|
||||
}
|
||||
|
||||
localPartBytes := make([]byte, 0, len(in)/2)
|
||||
|
||||
if in[0] == '"' {
|
||||
// Quoted-string = DQUOTE *qcontent DQUOTE
|
||||
// non-whitespace-control = %d1-8 / %d11 / %d12 / %d14-31 / %d127
|
||||
// qcontent = qtext / quoted-pair
|
||||
// qtext = non-whitespace-control /
|
||||
// %d33 / %d35-91 / %d93-126
|
||||
// quoted-pair = ("\" text) / obs-qp
|
||||
// text = %d1-9 / %d11 / %d12 / %d14-127 / obs-text
|
||||
//
|
||||
// (Names beginning with “obs-” are the obsolete syntax from
|
||||
// https://tools.ietf.org/html/rfc2822#section-4. Since it has
|
||||
// been 16 years, we no longer accept that.)
|
||||
in = in[1:]
|
||||
QuotedString:
|
||||
for {
|
||||
if len(in) == 0 {
|
||||
return mailbox, false
|
||||
}
|
||||
c := in[0]
|
||||
in = in[1:]
|
||||
|
||||
switch {
|
||||
case c == '"':
|
||||
break QuotedString
|
||||
|
||||
case c == '\\':
|
||||
// quoted-pair
|
||||
if len(in) == 0 {
|
||||
return mailbox, false
|
||||
}
|
||||
if in[0] == 11 ||
|
||||
in[0] == 12 ||
|
||||
(1 <= in[0] && in[0] <= 9) ||
|
||||
(14 <= in[0] && in[0] <= 127) {
|
||||
localPartBytes = append(localPartBytes, in[0])
|
||||
in = in[1:]
|
||||
} else {
|
||||
return mailbox, false
|
||||
}
|
||||
|
||||
case c == 11 ||
|
||||
c == 12 ||
|
||||
// Space (char 32) is not allowed based on the
|
||||
// BNF, but RFC 3696 gives an example that
|
||||
// assumes that it is. Several “verified”
|
||||
// errata continue to argue about this point.
|
||||
// We choose to accept it.
|
||||
c == 32 ||
|
||||
c == 33 ||
|
||||
c == 127 ||
|
||||
(1 <= c && c <= 8) ||
|
||||
(14 <= c && c <= 31) ||
|
||||
(35 <= c && c <= 91) ||
|
||||
(93 <= c && c <= 126):
|
||||
// qtext
|
||||
localPartBytes = append(localPartBytes, c)
|
||||
|
||||
default:
|
||||
return mailbox, false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Atom ("." Atom)*
|
||||
NextChar:
|
||||
for len(in) > 0 {
|
||||
// atext from https://tools.ietf.org/html/rfc2822#section-3.2.4
|
||||
c := in[0]
|
||||
|
||||
switch {
|
||||
case c == '\\':
|
||||
// Examples given in RFC 3696 suggest that
|
||||
// escaped characters can appear outside of a
|
||||
// quoted string. Several “verified” errata
|
||||
// continue to argue the point. We choose to
|
||||
// accept it.
|
||||
in = in[1:]
|
||||
if len(in) == 0 {
|
||||
return mailbox, false
|
||||
}
|
||||
fallthrough
|
||||
|
||||
case ('0' <= c && c <= '9') ||
|
||||
('a' <= c && c <= 'z') ||
|
||||
('A' <= c && c <= 'Z') ||
|
||||
c == '!' || c == '#' || c == '$' || c == '%' ||
|
||||
c == '&' || c == '\'' || c == '*' || c == '+' ||
|
||||
c == '-' || c == '/' || c == '=' || c == '?' ||
|
||||
c == '^' || c == '_' || c == '`' || c == '{' ||
|
||||
c == '|' || c == '}' || c == '~' || c == '.':
|
||||
localPartBytes = append(localPartBytes, in[0])
|
||||
in = in[1:]
|
||||
|
||||
default:
|
||||
break NextChar
|
||||
}
|
||||
}
|
||||
|
||||
if len(localPartBytes) == 0 {
|
||||
return mailbox, false
|
||||
}
|
||||
|
||||
// https://tools.ietf.org/html/rfc3696#section-3
|
||||
// “period (".") may also appear, but may not be used to start
|
||||
// or end the local part, nor may two or more consecutive
|
||||
// periods appear.”
|
||||
twoDots := []byte{'.', '.'}
|
||||
if localPartBytes[0] == '.' ||
|
||||
localPartBytes[len(localPartBytes)-1] == '.' ||
|
||||
bytes.Contains(localPartBytes, twoDots) {
|
||||
return mailbox, false
|
||||
}
|
||||
}
|
||||
|
||||
if len(in) == 0 || in[0] != '@' {
|
||||
return mailbox, false
|
||||
}
|
||||
in = in[1:]
|
||||
|
||||
// The RFC species a format for domains, but that's known to be
|
||||
// violated in practice so we accept that anything after an '@' is the
|
||||
// domain part.
|
||||
if _, ok := domainToReverseLabels(in); !ok {
|
||||
return mailbox, false
|
||||
}
|
||||
|
||||
mailbox.local = string(localPartBytes)
|
||||
mailbox.domain = in
|
||||
return mailbox, true
|
||||
}
|
||||
|
||||
// domainToReverseLabels converts a textual domain name like foo.example.com to
|
||||
// the list of labels in reverse order, e.g. ["com", "example", "foo"].
|
||||
func domainToReverseLabels(domain string) (reverseLabels []string, ok bool) {
|
||||
for len(domain) > 0 {
|
||||
if i := strings.LastIndexByte(domain, '.'); i == -1 {
|
||||
reverseLabels = append(reverseLabels, domain)
|
||||
domain = ""
|
||||
} else {
|
||||
reverseLabels = append(reverseLabels, domain[i+1:len(domain)])
|
||||
domain = domain[:i]
|
||||
}
|
||||
}
|
||||
|
||||
if len(reverseLabels) > 0 && len(reverseLabels[0]) == 0 {
|
||||
// An empty label at the end indicates an absolute value.
|
||||
return nil, false
|
||||
}
|
||||
|
||||
for _, label := range reverseLabels {
|
||||
if len(label) == 0 {
|
||||
// Empty labels are otherwise invalid.
|
||||
return nil, false
|
||||
}
|
||||
|
||||
for _, c := range label {
|
||||
if c < 33 || c > 126 {
|
||||
// Invalid character.
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return reverseLabels, true
|
||||
}
|
||||
|
||||
func matchEmailConstraint(mailbox rfc2821Mailbox, constraint string) (bool, error) {
|
||||
// If the constraint contains an @, then it specifies an exact mailbox
|
||||
// name.
|
||||
if strings.Contains(constraint, "@") {
|
||||
constraintMailbox, ok := parseRFC2821Mailbox(constraint)
|
||||
if !ok {
|
||||
return false, fmt.Errorf("x509: internal error: cannot parse constraint %q", constraint)
|
||||
}
|
||||
return mailbox.local == constraintMailbox.local && strings.EqualFold(mailbox.domain, constraintMailbox.domain), nil
|
||||
}
|
||||
|
||||
// Otherwise the constraint is like a DNS constraint of the domain part
|
||||
// of the mailbox.
|
||||
return matchDomainConstraint(mailbox.domain, constraint)
|
||||
}
|
||||
|
||||
func matchURIConstraint(uri *url.URL, constraint string) (bool, error) {
|
||||
// https://tools.ietf.org/html/rfc5280#section-4.2.1.10
|
||||
// “a uniformResourceIdentifier that does not include an authority
|
||||
// component with a host name specified as a fully qualified domain
|
||||
// name (e.g., if the URI either does not include an authority
|
||||
// component or includes an authority component in which the host name
|
||||
// is specified as an IP address), then the application MUST reject the
|
||||
// certificate.”
|
||||
|
||||
host := uri.Host
|
||||
if len(host) == 0 {
|
||||
return false, fmt.Errorf("URI with empty host (%q) cannot be matched against constraints", uri.String())
|
||||
}
|
||||
|
||||
if strings.Contains(host, ":") && !strings.HasSuffix(host, "]") {
|
||||
var err error
|
||||
host, _, err = net.SplitHostPort(uri.Host)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
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 matchDomainConstraint(host, constraint)
|
||||
}
|
||||
|
||||
func matchIPConstraint(ip net.IP, constraint *net.IPNet) (bool, error) {
|
||||
if len(ip) != len(constraint.IP) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
for i := range ip {
|
||||
if mask := constraint.Mask[i]; ip[i]&mask != constraint.IP[i]&mask {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func 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 len(constraint) == 0 {
|
||||
return true
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if len(domain) < len(constraint) {
|
||||
return false
|
||||
domainLabels, ok := domainToReverseLabels(domain)
|
||||
if !ok {
|
||||
return false, fmt.Errorf("x509: internal error: cannot parse domain %q", domain)
|
||||
}
|
||||
|
||||
prefixLen := len(domain) - len(constraint)
|
||||
if !strings.EqualFold(domain[prefixLen:], constraint) {
|
||||
return false
|
||||
// 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
|
||||
// behaviour for DNS constraints.
|
||||
|
||||
mustHaveSubdomains := false
|
||||
if constraint[0] == '.' {
|
||||
mustHaveSubdomains = true
|
||||
constraint = constraint[1:]
|
||||
}
|
||||
|
||||
if prefixLen == 0 {
|
||||
return true
|
||||
constraintLabels, ok := domainToReverseLabels(constraint)
|
||||
if !ok {
|
||||
return false, fmt.Errorf("x509: internal error: cannot parse domain %q", constraint)
|
||||
}
|
||||
|
||||
isSubdomain := domain[prefixLen-1] == '.'
|
||||
constraintHasLeadingDot := constraint[0] == '.'
|
||||
return isSubdomain != constraintHasLeadingDot
|
||||
if len(domainLabels) < len(constraintLabels) ||
|
||||
(mustHaveSubdomains && len(domainLabels) == len(constraintLabels)) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
for i, constraintLabel := range constraintLabels {
|
||||
if !strings.EqualFold(constraintLabel, domainLabels[i]) {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// isValid performs validity checks on the c.
|
||||
// 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.
|
||||
func (c *Certificate) checkNameConstraints(count *int,
|
||||
maxConstraintComparisons int,
|
||||
nameType string,
|
||||
name string,
|
||||
parsedName interface{},
|
||||
match func(parsedName, constraint interface{}) (match bool, err error),
|
||||
permitted, excluded interface{}) error {
|
||||
|
||||
excludedValue := reflect.ValueOf(excluded)
|
||||
|
||||
*count += excludedValue.Len()
|
||||
if *count > maxConstraintComparisons {
|
||||
return CertificateInvalidError{c, TooManyConstraints, ""}
|
||||
}
|
||||
|
||||
for i := 0; i < excludedValue.Len(); i++ {
|
||||
constraint := excludedValue.Index(i).Interface()
|
||||
match, err := match(parsedName, constraint)
|
||||
if err != nil {
|
||||
return CertificateInvalidError{c, CANotAuthorizedForThisName, err.Error()}
|
||||
}
|
||||
|
||||
if match {
|
||||
return CertificateInvalidError{c, CANotAuthorizedForThisName, fmt.Sprintf("%s %q is excluded by constraint %q", nameType, name, constraint)}
|
||||
}
|
||||
}
|
||||
|
||||
permittedValue := reflect.ValueOf(permitted)
|
||||
|
||||
*count += permittedValue.Len()
|
||||
if *count > maxConstraintComparisons {
|
||||
return CertificateInvalidError{c, TooManyConstraints, ""}
|
||||
}
|
||||
|
||||
ok := true
|
||||
for i := 0; i < permittedValue.Len(); i++ {
|
||||
constraint := permittedValue.Index(i).Interface()
|
||||
|
||||
var err error
|
||||
if ok, err = match(parsedName, constraint); err != nil {
|
||||
return CertificateInvalidError{c, CANotAuthorizedForThisName, err.Error()}
|
||||
}
|
||||
|
||||
if ok {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !ok {
|
||||
return CertificateInvalidError{c, CANotAuthorizedForThisName, fmt.Sprintf("%s %q is not permitted by any constraint", nameType, name)}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// isValid performs validity checks on c given that it is a candidate to append
|
||||
// to the chain in currentChain.
|
||||
func (c *Certificate) isValid(certType int, currentChain []*Certificate, opts *VerifyOptions) error {
|
||||
if len(c.UnhandledCriticalExtensions) > 0 {
|
||||
return UnhandledCriticalExtension{}
|
||||
@ -198,7 +547,7 @@ func (c *Certificate) isValid(certType int, currentChain []*Certificate, opts *V
|
||||
if len(currentChain) > 0 {
|
||||
child := currentChain[len(currentChain)-1]
|
||||
if !bytes.Equal(child.RawIssuer, c.RawSubject) {
|
||||
return CertificateInvalidError{c, NameMismatch}
|
||||
return CertificateInvalidError{c, NameMismatch, ""}
|
||||
}
|
||||
}
|
||||
|
||||
@ -207,26 +556,92 @@ func (c *Certificate) isValid(certType int, currentChain []*Certificate, opts *V
|
||||
now = time.Now()
|
||||
}
|
||||
if now.Before(c.NotBefore) || now.After(c.NotAfter) {
|
||||
return CertificateInvalidError{c, Expired}
|
||||
return CertificateInvalidError{c, Expired, ""}
|
||||
}
|
||||
|
||||
if len(c.PermittedDNSDomains) > 0 {
|
||||
ok := false
|
||||
for _, constraint := range c.PermittedDNSDomains {
|
||||
ok = matchNameConstraint(opts.DNSName, constraint)
|
||||
if ok {
|
||||
break
|
||||
}
|
||||
if (certType == intermediateCertificate || certType == rootCertificate) && c.hasNameConstraints() {
|
||||
maxConstraintComparisons := opts.MaxConstraintComparisions
|
||||
if maxConstraintComparisons == 0 {
|
||||
maxConstraintComparisons = 250000
|
||||
}
|
||||
count := 0
|
||||
|
||||
if len(currentChain) == 0 {
|
||||
return errors.New("x509: internal error: empty chain when appending CA cert")
|
||||
}
|
||||
leaf := currentChain[0]
|
||||
|
||||
sanExtension, ok := leaf.getSANExtension()
|
||||
if !ok {
|
||||
return CertificateInvalidError{c, CANotAuthorizedForThisName}
|
||||
}
|
||||
// This is the deprecated, legacy case of depending on
|
||||
// the CN as a hostname. Chains modern enough to be
|
||||
// using name constraints should not be depending on
|
||||
// CNs.
|
||||
return CertificateInvalidError{c, NameConstraintsWithoutSANs, ""}
|
||||
}
|
||||
|
||||
for _, constraint := range c.ExcludedDNSDomains {
|
||||
if matchNameConstraint(opts.DNSName, constraint) {
|
||||
return CertificateInvalidError{c, CANotAuthorizedForThisName}
|
||||
err := forEachSAN(sanExtension, func(tag int, data []byte) error {
|
||||
switch tag {
|
||||
case nameTypeEmail:
|
||||
name := string(data)
|
||||
mailbox, ok := parseRFC2821Mailbox(name)
|
||||
if !ok {
|
||||
// This certificate should not have parsed.
|
||||
return errors.New("x509: internal error: rfc822Name SAN failed to parse")
|
||||
}
|
||||
|
||||
if err := c.checkNameConstraints(&count, maxConstraintComparisons, "email address", name, mailbox,
|
||||
func(parsedName, constraint interface{}) (bool, error) {
|
||||
return matchEmailConstraint(parsedName.(rfc2821Mailbox), constraint.(string))
|
||||
}, c.PermittedEmailAddresses, c.ExcludedEmailAddresses); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case nameTypeDNS:
|
||||
name := string(data)
|
||||
if err := c.checkNameConstraints(&count, maxConstraintComparisons, "DNS name", name, name,
|
||||
func(parsedName, constraint interface{}) (bool, error) {
|
||||
return matchDomainConstraint(parsedName.(string), constraint.(string))
|
||||
}, c.PermittedDNSDomains, c.ExcludedDNSDomains); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case nameTypeURI:
|
||||
name := string(data)
|
||||
uri, err := url.Parse(name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("x509: internal error: URI SAN %q failed to parse", name)
|
||||
}
|
||||
|
||||
if err := c.checkNameConstraints(&count, maxConstraintComparisons, "URI", name, uri,
|
||||
func(parsedName, constraint interface{}) (bool, error) {
|
||||
return matchURIConstraint(parsedName.(*url.URL), constraint.(string))
|
||||
}, c.PermittedURIDomains, c.ExcludedURIDomains); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case nameTypeIP:
|
||||
ip := net.IP(data)
|
||||
if l := len(ip); l != net.IPv4len && l != net.IPv6len {
|
||||
return fmt.Errorf("x509: internal error: IP SAN %x failed to parse", data)
|
||||
}
|
||||
|
||||
if err := c.checkNameConstraints(&count, maxConstraintComparisons, "IP address", ip.String(), ip,
|
||||
func(parsedName, constraint interface{}) (bool, error) {
|
||||
return matchIPConstraint(parsedName.(net.IP), constraint.(*net.IPNet))
|
||||
}, c.PermittedIPRanges, c.ExcludedIPRanges); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
default:
|
||||
// Unknown SAN types are ignored.
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@ -248,13 +663,13 @@ func (c *Certificate) isValid(certType int, currentChain []*Certificate, opts *V
|
||||
// encryption key could only be used for Diffie-Hellman key agreement.
|
||||
|
||||
if certType == intermediateCertificate && (!c.BasicConstraintsValid || !c.IsCA) {
|
||||
return CertificateInvalidError{c, NotAuthorizedToSign}
|
||||
return CertificateInvalidError{c, NotAuthorizedToSign, ""}
|
||||
}
|
||||
|
||||
if c.BasicConstraintsValid && c.MaxPathLen >= 0 {
|
||||
numIntermediates := len(currentChain) - 1
|
||||
if numIntermediates > c.MaxPathLen {
|
||||
return CertificateInvalidError{c, TooManyIntermediates}
|
||||
return CertificateInvalidError{c, TooManyIntermediates, ""}
|
||||
}
|
||||
}
|
||||
|
||||
@ -337,7 +752,7 @@ func (c *Certificate) Verify(opts VerifyOptions) (chains [][]*Certificate, err e
|
||||
}
|
||||
|
||||
if len(chains) == 0 {
|
||||
err = CertificateInvalidError{c, IncompatibleUsage}
|
||||
err = CertificateInvalidError{c, IncompatibleUsage, ""}
|
||||
}
|
||||
|
||||
return
|
||||
|
@ -1551,22 +1551,37 @@ func TestUnknownAuthorityError(t *testing.T) {
|
||||
|
||||
var nameConstraintTests = []struct {
|
||||
constraint, domain string
|
||||
expectError bool
|
||||
shouldMatch bool
|
||||
}{
|
||||
{"", "anything.com", true},
|
||||
{"example.com", "example.com", true},
|
||||
{"example.com", "ExAmPle.coM", true},
|
||||
{"example.com", "exampl1.com", false},
|
||||
{"example.com", "www.ExAmPle.coM", true},
|
||||
{"example.com", "notexample.com", false},
|
||||
{".example.com", "example.com", false},
|
||||
{".example.com", "www.example.com", true},
|
||||
{".example.com", "www..example.com", false},
|
||||
{"", "anything.com", false, true},
|
||||
{"example.com", "example.com", false, true},
|
||||
{"example.com.", "example.com", true, false},
|
||||
{"example.com", "example.com.", true, false},
|
||||
{"example.com", "ExAmPle.coM", false, true},
|
||||
{"example.com", "exampl1.com", false, false},
|
||||
{"example.com", "www.ExAmPle.coM", false, true},
|
||||
{"example.com", "sub.www.ExAmPle.coM", false, true},
|
||||
{"example.com", "notexample.com", false, false},
|
||||
{".example.com", "example.com", false, false},
|
||||
{".example.com", "www.example.com", false, true},
|
||||
{".example.com", "www..example.com", true, false},
|
||||
}
|
||||
|
||||
func TestNameConstraints(t *testing.T) {
|
||||
for i, test := range nameConstraintTests {
|
||||
result := matchNameConstraint(test.domain, test.constraint)
|
||||
result, err := matchDomainConstraint(test.domain, test.constraint)
|
||||
|
||||
if err != nil && !test.expectError {
|
||||
t.Errorf("unexpected error for test #%d: domain=%s, constraint=%s, err=%s", i, test.domain, test.constraint, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if err == nil && test.expectError {
|
||||
t.Errorf("unexpected success for test #%d: domain=%s, constraint=%s", i, test.domain, test.constraint)
|
||||
continue
|
||||
}
|
||||
|
||||
if result != test.shouldMatch {
|
||||
t.Errorf("unexpected result for test #%d: domain=%s, constraint=%s, result=%t", i, test.domain, test.constraint, result)
|
||||
}
|
||||
|
@ -27,7 +27,9 @@ import (
|
||||
"io"
|
||||
"math/big"
|
||||
"net"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@ -698,11 +700,18 @@ type Certificate struct {
|
||||
DNSNames []string
|
||||
EmailAddresses []string
|
||||
IPAddresses []net.IP
|
||||
URIs []*url.URL
|
||||
|
||||
// Name constraints
|
||||
PermittedDNSDomainsCritical bool // if true then the name constraints are marked critical.
|
||||
PermittedDNSDomains []string
|
||||
ExcludedDNSDomains []string
|
||||
PermittedIPRanges []*net.IPNet
|
||||
ExcludedIPRanges []*net.IPNet
|
||||
PermittedEmailAddresses []string
|
||||
ExcludedEmailAddresses []string
|
||||
PermittedURIDomains []string
|
||||
ExcludedURIDomains []string
|
||||
|
||||
// CRL Distribution Points
|
||||
CRLDistributionPoints []string
|
||||
@ -821,6 +830,26 @@ func (c *Certificate) CheckSignature(algo SignatureAlgorithm, signed, signature
|
||||
return checkSignature(algo, signed, signature, c.PublicKey)
|
||||
}
|
||||
|
||||
func (c *Certificate) hasNameConstraints() bool {
|
||||
for _, e := range c.Extensions {
|
||||
if len(e.Id) == 4 && e.Id[0] == 2 && e.Id[1] == 5 && e.Id[2] == 29 && e.Id[3] == 30 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *Certificate) getSANExtension() ([]byte, bool) {
|
||||
for _, e := range c.Extensions {
|
||||
if len(e.Id) == 4 && e.Id[0] == 2 && e.Id[1] == 5 && e.Id[2] == 29 && e.Id[3] == 17 {
|
||||
return e.Value, true
|
||||
}
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func signaturePublicKeyAlgoMismatchError(expectedPubKeyAlgo PublicKeyAlgorithm, pubKey interface{}) error {
|
||||
return fmt.Errorf("x509: signature algorithm specifies an %s public key, but have public key of type %T", expectedPubKeyAlgo.String(), pubKey)
|
||||
}
|
||||
@ -930,8 +959,18 @@ type nameConstraints struct {
|
||||
Excluded []generalSubtree `asn1:"optional,tag:1"`
|
||||
}
|
||||
|
||||
const (
|
||||
nameTypeEmail = 1
|
||||
nameTypeDNS = 2
|
||||
nameTypeURI = 6
|
||||
nameTypeIP = 7
|
||||
)
|
||||
|
||||
type generalSubtree struct {
|
||||
Email string `asn1:"tag:1,optional,ia5"`
|
||||
Name string `asn1:"tag:2,optional,ia5"`
|
||||
URIDomain string `asn1:"tag:6,optional,ia5"`
|
||||
IPAndMask []byte `asn1:"tag:7,optional"`
|
||||
}
|
||||
|
||||
// RFC 5280, 4.2.2.1
|
||||
@ -1086,14 +1125,33 @@ func forEachSAN(extension []byte, callback func(tag int, data []byte) error) err
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseSANExtension(value []byte) (dnsNames, emailAddresses []string, ipAddresses []net.IP, err error) {
|
||||
func parseSANExtension(value []byte) (dnsNames, emailAddresses []string, ipAddresses []net.IP, uris []*url.URL, err error) {
|
||||
err = forEachSAN(value, func(tag int, data []byte) error {
|
||||
switch tag {
|
||||
case 1:
|
||||
emailAddresses = append(emailAddresses, string(data))
|
||||
case 2:
|
||||
dnsNames = append(dnsNames, string(data))
|
||||
case 7:
|
||||
case nameTypeEmail:
|
||||
mailbox := string(data)
|
||||
if _, ok := parseRFC2821Mailbox(mailbox); !ok {
|
||||
return fmt.Errorf("x509: cannot parse rfc822Name %q", mailbox)
|
||||
}
|
||||
emailAddresses = append(emailAddresses, mailbox)
|
||||
case nameTypeDNS:
|
||||
domain := string(data)
|
||||
if _, ok := domainToReverseLabels(domain); !ok {
|
||||
return fmt.Errorf("x509: cannot parse dnsName %q", string(data))
|
||||
}
|
||||
dnsNames = append(dnsNames, domain)
|
||||
case nameTypeURI:
|
||||
uri, err := url.Parse(string(data))
|
||||
if err != nil {
|
||||
return fmt.Errorf("x509: cannot parse URI %q: %s", string(data), err)
|
||||
}
|
||||
if len(uri.Host) > 0 {
|
||||
if _, ok := domainToReverseLabels(uri.Host); !ok {
|
||||
return fmt.Errorf("x509: cannot parse URI %q: invalid domain", string(data))
|
||||
}
|
||||
}
|
||||
uris = append(uris, uri)
|
||||
case nameTypeIP:
|
||||
switch len(data) {
|
||||
case net.IPv4len, net.IPv6len:
|
||||
ipAddresses = append(ipAddresses, data)
|
||||
@ -1108,6 +1166,160 @@ func parseSANExtension(value []byte) (dnsNames, emailAddresses []string, ipAddre
|
||||
return
|
||||
}
|
||||
|
||||
// isValidIPMask returns true iff mask consists of zero or more 1 bits, followed by zero bits.
|
||||
func isValidIPMask(mask []byte) bool {
|
||||
seenZero := false
|
||||
|
||||
for _, b := range mask {
|
||||
if seenZero {
|
||||
if b != 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
switch b {
|
||||
case 0x00, 0x80, 0xc0, 0xe0, 0xf0, 0xf8, 0xfc, 0xfe:
|
||||
seenZero = true
|
||||
case 0xff:
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func parseNameConstraintsExtension(out *Certificate, e pkix.Extension) (unhandled bool, err error) {
|
||||
// RFC 5280, 4.2.1.10
|
||||
|
||||
// NameConstraints ::= SEQUENCE {
|
||||
// permittedSubtrees [0] GeneralSubtrees OPTIONAL,
|
||||
// excludedSubtrees [1] GeneralSubtrees OPTIONAL }
|
||||
//
|
||||
// GeneralSubtrees ::= SEQUENCE SIZE (1..MAX) OF GeneralSubtree
|
||||
//
|
||||
// GeneralSubtree ::= SEQUENCE {
|
||||
// base GeneralName,
|
||||
// minimum [0] BaseDistance DEFAULT 0,
|
||||
// maximum [1] BaseDistance OPTIONAL }
|
||||
//
|
||||
// BaseDistance ::= INTEGER (0..MAX)
|
||||
|
||||
var constraints nameConstraints
|
||||
if rest, err := asn1.Unmarshal(e.Value, &constraints); err != nil {
|
||||
return false, err
|
||||
} else if len(rest) != 0 {
|
||||
return false, errors.New("x509: trailing data after X.509 NameConstraints")
|
||||
}
|
||||
|
||||
if len(constraints.Permitted) == 0 && len(constraints.Excluded) == 0 {
|
||||
// https://tools.ietf.org/html/rfc5280#section-4.2.1.10:
|
||||
// “either the permittedSubtrees field
|
||||
// or the excludedSubtrees MUST be
|
||||
// present”
|
||||
return false, errors.New("x509: empty name constraints extension")
|
||||
}
|
||||
|
||||
getValues := func(subtrees []generalSubtree) (dnsNames []string, ips []*net.IPNet, emails, uriDomains []string, err error) {
|
||||
for _, subtree := range subtrees {
|
||||
switch {
|
||||
case len(subtree.Name) != 0:
|
||||
domain := subtree.Name
|
||||
if len(domain) > 0 && domain[0] == '.' {
|
||||
// constraints can have a leading
|
||||
// period to exclude the domain
|
||||
// itself, but that's not valid in a
|
||||
// normal domain name.
|
||||
domain = domain[1:]
|
||||
}
|
||||
if _, ok := domainToReverseLabels(domain); !ok {
|
||||
return nil, nil, nil, nil, fmt.Errorf("x509: failed to parse dnsName constraint %q", subtree.Name)
|
||||
}
|
||||
dnsNames = append(dnsNames, subtree.Name)
|
||||
|
||||
case len(subtree.IPAndMask) != 0:
|
||||
l := len(subtree.IPAndMask)
|
||||
var ip, mask []byte
|
||||
|
||||
switch l {
|
||||
case 8:
|
||||
ip = subtree.IPAndMask[:4]
|
||||
mask = subtree.IPAndMask[4:]
|
||||
|
||||
case 32:
|
||||
ip = subtree.IPAndMask[:16]
|
||||
mask = subtree.IPAndMask[16:]
|
||||
|
||||
default:
|
||||
return nil, nil, nil, nil, fmt.Errorf("x509: IP constraint contained value of length %d", l)
|
||||
}
|
||||
|
||||
if !isValidIPMask(mask) {
|
||||
return nil, nil, nil, nil, fmt.Errorf("x509: IP constraint contained invalid mask %x", mask)
|
||||
}
|
||||
|
||||
ips = append(ips, &net.IPNet{IP: net.IP(ip), Mask: net.IPMask(mask)})
|
||||
|
||||
case len(subtree.Email) != 0:
|
||||
constraint := subtree.Email
|
||||
// If the constraint contains an @ then
|
||||
// it specifies an exact mailbox name.
|
||||
if strings.Contains(constraint, "@") {
|
||||
if _, ok := parseRFC2821Mailbox(constraint); !ok {
|
||||
return nil, nil, nil, nil, fmt.Errorf("x509: failed to parse rfc822Name constraint %q", constraint)
|
||||
}
|
||||
} else {
|
||||
// Otherwise it's a domain name.
|
||||
domain := constraint
|
||||
if len(domain) > 0 && domain[0] == '.' {
|
||||
domain = domain[1:]
|
||||
}
|
||||
if _, ok := domainToReverseLabels(domain); !ok {
|
||||
return nil, nil, nil, nil, fmt.Errorf("x509: failed to parse rfc822Name constraint %q", constraint)
|
||||
}
|
||||
}
|
||||
emails = append(emails, constraint)
|
||||
|
||||
case len(subtree.URIDomain) != 0:
|
||||
domain := subtree.URIDomain
|
||||
|
||||
if net.ParseIP(domain) != nil {
|
||||
return nil, nil, nil, nil, fmt.Errorf("x509: failed to parse URI constraint %q: cannot be IP address", subtree.URIDomain)
|
||||
}
|
||||
|
||||
if len(domain) > 0 && domain[0] == '.' {
|
||||
// constraints can have a leading
|
||||
// period to exclude the domain
|
||||
// itself, but that's not valid in a
|
||||
// normal domain name.
|
||||
domain = domain[1:]
|
||||
}
|
||||
if _, ok := domainToReverseLabels(domain); !ok {
|
||||
return nil, nil, nil, nil, fmt.Errorf("x509: failed to parse URI constraint %q", subtree.URIDomain)
|
||||
}
|
||||
uriDomains = append(uriDomains, subtree.URIDomain)
|
||||
|
||||
default:
|
||||
unhandled = true
|
||||
}
|
||||
}
|
||||
|
||||
return dnsNames, ips, emails, uriDomains, nil
|
||||
}
|
||||
|
||||
if out.PermittedDNSDomains, out.PermittedIPRanges, out.PermittedEmailAddresses, out.PermittedURIDomains, err = getValues(constraints.Permitted); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if out.ExcludedDNSDomains, out.ExcludedIPRanges, out.ExcludedEmailAddresses, out.ExcludedURIDomains, err = getValues(constraints.Excluded); err != nil {
|
||||
return false, err
|
||||
}
|
||||
out.PermittedDNSDomainsCritical = e.Critical
|
||||
|
||||
return unhandled, nil
|
||||
}
|
||||
|
||||
func parseCertificate(in *certificate) (*Certificate, error) {
|
||||
out := new(Certificate)
|
||||
out.Raw = in.Raw
|
||||
@ -1187,69 +1399,22 @@ func parseCertificate(in *certificate) (*Certificate, error) {
|
||||
out.MaxPathLenZero = out.MaxPathLen == 0
|
||||
// TODO: map out.MaxPathLen to 0 if it has the -1 default value? (Issue 19285)
|
||||
case 17:
|
||||
out.DNSNames, out.EmailAddresses, out.IPAddresses, err = parseSANExtension(e.Value)
|
||||
out.DNSNames, out.EmailAddresses, out.IPAddresses, out.URIs, err = parseSANExtension(e.Value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(out.DNSNames) == 0 && len(out.EmailAddresses) == 0 && len(out.IPAddresses) == 0 {
|
||||
if len(out.DNSNames) == 0 && len(out.EmailAddresses) == 0 && len(out.IPAddresses) == 0 && len(out.URIs) == 0 {
|
||||
// If we didn't parse anything then we do the critical check, below.
|
||||
unhandled = true
|
||||
}
|
||||
|
||||
case 30:
|
||||
// RFC 5280, 4.2.1.10
|
||||
|
||||
// NameConstraints ::= SEQUENCE {
|
||||
// permittedSubtrees [0] GeneralSubtrees OPTIONAL,
|
||||
// excludedSubtrees [1] GeneralSubtrees OPTIONAL }
|
||||
//
|
||||
// GeneralSubtrees ::= SEQUENCE SIZE (1..MAX) OF GeneralSubtree
|
||||
//
|
||||
// GeneralSubtree ::= SEQUENCE {
|
||||
// base GeneralName,
|
||||
// minimum [0] BaseDistance DEFAULT 0,
|
||||
// maximum [1] BaseDistance OPTIONAL }
|
||||
//
|
||||
// BaseDistance ::= INTEGER (0..MAX)
|
||||
|
||||
var constraints nameConstraints
|
||||
if rest, err := asn1.Unmarshal(e.Value, &constraints); err != nil {
|
||||
unhandled, err = parseNameConstraintsExtension(out, e)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if len(rest) != 0 {
|
||||
return nil, errors.New("x509: trailing data after X.509 NameConstraints")
|
||||
}
|
||||
|
||||
if len(constraints.Permitted) == 0 && len(constraints.Excluded) == 0 {
|
||||
// https://tools.ietf.org/html/rfc5280#section-4.2.1.10:
|
||||
// “either the permittedSubtrees field
|
||||
// or the excludedSubtrees MUST be
|
||||
// present”
|
||||
return nil, errors.New("x509: empty name constraints extension")
|
||||
}
|
||||
|
||||
getDNSNames := func(subtrees []generalSubtree, isCritical bool) (dnsNames []string, err error) {
|
||||
for _, subtree := range subtrees {
|
||||
if len(subtree.Name) == 0 {
|
||||
if isCritical {
|
||||
return nil, UnhandledCriticalExtension{}
|
||||
}
|
||||
continue
|
||||
}
|
||||
dnsNames = append(dnsNames, subtree.Name)
|
||||
}
|
||||
|
||||
return dnsNames, nil
|
||||
}
|
||||
|
||||
if out.PermittedDNSDomains, err = getDNSNames(constraints.Permitted, e.Critical); err != nil {
|
||||
return out, err
|
||||
}
|
||||
if out.ExcludedDNSDomains, err = getDNSNames(constraints.Excluded, e.Critical); err != nil {
|
||||
return out, err
|
||||
}
|
||||
out.PermittedDNSDomainsCritical = e.Critical
|
||||
|
||||
case 31:
|
||||
// RFC 5280, 4.2.1.13
|
||||
|
||||
@ -1483,13 +1648,13 @@ func oidInExtensions(oid asn1.ObjectIdentifier, extensions []pkix.Extension) boo
|
||||
|
||||
// marshalSANs marshals a list of addresses into a the contents of an X.509
|
||||
// SubjectAlternativeName extension.
|
||||
func marshalSANs(dnsNames, emailAddresses []string, ipAddresses []net.IP) (derBytes []byte, err error) {
|
||||
func marshalSANs(dnsNames, emailAddresses []string, ipAddresses []net.IP, uris []*url.URL) (derBytes []byte, err error) {
|
||||
var rawValues []asn1.RawValue
|
||||
for _, name := range dnsNames {
|
||||
rawValues = append(rawValues, asn1.RawValue{Tag: 2, Class: 2, Bytes: []byte(name)})
|
||||
rawValues = append(rawValues, asn1.RawValue{Tag: nameTypeDNS, Class: 2, Bytes: []byte(name)})
|
||||
}
|
||||
for _, email := range emailAddresses {
|
||||
rawValues = append(rawValues, asn1.RawValue{Tag: 1, Class: 2, Bytes: []byte(email)})
|
||||
rawValues = append(rawValues, asn1.RawValue{Tag: nameTypeEmail, Class: 2, Bytes: []byte(email)})
|
||||
}
|
||||
for _, rawIP := range ipAddresses {
|
||||
// If possible, we always want to encode IPv4 addresses in 4 bytes.
|
||||
@ -1497,7 +1662,10 @@ func marshalSANs(dnsNames, emailAddresses []string, ipAddresses []net.IP) (derBy
|
||||
if ip == nil {
|
||||
ip = rawIP
|
||||
}
|
||||
rawValues = append(rawValues, asn1.RawValue{Tag: 7, Class: 2, Bytes: ip})
|
||||
rawValues = append(rawValues, asn1.RawValue{Tag: nameTypeIP, Class: 2, Bytes: ip})
|
||||
}
|
||||
for _, uri := range uris {
|
||||
rawValues = append(rawValues, asn1.RawValue{Tag: nameTypeURI, Class: 2, Bytes: []byte(uri.String())})
|
||||
}
|
||||
return asn1.Marshal(rawValues)
|
||||
}
|
||||
@ -1608,10 +1776,10 @@ func buildExtensions(template *Certificate, authorityKeyId []byte) (ret []pkix.E
|
||||
n++
|
||||
}
|
||||
|
||||
if (len(template.DNSNames) > 0 || len(template.EmailAddresses) > 0 || len(template.IPAddresses) > 0) &&
|
||||
if (len(template.DNSNames) > 0 || len(template.EmailAddresses) > 0 || len(template.IPAddresses) > 0 || len(template.URIs) > 0) &&
|
||||
!oidInExtensions(oidExtensionSubjectAltName, template.ExtraExtensions) {
|
||||
ret[n].Id = oidExtensionSubjectAltName
|
||||
ret[n].Value, err = marshalSANs(template.DNSNames, template.EmailAddresses, template.IPAddresses)
|
||||
ret[n].Value, err = marshalSANs(template.DNSNames, template.EmailAddresses, template.IPAddresses, template.URIs)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@ -1632,20 +1800,50 @@ func buildExtensions(template *Certificate, authorityKeyId []byte) (ret []pkix.E
|
||||
n++
|
||||
}
|
||||
|
||||
if (len(template.PermittedDNSDomains) > 0 || len(template.ExcludedDNSDomains) > 0) &&
|
||||
if (len(template.PermittedDNSDomains) > 0 || len(template.ExcludedDNSDomains) > 0 ||
|
||||
len(template.PermittedIPRanges) > 0 || len(template.ExcludedIPRanges) > 0 ||
|
||||
len(template.PermittedEmailAddresses) > 0 || len(template.ExcludedEmailAddresses) > 0 ||
|
||||
len(template.PermittedURIDomains) > 0 || len(template.ExcludedURIDomains) > 0) &&
|
||||
!oidInExtensions(oidExtensionNameConstraints, template.ExtraExtensions) {
|
||||
ret[n].Id = oidExtensionNameConstraints
|
||||
ret[n].Critical = template.PermittedDNSDomainsCritical
|
||||
|
||||
var out nameConstraints
|
||||
|
||||
out.Permitted = make([]generalSubtree, len(template.PermittedDNSDomains))
|
||||
for i, permitted := range template.PermittedDNSDomains {
|
||||
out.Permitted[i] = generalSubtree{Name: permitted}
|
||||
ipAndMask := func(ipNet *net.IPNet) []byte {
|
||||
maskedIP := ipNet.IP.Mask(ipNet.Mask)
|
||||
ipAndMask := make([]byte, 0, len(maskedIP)+len(ipNet.Mask))
|
||||
ipAndMask = append(ipAndMask, maskedIP...)
|
||||
ipAndMask = append(ipAndMask, ipNet.Mask...)
|
||||
return ipAndMask
|
||||
}
|
||||
out.Excluded = make([]generalSubtree, len(template.ExcludedDNSDomains))
|
||||
for i, excluded := range template.ExcludedDNSDomains {
|
||||
out.Excluded[i] = generalSubtree{Name: excluded}
|
||||
|
||||
out.Permitted = make([]generalSubtree, 0, len(template.PermittedDNSDomains)+len(template.PermittedIPRanges))
|
||||
for _, permitted := range template.PermittedDNSDomains {
|
||||
out.Permitted = append(out.Permitted, generalSubtree{Name: permitted})
|
||||
}
|
||||
for _, permitted := range template.PermittedIPRanges {
|
||||
out.Permitted = append(out.Permitted, generalSubtree{IPAndMask: ipAndMask(permitted)})
|
||||
}
|
||||
for _, permitted := range template.PermittedEmailAddresses {
|
||||
out.Permitted = append(out.Permitted, generalSubtree{Email: permitted})
|
||||
}
|
||||
for _, permitted := range template.PermittedURIDomains {
|
||||
out.Permitted = append(out.Permitted, generalSubtree{URIDomain: permitted})
|
||||
}
|
||||
|
||||
out.Excluded = make([]generalSubtree, 0, len(template.ExcludedDNSDomains)+len(template.ExcludedIPRanges))
|
||||
for _, excluded := range template.ExcludedDNSDomains {
|
||||
out.Excluded = append(out.Excluded, generalSubtree{Name: excluded})
|
||||
}
|
||||
for _, excluded := range template.ExcludedIPRanges {
|
||||
out.Excluded = append(out.Excluded, generalSubtree{IPAndMask: ipAndMask(excluded)})
|
||||
}
|
||||
for _, excluded := range template.ExcludedEmailAddresses {
|
||||
out.Excluded = append(out.Excluded, generalSubtree{Email: excluded})
|
||||
}
|
||||
for _, excluded := range template.ExcludedURIDomains {
|
||||
out.Excluded = append(out.Excluded, generalSubtree{URIDomain: excluded})
|
||||
}
|
||||
|
||||
ret[n].Value, err = asn1.Marshal(out)
|
||||
@ -1997,6 +2195,7 @@ type CertificateRequest struct {
|
||||
DNSNames []string
|
||||
EmailAddresses []string
|
||||
IPAddresses []net.IP
|
||||
URIs []*url.URL
|
||||
}
|
||||
|
||||
// These structures reflect the ASN.1 structure of X.509 certificate
|
||||
@ -2088,7 +2287,7 @@ func parseCSRExtensions(rawAttributes []asn1.RawValue) ([]pkix.Extension, error)
|
||||
|
||||
// CreateCertificateRequest creates a new certificate request based on a
|
||||
// template. The following members of template are used: Attributes, DNSNames,
|
||||
// EmailAddresses, ExtraExtensions, IPAddresses, SignatureAlgorithm, and
|
||||
// EmailAddresses, ExtraExtensions, IPAddresses, URIs, SignatureAlgorithm, and
|
||||
// Subject. The private key is the private key of the signer.
|
||||
//
|
||||
// The returned slice is the certificate request in DER encoding.
|
||||
@ -2117,9 +2316,9 @@ func CreateCertificateRequest(rand io.Reader, template *CertificateRequest, priv
|
||||
|
||||
var extensions []pkix.Extension
|
||||
|
||||
if (len(template.DNSNames) > 0 || len(template.EmailAddresses) > 0 || len(template.IPAddresses) > 0) &&
|
||||
if (len(template.DNSNames) > 0 || len(template.EmailAddresses) > 0 || len(template.IPAddresses) > 0 || len(template.URIs) > 0) &&
|
||||
!oidInExtensions(oidExtensionSubjectAltName, template.ExtraExtensions) {
|
||||
sanBytes, err := marshalSANs(template.DNSNames, template.EmailAddresses, template.IPAddresses)
|
||||
sanBytes, err := marshalSANs(template.DNSNames, template.EmailAddresses, template.IPAddresses, template.URIs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -2294,7 +2493,7 @@ func parseCertificateRequest(in *certificateRequest) (*CertificateRequest, error
|
||||
|
||||
for _, extension := range out.Extensions {
|
||||
if extension.Id.Equal(oidExtensionSubjectAltName) {
|
||||
out.DNSNames, out.EmailAddresses, out.IPAddresses, err = parseSANExtension(extension.Value)
|
||||
out.DNSNames, out.EmailAddresses, out.IPAddresses, out.URIs, err = parseSANExtension(extension.Value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ import (
|
||||
"internal/testenv"
|
||||
"math/big"
|
||||
"net"
|
||||
"net/url"
|
||||
"os/exec"
|
||||
"reflect"
|
||||
"runtime"
|
||||
@ -352,6 +353,22 @@ var certBytes = "308203223082028ba00302010202106edf0d9499fd4533dd1297fc42a93be13
|
||||
"9048084225c53e8acb7feb6f04d16dc574a2f7a27c7b603c77cd0ece48027f012fb69b37e02a2a" +
|
||||
"36dcd585d6ace53f546f961e05af"
|
||||
|
||||
func parseCIDR(s string) *net.IPNet {
|
||||
_, net, err := net.ParseCIDR(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return net
|
||||
}
|
||||
|
||||
func parseURI(s string) *url.URL {
|
||||
uri, err := url.Parse(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return uri
|
||||
}
|
||||
|
||||
func TestCreateSelfSignedCertificate(t *testing.T) {
|
||||
random := rand.Reader
|
||||
|
||||
@ -423,10 +440,17 @@ func TestCreateSelfSignedCertificate(t *testing.T) {
|
||||
DNSNames: []string{"test.example.com"},
|
||||
EmailAddresses: []string{"gopher@golang.org"},
|
||||
IPAddresses: []net.IP{net.IPv4(127, 0, 0, 1).To4(), net.ParseIP("2001:4860:0:2001::68")},
|
||||
URIs: []*url.URL{parseURI("https://foo.com/wibble#foo")},
|
||||
|
||||
PolicyIdentifiers: []asn1.ObjectIdentifier{[]int{1, 2, 3}},
|
||||
PermittedDNSDomains: []string{".example.com", "example.com"},
|
||||
ExcludedDNSDomains: []string{"bar.example.com"},
|
||||
PermittedIPRanges: []*net.IPNet{parseCIDR("192.168.1.1/16"), parseCIDR("1.2.3.4/8")},
|
||||
ExcludedIPRanges: []*net.IPNet{parseCIDR("2001:db8::/48")},
|
||||
PermittedEmailAddresses: []string{"foo@example.com"},
|
||||
ExcludedEmailAddresses: []string{".example.com", "example.com"},
|
||||
PermittedURIDomains: []string{".bar.com", "bar.com"},
|
||||
ExcludedURIDomains: []string{".bar2.com", "bar2.com"},
|
||||
|
||||
CRLDistributionPoints: []string{"http://crl1.example.com/ca1.crl", "http://crl2.example.com/ca1.crl"},
|
||||
|
||||
@ -468,6 +492,30 @@ func TestCreateSelfSignedCertificate(t *testing.T) {
|
||||
t.Errorf("%s: failed to parse name constraint exclusions: %#v", test.name, cert.ExcludedDNSDomains)
|
||||
}
|
||||
|
||||
if len(cert.PermittedIPRanges) != 2 || cert.PermittedIPRanges[0].String() != "192.168.0.0/16" || cert.PermittedIPRanges[1].String() != "1.0.0.0/8" {
|
||||
t.Errorf("%s: failed to parse IP constraints: %#v", test.name, cert.PermittedIPRanges)
|
||||
}
|
||||
|
||||
if len(cert.ExcludedIPRanges) != 1 || cert.ExcludedIPRanges[0].String() != "2001:db8::/48" {
|
||||
t.Errorf("%s: failed to parse IP constraint exclusions: %#v", test.name, cert.ExcludedIPRanges)
|
||||
}
|
||||
|
||||
if len(cert.PermittedEmailAddresses) != 1 || cert.PermittedEmailAddresses[0] != "foo@example.com" {
|
||||
t.Errorf("%s: failed to parse permitted email addreses: %#v", test.name, cert.PermittedEmailAddresses)
|
||||
}
|
||||
|
||||
if len(cert.ExcludedEmailAddresses) != 2 || cert.ExcludedEmailAddresses[0] != ".example.com" || cert.ExcludedEmailAddresses[1] != "example.com" {
|
||||
t.Errorf("%s: failed to parse excluded email addreses: %#v", test.name, cert.ExcludedEmailAddresses)
|
||||
}
|
||||
|
||||
if len(cert.PermittedURIDomains) != 2 || cert.PermittedURIDomains[0] != ".bar.com" || cert.PermittedURIDomains[1] != "bar.com" {
|
||||
t.Errorf("%s: failed to parse permitted URIs: %#v", test.name, cert.PermittedURIDomains)
|
||||
}
|
||||
|
||||
if len(cert.ExcludedURIDomains) != 2 || cert.ExcludedURIDomains[0] != ".bar2.com" || cert.ExcludedURIDomains[1] != "bar2.com" {
|
||||
t.Errorf("%s: failed to parse excluded URIs: %#v", test.name, cert.ExcludedURIDomains)
|
||||
}
|
||||
|
||||
if cert.Subject.CommonName != commonName {
|
||||
t.Errorf("%s: subject wasn't correctly copied from the template. Got %s, want %s", test.name, cert.Subject.CommonName, commonName)
|
||||
}
|
||||
@ -519,6 +567,10 @@ func TestCreateSelfSignedCertificate(t *testing.T) {
|
||||
t.Errorf("%s: SAN emails differ from template. Got %v, want %v", test.name, cert.EmailAddresses, template.EmailAddresses)
|
||||
}
|
||||
|
||||
if len(cert.URIs) != 1 || cert.URIs[0].String() != "https://foo.com/wibble#foo" {
|
||||
t.Errorf("%s: URIs differ from template. Got %v, want %v", test.name, cert.URIs, template.URIs)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(cert.IPAddresses, template.IPAddresses) {
|
||||
t.Errorf("%s: SAN IPs differ from template. Got %v, want %v", test.name, cert.IPAddresses, template.IPAddresses)
|
||||
}
|
||||
@ -1012,7 +1064,7 @@ func marshalAndParseCSR(t *testing.T, template *CertificateRequest) *Certificate
|
||||
}
|
||||
|
||||
func TestCertificateRequestOverrides(t *testing.T) {
|
||||
sanContents, err := marshalSANs([]string{"foo.example.com"}, nil, nil)
|
||||
sanContents, err := marshalSANs([]string{"foo.example.com"}, nil, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@ -1069,7 +1121,7 @@ func TestCertificateRequestOverrides(t *testing.T) {
|
||||
t.Errorf("bad attributes: %#v\n", csr.Attributes)
|
||||
}
|
||||
|
||||
sanContents2, err := marshalSANs([]string{"foo2.example.com"}, nil, nil)
|
||||
sanContents2, err := marshalSANs([]string{"foo2.example.com"}, nil, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@ -1567,3 +1619,69 @@ func TestRDNSequenceString(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const criticalNameConstraintWithUnknownTypePEM = `
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIC/TCCAeWgAwIBAgICEjQwDQYJKoZIhvcNAQELBQAwKDEmMCQGA1UEAxMdRW1w
|
||||
dHkgbmFtZSBjb25zdHJhaW50cyBpc3N1ZXIwHhcNMTMwMjAxMDAwMDAwWhcNMjAw
|
||||
NTMwMTA0ODM4WjAhMR8wHQYDVQQDExZFbXB0eSBuYW1lIGNvbnN0cmFpbnRzMIIB
|
||||
IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwriElUIt3LCqmJObs+yDoWPD
|
||||
F5IqgWk6moIobYjPfextZiYU6I3EfvAwoNxPDkN2WowcocUZMJbEeEq5ebBksFnx
|
||||
f12gBxlIViIYwZAzu7aFvhDMyPKQI3C8CG0ZSC9ABZ1E3umdA3CEueNOmP/TChNq
|
||||
Cl23+BG1Qb/PJkpAO+GfpWSVhTcV53Mf/cKvFHcjGNrxzdSoq9fyW7a6gfcGEQY0
|
||||
LVkmwFWUfJ0wT8kaeLr0E0tozkIfo01KNWNzv6NcYP80QOBRDlApWu9ODmEVJHPD
|
||||
blx4jzTQ3JLa+4DvBNOjVUOp+mgRmjiW0rLdrxwOxIqIOwNjweMCp/hgxX/hTQID
|
||||
AQABozgwNjA0BgNVHR4BAf8EKjAooCQwIokgIACrzQAAAAAAAAAAAAAAAP////8A
|
||||
AAAAAAAAAAAAAAChADANBgkqhkiG9w0BAQsFAAOCAQEAWG+/zUMHQhP8uNCtgSHy
|
||||
im/vh7wminwAvWgMKxlkLBFns6nZeQqsOV1lABY7U0Zuoqa1Z5nb6L+iJa4ElREJ
|
||||
Oi/erLc9uLwBdDCAR0hUTKD7a6i4ooS39DTle87cUnj0MW1CUa6Hv5SsvpYW+1Xl
|
||||
eYJk/axQOOTcy4Es53dvnZsjXH0EA/QHnn7UV+JmlE3rtVxcYp6MLYPmRhTioROA
|
||||
/drghicRkiu9hxdPyxkYS16M5g3Zj30jdm+k/6C6PeNtN9YmOOganCOSyFYfGhqO
|
||||
ANYzpmuV+oIedAsPpIbfIzN8njYUs1zio+1IoI4o8ddM9sCbtPU8o+WoY6IsCKXV
|
||||
/g==
|
||||
-----END CERTIFICATE-----`
|
||||
|
||||
func TestCriticalNameConstraintWithUnknownType(t *testing.T) {
|
||||
block, _ := pem.Decode([]byte(criticalNameConstraintWithUnknownTypePEM))
|
||||
cert, err := ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected parsing failure: %s", err)
|
||||
}
|
||||
|
||||
if l := len(cert.UnhandledCriticalExtensions); l != 1 {
|
||||
t.Fatalf("expected one unhandled critical extension, but found %d", l)
|
||||
}
|
||||
}
|
||||
|
||||
const badIPMaskPEM = `
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICzzCCAbegAwIBAgICEjQwDQYJKoZIhvcNAQELBQAwHTEbMBkGA1UEAxMSQmFk
|
||||
IElQIG1hc2sgaXNzdWVyMB4XDTEzMDIwMTAwMDAwMFoXDTIwMDUzMDEwNDgzOFow
|
||||
FjEUMBIGA1UEAxMLQmFkIElQIG1hc2swggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw
|
||||
ggEKAoIBAQDCuISVQi3csKqYk5uz7IOhY8MXkiqBaTqagihtiM997G1mJhTojcR+
|
||||
8DCg3E8OQ3ZajByhxRkwlsR4Srl5sGSwWfF/XaAHGUhWIhjBkDO7toW+EMzI8pAj
|
||||
cLwIbRlIL0AFnUTe6Z0DcIS5406Y/9MKE2oKXbf4EbVBv88mSkA74Z+lZJWFNxXn
|
||||
cx/9wq8UdyMY2vHN1Kir1/JbtrqB9wYRBjQtWSbAVZR8nTBPyRp4uvQTS2jOQh+j
|
||||
TUo1Y3O/o1xg/zRA4FEOUCla704OYRUkc8NuXHiPNNDcktr7gO8E06NVQ6n6aBGa
|
||||
OJbSst2vHA7Eiog7A2PB4wKn+GDFf+FNAgMBAAGjIDAeMBwGA1UdHgEB/wQSMBCg
|
||||
DDAKhwgBAgME//8BAKEAMA0GCSqGSIb3DQEBCwUAA4IBAQBYb7/NQwdCE/y40K2B
|
||||
IfKKb++HvCaKfAC9aAwrGWQsEWezqdl5Cqw5XWUAFjtTRm6iprVnmdvov6IlrgSV
|
||||
EQk6L96stz24vAF0MIBHSFRMoPtrqLiihLf0NOV7ztxSePQxbUJRroe/lKy+lhb7
|
||||
VeV5gmT9rFA45NzLgSznd2+dmyNcfQQD9AeeftRX4maUTeu1XFxinowtg+ZGFOKh
|
||||
E4D92uCGJxGSK72HF0/LGRhLXozmDdmPfSN2b6T/oLo942031iY46BqcI5LIVh8a
|
||||
Go4A1jOma5X6gh50Cw+kht8jM3yeNhSzXOKj7Uigjijx10z2wJu09Tyj5ahjoiwI
|
||||
pdX+
|
||||
-----END CERTIFICATE-----`
|
||||
|
||||
func TestBadIPMask(t *testing.T) {
|
||||
block, _ := pem.Decode([]byte(badIPMaskPEM))
|
||||
_, err := ParseCertificate(block.Bytes)
|
||||
if err == nil {
|
||||
t.Fatalf("unexpected success")
|
||||
}
|
||||
|
||||
const expected = "contained invalid mask"
|
||||
if !strings.Contains(err.Error(), expected) {
|
||||
t.Fatalf("expected %q in error but got: %s", expected, err)
|
||||
}
|
||||
}
|
||||
|
@ -377,7 +377,7 @@ var pkgDeps = map[string][]string{
|
||||
},
|
||||
"crypto/x509": {
|
||||
"L4", "CRYPTO-MATH", "OS", "CGO",
|
||||
"crypto/x509/pkix", "encoding/pem", "encoding/hex", "net", "os/user", "syscall",
|
||||
"crypto/x509/pkix", "encoding/pem", "encoding/hex", "net", "os/user", "syscall", "net/url",
|
||||
},
|
||||
"crypto/x509/pkix": {"L4", "CRYPTO-MATH", "encoding/hex"},
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user