mirror of
https://github.com/tailscale/tailscale.git
synced 2024-12-01 14:05:39 +00:00
f5993f2440
(from patchset 1, 7cdc3c3e7427c9ef69e19224d6036c09c5ea1723, of https://go-review.googlesource.com/c/go/+/229917/1) This will allow building CertPools that consume less memory. (Most certs are never accessed. Different users/programs access different ones, but not many.) This CL only adds the new internal mechanism (and uses it for the old AddCert) but does not modify any existing root pool behavior. (That is, the default Unix roots are still all slurped into memory as of this CL) Change-Id: Ib3a42e4050627b5e34413c595d8ced839c7bfa14
2196 lines
45 KiB
Go
2196 lines
45 KiB
Go
// Copyright 2017 The Go Authors. All rights reserved.
|
||
// Use of this source code is governed by a BSD-style
|
||
// license that can be found in the LICENSE file.
|
||
|
||
package x509
|
||
|
||
import (
|
||
"bytes"
|
||
"crypto/ecdsa"
|
||
"crypto/elliptic"
|
||
"crypto/rand"
|
||
"crypto/x509/pkix"
|
||
"encoding/asn1"
|
||
"encoding/hex"
|
||
"encoding/pem"
|
||
"fmt"
|
||
"io/ioutil"
|
||
"math/big"
|
||
"net"
|
||
"net/url"
|
||
"os"
|
||
"os/exec"
|
||
"strconv"
|
||
"strings"
|
||
"sync"
|
||
"testing"
|
||
"time"
|
||
)
|
||
|
||
const (
|
||
// testNameConstraintsAgainstOpenSSL can be set to true to run tests
|
||
// against the system OpenSSL. This is disabled by default because Go
|
||
// cannot depend on having OpenSSL installed at testing time.
|
||
testNameConstraintsAgainstOpenSSL = false
|
||
|
||
// debugOpenSSLFailure can be set to true, when
|
||
// testNameConstraintsAgainstOpenSSL is also true, to cause
|
||
// intermediate files to be preserved for debugging.
|
||
debugOpenSSLFailure = false
|
||
)
|
||
|
||
type nameConstraintsTest struct {
|
||
roots []constraintsSpec
|
||
intermediates [][]constraintsSpec
|
||
leaf leafSpec
|
||
requestedEKUs []ExtKeyUsage
|
||
expectedError string
|
||
noOpenSSL bool
|
||
ignoreCN bool
|
||
}
|
||
|
||
type constraintsSpec struct {
|
||
ok []string
|
||
bad []string
|
||
ekus []string
|
||
}
|
||
|
||
type leafSpec struct {
|
||
sans []string
|
||
ekus []string
|
||
cn string
|
||
}
|
||
|
||
var nameConstraintsTests = []nameConstraintsTest{
|
||
// #0: dummy test for the certificate generation process itself.
|
||
{
|
||
roots: make([]constraintsSpec, 1),
|
||
leaf: leafSpec{
|
||
sans: []string{"dns:example.com"},
|
||
},
|
||
},
|
||
|
||
// #1: dummy test for the certificate generation process itself: single
|
||
// level of intermediate.
|
||
{
|
||
roots: make([]constraintsSpec, 1),
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"dns:example.com"},
|
||
},
|
||
},
|
||
|
||
// #2: dummy test for the certificate generation process itself: two
|
||
// levels of intermediates.
|
||
{
|
||
roots: make([]constraintsSpec, 1),
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{},
|
||
},
|
||
{
|
||
{},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"dns:example.com"},
|
||
},
|
||
},
|
||
|
||
// #3: matching DNS constraint in root
|
||
{
|
||
roots: []constraintsSpec{
|
||
{
|
||
ok: []string{"dns:example.com"},
|
||
},
|
||
},
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"dns:example.com"},
|
||
},
|
||
},
|
||
|
||
// #4: matching DNS constraint in intermediate.
|
||
{
|
||
roots: make([]constraintsSpec, 1),
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{
|
||
ok: []string{"dns:example.com"},
|
||
},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"dns:example.com"},
|
||
},
|
||
},
|
||
|
||
// #5: .example.com only matches subdomains.
|
||
{
|
||
roots: []constraintsSpec{
|
||
{
|
||
ok: []string{"dns:.example.com"},
|
||
},
|
||
},
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"dns:example.com"},
|
||
},
|
||
expectedError: "\"example.com\" is not permitted",
|
||
},
|
||
|
||
// #6: .example.com matches subdomains.
|
||
{
|
||
roots: make([]constraintsSpec, 1),
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{
|
||
ok: []string{"dns:.example.com"},
|
||
},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"dns:foo.example.com"},
|
||
},
|
||
},
|
||
|
||
// #7: .example.com matches multiple levels of subdomains
|
||
{
|
||
roots: []constraintsSpec{
|
||
{
|
||
ok: []string{"dns:.example.com"},
|
||
},
|
||
},
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"dns:foo.bar.example.com"},
|
||
},
|
||
},
|
||
|
||
// #8: specifying a permitted list of names does not exclude other name
|
||
// types
|
||
{
|
||
roots: []constraintsSpec{
|
||
{
|
||
ok: []string{"dns:.example.com"},
|
||
},
|
||
},
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"ip:10.1.1.1"},
|
||
},
|
||
},
|
||
|
||
// #9: specifying a permitted list of names does not exclude other name
|
||
// types
|
||
{
|
||
roots: []constraintsSpec{
|
||
{
|
||
ok: []string{"ip:10.0.0.0/8"},
|
||
},
|
||
},
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"dns:example.com"},
|
||
},
|
||
},
|
||
|
||
// #10: intermediates can try to permit other names, which isn't
|
||
// forbidden if the leaf doesn't mention them. I.e. name constraints
|
||
// apply to names, not constraints themselves.
|
||
{
|
||
roots: []constraintsSpec{
|
||
{
|
||
ok: []string{"dns:example.com"},
|
||
},
|
||
},
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{
|
||
ok: []string{"dns:example.com", "dns:foo.com"},
|
||
},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"dns:example.com"},
|
||
},
|
||
},
|
||
|
||
// #11: intermediates cannot add permitted names that the root doesn't
|
||
// grant them.
|
||
{
|
||
roots: []constraintsSpec{
|
||
{
|
||
ok: []string{"dns:example.com"},
|
||
},
|
||
},
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{
|
||
ok: []string{"dns:example.com", "dns:foo.com"},
|
||
},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"dns:foo.com"},
|
||
},
|
||
expectedError: "\"foo.com\" is not permitted",
|
||
},
|
||
|
||
// #12: intermediates can further limit their scope if they wish.
|
||
{
|
||
roots: []constraintsSpec{
|
||
{
|
||
ok: []string{"dns:.example.com"},
|
||
},
|
||
},
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{
|
||
ok: []string{"dns:.bar.example.com"},
|
||
},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"dns:foo.bar.example.com"},
|
||
},
|
||
},
|
||
|
||
// #13: intermediates can further limit their scope and that limitation
|
||
// is effective
|
||
{
|
||
roots: []constraintsSpec{
|
||
{
|
||
ok: []string{"dns:.example.com"},
|
||
},
|
||
},
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{
|
||
ok: []string{"dns:.bar.example.com"},
|
||
},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"dns:foo.notbar.example.com"},
|
||
},
|
||
expectedError: "\"foo.notbar.example.com\" is not permitted",
|
||
},
|
||
|
||
// #14: roots can exclude subtrees and that doesn't affect other names.
|
||
{
|
||
roots: []constraintsSpec{
|
||
{
|
||
bad: []string{"dns:.example.com"},
|
||
},
|
||
},
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"dns:foo.com"},
|
||
},
|
||
},
|
||
|
||
// #15: roots exclusions are effective.
|
||
{
|
||
roots: []constraintsSpec{
|
||
{
|
||
bad: []string{"dns:.example.com"},
|
||
},
|
||
},
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"dns:foo.example.com"},
|
||
},
|
||
expectedError: "\"foo.example.com\" is excluded",
|
||
},
|
||
|
||
// #16: intermediates can also exclude names and that doesn't affect
|
||
// other names.
|
||
{
|
||
roots: make([]constraintsSpec, 1),
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{
|
||
bad: []string{"dns:.example.com"},
|
||
},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"dns:foo.com"},
|
||
},
|
||
},
|
||
|
||
// #17: intermediate exclusions are effective.
|
||
{
|
||
roots: make([]constraintsSpec, 1),
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{
|
||
bad: []string{"dns:.example.com"},
|
||
},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"dns:foo.example.com"},
|
||
},
|
||
expectedError: "\"foo.example.com\" is excluded",
|
||
},
|
||
|
||
// #18: having an exclusion doesn't prohibit other types of names.
|
||
{
|
||
roots: []constraintsSpec{
|
||
{
|
||
bad: []string{"dns:.example.com"},
|
||
},
|
||
},
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"dns:foo.com", "ip:10.1.1.1"},
|
||
},
|
||
},
|
||
|
||
// #19: IP-based exclusions are permitted and don't affect unrelated IP
|
||
// addresses.
|
||
{
|
||
roots: []constraintsSpec{
|
||
{
|
||
bad: []string{"ip:10.0.0.0/8"},
|
||
},
|
||
},
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"ip:192.168.1.1"},
|
||
},
|
||
},
|
||
|
||
// #20: IP-based exclusions are effective
|
||
{
|
||
roots: []constraintsSpec{
|
||
{
|
||
bad: []string{"ip:10.0.0.0/8"},
|
||
},
|
||
},
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"ip:10.0.0.1"},
|
||
},
|
||
expectedError: "\"10.0.0.1\" is excluded",
|
||
},
|
||
|
||
// #21: intermediates can further constrain IP ranges.
|
||
{
|
||
roots: []constraintsSpec{
|
||
{
|
||
bad: []string{"ip:0.0.0.0/1"},
|
||
},
|
||
},
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{
|
||
bad: []string{"ip:11.0.0.0/8"},
|
||
},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"ip:11.0.0.1"},
|
||
},
|
||
expectedError: "\"11.0.0.1\" is excluded",
|
||
},
|
||
|
||
// #22: when multiple intermediates are present, chain building can
|
||
// avoid intermediates with incompatible constraints.
|
||
{
|
||
roots: make([]constraintsSpec, 1),
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{
|
||
ok: []string{"dns:.foo.com"},
|
||
},
|
||
{
|
||
ok: []string{"dns:.example.com"},
|
||
},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"dns:foo.example.com"},
|
||
},
|
||
noOpenSSL: true, // OpenSSL's chain building is not informed by constraints.
|
||
},
|
||
|
||
// #23: (same as the previous test, but in the other order in ensure
|
||
// that we don't pass it by luck.)
|
||
{
|
||
roots: make([]constraintsSpec, 1),
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{
|
||
ok: []string{"dns:.example.com"},
|
||
},
|
||
{
|
||
ok: []string{"dns:.foo.com"},
|
||
},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"dns:foo.example.com"},
|
||
},
|
||
noOpenSSL: true, // OpenSSL's chain building is not informed by constraints.
|
||
},
|
||
|
||
// #24: when multiple roots are valid, chain building can avoid roots
|
||
// with incompatible constraints.
|
||
{
|
||
roots: []constraintsSpec{
|
||
{},
|
||
{
|
||
ok: []string{"dns:foo.com"},
|
||
},
|
||
},
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"dns:example.com"},
|
||
},
|
||
noOpenSSL: true, // OpenSSL's chain building is not informed by constraints.
|
||
},
|
||
|
||
// #25: (same as the previous test, but in the other order in ensure
|
||
// that we don't pass it by luck.)
|
||
{
|
||
roots: []constraintsSpec{
|
||
{
|
||
ok: []string{"dns:foo.com"},
|
||
},
|
||
{},
|
||
},
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"dns:example.com"},
|
||
},
|
||
noOpenSSL: true, // OpenSSL's chain building is not informed by constraints.
|
||
},
|
||
|
||
// #26: chain building can find a valid path even with multiple levels
|
||
// of alternative intermediates and alternative roots.
|
||
{
|
||
roots: []constraintsSpec{
|
||
{
|
||
ok: []string{"dns:foo.com"},
|
||
},
|
||
{
|
||
ok: []string{"dns:example.com"},
|
||
},
|
||
{},
|
||
},
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{},
|
||
{
|
||
ok: []string{"dns:foo.com"},
|
||
},
|
||
},
|
||
{
|
||
{},
|
||
{
|
||
ok: []string{"dns:foo.com"},
|
||
},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"dns:bar.com"},
|
||
},
|
||
noOpenSSL: true, // OpenSSL's chain building is not informed by constraints.
|
||
},
|
||
|
||
// #27: chain building doesn't get stuck when there is no valid path.
|
||
{
|
||
roots: []constraintsSpec{
|
||
{
|
||
ok: []string{"dns:foo.com"},
|
||
},
|
||
{
|
||
ok: []string{"dns:example.com"},
|
||
},
|
||
},
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{},
|
||
{
|
||
ok: []string{"dns:foo.com"},
|
||
},
|
||
},
|
||
{
|
||
{
|
||
ok: []string{"dns:bar.com"},
|
||
},
|
||
{
|
||
ok: []string{"dns:foo.com"},
|
||
},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"dns:bar.com"},
|
||
},
|
||
expectedError: "\"bar.com\" is not permitted",
|
||
},
|
||
|
||
// #28: unknown name types don't cause a problem without constraints.
|
||
{
|
||
roots: make([]constraintsSpec, 1),
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"unknown:"},
|
||
},
|
||
},
|
||
|
||
// #29: unknown name types are allowed even in constrained chains.
|
||
{
|
||
roots: []constraintsSpec{
|
||
{
|
||
ok: []string{"dns:foo.com", "dns:.foo.com"},
|
||
},
|
||
},
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"unknown:"},
|
||
},
|
||
},
|
||
|
||
// #30: without SANs, a certificate with a CN is rejected in a constrained chain.
|
||
{
|
||
roots: []constraintsSpec{
|
||
{
|
||
ok: []string{"dns:foo.com", "dns:.foo.com"},
|
||
},
|
||
},
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{},
|
||
cn: "foo.com",
|
||
},
|
||
expectedError: "leaf doesn't have a SAN extension",
|
||
},
|
||
|
||
// #31: IPv6 addresses work in constraints: roots can permit them as
|
||
// expected.
|
||
{
|
||
roots: []constraintsSpec{
|
||
{
|
||
ok: []string{"ip:2000:abcd::/32"},
|
||
},
|
||
},
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"ip:2000:abcd:1234::"},
|
||
},
|
||
},
|
||
|
||
// #32: IPv6 addresses work in constraints: root restrictions are
|
||
// effective.
|
||
{
|
||
roots: []constraintsSpec{
|
||
{
|
||
ok: []string{"ip:2000:abcd::/32"},
|
||
},
|
||
},
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"ip:2000:1234:abcd::"},
|
||
},
|
||
expectedError: "\"2000:1234:abcd::\" is not permitted",
|
||
},
|
||
|
||
// #33: An IPv6 permitted subtree doesn't affect DNS names.
|
||
{
|
||
roots: []constraintsSpec{
|
||
{
|
||
ok: []string{"ip:2000:abcd::/32"},
|
||
},
|
||
},
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"ip:2000:abcd::", "dns:foo.com"},
|
||
},
|
||
},
|
||
|
||
// #34: IPv6 exclusions don't affect unrelated addresses.
|
||
{
|
||
roots: []constraintsSpec{
|
||
{
|
||
bad: []string{"ip:2000:abcd::/32"},
|
||
},
|
||
},
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"ip:2000:1234::"},
|
||
},
|
||
},
|
||
|
||
// #35: IPv6 exclusions are effective.
|
||
{
|
||
roots: []constraintsSpec{
|
||
{
|
||
bad: []string{"ip:2000:abcd::/32"},
|
||
},
|
||
},
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"ip:2000:abcd::"},
|
||
},
|
||
expectedError: "\"2000:abcd::\" is excluded",
|
||
},
|
||
|
||
// #36: IPv6 constraints do not permit IPv4 addresses.
|
||
{
|
||
roots: []constraintsSpec{
|
||
{
|
||
ok: []string{"ip:2000:abcd::/32"},
|
||
},
|
||
},
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"ip:10.0.0.1"},
|
||
},
|
||
expectedError: "\"10.0.0.1\" is not permitted",
|
||
},
|
||
|
||
// #37: IPv4 constraints do not permit IPv6 addresses.
|
||
{
|
||
roots: []constraintsSpec{
|
||
{
|
||
ok: []string{"ip:10.0.0.0/8"},
|
||
},
|
||
},
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"ip:2000:abcd::"},
|
||
},
|
||
expectedError: "\"2000:abcd::\" is not permitted",
|
||
},
|
||
|
||
// #38: an exclusion of an unknown type doesn't affect other names.
|
||
{
|
||
roots: []constraintsSpec{
|
||
{
|
||
bad: []string{"unknown:"},
|
||
},
|
||
},
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"dns:example.com"},
|
||
},
|
||
},
|
||
|
||
// #39: a permitted subtree of an unknown type doesn't affect other
|
||
// name types.
|
||
{
|
||
roots: []constraintsSpec{
|
||
{
|
||
ok: []string{"unknown:"},
|
||
},
|
||
},
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"dns:example.com"},
|
||
},
|
||
},
|
||
|
||
// #40: exact email constraints work
|
||
{
|
||
roots: []constraintsSpec{
|
||
{
|
||
ok: []string{"email:foo@example.com"},
|
||
},
|
||
},
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"email:foo@example.com"},
|
||
},
|
||
},
|
||
|
||
// #41: exact email constraints are effective
|
||
{
|
||
roots: []constraintsSpec{
|
||
{
|
||
ok: []string{"email:foo@example.com"},
|
||
},
|
||
},
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"email:bar@example.com"},
|
||
},
|
||
expectedError: "\"bar@example.com\" is not permitted",
|
||
},
|
||
|
||
// #42: email canonicalisation works.
|
||
{
|
||
roots: []constraintsSpec{
|
||
{
|
||
ok: []string{"email:foo@example.com"},
|
||
},
|
||
},
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"email:\"\\f\\o\\o\"@example.com"},
|
||
},
|
||
noOpenSSL: true, // OpenSSL doesn't canonicalise email addresses before matching
|
||
},
|
||
|
||
// #43: limiting email addresses to a host works.
|
||
{
|
||
roots: []constraintsSpec{
|
||
{
|
||
ok: []string{"email:example.com"},
|
||
},
|
||
},
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"email:foo@example.com"},
|
||
},
|
||
},
|
||
|
||
// #44: a leading dot matches hosts one level deep
|
||
{
|
||
roots: []constraintsSpec{
|
||
{
|
||
ok: []string{"email:.example.com"},
|
||
},
|
||
},
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"email:foo@sub.example.com"},
|
||
},
|
||
},
|
||
|
||
// #45: a leading dot does not match the host itself
|
||
{
|
||
roots: []constraintsSpec{
|
||
{
|
||
ok: []string{"email:.example.com"},
|
||
},
|
||
},
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"email:foo@example.com"},
|
||
},
|
||
expectedError: "\"foo@example.com\" is not permitted",
|
||
},
|
||
|
||
// #46: a leading dot also matches two (or more) levels deep.
|
||
{
|
||
roots: []constraintsSpec{
|
||
{
|
||
ok: []string{"email:.example.com"},
|
||
},
|
||
},
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"email:foo@sub.sub.example.com"},
|
||
},
|
||
},
|
||
|
||
// #47: the local part of an email is case-sensitive
|
||
{
|
||
roots: []constraintsSpec{
|
||
{
|
||
ok: []string{"email:foo@example.com"},
|
||
},
|
||
},
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"email:Foo@example.com"},
|
||
},
|
||
expectedError: "\"Foo@example.com\" is not permitted",
|
||
},
|
||
|
||
// #48: the domain part of an email is not case-sensitive
|
||
{
|
||
roots: []constraintsSpec{
|
||
{
|
||
ok: []string{"email:foo@EXAMPLE.com"},
|
||
},
|
||
},
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"email:foo@example.com"},
|
||
},
|
||
},
|
||
|
||
// #49: the domain part of a DNS constraint is also not case-sensitive.
|
||
{
|
||
roots: []constraintsSpec{
|
||
{
|
||
ok: []string{"dns:EXAMPLE.com"},
|
||
},
|
||
},
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"dns:example.com"},
|
||
},
|
||
},
|
||
|
||
// #50: URI constraints only cover the host part of the URI
|
||
{
|
||
roots: []constraintsSpec{
|
||
{
|
||
ok: []string{"uri:example.com"},
|
||
},
|
||
},
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{
|
||
"uri:http://example.com/bar",
|
||
"uri:http://example.com:8080/",
|
||
"uri:https://example.com/wibble#bar",
|
||
},
|
||
},
|
||
},
|
||
|
||
// #51: URIs with IPs are rejected
|
||
{
|
||
roots: []constraintsSpec{
|
||
{
|
||
ok: []string{"uri:example.com"},
|
||
},
|
||
},
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"uri:http://1.2.3.4/"},
|
||
},
|
||
expectedError: "URI with IP",
|
||
},
|
||
|
||
// #52: URIs with IPs and ports are rejected
|
||
{
|
||
roots: []constraintsSpec{
|
||
{
|
||
ok: []string{"uri:example.com"},
|
||
},
|
||
},
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"uri:http://1.2.3.4:43/"},
|
||
},
|
||
expectedError: "URI with IP",
|
||
},
|
||
|
||
// #53: URIs with IPv6 addresses are also rejected
|
||
{
|
||
roots: []constraintsSpec{
|
||
{
|
||
ok: []string{"uri:example.com"},
|
||
},
|
||
},
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"uri:http://[2006:abcd::1]/"},
|
||
},
|
||
expectedError: "URI with IP",
|
||
},
|
||
|
||
// #54: URIs with IPv6 addresses with ports are also rejected
|
||
{
|
||
roots: []constraintsSpec{
|
||
{
|
||
ok: []string{"uri:example.com"},
|
||
},
|
||
},
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"uri:http://[2006:abcd::1]:16/"},
|
||
},
|
||
expectedError: "URI with IP",
|
||
},
|
||
|
||
// #55: URI constraints are effective
|
||
{
|
||
roots: []constraintsSpec{
|
||
{
|
||
ok: []string{"uri:example.com"},
|
||
},
|
||
},
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"uri:http://bar.com/"},
|
||
},
|
||
expectedError: "\"http://bar.com/\" is not permitted",
|
||
},
|
||
|
||
// #56: URI constraints are effective
|
||
{
|
||
roots: []constraintsSpec{
|
||
{
|
||
bad: []string{"uri:foo.com"},
|
||
},
|
||
},
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"uri:http://foo.com/"},
|
||
},
|
||
expectedError: "\"http://foo.com/\" is excluded",
|
||
},
|
||
|
||
// #57: URI constraints can allow subdomains
|
||
{
|
||
roots: []constraintsSpec{
|
||
{
|
||
ok: []string{"uri:.foo.com"},
|
||
},
|
||
},
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"uri:http://www.foo.com/"},
|
||
},
|
||
},
|
||
|
||
// #58: excluding an IPv4-mapped-IPv6 address doesn't affect the IPv4
|
||
// version of that address.
|
||
{
|
||
roots: []constraintsSpec{
|
||
{
|
||
bad: []string{"ip:::ffff:1.2.3.4/128"},
|
||
},
|
||
},
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"ip:1.2.3.4"},
|
||
},
|
||
},
|
||
|
||
// #59: a URI constraint isn't matched by a URN.
|
||
{
|
||
roots: []constraintsSpec{
|
||
{
|
||
ok: []string{"uri:example.com"},
|
||
},
|
||
},
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"uri:urn:example"},
|
||
},
|
||
expectedError: "URI with empty host",
|
||
},
|
||
|
||
// #60: excluding all IPv6 addresses doesn't exclude all IPv4 addresses
|
||
// too, even though IPv4 is mapped into the IPv6 range.
|
||
{
|
||
roots: []constraintsSpec{
|
||
{
|
||
ok: []string{"ip:1.2.3.0/24"},
|
||
bad: []string{"ip:::0/0"},
|
||
},
|
||
},
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"ip:1.2.3.4"},
|
||
},
|
||
},
|
||
|
||
// #61: omitting extended key usage in a CA certificate implies that
|
||
// any usage is ok.
|
||
{
|
||
roots: make([]constraintsSpec, 1),
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"dns:example.com"},
|
||
ekus: []string{"serverAuth", "other"},
|
||
},
|
||
},
|
||
|
||
// #62: The “any” EKU also means that any usage is ok.
|
||
{
|
||
roots: make([]constraintsSpec, 1),
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{
|
||
ekus: []string{"any"},
|
||
},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"dns:example.com"},
|
||
ekus: []string{"serverAuth", "other"},
|
||
},
|
||
},
|
||
|
||
// #63: An intermediate with enumerated EKUs causes a failure if we
|
||
// test for an EKU not in that set. (ServerAuth is required by
|
||
// default.)
|
||
{
|
||
roots: make([]constraintsSpec, 1),
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{
|
||
ekus: []string{"email"},
|
||
},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"dns:example.com"},
|
||
ekus: []string{"serverAuth"},
|
||
},
|
||
expectedError: "incompatible key usage",
|
||
},
|
||
|
||
// #64: an unknown EKU in the leaf doesn't break anything, even if it's not
|
||
// correctly nested.
|
||
{
|
||
roots: make([]constraintsSpec, 1),
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{
|
||
ekus: []string{"email"},
|
||
},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"dns:example.com"},
|
||
ekus: []string{"other"},
|
||
},
|
||
requestedEKUs: []ExtKeyUsage{ExtKeyUsageAny},
|
||
},
|
||
|
||
// #65: trying to add extra permitted key usages in an intermediate
|
||
// (after a limitation in the root) is acceptable so long as the leaf
|
||
// certificate doesn't use them.
|
||
{
|
||
roots: []constraintsSpec{
|
||
{
|
||
ekus: []string{"serverAuth"},
|
||
},
|
||
},
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{
|
||
ekus: []string{"serverAuth", "email"},
|
||
},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"dns:example.com"},
|
||
ekus: []string{"serverAuth"},
|
||
},
|
||
},
|
||
|
||
// #66: EKUs in roots are not ignored.
|
||
{
|
||
roots: []constraintsSpec{
|
||
{
|
||
ekus: []string{"email"},
|
||
},
|
||
},
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{
|
||
ekus: []string{"serverAuth"},
|
||
},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"dns:example.com"},
|
||
ekus: []string{"serverAuth"},
|
||
},
|
||
expectedError: "incompatible key usage",
|
||
},
|
||
|
||
// #67: in order to support COMODO chains, SGC key usages permit
|
||
// serverAuth and clientAuth.
|
||
{
|
||
roots: []constraintsSpec{
|
||
{},
|
||
},
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{
|
||
ekus: []string{"netscapeSGC"},
|
||
},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"dns:example.com"},
|
||
ekus: []string{"serverAuth", "clientAuth"},
|
||
},
|
||
},
|
||
|
||
// #68: in order to support COMODO chains, SGC key usages permit
|
||
// serverAuth and clientAuth.
|
||
{
|
||
roots: make([]constraintsSpec, 1),
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{
|
||
ekus: []string{"msSGC"},
|
||
},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"dns:example.com"},
|
||
ekus: []string{"serverAuth", "clientAuth"},
|
||
},
|
||
},
|
||
|
||
// #69: an empty DNS constraint should allow anything.
|
||
{
|
||
roots: []constraintsSpec{
|
||
{
|
||
ok: []string{"dns:"},
|
||
},
|
||
},
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"dns:example.com"},
|
||
},
|
||
},
|
||
|
||
// #70: an empty DNS constraint should also reject everything.
|
||
{
|
||
roots: []constraintsSpec{
|
||
{
|
||
bad: []string{"dns:"},
|
||
},
|
||
},
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"dns:example.com"},
|
||
},
|
||
expectedError: "\"example.com\" is excluded",
|
||
},
|
||
|
||
// #71: an empty email constraint should allow anything
|
||
{
|
||
roots: []constraintsSpec{
|
||
{
|
||
ok: []string{"email:"},
|
||
},
|
||
},
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"email:foo@example.com"},
|
||
},
|
||
},
|
||
|
||
// #72: an empty email constraint should also reject everything.
|
||
{
|
||
roots: []constraintsSpec{
|
||
{
|
||
bad: []string{"email:"},
|
||
},
|
||
},
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"email:foo@example.com"},
|
||
},
|
||
expectedError: "\"foo@example.com\" is excluded",
|
||
},
|
||
|
||
// #73: an empty URI constraint should allow anything
|
||
{
|
||
roots: []constraintsSpec{
|
||
{
|
||
ok: []string{"uri:"},
|
||
},
|
||
},
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"uri:https://example.com/test"},
|
||
},
|
||
},
|
||
|
||
// #74: an empty URI constraint should also reject everything.
|
||
{
|
||
roots: []constraintsSpec{
|
||
{
|
||
bad: []string{"uri:"},
|
||
},
|
||
},
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"uri:https://example.com/test"},
|
||
},
|
||
expectedError: "\"https://example.com/test\" is excluded",
|
||
},
|
||
|
||
// #75: serverAuth in a leaf shouldn't permit clientAuth when requested in
|
||
// VerifyOptions.
|
||
{
|
||
roots: make([]constraintsSpec, 1),
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"dns:example.com"},
|
||
ekus: []string{"serverAuth"},
|
||
},
|
||
requestedEKUs: []ExtKeyUsage{ExtKeyUsageClientAuth},
|
||
expectedError: "incompatible key usage",
|
||
},
|
||
|
||
// #76: However, MSSGC in a leaf should match a request for serverAuth.
|
||
{
|
||
roots: make([]constraintsSpec, 1),
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"dns:example.com"},
|
||
ekus: []string{"msSGC"},
|
||
},
|
||
requestedEKUs: []ExtKeyUsage{ExtKeyUsageServerAuth},
|
||
},
|
||
|
||
// An invalid DNS SAN should be detected only at validation time so
|
||
// that we can process CA certificates in the wild that have invalid SANs.
|
||
// See https://github.com/golang/go/issues/23995
|
||
|
||
// #77: an invalid DNS or mail SAN will not be detected if name constraint
|
||
// checking is not triggered.
|
||
{
|
||
roots: make([]constraintsSpec, 1),
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"dns:this is invalid", "email:this @ is invalid"},
|
||
},
|
||
},
|
||
|
||
// #78: an invalid DNS SAN will be detected if any name constraint checking
|
||
// is triggered.
|
||
{
|
||
roots: []constraintsSpec{
|
||
{
|
||
bad: []string{"uri:"},
|
||
},
|
||
},
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"dns:this is invalid"},
|
||
},
|
||
expectedError: "cannot parse dnsName",
|
||
},
|
||
|
||
// #79: an invalid email SAN will be detected if any name constraint
|
||
// checking is triggered.
|
||
{
|
||
roots: []constraintsSpec{
|
||
{
|
||
bad: []string{"uri:"},
|
||
},
|
||
},
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"email:this @ is invalid"},
|
||
},
|
||
expectedError: "cannot parse rfc822Name",
|
||
},
|
||
|
||
// #80: if several EKUs are requested, satisfying any of them is sufficient.
|
||
{
|
||
roots: make([]constraintsSpec, 1),
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"dns:example.com"},
|
||
ekus: []string{"email"},
|
||
},
|
||
requestedEKUs: []ExtKeyUsage{ExtKeyUsageClientAuth, ExtKeyUsageEmailProtection},
|
||
},
|
||
|
||
// #81: EKUs that are not asserted in VerifyOpts are not required to be
|
||
// nested.
|
||
{
|
||
roots: make([]constraintsSpec, 1),
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{
|
||
ekus: []string{"serverAuth"},
|
||
},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"dns:example.com"},
|
||
// There's no email EKU in the intermediate. This would be rejected if
|
||
// full nesting was required.
|
||
ekus: []string{"email", "serverAuth"},
|
||
},
|
||
},
|
||
|
||
// #82: a certificate without SANs and CN is accepted in a constrained chain.
|
||
{
|
||
roots: []constraintsSpec{
|
||
{
|
||
ok: []string{"dns:foo.com", "dns:.foo.com"},
|
||
},
|
||
},
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{},
|
||
},
|
||
},
|
||
|
||
// #83: a certificate without SANs and with a CN that does not parse as a
|
||
// hostname is accepted in a constrained chain.
|
||
{
|
||
roots: []constraintsSpec{
|
||
{
|
||
ok: []string{"dns:foo.com", "dns:.foo.com"},
|
||
},
|
||
},
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{},
|
||
cn: "foo,bar",
|
||
},
|
||
},
|
||
|
||
// #84: a certificate with SANs and CN is accepted in a constrained chain.
|
||
{
|
||
roots: []constraintsSpec{
|
||
{
|
||
ok: []string{"dns:foo.com", "dns:.foo.com"},
|
||
},
|
||
},
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{"dns:foo.com"},
|
||
cn: "foo.bar",
|
||
},
|
||
},
|
||
|
||
// #85: without SANs, a certificate with a valid CN is accepted in a
|
||
// constrained chain if x509ignoreCN is set.
|
||
{
|
||
roots: []constraintsSpec{
|
||
{
|
||
ok: []string{"dns:foo.com", "dns:.foo.com"},
|
||
},
|
||
},
|
||
intermediates: [][]constraintsSpec{
|
||
{
|
||
{},
|
||
},
|
||
},
|
||
leaf: leafSpec{
|
||
sans: []string{},
|
||
cn: "foo.com",
|
||
},
|
||
ignoreCN: true,
|
||
},
|
||
}
|
||
|
||
func makeConstraintsCACert(constraints constraintsSpec, name string, key *ecdsa.PrivateKey, parent *Certificate, parentKey *ecdsa.PrivateKey) (*Certificate, error) {
|
||
var serialBytes [16]byte
|
||
rand.Read(serialBytes[:])
|
||
|
||
template := &Certificate{
|
||
SerialNumber: new(big.Int).SetBytes(serialBytes[:]),
|
||
Subject: pkix.Name{
|
||
CommonName: name,
|
||
},
|
||
NotBefore: time.Unix(1000, 0),
|
||
NotAfter: time.Unix(2000, 0),
|
||
KeyUsage: KeyUsageCertSign,
|
||
BasicConstraintsValid: true,
|
||
IsCA: true,
|
||
}
|
||
|
||
if err := addConstraintsToTemplate(constraints, template); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
if parent == nil {
|
||
parent = template
|
||
}
|
||
derBytes, err := CreateCertificate(rand.Reader, template, parent, &key.PublicKey, parentKey)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
caCert, err := ParseCertificate(derBytes)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return caCert, nil
|
||
}
|
||
|
||
func makeConstraintsLeafCert(leaf leafSpec, key *ecdsa.PrivateKey, parent *Certificate, parentKey *ecdsa.PrivateKey) (*Certificate, error) {
|
||
var serialBytes [16]byte
|
||
rand.Read(serialBytes[:])
|
||
|
||
template := &Certificate{
|
||
SerialNumber: new(big.Int).SetBytes(serialBytes[:]),
|
||
Subject: pkix.Name{
|
||
OrganizationalUnit: []string{"Leaf"},
|
||
CommonName: leaf.cn,
|
||
},
|
||
NotBefore: time.Unix(1000, 0),
|
||
NotAfter: time.Unix(2000, 0),
|
||
KeyUsage: KeyUsageDigitalSignature,
|
||
BasicConstraintsValid: true,
|
||
IsCA: false,
|
||
}
|
||
|
||
for _, name := range leaf.sans {
|
||
switch {
|
||
case strings.HasPrefix(name, "dns:"):
|
||
template.DNSNames = append(template.DNSNames, name[4:])
|
||
|
||
case strings.HasPrefix(name, "ip:"):
|
||
ip := net.ParseIP(name[3:])
|
||
if ip == nil {
|
||
return nil, fmt.Errorf("cannot parse IP %q", name[3:])
|
||
}
|
||
template.IPAddresses = append(template.IPAddresses, ip)
|
||
|
||
case strings.HasPrefix(name, "invalidip:"):
|
||
ipBytes, err := hex.DecodeString(name[10:])
|
||
if err != nil {
|
||
return nil, fmt.Errorf("cannot parse invalid IP: %s", err)
|
||
}
|
||
template.IPAddresses = append(template.IPAddresses, net.IP(ipBytes))
|
||
|
||
case strings.HasPrefix(name, "email:"):
|
||
template.EmailAddresses = append(template.EmailAddresses, name[6:])
|
||
|
||
case strings.HasPrefix(name, "uri:"):
|
||
uri, err := url.Parse(name[4:])
|
||
if err != nil {
|
||
return nil, fmt.Errorf("cannot parse URI %q: %s", name[4:], err)
|
||
}
|
||
template.URIs = append(template.URIs, uri)
|
||
|
||
case strings.HasPrefix(name, "unknown:"):
|
||
// This is a special case for testing unknown
|
||
// name types. A custom SAN extension is
|
||
// injected into the certificate.
|
||
if len(leaf.sans) != 1 {
|
||
panic("when using unknown name types, it must be the sole name")
|
||
}
|
||
|
||
template.ExtraExtensions = append(template.ExtraExtensions, pkix.Extension{
|
||
Id: []int{2, 5, 29, 17},
|
||
Value: []byte{
|
||
0x30, // SEQUENCE
|
||
3, // three bytes
|
||
9, // undefined GeneralName type 9
|
||
1,
|
||
1,
|
||
},
|
||
})
|
||
|
||
default:
|
||
return nil, fmt.Errorf("unknown name type %q", name)
|
||
}
|
||
}
|
||
|
||
var err error
|
||
if template.ExtKeyUsage, template.UnknownExtKeyUsage, err = parseEKUs(leaf.ekus); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
if parent == nil {
|
||
parent = template
|
||
}
|
||
|
||
derBytes, err := CreateCertificate(rand.Reader, template, parent, &key.PublicKey, parentKey)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return ParseCertificate(derBytes)
|
||
}
|
||
|
||
func customConstraintsExtension(typeNum int, constraint []byte, isExcluded bool) pkix.Extension {
|
||
appendConstraint := func(contents []byte, tag uint8) []byte {
|
||
contents = append(contents, tag|32 /* constructed */ |0x80 /* context-specific */)
|
||
contents = append(contents, byte(4+len(constraint)) /* length */)
|
||
contents = append(contents, 0x30 /* SEQUENCE */)
|
||
contents = append(contents, byte(2+len(constraint)) /* length */)
|
||
contents = append(contents, byte(typeNum) /* GeneralName type */)
|
||
contents = append(contents, byte(len(constraint)))
|
||
return append(contents, constraint...)
|
||
}
|
||
|
||
var contents []byte
|
||
if !isExcluded {
|
||
contents = appendConstraint(contents, 0 /* tag 0 for permitted */)
|
||
} else {
|
||
contents = appendConstraint(contents, 1 /* tag 1 for excluded */)
|
||
}
|
||
|
||
var value []byte
|
||
value = append(value, 0x30 /* SEQUENCE */)
|
||
value = append(value, byte(len(contents)))
|
||
value = append(value, contents...)
|
||
|
||
return pkix.Extension{
|
||
Id: []int{2, 5, 29, 30},
|
||
Value: value,
|
||
}
|
||
}
|
||
|
||
func addConstraintsToTemplate(constraints constraintsSpec, template *Certificate) error {
|
||
parse := func(constraints []string) (dnsNames []string, ips []*net.IPNet, emailAddrs []string, uriDomains []string, err error) {
|
||
for _, constraint := range constraints {
|
||
switch {
|
||
case strings.HasPrefix(constraint, "dns:"):
|
||
dnsNames = append(dnsNames, constraint[4:])
|
||
|
||
case strings.HasPrefix(constraint, "ip:"):
|
||
_, ipNet, err := net.ParseCIDR(constraint[3:])
|
||
if err != nil {
|
||
return nil, nil, nil, nil, err
|
||
}
|
||
ips = append(ips, ipNet)
|
||
|
||
case strings.HasPrefix(constraint, "email:"):
|
||
emailAddrs = append(emailAddrs, constraint[6:])
|
||
|
||
case strings.HasPrefix(constraint, "uri:"):
|
||
uriDomains = append(uriDomains, constraint[4:])
|
||
|
||
default:
|
||
return nil, nil, nil, nil, fmt.Errorf("unknown constraint %q", constraint)
|
||
}
|
||
}
|
||
|
||
return dnsNames, ips, emailAddrs, uriDomains, err
|
||
}
|
||
|
||
handleSpecialConstraint := func(constraint string, isExcluded bool) bool {
|
||
switch {
|
||
case constraint == "unknown:":
|
||
template.ExtraExtensions = append(template.ExtraExtensions, customConstraintsExtension(9 /* undefined GeneralName type */, []byte{1}, isExcluded))
|
||
|
||
default:
|
||
return false
|
||
}
|
||
|
||
return true
|
||
}
|
||
|
||
if len(constraints.ok) == 1 && len(constraints.bad) == 0 {
|
||
if handleSpecialConstraint(constraints.ok[0], false) {
|
||
return nil
|
||
}
|
||
}
|
||
|
||
if len(constraints.bad) == 1 && len(constraints.ok) == 0 {
|
||
if handleSpecialConstraint(constraints.bad[0], true) {
|
||
return nil
|
||
}
|
||
}
|
||
|
||
var err error
|
||
template.PermittedDNSDomains, template.PermittedIPRanges, template.PermittedEmailAddresses, template.PermittedURIDomains, err = parse(constraints.ok)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
template.ExcludedDNSDomains, template.ExcludedIPRanges, template.ExcludedEmailAddresses, template.ExcludedURIDomains, err = parse(constraints.bad)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
if template.ExtKeyUsage, template.UnknownExtKeyUsage, err = parseEKUs(constraints.ekus); err != nil {
|
||
return err
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
func parseEKUs(ekuStrs []string) (ekus []ExtKeyUsage, unknowns []asn1.ObjectIdentifier, err error) {
|
||
for _, s := range ekuStrs {
|
||
switch s {
|
||
case "serverAuth":
|
||
ekus = append(ekus, ExtKeyUsageServerAuth)
|
||
case "clientAuth":
|
||
ekus = append(ekus, ExtKeyUsageClientAuth)
|
||
case "email":
|
||
ekus = append(ekus, ExtKeyUsageEmailProtection)
|
||
case "netscapeSGC":
|
||
ekus = append(ekus, ExtKeyUsageNetscapeServerGatedCrypto)
|
||
case "msSGC":
|
||
ekus = append(ekus, ExtKeyUsageMicrosoftServerGatedCrypto)
|
||
case "any":
|
||
ekus = append(ekus, ExtKeyUsageAny)
|
||
case "other":
|
||
unknowns = append(unknowns, asn1.ObjectIdentifier{2, 4, 1, 2, 3})
|
||
default:
|
||
return nil, nil, fmt.Errorf("unknown EKU %q", s)
|
||
}
|
||
}
|
||
|
||
return
|
||
}
|
||
|
||
func TestConstraintCases(t *testing.T) {
|
||
defer func(savedIgnoreCN bool) {
|
||
ignoreCN = savedIgnoreCN
|
||
}(ignoreCN)
|
||
|
||
privateKeys := sync.Pool{
|
||
New: func() interface{} {
|
||
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||
if err != nil {
|
||
panic(err)
|
||
}
|
||
return priv
|
||
},
|
||
}
|
||
|
||
for i, test := range nameConstraintsTests {
|
||
rootPool := NewCertPool()
|
||
rootKey := privateKeys.Get().(*ecdsa.PrivateKey)
|
||
rootName := "Root " + strconv.Itoa(i)
|
||
|
||
// keys keeps track of all the private keys used in a given
|
||
// test and puts them back in the privateKeys pool at the end.
|
||
keys := []*ecdsa.PrivateKey{rootKey}
|
||
|
||
// At each level (root, intermediate(s), leaf), parent points to
|
||
// an example parent certificate and parentKey the key for the
|
||
// parent level. Since all certificates at a given level have
|
||
// the same name and public key, any parent certificate is
|
||
// sufficient to get the correct issuer name and authority
|
||
// key ID.
|
||
var parent *Certificate
|
||
parentKey := rootKey
|
||
|
||
for _, root := range test.roots {
|
||
rootCert, err := makeConstraintsCACert(root, rootName, rootKey, nil, rootKey)
|
||
if err != nil {
|
||
t.Fatalf("#%d: failed to create root: %s", i, err)
|
||
}
|
||
|
||
parent = rootCert
|
||
rootPool.AddCert(rootCert)
|
||
}
|
||
|
||
intermediatePool := NewCertPool()
|
||
|
||
for level, intermediates := range test.intermediates {
|
||
levelKey := privateKeys.Get().(*ecdsa.PrivateKey)
|
||
keys = append(keys, levelKey)
|
||
levelName := "Intermediate level " + strconv.Itoa(level)
|
||
var last *Certificate
|
||
|
||
for _, intermediate := range intermediates {
|
||
caCert, err := makeConstraintsCACert(intermediate, levelName, levelKey, parent, parentKey)
|
||
if err != nil {
|
||
t.Fatalf("#%d: failed to create %q: %s", i, levelName, err)
|
||
}
|
||
|
||
last = caCert
|
||
intermediatePool.AddCert(caCert)
|
||
}
|
||
|
||
parent = last
|
||
parentKey = levelKey
|
||
}
|
||
|
||
leafKey := privateKeys.Get().(*ecdsa.PrivateKey)
|
||
keys = append(keys, leafKey)
|
||
|
||
leafCert, err := makeConstraintsLeafCert(test.leaf, leafKey, parent, parentKey)
|
||
if err != nil {
|
||
t.Fatalf("#%d: cannot create leaf: %s", i, err)
|
||
}
|
||
|
||
// Skip tests with CommonName set because OpenSSL will try to match it
|
||
// against name constraints, while we ignore it when it's not hostname-looking.
|
||
if !test.noOpenSSL && testNameConstraintsAgainstOpenSSL && test.leaf.cn == "" {
|
||
output, err := testChainAgainstOpenSSL(leafCert, intermediatePool, rootPool)
|
||
if err == nil && len(test.expectedError) > 0 {
|
||
t.Errorf("#%d: unexpectedly succeeded against OpenSSL", i)
|
||
if debugOpenSSLFailure {
|
||
return
|
||
}
|
||
}
|
||
|
||
if err != nil {
|
||
if _, ok := err.(*exec.ExitError); !ok {
|
||
t.Errorf("#%d: OpenSSL failed to run: %s", i, err)
|
||
} else if len(test.expectedError) == 0 {
|
||
t.Errorf("#%d: OpenSSL unexpectedly failed: %v", i, output)
|
||
if debugOpenSSLFailure {
|
||
return
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
ignoreCN = test.ignoreCN
|
||
verifyOpts := VerifyOptions{
|
||
Roots: rootPool,
|
||
Intermediates: intermediatePool,
|
||
CurrentTime: time.Unix(1500, 0),
|
||
KeyUsages: test.requestedEKUs,
|
||
}
|
||
_, err = leafCert.Verify(verifyOpts)
|
||
|
||
logInfo := true
|
||
if len(test.expectedError) == 0 {
|
||
if err != nil {
|
||
t.Errorf("#%d: unexpected failure: %s", i, err)
|
||
} else {
|
||
logInfo = false
|
||
}
|
||
} else {
|
||
if err == nil {
|
||
t.Errorf("#%d: unexpected success", i)
|
||
} else if !strings.Contains(err.Error(), test.expectedError) {
|
||
t.Errorf("#%d: expected error containing %q, but got: %s", i, test.expectedError, err)
|
||
} else {
|
||
logInfo = false
|
||
}
|
||
}
|
||
|
||
if logInfo {
|
||
certAsPEM := func(cert *Certificate) string {
|
||
var buf bytes.Buffer
|
||
pem.Encode(&buf, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw})
|
||
return buf.String()
|
||
}
|
||
t.Errorf("#%d: root:\n%s", i, certAsPEM(rootPool.mustCert(0)))
|
||
t.Errorf("#%d: leaf:\n%s", i, certAsPEM(leafCert))
|
||
}
|
||
|
||
for _, key := range keys {
|
||
privateKeys.Put(key)
|
||
}
|
||
keys = keys[:0]
|
||
}
|
||
}
|
||
|
||
func writePEMsToTempFile(certs []*Certificate) *os.File {
|
||
file, err := ioutil.TempFile("", "name_constraints_test")
|
||
if err != nil {
|
||
panic("cannot create tempfile")
|
||
}
|
||
|
||
pemBlock := &pem.Block{Type: "CERTIFICATE"}
|
||
for _, cert := range certs {
|
||
pemBlock.Bytes = cert.Raw
|
||
pem.Encode(file, pemBlock)
|
||
}
|
||
|
||
return file
|
||
}
|
||
|
||
func allCerts(p *CertPool) []*Certificate {
|
||
all := make([]*Certificate, p.len())
|
||
for i := range all {
|
||
all[i] = p.mustCert(i)
|
||
}
|
||
return all
|
||
}
|
||
|
||
func testChainAgainstOpenSSL(leaf *Certificate, intermediates, roots *CertPool) (string, error) {
|
||
args := []string{"verify", "-no_check_time"}
|
||
|
||
rootsFile := writePEMsToTempFile(allCerts(roots))
|
||
if debugOpenSSLFailure {
|
||
println("roots file:", rootsFile.Name())
|
||
} else {
|
||
defer os.Remove(rootsFile.Name())
|
||
}
|
||
args = append(args, "-CAfile", rootsFile.Name())
|
||
|
||
if intermediates.len() > 0 {
|
||
intermediatesFile := writePEMsToTempFile(allCerts(intermediates))
|
||
if debugOpenSSLFailure {
|
||
println("intermediates file:", intermediatesFile.Name())
|
||
} else {
|
||
defer os.Remove(intermediatesFile.Name())
|
||
}
|
||
args = append(args, "-untrusted", intermediatesFile.Name())
|
||
}
|
||
|
||
leafFile := writePEMsToTempFile([]*Certificate{leaf})
|
||
if debugOpenSSLFailure {
|
||
println("leaf file:", leafFile.Name())
|
||
} else {
|
||
defer os.Remove(leafFile.Name())
|
||
}
|
||
args = append(args, leafFile.Name())
|
||
|
||
var output bytes.Buffer
|
||
cmd := exec.Command("openssl", args...)
|
||
cmd.Stdout = &output
|
||
cmd.Stderr = &output
|
||
|
||
err := cmd.Run()
|
||
return output.String(), err
|
||
}
|
||
|
||
var rfc2821Tests = []struct {
|
||
in string
|
||
localPart, domain string
|
||
}{
|
||
{"foo@example.com", "foo", "example.com"},
|
||
{"@example.com", "", ""},
|
||
{"\"@example.com", "", ""},
|
||
{"\"\"@example.com", "", "example.com"},
|
||
{"\"a\"@example.com", "a", "example.com"},
|
||
{"\"\\a\"@example.com", "a", "example.com"},
|
||
{"a\"@example.com", "", ""},
|
||
{"foo..bar@example.com", "", ""},
|
||
{".foo.bar@example.com", "", ""},
|
||
{"foo.bar.@example.com", "", ""},
|
||
{"|{}?'@example.com", "|{}?'", "example.com"},
|
||
|
||
// Examples from RFC 3696
|
||
{"Abc\\@def@example.com", "Abc@def", "example.com"},
|
||
{"Fred\\ Bloggs@example.com", "Fred Bloggs", "example.com"},
|
||
{"Joe.\\\\Blow@example.com", "Joe.\\Blow", "example.com"},
|
||
{"\"Abc@def\"@example.com", "Abc@def", "example.com"},
|
||
{"\"Fred Bloggs\"@example.com", "Fred Bloggs", "example.com"},
|
||
{"customer/department=shipping@example.com", "customer/department=shipping", "example.com"},
|
||
{"$A12345@example.com", "$A12345", "example.com"},
|
||
{"!def!xyz%abc@example.com", "!def!xyz%abc", "example.com"},
|
||
{"_somename@example.com", "_somename", "example.com"},
|
||
}
|
||
|
||
func TestRFC2821Parsing(t *testing.T) {
|
||
for i, test := range rfc2821Tests {
|
||
mailbox, ok := parseRFC2821Mailbox(test.in)
|
||
expectedFailure := len(test.localPart) == 0 && len(test.domain) == 0
|
||
|
||
if ok && expectedFailure {
|
||
t.Errorf("#%d: %q unexpectedly parsed as (%q, %q)", i, test.in, mailbox.local, mailbox.domain)
|
||
continue
|
||
}
|
||
|
||
if !ok && !expectedFailure {
|
||
t.Errorf("#%d: unexpected failure for %q", i, test.in)
|
||
continue
|
||
}
|
||
|
||
if !ok {
|
||
continue
|
||
}
|
||
|
||
if mailbox.local != test.localPart || mailbox.domain != test.domain {
|
||
t.Errorf("#%d: %q parsed as (%q, %q), but wanted (%q, %q)", i, test.in, mailbox.local, mailbox.domain, test.localPart, test.domain)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestBadNamesInConstraints(t *testing.T) {
|
||
constraintParseError := func(err error) bool {
|
||
str := err.Error()
|
||
return strings.Contains(str, "failed to parse ") && strings.Contains(str, "constraint")
|
||
}
|
||
|
||
encodingError := func(err error) bool {
|
||
return strings.Contains(err.Error(), "cannot be encoded as an IA5String")
|
||
}
|
||
|
||
// Bad names in constraints should not parse.
|
||
badNames := []struct {
|
||
name string
|
||
matcher func(error) bool
|
||
}{
|
||
{"dns:foo.com.", constraintParseError},
|
||
{"email:abc@foo.com.", constraintParseError},
|
||
{"email:foo.com.", constraintParseError},
|
||
{"uri:example.com.", constraintParseError},
|
||
{"uri:1.2.3.4", constraintParseError},
|
||
{"uri:ffff::1", constraintParseError},
|
||
{"dns:not–hyphen.com", encodingError},
|
||
{"email:foo@not–hyphen.com", encodingError},
|
||
{"uri:not–hyphen.com", encodingError},
|
||
}
|
||
|
||
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||
if err != nil {
|
||
panic(err)
|
||
}
|
||
|
||
for _, test := range badNames {
|
||
_, err := makeConstraintsCACert(constraintsSpec{
|
||
ok: []string{test.name},
|
||
}, "TestAbsoluteNamesInConstraints", priv, nil, priv)
|
||
|
||
if err == nil {
|
||
t.Errorf("bad name %q unexpectedly accepted in name constraint", test.name)
|
||
continue
|
||
} else {
|
||
if !test.matcher(err) {
|
||
t.Errorf("bad name %q triggered unrecognised error: %s", test.name, err)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestBadNamesInSANs(t *testing.T) {
|
||
// Bad names in URI and IP SANs should not parse. Bad DNS and email SANs
|
||
// will parse and are tested in name constraint tests at the top of this
|
||
// file.
|
||
badNames := []string{
|
||
"uri:https://example.com./dsf",
|
||
"invalidip:0102",
|
||
"invalidip:0102030405",
|
||
}
|
||
|
||
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||
if err != nil {
|
||
panic(err)
|
||
}
|
||
|
||
for _, badName := range badNames {
|
||
_, err := makeConstraintsLeafCert(leafSpec{sans: []string{badName}}, priv, nil, priv)
|
||
|
||
if err == nil {
|
||
t.Errorf("bad name %q unexpectedly accepted in SAN", badName)
|
||
continue
|
||
}
|
||
|
||
if str := err.Error(); !strings.Contains(str, "cannot parse ") {
|
||
t.Errorf("bad name %q triggered unrecognised error: %s", badName, str)
|
||
}
|
||
}
|
||
}
|