Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change behavior of TTL in sign-intermediate #3325

Merged
merged 2 commits into from
Sep 13, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 100 additions & 2 deletions builtin/logical/pki/backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2461,6 +2461,101 @@ func TestBackend_Permitted_DNS_Domains(t *testing.T) {
checkIssue(true, "common_name", "host.xyz.com")
}

func TestBackend_SignIntermediate_AllowedPastCA(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)
}

// Direct issuing from root
_, err = client.Logical().Write("root/root/generate/internal", map[string]interface{}{
"ttl": "40h",
"common_name": "myvault.com",
})
if err != nil {
t.Fatal(err)
}

_, err = client.Logical().Write("root/roles/test", map[string]interface{}{
"allow_bare_domains": true,
"allow_subdomains": true,
})
if err != nil {
t.Fatal(err)
}

resp, err := client.Logical().Write("int/intermediate/generate/internal", map[string]interface{}{
"common_name": "myint.com",
})
if err != nil {
t.Fatal(err)
}

csr := resp.Data["csr"]

_, err = client.Logical().Write("root/sign/test", map[string]interface{}{
"common_name": "myint.com",
"csr": csr,
"ttl": "60h",
})
if err == nil {
t.Fatal("expected error")
}

_, err = client.Logical().Write("root/sign-verbatim/test", map[string]interface{}{
"common_name": "myint.com",
"csr": csr,
"ttl": "60h",
})
if err == nil {
t.Fatal("expected error")
}

resp, err = client.Logical().Write("root/root/sign-intermediate", map[string]interface{}{
"common_name": "myint.com",
"csr": csr,
"ttl": "60h",
})
if err != nil {
t.Fatalf("got error: %v", err)
}
if resp == nil {
t.Fatal("got nil response")
}
if len(resp.Warnings) == 0 {
t.Fatal("expected warnings")
}
}

