import (
"bytes"
+ "crypto"
"errors"
"fmt"
"net"
leaf = currentChain[0]
}
- if (certType == intermediateCertificate || certType == rootCertificate) &&
- c.hasNameConstraints() && leaf.hasSANExtension() {
- err := forEachSAN(leaf.getSANExtension(), func(tag int, data []byte) error {
- switch tag {
- case nameTypeEmail:
- name := string(data)
- mailbox, ok := parseRFC2821Mailbox(name)
- if !ok {
- return fmt.Errorf("x509: cannot parse rfc822Name %q", mailbox)
- }
-
- if err := c.checkNameConstraints(&comparisonCount, maxConstraintComparisons, "email address", name, mailbox,
- func(parsedName, constraint any) (bool, error) {
- return matchEmailConstraint(parsedName.(rfc2821Mailbox), constraint.(string))
- }, c.PermittedEmailAddresses, c.ExcludedEmailAddresses); err != nil {
- return err
- }
-
- case nameTypeDNS:
- name := string(data)
- if _, ok := domainToReverseLabels(name); !ok {
- return fmt.Errorf("x509: cannot parse dnsName %q", name)
- }
-
- if err := c.checkNameConstraints(&comparisonCount, maxConstraintComparisons, "DNS name", name, name,
- func(parsedName, constraint any) (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(&comparisonCount, maxConstraintComparisons, "URI", name, uri,
- func(parsedName, constraint any) (bool, error) {
- return matchURIConstraint(parsedName.(*url.URL), constraint.(string))
- }, c.PermittedURIDomains, c.ExcludedURIDomains); err != nil {
- return err
+ if (len(c.ExtKeyUsage) > 0 || len(c.UnknownExtKeyUsage) > 0) && len(opts.KeyUsages) > 0 {
+ acceptableUsage := false
+ um := make(map[ExtKeyUsage]bool, len(opts.KeyUsages))
+ for _, u := range opts.KeyUsages {
+ um[u] = true
+ }
+ if !um[ExtKeyUsageAny] {
+ for _, u := range c.ExtKeyUsage {
+ if u == ExtKeyUsageAny || um[u] {
+ acceptableUsage = true
+ break
}
+ }
+ if !acceptableUsage {
+ return CertificateInvalidError{c, IncompatibleUsage, ""}
+ }
+ }
+ }
- 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 (certType == intermediateCertificate || certType == rootCertificate) &&
+ c.hasNameConstraints() {
+ toCheck := []*Certificate{}
+ if leaf.hasSANExtension() {
+ toCheck = append(toCheck, leaf)
+ }
+ if c.hasSANExtension() {
+ toCheck = append(toCheck, c)
+ }
+ for _, sanCert := range toCheck {
+ err := forEachSAN(sanCert.getSANExtension(), func(tag int, data []byte) error {
+ switch tag {
+ case nameTypeEmail:
+ name := string(data)
+ mailbox, ok := parseRFC2821Mailbox(name)
+ if !ok {
+ return fmt.Errorf("x509: cannot parse rfc822Name %q", mailbox)
+ }
+
+ if err := c.checkNameConstraints(&comparisonCount, maxConstraintComparisons, "email address", name, mailbox,
+ func(parsedName, constraint any) (bool, error) {
+ return matchEmailConstraint(parsedName.(rfc2821Mailbox), constraint.(string))
+ }, c.PermittedEmailAddresses, c.ExcludedEmailAddresses); err != nil {
+ return err
+ }
+
+ case nameTypeDNS:
+ name := string(data)
+ if _, ok := domainToReverseLabels(name); !ok {
+ return fmt.Errorf("x509: cannot parse dnsName %q", name)
+ }
+
+ if err := c.checkNameConstraints(&comparisonCount, maxConstraintComparisons, "DNS name", name, name,
+ func(parsedName, constraint any) (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(&comparisonCount, maxConstraintComparisons, "URI", name, uri,
+ func(parsedName, constraint any) (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(&comparisonCount, maxConstraintComparisons, "IP address", ip.String(), ip,
+ func(parsedName, constraint any) (bool, error) {
+ return matchIPConstraint(parsedName.(net.IP), constraint.(*net.IPNet))
+ }, c.PermittedIPRanges, c.ExcludedIPRanges); err != nil {
+ return err
+ }
+
+ default:
+ // Unknown SAN types are ignored.
}
- if err := c.checkNameConstraints(&comparisonCount, maxConstraintComparisons, "IP address", ip.String(), ip,
- func(parsedName, constraint any) (bool, error) {
- return matchIPConstraint(parsedName.(net.IP), constraint.(*net.IPNet))
- }, c.PermittedIPRanges, c.ExcludedIPRanges); err != nil {
- return err
- }
+ return nil
+ })
- default:
- // Unknown SAN types are ignored.
+ if err != nil {
+ return err
}
-
- return nil
- })
-
- if err != nil {
- return err
}
}
}
}
+ if len(opts.KeyUsages) == 0 {
+ opts.KeyUsages = []ExtKeyUsage{ExtKeyUsageServerAuth}
+ }
+
err = c.isValid(leafCertificate, nil, &opts)
if err != nil {
return
}
}
- var candidateChains [][]*Certificate
if opts.Roots.contains(c) {
- candidateChains = append(candidateChains, []*Certificate{c})
- } else {
- if candidateChains, err = c.buildChains(nil, []*Certificate{c}, nil, &opts); err != nil {
- return nil, err
- }
- }
-
- keyUsages := opts.KeyUsages
- if len(keyUsages) == 0 {
- keyUsages = []ExtKeyUsage{ExtKeyUsageServerAuth}
- }
-
- // If any key usage is acceptable then we're done.
- for _, usage := range keyUsages {
- if usage == ExtKeyUsageAny {
- return candidateChains, nil
- }
- }
-
- for _, candidate := range candidateChains {
- if checkChainForKeyUsage(candidate, keyUsages) {
- chains = append(chains, candidate)
- }
- }
-
- if len(chains) == 0 {
- return nil, CertificateInvalidError{c, IncompatibleUsage, ""}
+ return [][]*Certificate{{c}}, nil
}
-
- return chains, nil
+ return c.buildChains([]*Certificate{c}, nil, &opts)
}
func appendToFreshChain(chain []*Certificate, cert *Certificate) []*Certificate {
// for failed checks due to different intermediates having the same Subject.
const maxChainSignatureChecks = 100
-func (c *Certificate) buildChains(cache map[*Certificate][][]*Certificate, currentChain []*Certificate, sigChecks *int, opts *VerifyOptions) (chains [][]*Certificate, err error) {
+func (c *Certificate) buildChains(currentChain []*Certificate, sigChecks *int, opts *VerifyOptions) (chains [][]*Certificate, err error) {
var (
hintErr error
hintCert *Certificate
)
+ type pubKeyEqual interface {
+ Equal(crypto.PublicKey) bool
+ }
+
considerCandidate := func(certType int, candidate *Certificate) {
for _, cert := range currentChain {
- if cert.Equal(candidate) {
+ // If a certificate already appeared in the chain we've built, don't
+ // reconsider it. This prevents loops, for isntance those created by
+ // mutual cross-signatures, or other cross-signature bridges oddities.
+ if bytes.Equal(cert.RawSubject, candidate.RawSubject) && cert.PublicKey.(pubKeyEqual).Equal(candidate.PublicKey) {
return
}
}
case rootCertificate:
chains = append(chains, appendToFreshChain(currentChain, candidate))
case intermediateCertificate:
- if cache == nil {
- cache = make(map[*Certificate][][]*Certificate)
- }
- childChains, ok := cache[candidate]
- if !ok {
- childChains, err = candidate.buildChains(cache, appendToFreshChain(currentChain, candidate), sigChecks, opts)
- cache[candidate] = childChains
- }
+ var childChains [][]*Certificate
+ childChains, err = candidate.buildChains(appendToFreshChain(currentChain, candidate), sigChecks, opts)
chains = append(chains, childChains...)
}
}
"fmt"
"internal/testenv"
"math/big"
+ "reflect"
"runtime"
+ "sort"
"strings"
"testing"
"time"
}
})
}
+
+type trustGraphEdge struct {
+ Issuer string
+ Subject string
+ Type int
+ MutateTemplate func(*Certificate)
+}
+
+type trustGraphDescription struct {
+ Roots []string
+ Leaf string
+ Graph []trustGraphEdge
+}
+
+func genCertEdge(t *testing.T, subject string, key crypto.Signer, mutateTmpl func(*Certificate), certType int, issuer *Certificate, signer crypto.Signer) *Certificate {
+ t.Helper()
+
+ serial, err := rand.Int(rand.Reader, big.NewInt(100))
+ if err != nil {
+ t.Fatalf("failed to generate test serial: %s", err)
+ }
+ tmpl := &Certificate{
+ SerialNumber: serial,
+ Subject: pkix.Name{CommonName: subject},
+ NotBefore: time.Now().Add(-time.Hour),
+ NotAfter: time.Now().Add(time.Hour),
+ }
+ if certType == rootCertificate || certType == intermediateCertificate {
+ tmpl.IsCA, tmpl.BasicConstraintsValid = true, true
+ tmpl.KeyUsage = KeyUsageCertSign
+ } else if certType == leafCertificate {
+ tmpl.DNSNames = []string{"localhost"}
+ }
+ if mutateTmpl != nil {
+ mutateTmpl(tmpl)
+ }
+
+ if certType == rootCertificate {
+ issuer = tmpl
+ signer = key
+ }
+
+ d, err := CreateCertificate(rand.Reader, tmpl, issuer, key.Public(), signer)
+ if err != nil {
+ t.Fatalf("failed to generate test cert: %s", err)
+ }
+ c, err := ParseCertificate(d)
+ if err != nil {
+ t.Fatalf("failed to parse test cert: %s", err)
+ }
+ return c
+}
+
+func buildTrustGraph(t *testing.T, d trustGraphDescription) (*CertPool, *CertPool, *Certificate) {
+ t.Helper()
+
+ certs := map[string]*Certificate{}
+ keys := map[string]crypto.Signer{}
+ roots := []*Certificate{}
+ for _, r := range d.Roots {
+ k, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+ if err != nil {
+ t.Fatalf("failed to generate test key: %s", err)
+ }
+ root := genCertEdge(t, r, k, nil, rootCertificate, nil, nil)
+ roots = append(roots, root)
+ certs[r] = root
+ keys[r] = k
+ }
+
+ intermediates := []*Certificate{}
+ var leaf *Certificate
+ for _, e := range d.Graph {
+ issuerCert, ok := certs[e.Issuer]
+ if !ok {
+ t.Fatalf("unknown issuer %s", e.Issuer)
+ }
+ issuerKey, ok := keys[e.Issuer]
+ if !ok {
+ t.Fatalf("unknown issuer %s", e.Issuer)
+ }
+
+ k, ok := keys[e.Subject]
+ if !ok {
+ var err error
+ k, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+ if err != nil {
+ t.Fatalf("failed to generate test key: %s", err)
+ }
+ keys[e.Subject] = k
+ }
+ cert := genCertEdge(t, e.Subject, k, e.MutateTemplate, e.Type, issuerCert, issuerKey)
+ certs[e.Subject] = cert
+ if e.Subject == d.Leaf {
+ leaf = cert
+ } else {
+ intermediates = append(intermediates, cert)
+ }
+ }
+
+ rootPool, intermediatePool := NewCertPool(), NewCertPool()
+ for i := len(roots) - 1; i >= 0; i-- {
+ rootPool.AddCert(roots[i])
+ }
+ for i := len(intermediates) - 1; i >= 0; i-- {
+ intermediatePool.AddCert(intermediates[i])
+ }
+
+ return rootPool, intermediatePool, leaf
+}
+
+func chainsToStrings(chains [][]*Certificate) []string {
+ chainStrings := []string{}
+ for _, chain := range chains {
+ names := []string{}
+ for _, c := range chain {
+ names = append(names, c.Subject.String())
+ }
+ chainStrings = append(chainStrings, strings.Join(names, " -> "))
+ }
+ sort.Strings(chainStrings)
+ return chainStrings
+}
+
+func TestPathBuilding(t *testing.T) {
+ tests := []struct {
+ name string
+ graph trustGraphDescription
+ expectedChains []string
+ expectedErr string
+ }{
+ {
+ // Build the following graph from RFC 4158, figure 7 (note that in this graph edges represent
+ // certificates where the parent is the issuer and the child is the subject.) For the certificate
+ // C->B, use an unsupported ExtKeyUsage (in this case ExtKeyUsageCodeSigning) which invalidates
+ // the path Trust Anchor -> C -> B -> EE. The remaining valid paths should be:
+ // * Trust Anchor -> A -> B -> EE
+ // * Trust Anchor -> C -> A -> B -> EE
+ //
+ // +---------+
+ // | Trust |
+ // | Anchor |
+ // +---------+
+ // | |
+ // v v
+ // +---+ +---+
+ // | A |<-->| C |
+ // +---+ +---+
+ // | |
+ // | +---+ |
+ // +->| B |<-+
+ // +---+
+ // |
+ // v
+ // +----+
+ // | EE |
+ // +----+
+ name: "bad EKU",
+ graph: trustGraphDescription{
+ Roots: []string{"root"},
+ Leaf: "leaf",
+ Graph: []trustGraphEdge{
+ {
+ Issuer: "root",
+ Subject: "inter a",
+ Type: intermediateCertificate,
+ },
+ {
+ Issuer: "root",
+ Subject: "inter c",
+ Type: intermediateCertificate,
+ },
+ {
+ Issuer: "inter c",
+ Subject: "inter a",
+ Type: intermediateCertificate,
+ },
+ {
+ Issuer: "inter a",
+ Subject: "inter c",
+ Type: intermediateCertificate,
+ },
+ {
+ Issuer: "inter c",
+ Subject: "inter b",
+ Type: intermediateCertificate,
+ MutateTemplate: func(t *Certificate) {
+ t.ExtKeyUsage = []ExtKeyUsage{ExtKeyUsageCodeSigning}
+ },
+ },
+ {
+ Issuer: "inter a",
+ Subject: "inter b",
+ Type: intermediateCertificate,
+ },
+ {
+ Issuer: "inter b",
+ Subject: "leaf",
+ Type: leafCertificate,
+ },
+ },
+ },
+ expectedChains: []string{
+ "CN=leaf -> CN=inter b -> CN=inter a -> CN=inter c -> CN=root",
+ "CN=leaf -> CN=inter b -> CN=inter a -> CN=root",
+ },
+ },
+ {
+ // Build the following graph from RFC 4158, figure 7 (note that in this graph edges represent
+ // certificates where the parent is the issuer and the child is the subject.) For the certificate
+ // C->B, use a unconstrained SAN which invalidates the path Trust Anchor -> C -> B -> EE. The
+ // remaining valid paths should be:
+ // * Trust Anchor -> A -> B -> EE
+ // * Trust Anchor -> C -> A -> B -> EE
+ //
+ // +---------+
+ // | Trust |
+ // | Anchor |
+ // +---------+
+ // | |
+ // v v
+ // +---+ +---+
+ // | A |<-->| C |
+ // +---+ +---+
+ // | |
+ // | +---+ |
+ // +->| B |<-+
+ // +---+
+ // |
+ // v
+ // +----+
+ // | EE |
+ // +----+
+ name: "bad EKU",
+ graph: trustGraphDescription{
+ Roots: []string{"root"},
+ Leaf: "leaf",
+ Graph: []trustGraphEdge{
+ {
+ Issuer: "root",
+ Subject: "inter a",
+ Type: intermediateCertificate,
+ },
+ {
+ Issuer: "root",
+ Subject: "inter c",
+ Type: intermediateCertificate,
+ },
+ {
+ Issuer: "inter c",
+ Subject: "inter a",
+ Type: intermediateCertificate,
+ },
+ {
+ Issuer: "inter a",
+ Subject: "inter c",
+ Type: intermediateCertificate,
+ },
+ {
+ Issuer: "inter c",
+ Subject: "inter b",
+ Type: intermediateCertificate,
+ MutateTemplate: func(t *Certificate) {
+ t.PermittedDNSDomains = []string{"good"}
+ t.DNSNames = []string{"bad"}
+ },
+ },
+ {
+ Issuer: "inter a",
+ Subject: "inter b",
+ Type: intermediateCertificate,
+ },
+ {
+ Issuer: "inter b",
+ Subject: "leaf",
+ Type: leafCertificate,
+ },
+ },
+ },
+ expectedChains: []string{
+ "CN=leaf -> CN=inter b -> CN=inter a -> CN=inter c -> CN=root",
+ "CN=leaf -> CN=inter b -> CN=inter a -> CN=root",
+ },
+ },
+ {
+ // Build the following graph, we should find both paths:
+ // * Trust Anchor -> A -> C -> EE
+ // * Trust Anchor -> A -> B -> C -> EE
+ //
+ // +---------+
+ // | Trust |
+ // | Anchor |
+ // +---------+
+ // |
+ // v
+ // +---+
+ // | A |
+ // +---+
+ // | |
+ // | +----+
+ // | v
+ // | +---+
+ // | | B |
+ // | +---+
+ // | |
+ // | +---v
+ // v v
+ // +---+
+ // | C |
+ // +---+
+ // |
+ // v
+ // +----+
+ // | EE |
+ // +----+
+ name: "all paths",
+ graph: trustGraphDescription{
+ Roots: []string{"root"},
+ Leaf: "leaf",
+ Graph: []trustGraphEdge{
+ {
+ Issuer: "root",
+ Subject: "inter a",
+ Type: intermediateCertificate,
+ },
+ {
+ Issuer: "inter a",
+ Subject: "inter b",
+ Type: intermediateCertificate,
+ },
+ {
+ Issuer: "inter a",
+ Subject: "inter c",
+ Type: intermediateCertificate,
+ },
+ {
+ Issuer: "inter b",
+ Subject: "inter c",
+ Type: intermediateCertificate,
+ },
+ {
+ Issuer: "inter c",
+ Subject: "leaf",
+ Type: leafCertificate,
+ },
+ },
+ },
+ expectedChains: []string{
+ "CN=leaf -> CN=inter c -> CN=inter a -> CN=root",
+ "CN=leaf -> CN=inter c -> CN=inter b -> CN=inter a -> CN=root",
+ },
+ },
+ {
+ // Build the following graph, which contains a cross-signature loop
+ // (A and C cross sign each other). Paths that include the A -> C -> A
+ // (and vice versa) loop should be ignored, resulting in the paths:
+ // * Trust Anchor -> A -> B -> EE
+ // * Trust Anchor -> C -> B -> EE
+ // * Trust Anchor -> A -> C -> B -> EE
+ // * Trust Anchor -> C -> A -> B -> EE
+ //
+ // +---------+
+ // | Trust |
+ // | Anchor |
+ // +---------+
+ // | |
+ // v v
+ // +---+ +---+
+ // | A |<-->| C |
+ // +---+ +---+
+ // | |
+ // | +---+ |
+ // +->| B |<-+
+ // +---+
+ // |
+ // v
+ // +----+
+ // | EE |
+ // +----+
+ name: "ignore cross-sig loops",
+ graph: trustGraphDescription{
+ Roots: []string{"root"},
+ Leaf: "leaf",
+ Graph: []trustGraphEdge{
+ {
+ Issuer: "root",
+ Subject: "inter a",
+ Type: intermediateCertificate,
+ },
+ {
+ Issuer: "root",
+ Subject: "inter c",
+ Type: intermediateCertificate,
+ },
+ {
+ Issuer: "inter c",
+ Subject: "inter a",
+ Type: intermediateCertificate,
+ },
+ {
+ Issuer: "inter a",
+ Subject: "inter c",
+ Type: intermediateCertificate,
+ },
+ {
+ Issuer: "inter c",
+ Subject: "inter b",
+ Type: intermediateCertificate,
+ },
+ {
+ Issuer: "inter a",
+ Subject: "inter b",
+ Type: intermediateCertificate,
+ },
+ {
+ Issuer: "inter b",
+ Subject: "leaf",
+ Type: leafCertificate,
+ },
+ },
+ },
+ expectedChains: []string{
+ "CN=leaf -> CN=inter b -> CN=inter a -> CN=inter c -> CN=root",
+ "CN=leaf -> CN=inter b -> CN=inter a -> CN=root",
+ "CN=leaf -> CN=inter b -> CN=inter c -> CN=inter a -> CN=root",
+ "CN=leaf -> CN=inter b -> CN=inter c -> CN=root",
+ },
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ roots, intermediates, leaf := buildTrustGraph(t, tc.graph)
+ chains, err := leaf.Verify(VerifyOptions{
+ Roots: roots,
+ Intermediates: intermediates,
+ })
+ if err != nil && err.Error() != tc.expectedErr {
+ t.Fatalf("unexpected error: got %q, want %q", err, tc.expectedErr)
+ }
+ gotChains := chainsToStrings(chains)
+ if !reflect.DeepEqual(gotChains, tc.expectedChains) {
+ t.Errorf("unexpected chains returned:\ngot:\n\t%s\nwant:\n\t%s", strings.Join(gotChains, "\n\t"), strings.Join(tc.expectedChains, "\n\t"))
+ }
+ })
+ }
+}