mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-11-02 19:47:54 +00:00
Add permitted dns domains to pki (#3164)
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user