Add permitted dns domains to pki (#3164)

This commit is contained in:
Jeff Mitchell
2017-08-15 16:10:36 -04:00
committed by GitHub
parent 542b5da8f2
commit e6b43f7278
4 changed files with 256 additions and 6 deletions

View File

@@ -2264,6 +2264,172 @@ func TestBackend_Root_Idempotentcy(t *testing.T) {
} }
} }
func TestBackend_Permitted_DNS_Domains(t *testing.T) {
coreConfig := &vault.CoreConfig{
LogicalBackends: map[string]logical.Factory{
"pki": Factory,
},
}
cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{
HandlerFunc: vaulthttp.Handler,
})
cluster.Start()
defer cluster.Cleanup()
client := cluster.Cores[0].Client
var err error
err = client.Sys().Mount("root", &api.MountInput{
Type: "pki",
Config: api.MountConfigInput{
DefaultLeaseTTL: "16h",
MaxLeaseTTL: "32h",
},
})
if err != nil {
t.Fatal(err)
}
err = client.Sys().Mount("int", &api.MountInput{
Type: "pki",
Config: api.MountConfigInput{
DefaultLeaseTTL: "4h",
MaxLeaseTTL: "20h",
},
})
if err != nil {
t.Fatal(err)
}
_, err = client.Logical().Write("root/roles/example", map[string]interface{}{
"allowed_domains": "foobar.com,zipzap.com,abc.com,xyz.com",
"allow_bare_domains": true,
"allow_subdomains": true,
"max_ttl": "2h",
})
if err != nil {
t.Fatal(err)
}
_, err = client.Logical().Write("int/roles/example", map[string]interface{}{
"allowed_domains": "foobar.com,zipzap.com,abc.com,xyz.com",
"allow_subdomains": true,
"allow_bare_domains": true,
"max_ttl": "2h",
})
if err != nil {
t.Fatal(err)
}
// Direct issuing from root
_, err = client.Logical().Write("root/root/generate/internal", map[string]interface{}{
"common_name": "myvault.com",
"permitted_dns_domains": []string{"foobar.com", ".zipzap.com"},
})
if err != nil {
t.Fatal(err)
}
clientKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatal(err)
}
path := "root/"
checkIssue := func(valid bool, args ...interface{}) {
argMap := map[string]interface{}{}
var currString string
for i, arg := range args {
if i%2 == 0 {
currString = arg.(string)
} else {
argMap[currString] = arg
}
}
_, err = client.Logical().Write(path+"issue/example", argMap)
switch {
case valid && err != nil:
t.Fatal(err)
case !valid && err == nil:
t.Fatal("expected error")
}
csr, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{
Subject: pkix.Name{
CommonName: argMap["common_name"].(string),
},
}, clientKey)
if err != nil {
t.Fatal(err)
}
delete(argMap, "common_name")
argMap["csr"] = string(pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE REQUEST",
Bytes: csr,
}))
_, err = client.Logical().Write(path+"sign/example", argMap)
switch {
case valid && err != nil:
t.Fatal(err)
case !valid && err == nil:
t.Fatal("expected error")
}
}
// Check issuing and signing against root's permitted domains
checkIssue(false, "common_name", "zipzap.com")
checkIssue(false, "common_name", "host.foobar.com")
checkIssue(true, "common_name", "host.zipzap.com")
checkIssue(true, "common_name", "foobar.com")
// Verify that root also won't issue an intermediate outside of its permitted domains
resp, err := client.Logical().Write("int/intermediate/generate/internal", map[string]interface{}{
"common_name": "issuer.abc.com",
})
if err != nil {
t.Fatal(err)
}
_, err = client.Logical().Write("root/root/sign-intermediate", map[string]interface{}{
"common_name": "issuer.abc.com",
"csr": resp.Data["csr"],
"permitted_dns_domains": []string{"abc.com", ".xyz.com"},
"ttl": "5h",
})
if err == nil {
t.Fatal("expected error")
}
_, err = client.Logical().Write("root/root/sign-intermediate", map[string]interface{}{
"use_csr_values": true,
"csr": resp.Data["csr"],
"permitted_dns_domains": []string{"abc.com", ".xyz.com"},
"ttl": "5h",
})
if err == nil {
t.Fatal("expected error")
}
// Sign a valid intermediate
resp, err = client.Logical().Write("root/root/sign-intermediate", map[string]interface{}{
"common_name": "issuer.zipzap.com",
"csr": resp.Data["csr"],
"permitted_dns_domains": []string{"abc.com", ".xyz.com"},
"ttl": "5h",
})
if err != nil {
t.Fatal(err)
}
resp, err = client.Logical().Write("int/intermediate/set-signed", map[string]interface{}{
"certificate": resp.Data["certificate"],
})
if err != nil {
t.Fatal(err)
}
// Check enforcement with the intermediate's set values
path = "int/"
checkIssue(false, "common_name", "host.abc.com")
checkIssue(false, "common_name", "xyz.com")
checkIssue(true, "common_name", "abc.com")
checkIssue(true, "common_name", "host.xyz.com")
}
const ( const (
rsaCAKey string = `-----BEGIN RSA PRIVATE KEY----- rsaCAKey string = `-----BEGIN RSA PRIVATE KEY-----
MIIEogIBAAKCAQEAmPQlK7xD5p+E8iLQ8XlVmll5uU2NKMxKY3UF5tbh+0vkc+Fy MIIEogIBAAKCAQEAmPQlK7xD5p+E8iLQ8XlVmll5uU2NKMxKY3UF5tbh+0vkc+Fy

View File

@@ -45,12 +45,13 @@ type creationBundle struct {
KeyType string KeyType string
KeyBits int KeyBits int
SigningBundle *caInfoBundle SigningBundle *caInfoBundle
TTL time.Duration NotAfter time.Time
KeyUsage x509.KeyUsage KeyUsage x509.KeyUsage
ExtKeyUsage certExtKeyUsage ExtKeyUsage certExtKeyUsage
// Only used when signing a CA cert // Only used when signing a CA cert
UseCSRValues bool UseCSRValues bool
PermittedDNSDomains []string
// URLs to encode into the certificate // URLs to encode into the certificate
URLs *urlEntries URLs *urlEntries
@@ -434,6 +435,8 @@ func generateCert(b *backend,
if isCA { if isCA {
creationBundle.IsCA = isCA creationBundle.IsCA = isCA
creationBundle.PermittedDNSDomains = data.Get("permitted_dns_domains").([]string)
if signingBundle == nil { if signingBundle == nil {
// Generating a self-signed root certificate // Generating a self-signed root certificate
entries, err := getURLs(req) entries, err := getURLs(req)
@@ -581,6 +584,10 @@ func signCert(b *backend,
creationBundle.IsCA = isCA creationBundle.IsCA = isCA
creationBundle.UseCSRValues = useCSRValues creationBundle.UseCSRValues = useCSRValues
if isCA {
creationBundle.PermittedDNSDomains = data.Get("permitted_dns_domains").([]string)
}
parsedBundle, err := signCertificate(creationBundle, csr) parsedBundle, err := signCertificate(creationBundle, csr)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -724,6 +731,7 @@ func generateCreationBundle(b *backend,
var ttlField string var ttlField string
var ttl time.Duration var ttl time.Duration
var maxTTL time.Duration var maxTTL time.Duration
var notAfter time.Time
var ttlFieldInt interface{} var ttlFieldInt interface{}
{ {
ttlFieldInt, ok = data.GetOk("ttl") ttlFieldInt, ok = data.GetOk("ttl")
@@ -764,10 +772,13 @@ func generateCreationBundle(b *backend,
} }
} }
notAfter = time.Now().Add(ttl)
// If it's not self-signed, verify that the issued certificate won't be // If it's not self-signed, verify that the issued certificate won't be
// valid past the lifetime of the CA certificate // valid past the lifetime of the CA certificate
if signingBundle != nil && if signingBundle != nil &&
time.Now().Add(ttl).After(signingBundle.Certificate.NotAfter) { notAfter.After(signingBundle.Certificate.NotAfter) {
return nil, errutil.UserError{Err: fmt.Sprintf( return nil, errutil.UserError{Err: fmt.Sprintf(
"cannot satisfy request, as TTL is beyond the expiration of the CA certificate")} "cannot satisfy request, as TTL is beyond the expiration of the CA certificate")}
} }
@@ -800,7 +811,7 @@ func generateCreationBundle(b *backend,
KeyType: role.KeyType, KeyType: role.KeyType,
KeyBits: role.KeyBits, KeyBits: role.KeyBits,
SigningBundle: signingBundle, SigningBundle: signingBundle,
TTL: ttl, NotAfter: notAfter,
KeyUsage: x509.KeyUsage(parseKeyUsages(role.KeyUsage)), KeyUsage: x509.KeyUsage(parseKeyUsages(role.KeyUsage)),
ExtKeyUsage: extUsage, ExtKeyUsage: extUsage,
} }
@@ -893,7 +904,7 @@ func createCertificate(creationInfo *creationBundle) (*certutil.ParsedCertBundle
SerialNumber: serialNumber, SerialNumber: serialNumber,
Subject: subject, Subject: subject,
NotBefore: time.Now().Add(-30 * time.Second), NotBefore: time.Now().Add(-30 * time.Second),
NotAfter: time.Now().Add(creationInfo.TTL), NotAfter: creationInfo.NotAfter,
IsCA: false, IsCA: false,
SubjectKeyId: subjKeyID, SubjectKeyId: subjKeyID,
DNSNames: creationInfo.DNSNames, DNSNames: creationInfo.DNSNames,
@@ -906,6 +917,12 @@ func createCertificate(creationInfo *creationBundle) (*certutil.ParsedCertBundle
certTemplate.IsCA = true certTemplate.IsCA = true
} }
// This will only be filled in from the generation paths
if len(creationInfo.PermittedDNSDomains) > 0 {
certTemplate.PermittedDNSDomains = creationInfo.PermittedDNSDomains
certTemplate.PermittedDNSDomainsCritical = true
}
addKeyUsages(creationInfo, certTemplate) addKeyUsages(creationInfo, certTemplate)
certTemplate.IssuingCertificateURL = creationInfo.URLs.IssuingCertificates certTemplate.IssuingCertificateURL = creationInfo.URLs.IssuingCertificates
@@ -923,6 +940,11 @@ func createCertificate(creationInfo *creationBundle) (*certutil.ParsedCertBundle
caCert := creationInfo.SigningBundle.Certificate caCert := creationInfo.SigningBundle.Certificate
err = checkPermittedDNSDomains(certTemplate, caCert)
if err != nil {
return nil, errutil.UserError{Err: err.Error()}
}
certBytes, err = x509.CreateCertificate(rand.Reader, certTemplate, caCert, result.PrivateKey.Public(), creationInfo.SigningBundle.PrivateKey) certBytes, err = x509.CreateCertificate(rand.Reader, certTemplate, caCert, result.PrivateKey.Public(), creationInfo.SigningBundle.PrivateKey)
} else { } else {
// Creating a self-signed root // Creating a self-signed root
@@ -1057,7 +1079,7 @@ func signCertificate(creationInfo *creationBundle,
SerialNumber: serialNumber, SerialNumber: serialNumber,
Subject: subject, Subject: subject,
NotBefore: time.Now().Add(-30 * time.Second), NotBefore: time.Now().Add(-30 * time.Second),
NotAfter: time.Now().Add(creationInfo.TTL), NotAfter: creationInfo.NotAfter,
SubjectKeyId: subjKeyID[:], SubjectKeyId: subjKeyID[:],
} }
@@ -1106,6 +1128,15 @@ func signCertificate(creationInfo *creationBundle,
} }
} }
if len(creationInfo.PermittedDNSDomains) > 0 {
certTemplate.PermittedDNSDomains = creationInfo.PermittedDNSDomains
certTemplate.PermittedDNSDomainsCritical = true
}
err = checkPermittedDNSDomains(certTemplate, caCert)
if err != nil {
return nil, errutil.UserError{Err: err.Error()}
}
certBytes, err = x509.CreateCertificate(rand.Reader, certTemplate, caCert, csr.PublicKey, creationInfo.SigningBundle.PrivateKey) certBytes, err = x509.CreateCertificate(rand.Reader, certTemplate, caCert, csr.PublicKey, creationInfo.SigningBundle.PrivateKey)
if err != nil { if err != nil {
@@ -1122,3 +1153,39 @@ func signCertificate(creationInfo *creationBundle,
return result, nil return result, nil
} }
func checkPermittedDNSDomains(template, ca *x509.Certificate) error {
if len(ca.PermittedDNSDomains) == 0 {
return nil
}
namesToCheck := map[string]struct{}{
template.Subject.CommonName: struct{}{},
}
for _, name := range template.DNSNames {
namesToCheck[name] = struct{}{}
}
var badName string
NameCheck:
for name := range namesToCheck {
for _, perm := range ca.PermittedDNSDomains {
switch {
case strings.HasPrefix(perm, ".") && strings.HasSuffix(name, perm):
// .example.com matches my.host.example.com and
// host.example.com but does not match example.com
break NameCheck
case perm == name:
break NameCheck
}
}
badName = name
break
}
if badName == "" {
return nil
}
return fmt.Errorf("name %q disallowed by CA's permitted DNS domains", badName)
}

View File

@@ -144,5 +144,10 @@ func addCAIssueFields(fields map[string]*framework.FieldSchema) map[string]*fram
Description: "The maximum allowable path length", Description: "The maximum allowable path length",
} }
fields["permitted_dns_domains"] = &framework.FieldSchema{
Type: framework.TypeCommaStringSlice,
Description: `Domains for which this certificate is allowed to sign or issue child certificates. If set, all DNS names (subject and alt) on child certs must be exact matches or subsets of the given domains (see https://tools.ietf.org/html/rfc5280#section-4.2.1.10).`,
}
return fields return fields
} }

View File

@@ -944,6 +944,12 @@ Vault would overwrite the existing cert/key with new values.
Useful if the CN is not a hostname or email address, but is instead some Useful if the CN is not a hostname or email address, but is instead some
human-readable identifier. human-readable identifier.
- `permitted_dns_domains` `(string: "")`  A comma separated string (or, string
array) containing DNS domains for which certificates are allowed to be issued
or signed by this CA certificate. Supports subdomains via a `.` in front of
the domain, as per
[RFC](https://tools.ietf.org/html/rfc5280#section-4.2.1.10).
### Sample Payload ### Sample Payload
```json ```json
@@ -1053,6 +1059,12 @@ verbatim.
path; 3) Extensions requested in the CSR will be copied into the issued path; 3) Extensions requested in the CSR will be copied into the issued
certificate. certificate.
- `permitted_dns_domains` `(string: "")`  A comma separated string (or, string
array) containing DNS domains for which certificates are allowed to be issued
or signed by this CA certificate. Supports subdomains via a `.` in front of
the domain, as per
[RFC](https://tools.ietf.org/html/rfc5280#section-4.2.1.10).
### Sample Payload ### Sample Payload
```json ```json