func TestBackend_SignSelfIssued(t *testing.T) {
// create the backend
config := logical.TestBackendConfig()
Expand Down Expand Up @@ -2601,8 +2696,11 @@ func TestBackend_SignSelfIssued(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(newCert.Subject, newCert.Issuer) {
t.Fatal("expected same subject/issuer")
if reflect.DeepEqual(newCert.Subject, newCert.Issuer) {
t.Fatal("expected different subject/issuer")
}
if !reflect.DeepEqual(newCert.Issuer, signingBundle.Certificate.Subject) {
t.Fatalf("expected matching issuer/CA subject\n\nIssuer:\n%#v\nSubject:\n%#v\n", newCert.Issuer, signingBundle.Certificate.Subject)
}
if bytes.Equal(newCert.AuthorityKeyId, newCert.SubjectKeyId) {
t.Fatal("expected different authority/subject")
Expand Down
2 changes: 1 addition & 1 deletion builtin/logical/pki/cert_util.go
Original file line number Diff line number Diff line change
Expand Up @@ -767,7 +767,7 @@ func generateCreationBundle(b *backend,
// If it's not self-signed, verify that the issued certificate won't be
// valid past the lifetime of the CA certificate
if signingBundle != nil &&
notAfter.After(signingBundle.Certificate.NotAfter) {
notAfter.After(signingBundle.Certificate.NotAfter) && !role.AllowExpirationPastCA {

return nil, errutil.UserError{Err: fmt.Sprintf(
"cannot satisfy request, as TTL is beyond the expiration of the CA certificate")}
Expand Down
3 changes: 3 additions & 0 deletions builtin/logical/pki/path_roles.go
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,9 @@ type roleEntry struct {
Organization string `json:"organization" structs:"organization" mapstructure:"organization"`
GenerateLease *bool `json:"generate_lease,omitempty" structs:"generate_lease,omitempty"`
NoStore bool `json:"no_store" structs:"no_store" mapstructure:"no_store"`

// Used internally for signing intermediates
AllowExpirationPastCA bool
}

const pathListRolesHelpSyn = `List the existing roles in this backend`
Expand Down
24 changes: 15 additions & 9 deletions builtin/logical/pki/path_root.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,12 +242,13 @@ func (b *backend) pathCASignIntermediate(
}

role := &roleEntry{
TTL: (time.Duration(data.Get("ttl").(int)) * time.Second).String(),
AllowLocalhost: true,
AllowAnyName: true,
AllowIPSANs: true,
EnforceHostnames: false,
KeyType: "any",
TTL: (time.Duration(data.Get("ttl").(int)) * time.Second).String(),
AllowLocalhost: true,
AllowAnyName: true,
AllowIPSANs: true,
EnforceHostnames: false,
KeyType: "any",
AllowExpirationPastCA: true,
}

if cn := data.Get("common_name").(string); len(cn) == 0 {
Expand Down Expand Up @@ -304,6 +305,10 @@ func (b *backend) pathCASignIntermediate(
},
}

if signingBundle.Certificate.NotAfter.Before(parsedBundle.Certificate.NotAfter) {
resp.AddWarning("The expiration time for the signed certificate is after the CA's expiration time. If the new certificate is not treated as a root, validation paths with the certificate past the issuing CA's expiration time will fail.")
}

switch format {
case "pem":
resp.Data["certificate"] = cb.Certificate
Expand Down Expand Up @@ -388,7 +393,6 @@ func (b *backend) pathCASignSelfIssued(
return nil, fmt.Errorf("Error converting raw signing bundle to cert bundle: %s", err)
}

cert.AuthorityKeyId = signingBundle.Certificate.SubjectKeyId
urls := &urlEntries{}
if signingBundle.URLs != nil {
urls = signingBundle.URLs
Expand All @@ -397,7 +401,7 @@ func (b *backend) pathCASignSelfIssued(
cert.CRLDistributionPoints = urls.CRLDistributionPoints
cert.OCSPServer = urls.OCSPServers

newCert, err := x509.CreateCertificate(rand.Reader, cert, cert, signingBundle.PrivateKey.Public(), signingBundle.PrivateKey)
newCert, err := x509.CreateCertificate(rand.Reader, cert, signingBundle.Certificate, cert.PublicKey, signingBundle.PrivateKey)
if err != nil {
return nil, errwrap.Wrapf("error signing self-issued certificate: {{err}}", err)
}
Expand Down Expand Up @@ -448,5 +452,7 @@ Signs another CA's self-issued certificate.
const pathSignSelfIssuedHelpDesc = `
Signs another CA's self-issued certificate. This is most often used for rolling roots; unless you know you need this you probably want to use sign-intermediate instead.

Note that this is a very privileged operation and should be extremely restricted in terms of who is allowed to use it. All values will be taken directly from the incoming certificate and no verification of host names, path lengths, or any other values will be performed.
Note that this is a very privileged operation and should be extremely restricted in terms of who is allowed to use it. All values will be taken directly from the incoming certificate and only verification that it is self-issued will be performed.

Configured URLs for CRLs/OCSP/etc. will be copied over and the issuer will be this mount's CA cert. Other than that, all other values will be used verbatim.
`
13 changes: 8 additions & 5 deletions website/source/api/secret/pki/index.html.md
Original file line number Diff line number Diff line change
Expand Up @@ -1035,7 +1035,8 @@ verbatim.

- `ttl` `(string: "")` – Specifies the requested Time To Live (after which the
certificate will be expired). This cannot be larger than the mount max (or, if
not set, the system max).
not set, the system max). However, this can be after the expiration of the
signing CA.

- `format` `(string: "pem")` – Specifies the format for returned data. Can be
`pem`, `der`, or `pem_bundle`. If `der`, the output is base64 encoded. If
Expand Down Expand Up @@ -1111,11 +1112,13 @@ certificate (which will usually be a self-signed certificate as well).
**_This is an extremely privileged endpoint_**. The given certificate will be
signed as-is with only minimal validation performed (is it a CA cert, and is it
actually self-issued). The only values that will be changed will be the
authority key ID and, if set, any distribution points.
authority key ID, the issuer DN, and, if set, any distribution points.

This is generally only needed for root certificate rolling. If you don't know
whether you need this endpoint, you most likely should be using a different
endpoint (such as `sign-intermediate`).
This is generally only needed for root certificate rolling in cases where you
don't want/can't get access to a CSR (such as if it's a root stored in Vault
where the key is not exposed). If you don't know whether you need this
endpoint, you most likely should be using a different endpoint (such as
`sign-intermediate`).

This endpoint requires `sudo` capability.

Expand Down