From 3437af07114c3b7d3da3ee5d24f308fcac33bce8 Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Wed, 18 Nov 2015 10:16:09 -0500 Subject: [PATCH] Split root and intermediate functionality into their own sections in the API. Update documentation. Add sign-verbatim endpoint. --- builtin/logical/pki/backend.go | 9 +- builtin/logical/pki/backend_test.go | 105 ++- builtin/logical/pki/ca_util.go | 42 ++ builtin/logical/pki/cert_util.go | 53 +- builtin/logical/pki/path_config_ca.go | 454 +----------- builtin/logical/pki/path_intermediate.go | 231 ++++++ builtin/logical/pki/path_issue_sign.go | 74 +- builtin/logical/pki/path_root.go | 261 +++++++ website/source/docs/secrets/pki/index.html.md | 697 ++++++++++-------- 9 files changed, 1143 insertions(+), 783 deletions(-) create mode 100644 builtin/logical/pki/ca_util.go create mode 100644 builtin/logical/pki/path_intermediate.go create mode 100644 builtin/logical/pki/path_root.go diff --git a/builtin/logical/pki/backend.go b/builtin/logical/pki/backend.go index baf29ecb52..8a91f63728 100644 --- a/builtin/logical/pki/backend.go +++ b/builtin/logical/pki/backend.go @@ -32,13 +32,14 @@ func Backend() *framework.Backend { Paths: []*framework.Path{ pathRoles(&b), - pathGenerateRootCA(&b), - pathGenerateIntermediateCA(&b), - pathSignIntermediateCA(&b), - pathSetCA(&b), + pathGenerateRoot(&b), + pathGenerateIntermediate(&b), + pathSetSignedIntermediate(&b), + pathSignIntermediate(&b), pathConfigCA(&b), pathConfigCRL(&b), pathConfigURLs(&b), + pathSignVerbatim(&b), pathSign(&b), pathIssue(&b), pathRotateCRL(&b), diff --git a/builtin/logical/pki/backend_test.go b/builtin/logical/pki/backend_test.go index 4e1af6da3b..0af8886bbf 100644 --- a/builtin/logical/pki/backend_test.go +++ b/builtin/logical/pki/backend_test.go @@ -311,7 +311,6 @@ func checkCertsAndPrivateKey(keyType string, key crypto.Signer, usage certUsage, } func generateURLSteps(t *testing.T, caCert, caKey string, intdata, reqdata map[string]interface{}) []logicaltest.TestStep { - expected := urlEntries{ IssuingCertificates: []string{ "http://example.com/ca1", @@ -342,7 +341,7 @@ func generateURLSteps(t *testing.T, caCert, caKey string, intdata, reqdata map[s ret := []logicaltest.TestStep{ logicaltest.TestStep{ Operation: logical.WriteOperation, - Path: "config/ca/generate/root/exported", + Path: "root/generate/exported", Data: map[string]interface{}{ "common_name": "Root Cert", "ttl": "180h", @@ -382,7 +381,7 @@ func generateURLSteps(t *testing.T, caCert, caKey string, intdata, reqdata map[s logicaltest.TestStep{ Operation: logical.WriteOperation, - Path: "config/ca/sign", + Path: "root/sign-intermediate", Data: map[string]interface{}{ "common_name": "Intermediate Cert", "csr": string(csrPem), @@ -452,7 +451,7 @@ func generateCSRSteps(t *testing.T, caCert, caKey string, intdata, reqdata map[s ret := []logicaltest.TestStep{ logicaltest.TestStep{ Operation: logical.WriteOperation, - Path: "config/ca/generate/root/exported", + Path: "root/generate/exported", Data: map[string]interface{}{ "common_name": "Root Cert", "ttl": "180h", @@ -462,7 +461,7 @@ func generateCSRSteps(t *testing.T, caCert, caKey string, intdata, reqdata map[s logicaltest.TestStep{ Operation: logical.WriteOperation, - Path: "config/ca/sign", + Path: "root/sign-intermediate", Data: map[string]interface{}{ "use_csr_values": true, "csr": string(csrPem), @@ -473,7 +472,7 @@ func generateCSRSteps(t *testing.T, caCert, caKey string, intdata, reqdata map[s logicaltest.TestStep{ Operation: logical.WriteOperation, - Path: "config/ca/generate/root/exported", + Path: "root/generate/exported", Data: map[string]interface{}{ "common_name": "Root Cert", "ttl": "180h", @@ -483,7 +482,7 @@ func generateCSRSteps(t *testing.T, caCert, caKey string, intdata, reqdata map[s logicaltest.TestStep{ Operation: logical.WriteOperation, - Path: "config/ca/sign", + Path: "root/sign-intermediate", Data: map[string]interface{}{ "use_csr_values": true, "csr": string(csrPem), @@ -612,26 +611,25 @@ func generateCATestingSteps(t *testing.T, caCert, caKey, otherCaCert string, int }, }, - // Now test uploading when the private key is already stored, such - // as when uploading a CA signed as the result of a generated CSR - // First we test the wrong one, to ensure that the key comparator is - // working correctly - logicaltest.TestStep{ - Operation: logical.WriteOperation, - Path: "config/ca/set", - Data: map[string]interface{}{ - "pem_bundle": otherCaCert, - }, - ErrorOk: true, - }, - - // Now, the right one + // Ensure that both parts of the PEM bundle are required + // Here, just the cert logicaltest.TestStep{ Operation: logical.WriteOperation, Path: "config/ca/set", Data: map[string]interface{}{ "pem_bundle": caCert, }, + ErrorOk: true, + }, + + // Here, just the key + logicaltest.TestStep{ + Operation: logical.WriteOperation, + Path: "config/ca/set", + Data: map[string]interface{}{ + "pem_bundle": caKey, + }, + ErrorOk: true, }, // Ensure we can fetch it back via unauthenticated means, in various formats @@ -686,7 +684,7 @@ func generateCATestingSteps(t *testing.T, caCert, caKey, otherCaCert string, int // Test a bunch of generation stuff logicaltest.TestStep{ Operation: logical.WriteOperation, - Path: "config/ca/generate/root/exported", + Path: "root/generate/exported", Data: map[string]interface{}{ "common_name": "Root Cert", "ttl": "180h", @@ -701,7 +699,7 @@ func generateCATestingSteps(t *testing.T, caCert, caKey, otherCaCert string, int logicaltest.TestStep{ Operation: logical.WriteOperation, - Path: "config/ca/generate/intermediate/exported", + Path: "intermediate/generate/exported", Data: map[string]interface{}{ "common_name": "Intermediate Cert", }, @@ -712,6 +710,7 @@ func generateCATestingSteps(t *testing.T, caCert, caKey, otherCaCert string, int }, }, + // Re-load the root key in so we can sign it logicaltest.TestStep{ Operation: logical.WriteOperation, Path: "config/ca/set", @@ -728,14 +727,38 @@ func generateCATestingSteps(t *testing.T, caCert, caKey, otherCaCert string, int logicaltest.TestStep{ Operation: logical.WriteOperation, - Path: "config/ca/sign", + Path: "root/sign-intermediate", Data: reqdata, Check: func(resp *logical.Response) error { - intdata["intermediatecert"] = resp.Data["certificate"].(string) delete(reqdata, "csr") delete(reqdata, "common_name") delete(reqdata, "ttl") + intdata["intermediatecert"] = resp.Data["certificate"].(string) reqdata["serial_number"] = resp.Data["serial_number"].(string) + reqdata["certificate"] = resp.Data["certificate"].(string) + reqdata["pem_bundle"] = intdata["intermediatekey"].(string) + "\n" + resp.Data["certificate"].(string) + return nil + }, + }, + + // First load in this way to populate the private key + logicaltest.TestStep{ + Operation: logical.WriteOperation, + Path: "config/ca/set", + Data: reqdata, + Check: func(resp *logical.Response) error { + delete(reqdata, "pem_bundle") + return nil + }, + }, + + // Now test setting the intermediate, signed CA cert + logicaltest.TestStep{ + Operation: logical.WriteOperation, + Path: "intermediate/set-signed", + Data: reqdata, + Check: func(resp *logical.Response) error { + delete(reqdata, "certificate") return nil }, }, @@ -772,7 +795,7 @@ func generateCATestingSteps(t *testing.T, caCert, caKey, otherCaCert string, int // Do it all again, with EC keys and DER format logicaltest.TestStep{ Operation: logical.WriteOperation, - Path: "config/ca/generate/root/exported", + Path: "root/generate/exported", Data: map[string]interface{}{ "common_name": "Root Cert", "ttl": "180h", @@ -800,7 +823,7 @@ func generateCATestingSteps(t *testing.T, caCert, caKey, otherCaCert string, int logicaltest.TestStep{ Operation: logical.WriteOperation, - Path: "config/ca/generate/intermediate/exported", + Path: "intermediate/generate/exported", Data: map[string]interface{}{ "format": "der", "key_type": "ec", @@ -840,14 +863,38 @@ func generateCATestingSteps(t *testing.T, caCert, caKey, otherCaCert string, int logicaltest.TestStep{ Operation: logical.WriteOperation, - Path: "config/ca/sign", + Path: "root/sign-intermediate", Data: reqdata, Check: func(resp *logical.Response) error { - intdata["intermediatecert"] = resp.Data["certificate"].(string) delete(reqdata, "csr") delete(reqdata, "common_name") delete(reqdata, "ttl") + intdata["intermediatecert"] = resp.Data["certificate"].(string) reqdata["serial_number"] = resp.Data["serial_number"].(string) + reqdata["certificate"] = resp.Data["certificate"].(string) + reqdata["pem_bundle"] = intdata["intermediatekey"].(string) + "\n" + resp.Data["certificate"].(string) + return nil + }, + }, + + // First load in this way to populate the private key + logicaltest.TestStep{ + Operation: logical.WriteOperation, + Path: "config/ca/set", + Data: reqdata, + Check: func(resp *logical.Response) error { + delete(reqdata, "pem_bundle") + return nil + }, + }, + + // Now test setting the intermediate, signed CA cert + logicaltest.TestStep{ + Operation: logical.WriteOperation, + Path: "intermediate/set-signed", + Data: reqdata, + Check: func(resp *logical.Response) error { + delete(reqdata, "certificate") return nil }, }, diff --git a/builtin/logical/pki/ca_util.go b/builtin/logical/pki/ca_util.go new file mode 100644 index 0000000000..6e757f0145 --- /dev/null +++ b/builtin/logical/pki/ca_util.go @@ -0,0 +1,42 @@ +package pki + +import ( + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" +) + +func (b *backend) getGenerationParams( + data *framework.FieldData, +) (exported bool, format string, role *roleEntry, errorResp *logical.Response) { + exportedStr := data.Get("exported").(string) + switch exportedStr { + case "exported": + exported = true + case "internal": + default: + errorResp = logical.ErrorResponse( + `The "exported" path parameter must be "internal" or "exported"`) + return + } + + format = getFormat(data) + if format == "" { + errorResp = logical.ErrorResponse( + `The "format" path parameter must be "pem" or "der"`) + return + } + + role = &roleEntry{ + TTL: data.Get("ttl").(string), + KeyType: data.Get("key_type").(string), + KeyBits: data.Get("key_bits").(int), + AllowLocalhost: true, + AllowAnyName: true, + AllowIPSANs: true, + EnforceHostnames: false, + } + + errorResp = validateKeyTypeLength(role.KeyType, role.KeyBits) + + return +} diff --git a/builtin/logical/pki/cert_util.go b/builtin/logical/pki/cert_util.go index a0ee31ca07..c04894e7d1 100644 --- a/builtin/logical/pki/cert_util.go +++ b/builtin/logical/pki/cert_util.go @@ -5,6 +5,7 @@ import ( "crypto/sha1" "crypto/x509" "crypto/x509/pkix" + "encoding/asn1" "encoding/pem" "fmt" "net" @@ -54,7 +55,19 @@ type caInfoBundle struct { URLs *urlEntries } -var hostnameRegex = regexp.MustCompile(`^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$`) +var ( + hostnameRegex = regexp.MustCompile(`^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$`) + oidExtensionBasicConstraints = []int{2, 5, 29, 19} +) + +func oidInExtensions(oid asn1.ObjectIdentifier, extensions []pkix.Extension) bool { + for _, e := range extensions { + if e.Id.Equal(oid) { + return true + } + } + return false +} func getFormat(data *framework.FieldData) string { format := data.Get("format").(string) @@ -370,10 +383,8 @@ func signCert(b *backend, return nil, err } - if isCA { - creationBundle.IsCA = isCA - creationBundle.UseCSRValues = useCSRValues - } + creationBundle.IsCA = isCA + creationBundle.UseCSRValues = useCSRValues parsedBundle, err := signCertificate(creationBundle, csr) if err != nil { @@ -591,12 +602,11 @@ func createCertificate(creationInfo *creationBundle) (*certutil.ParsedCertBundle } certTemplate := &x509.Certificate{ - SerialNumber: serialNumber, - Subject: subject, - NotBefore: time.Now(), - NotAfter: time.Now().Add(creationInfo.TTL), - KeyUsage: x509.KeyUsage(x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement), - BasicConstraintsValid: true, + SerialNumber: serialNumber, + Subject: subject, + NotBefore: time.Now(), + NotAfter: time.Now().Add(creationInfo.TTL), + KeyUsage: x509.KeyUsage(x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement), IsCA: false, SubjectKeyId: subjKeyID, DNSNames: creationInfo.DNSNames, @@ -649,6 +659,7 @@ func createCertificate(creationInfo *creationBundle) (*certutil.ParsedCertBundle certTemplate.SignatureAlgorithm = x509.ECDSAWithSHA256 } + certTemplate.BasicConstraintsValid = true certTemplate.IsCA = true certTemplate.KeyUsage = x509.KeyUsage(certTemplate.KeyUsage | x509.KeyUsageCertSign | x509.KeyUsageCRLSign) certTemplate.ExtKeyUsage = append(certTemplate.ExtKeyUsage, x509.ExtKeyUsageOCSPSigning) @@ -758,12 +769,11 @@ func signCertificate(creationInfo *creationBundle, } certTemplate := &x509.Certificate{ - SerialNumber: serialNumber, - Subject: subject, - NotBefore: time.Now(), - NotAfter: time.Now().Add(creationInfo.TTL), - BasicConstraintsValid: true, - SubjectKeyId: subjKeyID[:], + SerialNumber: serialNumber, + Subject: subject, + NotBefore: time.Now(), + NotAfter: time.Now().Add(creationInfo.TTL), + SubjectKeyId: subjKeyID[:], } switch creationInfo.SigningBundle.PrivateKeyType { @@ -780,9 +790,13 @@ func signCertificate(creationInfo *creationBundle, certTemplate.EmailAddresses = csr.EmailAddresses certTemplate.IPAddresses = csr.IPAddresses - if creationInfo.IsCA { - certTemplate.ExtraExtensions = csr.Extensions + certTemplate.ExtraExtensions = csr.Extensions + // Do not sign a CA certificate if they didn't go through the sign-intermediate + // endpoint + if !creationInfo.IsCA && oidInExtensions(oidExtensionBasicConstraints, certTemplate.ExtraExtensions) { + return nil, certutil.UserError{Err: "will not sign a CSR asking for CA rights through this endpoint"} } + } else { certTemplate.DNSNames = creationInfo.DNSNames certTemplate.EmailAddresses = creationInfo.EmailAddresses @@ -817,6 +831,7 @@ func signCertificate(creationInfo *creationBundle, certTemplate.OCSPServer = creationInfo.SigningBundle.URLs.OCSPServers if creationInfo.IsCA { + certTemplate.BasicConstraintsValid = true certTemplate.IsCA = true if creationInfo.SigningBundle.Certificate.MaxPathLen == 0 && diff --git a/builtin/logical/pki/path_config_ca.go b/builtin/logical/pki/path_config_ca.go index ba14c80d2f..586c321e62 100644 --- a/builtin/logical/pki/path_config_ca.go +++ b/builtin/logical/pki/path_config_ca.go @@ -1,9 +1,7 @@ package pki import ( - "encoding/base64" "fmt" - "time" "github.com/hashicorp/vault/helper/certutil" "github.com/hashicorp/vault/logical" @@ -13,25 +11,6 @@ import ( func pathConfigCA(b *backend) *framework.Path { return &framework.Path{ Pattern: "config/ca", - Fields: map[string]*framework.FieldSchema{ - "pem_bundle": &framework.FieldSchema{ - Type: framework.TypeString, - Description: `DEPRECATED: use "config/ca/set" instead.`, - }, - }, - - Callbacks: map[logical.Operation]framework.OperationFunc{ - logical.WriteOperation: b.pathCASetWrite, - }, - - HelpSynopsis: pathConfigCASetHelpSyn, - HelpDescription: pathConfigCASetHelpDesc, - } -} - -func pathSetCA(b *backend) *framework.Path { - return &framework.Path{ - Pattern: "config/ca/set", Fields: map[string]*framework.FieldSchema{ "pem_bundle": &framework.FieldSchema{ Type: framework.TypeString, @@ -43,363 +22,15 @@ endpoint, just the signed certificate.`, }, Callbacks: map[logical.Operation]framework.OperationFunc{ - logical.WriteOperation: b.pathCASetWrite, + logical.WriteOperation: b.pathCAWrite, }, - HelpSynopsis: pathConfigCASetHelpSyn, - HelpDescription: pathConfigCASetHelpDesc, + HelpSynopsis: pathConfigCAHelpSyn, + HelpDescription: pathConfigCAHelpDesc, } } -func pathGenerateRootCA(b *backend) *framework.Path { - ret := &framework.Path{ - Pattern: "config/ca/generate/root/" + framework.GenericNameRegex("exported"), - - Callbacks: map[logical.Operation]framework.OperationFunc{ - logical.WriteOperation: b.pathCAGenerateRoot, - }, - - HelpSynopsis: pathConfigCAGenerateHelpSyn, - HelpDescription: pathConfigCAGenerateHelpDesc, - } - - ret.Fields = addCACommonFields(map[string]*framework.FieldSchema{}) - ret.Fields = addCAKeyGenerationFields(ret.Fields) - ret.Fields = addCAIssueFields(ret.Fields) - - return ret -} - -func pathGenerateIntermediateCA(b *backend) *framework.Path { - ret := &framework.Path{ - Pattern: "config/ca/generate/intermediate/" + framework.GenericNameRegex("exported"), - - Callbacks: map[logical.Operation]framework.OperationFunc{ - logical.WriteOperation: b.pathCAGenerateIntermediate, - }, - - HelpSynopsis: pathConfigCAGenerateHelpSyn, - HelpDescription: pathConfigCAGenerateHelpDesc, - } - - ret.Fields = addCACommonFields(map[string]*framework.FieldSchema{}) - ret.Fields = addCAKeyGenerationFields(ret.Fields) - - return ret -} - -func pathSignIntermediateCA(b *backend) *framework.Path { - ret := &framework.Path{ - Pattern: "config/ca/sign", - - Callbacks: map[logical.Operation]framework.OperationFunc{ - logical.WriteOperation: b.pathCASignIntermediate, - }, - - HelpSynopsis: pathConfigCASignHelpSyn, - HelpDescription: pathConfigCASignHelpDesc, - } - - ret.Fields = addCACommonFields(map[string]*framework.FieldSchema{}) - ret.Fields = addCAIssueFields(ret.Fields) - - ret.Fields["csr"] = &framework.FieldSchema{ - Type: framework.TypeString, - Default: "", - Description: `PEM-format CSR to be signed.`, - } - - ret.Fields["use_csr_values"] = &framework.FieldSchema{ - Type: framework.TypeBool, - Default: false, - Description: `If true, then: -1) Subject information, including names and alternate -names, will be preserved from the CSR rather than -using values provided in the other parameters to -this path; -2) Any key usages requested in the CSR will be -added to the basic set of key usages used for CA -certs signed by this path; for instance, -the non-repudiation flag.`, - } - - return ret -} - -func (b *backend) getGenerationParams( - data *framework.FieldData, -) (exported bool, format string, role *roleEntry, errorResp *logical.Response) { - exportedStr := data.Get("exported").(string) - switch exportedStr { - case "exported": - exported = true - case "internal": - default: - errorResp = logical.ErrorResponse( - `The "exported" path parameter must be "internal" or "exported"`) - return - } - - format = getFormat(data) - if format == "" { - errorResp = logical.ErrorResponse( - `The "format" path parameter must be "pem" or "der"`) - return - } - - role = &roleEntry{ - TTL: data.Get("ttl").(string), - KeyType: data.Get("key_type").(string), - KeyBits: data.Get("key_bits").(int), - AllowLocalhost: true, - AllowAnyName: true, - AllowIPSANs: true, - EnforceHostnames: false, - } - - errorResp = validateKeyTypeLength(role.KeyType, role.KeyBits) - - return -} - -func (b *backend) pathCAGenerateRoot( - req *logical.Request, data *framework.FieldData) (*logical.Response, error) { - var err error - - exported, format, role, errorResp := b.getGenerationParams(data) - if errorResp != nil { - return errorResp, nil - } - - maxPathLengthIface, ok := data.GetOk("max_path_length") - if ok { - maxPathLength := maxPathLengthIface.(int) - role.MaxPathLength = &maxPathLength - } - - var resp *logical.Response - parsedBundle, err := generateCert(b, role, nil, true, req, data) - if err != nil { - switch err.(type) { - case certutil.UserError: - return logical.ErrorResponse(err.Error()), nil - case certutil.InternalError: - return nil, err - } - } - - cb, err := parsedBundle.ToCertBundle() - if err != nil { - return nil, fmt.Errorf("error converting raw cert bundle to cert bundle: %s", err) - } - - resp = &logical.Response{ - Data: map[string]interface{}{ - "serial_number": cb.SerialNumber, - "expiration": int64(parsedBundle.Certificate.NotAfter.Unix()), - }, - } - - switch format { - case "pem": - resp.Data["certificate"] = cb.Certificate - resp.Data["issuing_ca"] = cb.IssuingCA - if exported { - resp.Data["private_key"] = cb.PrivateKey - resp.Data["private_key_type"] = cb.PrivateKeyType - } - case "der": - resp.Data["certificate"] = base64.StdEncoding.EncodeToString(parsedBundle.CertificateBytes) - resp.Data["issuing_ca"] = base64.StdEncoding.EncodeToString(parsedBundle.IssuingCABytes) - if exported { - resp.Data["private_key"] = base64.StdEncoding.EncodeToString(parsedBundle.PrivateKeyBytes) - resp.Data["private_key_type"] = cb.PrivateKeyType - } - } - - entry, err := logical.StorageEntryJSON("config/ca_bundle", cb) - if err != nil { - return nil, err - } - err = req.Storage.Put(entry) - if err != nil { - return nil, err - } - - // For ease of later use, also store just the certificate at a known - // location, plus a fresh CRL - entry.Key = "ca" - entry.Value = parsedBundle.CertificateBytes - err = req.Storage.Put(entry) - if err != nil { - return nil, err - } - - err = buildCRL(b, req) - if err != nil { - return nil, err - } - - if parsedBundle.Certificate.MaxPathLen == 0 { - resp.AddWarning("Max path length of the generated certificate is zero. This certificate cannot be used to issue intermediate CA certificates.") - } - - return resp, nil -} - -func (b *backend) pathCAGenerateIntermediate( - req *logical.Request, data *framework.FieldData) (*logical.Response, error) { - var err error - - exported, format, role, errorResp := b.getGenerationParams(data) - if errorResp != nil { - return errorResp, nil - } - - var resp *logical.Response - parsedBundle, err := generateIntermediateCSR(b, role, nil, req, data) - if err != nil { - switch err.(type) { - case certutil.UserError: - return logical.ErrorResponse(err.Error()), nil - case certutil.InternalError: - return nil, err - } - } - - csrb, err := parsedBundle.ToCSRBundle() - if err != nil { - return nil, fmt.Errorf("Error converting raw CSR bundle to CSR bundle: %s", err) - } - - resp = &logical.Response{ - Data: map[string]interface{}{}, - } - - switch format { - case "pem": - resp.Data["csr"] = csrb.CSR - if exported { - resp.Data["private_key"] = csrb.PrivateKey - resp.Data["private_key_type"] = csrb.PrivateKeyType - } - case "der": - resp.Data["csr"] = base64.StdEncoding.EncodeToString(parsedBundle.CSRBytes) - if exported { - resp.Data["private_key"] = base64.StdEncoding.EncodeToString(parsedBundle.PrivateKeyBytes) - resp.Data["private_key_type"] = csrb.PrivateKeyType - } - } - - cb := &certutil.CertBundle{} - cb.PrivateKey = csrb.PrivateKey - cb.PrivateKeyType = csrb.PrivateKeyType - - entry, err := logical.StorageEntryJSON("config/ca_bundle", cb) - if err != nil { - return nil, err - } - err = req.Storage.Put(entry) - if err != nil { - return nil, err - } - - return resp, nil -} - -func (b *backend) pathCASignIntermediate( - req *logical.Request, data *framework.FieldData) (*logical.Response, error) { - var err error - - format := getFormat(data) - if format == "" { - return logical.ErrorResponse( - `The "format" path parameter must be "pem" or "der"`, - ), nil - } - - role := &roleEntry{ - TTL: data.Get("ttl").(string), - AllowLocalhost: true, - AllowAnyName: true, - AllowIPSANs: true, - EnforceHostnames: false, - } - - if cn := data.Get("common_name").(string); len(cn) == 0 { - role.UseCSRCommonName = true - } - - var caErr error - signingBundle, caErr := fetchCAInfo(req) - switch caErr.(type) { - case certutil.UserError: - return nil, certutil.UserError{Err: fmt.Sprintf( - "could not fetch the CA certificate (was one set?): %s", caErr)} - case certutil.InternalError: - return nil, certutil.InternalError{Err: fmt.Sprintf( - "error fetching CA certificate: %s", caErr)} - } - - useCSRValues := data.Get("use_csr_values").(bool) - - maxPathLengthIface, ok := data.GetOk("max_path_length") - if ok { - maxPathLength := maxPathLengthIface.(int) - role.MaxPathLength = &maxPathLength - } - - parsedBundle, err := signCert(b, role, signingBundle, true, useCSRValues, req, data) - if err != nil { - switch err.(type) { - case certutil.UserError: - return logical.ErrorResponse(err.Error()), nil - case certutil.InternalError: - return nil, err - } - } - - cb, err := parsedBundle.ToCertBundle() - if err != nil { - return nil, fmt.Errorf("Error converting raw cert bundle to cert bundle: %s", err) - } - - resp := b.Secret(SecretCertsType).Response( - map[string]interface{}{ - "expiration": int64(parsedBundle.Certificate.NotAfter.Unix()), - "serial_number": cb.SerialNumber, - }, - map[string]interface{}{ - "serial_number": cb.SerialNumber, - }) - - switch format { - case "pem": - resp.Data["certificate"] = cb.Certificate - resp.Data["issuing_ca"] = cb.IssuingCA - case "der": - resp.Data["certificate"] = base64.StdEncoding.EncodeToString(parsedBundle.CertificateBytes) - resp.Data["issuing_ca"] = base64.StdEncoding.EncodeToString(parsedBundle.IssuingCABytes) - } - - resp.Secret.TTL = parsedBundle.Certificate.NotAfter.Sub(time.Now()) - - err = req.Storage.Put(&logical.StorageEntry{ - Key: "certs/" + cb.SerialNumber, - Value: parsedBundle.CertificateBytes, - }) - if err != nil { - return nil, fmt.Errorf("Unable to store certificate locally") - } - - if parsedBundle.Certificate.MaxPathLen == 0 { - resp.AddWarning("Max path length of the signed certificate is zero. This certificate cannot be used to issue intermediate CA certificates.") - } - - return resp, nil -} - -func (b *backend) pathCASetWrite( +func (b *backend) pathCAWrite( req *logical.Request, data *framework.FieldData) (*logical.Response, error) { pemBundle := data.Get("pem_bundle").(string) @@ -413,67 +44,42 @@ func (b *backend) pathCASetWrite( } } - // Handle the case of a self-signed certificate - if parsedBundle.Certificate == nil && parsedBundle.IssuingCA != nil { + if parsedBundle.PrivateKey == nil || + parsedBundle.PrivateKeyType == certutil.UnknownPrivateKey { + return logical.ErrorResponse("private key not found in the PEM bundle"), nil + } + + // Handle the case of a self-signed certificate; the parsing function will + // see the CA and put it into the issuer + if parsedBundle.Certificate == nil && + parsedBundle.IssuingCA != nil { + equal, err := certutil.ComparePublicKeys(parsedBundle.IssuingCA.PublicKey, parsedBundle.PrivateKey.Public()) + if err != nil { + return logical.ErrorResponse(fmt.Sprintf( + "got only a CA and private key but could not verify the public keys match: %v", err)), nil + } + if !equal { + return logical.ErrorResponse( + "got only a CA and private key but keys do not match"), nil + } parsedBundle.Certificate = parsedBundle.IssuingCA parsedBundle.CertificateBytes = parsedBundle.IssuingCABytes } - cb := &certutil.CertBundle{} - entry, err := req.Storage.Get("config/ca_bundle") - if err != nil { - return nil, err - } - if entry != nil { - err = entry.DecodeJSON(cb) - if err != nil { - return nil, err - } - // If we have a stored private key and did not get one, attempt to - // correlate the two -- this could be due to a CSR being signed - // for a generated CA cert and the resulting cert now being uploaded - if len(cb.PrivateKey) != 0 && - cb.PrivateKeyType != "" && - parsedBundle.PrivateKeyType == certutil.UnknownPrivateKey && - (parsedBundle.PrivateKeyBytes == nil || len(parsedBundle.PrivateKeyBytes) == 0) { - parsedCB, err := cb.ToParsedCertBundle() - if err != nil { - return nil, err - } - if parsedCB.PrivateKey == nil { - return nil, fmt.Errorf("Encountered nil private key from saved key") - } - // If true, the stored private key corresponds to the cert's - // public key, so fill it in - equal, err := certutil.ComparePublicKeys(parsedCB.PrivateKey.Public(), parsedBundle.Certificate.PublicKey) - if err != nil { - return logical.ErrorResponse( - "stored public key does not match the public key on the certificate"), nil - } - if equal { - parsedBundle.PrivateKey = parsedCB.PrivateKey - parsedBundle.PrivateKeyType = parsedCB.PrivateKeyType - parsedBundle.PrivateKeyBytes = parsedCB.PrivateKeyBytes - } - } - } - - if parsedBundle.PrivateKey == nil || - parsedBundle.PrivateKeyBytes == nil || - len(parsedBundle.PrivateKeyBytes) == 0 { - return logical.ErrorResponse("No private key given and no matching key stored"), nil + if parsedBundle.Certificate == nil { + return logical.ErrorResponse("no certificate found in the PEM bundle"), nil } if !parsedBundle.Certificate.IsCA { - return logical.ErrorResponse("The given certificate is not marked for CA use and cannot be used with this backend"), nil + return logical.ErrorResponse("the given certificate is not marked for CA use and cannot be used with this backend"), nil } - cb, err = parsedBundle.ToCertBundle() + cb, err := parsedBundle.ToCertBundle() if err != nil { - return nil, fmt.Errorf("Error converting raw values into cert bundle: %s", err) + return nil, fmt.Errorf("error converting raw values into cert bundle: %s", err) } - entry, err = logical.StorageEntryJSON("config/ca_bundle", cb) + entry, err := logical.StorageEntryJSON("config/ca_bundle", cb) if err != nil { return nil, err } @@ -496,11 +102,11 @@ func (b *backend) pathCASetWrite( return nil, err } -const pathConfigCASetHelpSyn = ` +const pathConfigCAHelpSyn = ` Set the CA certificate and private key used for generated credentials. ` -const pathConfigCASetHelpDesc = ` +const pathConfigCAHelpDesc = ` This sets the CA information used for credentials generated by this by this mount. This must be a PEM-format, concatenated unencrypted secret key and certificate. diff --git a/builtin/logical/pki/path_intermediate.go b/builtin/logical/pki/path_intermediate.go new file mode 100644 index 0000000000..c3dee371a8 --- /dev/null +++ b/builtin/logical/pki/path_intermediate.go @@ -0,0 +1,231 @@ +package pki + +import ( + "encoding/base64" + "fmt" + + "github.com/hashicorp/vault/helper/certutil" + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" +) + +func pathGenerateIntermediate(b *backend) *framework.Path { + ret := &framework.Path{ + Pattern: "intermediate/generate/" + framework.GenericNameRegex("exported"), + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.WriteOperation: b.pathGenerateIntermediate, + }, + + HelpSynopsis: pathGenerateIntermediateHelpSyn, + HelpDescription: pathGenerateIntermediateHelpDesc, + } + + ret.Fields = addCACommonFields(map[string]*framework.FieldSchema{}) + ret.Fields = addCAKeyGenerationFields(ret.Fields) + + return ret +} + +func pathSetSignedIntermediate(b *backend) *framework.Path { + ret := &framework.Path{ + Pattern: "intermediate/set-signed", + + Fields: map[string]*framework.FieldSchema{ + "certificate": &framework.FieldSchema{ + Type: framework.TypeString, + Description: `PEM-format certificate. This must be a CA +certificate with a public key matching the +previously-generated key from the generation +endpoint.`, + }, + }, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.WriteOperation: b.pathSetSignedIntermediate, + }, + + HelpSynopsis: pathSetSignedIntermediateHelpSyn, + HelpDescription: pathSetSignedIntermediateHelpDesc, + } + + return ret +} + +func (b *backend) pathGenerateIntermediate( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + var err error + + exported, format, role, errorResp := b.getGenerationParams(data) + if errorResp != nil { + return errorResp, nil + } + + var resp *logical.Response + parsedBundle, err := generateIntermediateCSR(b, role, nil, req, data) + if err != nil { + switch err.(type) { + case certutil.UserError: + return logical.ErrorResponse(err.Error()), nil + case certutil.InternalError: + return nil, err + } + } + + csrb, err := parsedBundle.ToCSRBundle() + if err != nil { + return nil, fmt.Errorf("Error converting raw CSR bundle to CSR bundle: %s", err) + } + + resp = &logical.Response{ + Data: map[string]interface{}{}, + } + + switch format { + case "pem": + resp.Data["csr"] = csrb.CSR + if exported { + resp.Data["private_key"] = csrb.PrivateKey + resp.Data["private_key_type"] = csrb.PrivateKeyType + } + case "der": + resp.Data["csr"] = base64.StdEncoding.EncodeToString(parsedBundle.CSRBytes) + if exported { + resp.Data["private_key"] = base64.StdEncoding.EncodeToString(parsedBundle.PrivateKeyBytes) + resp.Data["private_key_type"] = csrb.PrivateKeyType + } + } + + cb := &certutil.CertBundle{} + cb.PrivateKey = csrb.PrivateKey + cb.PrivateKeyType = csrb.PrivateKeyType + + entry, err := logical.StorageEntryJSON("config/ca_bundle", cb) + if err != nil { + return nil, err + } + err = req.Storage.Put(entry) + if err != nil { + return nil, err + } + + return resp, nil +} + +func (b *backend) pathSetSignedIntermediate( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + cert := data.Get("certificate").(string) + + if cert == "" { + return logical.ErrorResponse("no certificate provided in the \"certficate\" parameter"), nil + } + + inputBundle, err := certutil.ParsePEMBundle(cert) + if err != nil { + switch err.(type) { + case certutil.InternalError: + return nil, err + default: + return logical.ErrorResponse(err.Error()), nil + } + } + + // If only one certificate is provided and it's a CA + // the parsing will assign it to the IssuingCA, so move it over + if inputBundle.Certificate == nil && inputBundle.IssuingCA != nil { + inputBundle.Certificate = inputBundle.IssuingCA + inputBundle.IssuingCA = nil + inputBundle.CertificateBytes = inputBundle.IssuingCABytes + inputBundle.IssuingCABytes = nil + } + + if inputBundle.Certificate == nil { + return logical.ErrorResponse("supplied certificate could not be successfully parsed"), nil + } + + cb := &certutil.CertBundle{} + entry, err := req.Storage.Get("config/ca_bundle") + if err != nil { + return nil, err + } + if entry == nil { + return logical.ErrorResponse("could not find any existing entry with a private key"), nil + } + + err = entry.DecodeJSON(cb) + if err != nil { + return nil, err + } + + if len(cb.PrivateKey) == 0 || cb.PrivateKeyType == "" { + return logical.ErrorResponse("could not find an existing privat key"), nil + } + + parsedCB, err := cb.ToParsedCertBundle() + if err != nil { + return nil, err + } + if parsedCB.PrivateKey == nil { + return nil, fmt.Errorf("saved key could not be parsed successfully") + } + + equal, err := certutil.ComparePublicKeys(parsedCB.PrivateKey.Public(), inputBundle.Certificate.PublicKey) + if err != nil { + return logical.ErrorResponse(fmt.Sprintf( + "error matching public keys: %v", err)), nil + } + if !equal { + return logical.ErrorResponse("key in certificate does not match stored key"), nil + } + + inputBundle.PrivateKey = parsedCB.PrivateKey + inputBundle.PrivateKeyType = parsedCB.PrivateKeyType + inputBundle.PrivateKeyBytes = parsedCB.PrivateKeyBytes + + if !inputBundle.Certificate.IsCA { + return logical.ErrorResponse("the given certificate is not marked for CA use and cannot be used with this backend"), nil + } + + cb, err = inputBundle.ToCertBundle() + if err != nil { + return nil, fmt.Errorf("error converting raw values into cert bundle: %s", err) + } + + entry, err = logical.StorageEntryJSON("config/ca_bundle", cb) + if err != nil { + return nil, err + } + err = req.Storage.Put(entry) + if err != nil { + return nil, err + } + + // For ease of later use, also store just the certificate at a known + // location, plus a fresh CRL + entry.Key = "ca" + entry.Value = inputBundle.CertificateBytes + err = req.Storage.Put(entry) + if err != nil { + return nil, err + } + + err = buildCRL(b, req) + + return nil, err +} + +const pathGenerateIntermediateHelpSyn = ` +Generate a new CSR and private key used for signing. +` + +const pathGenerateIntermediateHelpDesc = ` +See the API documentation for more information. +` + +const pathSetSignedIntermediateHelpSyn = ` +Provide the signed intermediate CA cert. +` + +const pathSetSignedIntermediateHelpDesc = ` +See the API documentation for more information. +` diff --git a/builtin/logical/pki/path_issue_sign.go b/builtin/logical/pki/path_issue_sign.go index a4d9e16d3b..5d17808999 100644 --- a/builtin/logical/pki/path_issue_sign.go +++ b/builtin/logical/pki/path_issue_sign.go @@ -51,18 +51,33 @@ func pathSign(b *backend) *framework.Path { return ret } +func pathSignVerbatim(b *backend) *framework.Path { + ret := &framework.Path{ + Pattern: "sign-verbatim/" + framework.GenericNameRegex("role"), + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.WriteOperation: b.pathSignVerbatim, + }, + + HelpSynopsis: pathSignHelpSyn, + HelpDescription: pathSignHelpDesc, + } + + ret.Fields = addNonCACommonFields(map[string]*framework.FieldSchema{}) + + ret.Fields["csr"] = &framework.FieldSchema{ + Type: framework.TypeString, + Default: "", + Description: `PEM-format CSR to be signed. Values will be +taken verbatim from the CSR, except for +basic constraints.`, + } + + return ret +} + func (b *backend) pathIssue( req *logical.Request, data *framework.FieldData) (*logical.Response, error) { - return b.pathIssueSignCert(req, data, false) -} - -func (b *backend) pathSign( - req *logical.Request, data *framework.FieldData) (*logical.Response, error) { - return b.pathIssueSignCert(req, data, true) -} - -func (b *backend) pathIssueSignCert( - req *logical.Request, data *framework.FieldData, useCSR bool) (*logical.Response, error) { roleName := data.Get("role").(string) // Get the role @@ -74,6 +89,42 @@ func (b *backend) pathIssueSignCert( return logical.ErrorResponse(fmt.Sprintf("Unknown role: %s", roleName)), nil } + return b.pathIssueSignCert(req, data, role, false, false) +} + +func (b *backend) pathSign( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + roleName := data.Get("role").(string) + + // Get the role + role, err := b.getRole(req.Storage, roleName) + if err != nil { + return nil, err + } + if role == nil { + return logical.ErrorResponse(fmt.Sprintf("Unknown role: %s", roleName)), nil + } + + return b.pathIssueSignCert(req, data, role, true, false) +} + +func (b *backend) pathSignVerbatim( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + + ttl := b.System().DefaultLeaseTTL() + role := &roleEntry{ + TTL: ttl.String(), + AllowLocalhost: true, + AllowAnyName: true, + AllowIPSANs: true, + EnforceHostnames: false, + } + + return b.pathIssueSignCert(req, data, role, true, true) +} + +func (b *backend) pathIssueSignCert( + req *logical.Request, data *framework.FieldData, role *roleEntry, useCSR, useCSRValues bool) (*logical.Response, error) { format := getFormat(data) if format == "" { return logical.ErrorResponse( @@ -92,8 +143,9 @@ func (b *backend) pathIssueSignCert( } var parsedBundle *certutil.ParsedCertBundle + var err error if useCSR { - parsedBundle, err = signCert(b, role, signingBundle, false, false, req, data) + parsedBundle, err = signCert(b, role, signingBundle, false, useCSRValues, req, data) } else { parsedBundle, err = generateCert(b, role, signingBundle, false, req, data) } diff --git a/builtin/logical/pki/path_root.go b/builtin/logical/pki/path_root.go new file mode 100644 index 0000000000..0102413dad --- /dev/null +++ b/builtin/logical/pki/path_root.go @@ -0,0 +1,261 @@ +package pki + +import ( + "encoding/base64" + "fmt" + "time" + + "github.com/hashicorp/vault/helper/certutil" + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" +) + +func pathGenerateRoot(b *backend) *framework.Path { + ret := &framework.Path{ + Pattern: "root/generate/" + framework.GenericNameRegex("exported"), + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.WriteOperation: b.pathCAGenerateRoot, + }, + + HelpSynopsis: pathGenerateRootHelpSyn, + HelpDescription: pathGenerateRootHelpDesc, + } + + ret.Fields = addCACommonFields(map[string]*framework.FieldSchema{}) + ret.Fields = addCAKeyGenerationFields(ret.Fields) + ret.Fields = addCAIssueFields(ret.Fields) + + return ret +} + +func pathSignIntermediate(b *backend) *framework.Path { + ret := &framework.Path{ + Pattern: "root/sign-intermediate", + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.WriteOperation: b.pathCASignIntermediate, + }, + + HelpSynopsis: pathSignIntermediateHelpSyn, + HelpDescription: pathSignIntermediateHelpDesc, + } + + ret.Fields = addCACommonFields(map[string]*framework.FieldSchema{}) + ret.Fields = addCAIssueFields(ret.Fields) + + ret.Fields["csr"] = &framework.FieldSchema{ + Type: framework.TypeString, + Default: "", + Description: `PEM-format CSR to be signed.`, + } + + ret.Fields["use_csr_values"] = &framework.FieldSchema{ + Type: framework.TypeBool, + Default: false, + Description: `If true, then: +1) Subject information, including names and alternate +names, will be preserved from the CSR rather than +using values provided in the other parameters to +this path; +2) Any key usages requested in the CSR will be +added to the basic set of key usages used for CA +certs signed by this path; for instance, +the non-repudiation flag.`, + } + + return ret +} + +func (b *backend) pathCAGenerateRoot( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + var err error + + exported, format, role, errorResp := b.getGenerationParams(data) + if errorResp != nil { + return errorResp, nil + } + + maxPathLengthIface, ok := data.GetOk("max_path_length") + if ok { + maxPathLength := maxPathLengthIface.(int) + role.MaxPathLength = &maxPathLength + } + + var resp *logical.Response + parsedBundle, err := generateCert(b, role, nil, true, req, data) + if err != nil { + switch err.(type) { + case certutil.UserError: + return logical.ErrorResponse(err.Error()), nil + case certutil.InternalError: + return nil, err + } + } + + cb, err := parsedBundle.ToCertBundle() + if err != nil { + return nil, fmt.Errorf("error converting raw cert bundle to cert bundle: %s", err) + } + + resp = &logical.Response{ + Data: map[string]interface{}{ + "serial_number": cb.SerialNumber, + "expiration": int64(parsedBundle.Certificate.NotAfter.Unix()), + }, + } + + switch format { + case "pem": + resp.Data["certificate"] = cb.Certificate + resp.Data["issuing_ca"] = cb.IssuingCA + if exported { + resp.Data["private_key"] = cb.PrivateKey + resp.Data["private_key_type"] = cb.PrivateKeyType + } + case "der": + resp.Data["certificate"] = base64.StdEncoding.EncodeToString(parsedBundle.CertificateBytes) + resp.Data["issuing_ca"] = base64.StdEncoding.EncodeToString(parsedBundle.IssuingCABytes) + if exported { + resp.Data["private_key"] = base64.StdEncoding.EncodeToString(parsedBundle.PrivateKeyBytes) + resp.Data["private_key_type"] = cb.PrivateKeyType + } + } + + entry, err := logical.StorageEntryJSON("config/ca_bundle", cb) + if err != nil { + return nil, err + } + err = req.Storage.Put(entry) + if err != nil { + return nil, err + } + + // For ease of later use, also store just the certificate at a known + // location, plus a fresh CRL + entry.Key = "ca" + entry.Value = parsedBundle.CertificateBytes + err = req.Storage.Put(entry) + if err != nil { + return nil, err + } + + err = buildCRL(b, req) + if err != nil { + return nil, err + } + + if parsedBundle.Certificate.MaxPathLen == 0 { + resp.AddWarning("Max path length of the generated certificate is zero. This certificate cannot be used to issue intermediate CA certificates.") + } + + return resp, nil +} + +func (b *backend) pathCASignIntermediate( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + var err error + + format := getFormat(data) + if format == "" { + return logical.ErrorResponse( + `The "format" path parameter must be "pem" or "der"`, + ), nil + } + + role := &roleEntry{ + TTL: data.Get("ttl").(string), + AllowLocalhost: true, + AllowAnyName: true, + AllowIPSANs: true, + EnforceHostnames: false, + } + + if cn := data.Get("common_name").(string); len(cn) == 0 { + role.UseCSRCommonName = true + } + + var caErr error + signingBundle, caErr := fetchCAInfo(req) + switch caErr.(type) { + case certutil.UserError: + return nil, certutil.UserError{Err: fmt.Sprintf( + "could not fetch the CA certificate (was one set?): %s", caErr)} + case certutil.InternalError: + return nil, certutil.InternalError{Err: fmt.Sprintf( + "error fetching CA certificate: %s", caErr)} + } + + useCSRValues := data.Get("use_csr_values").(bool) + + maxPathLengthIface, ok := data.GetOk("max_path_length") + if ok { + maxPathLength := maxPathLengthIface.(int) + role.MaxPathLength = &maxPathLength + } + + parsedBundle, err := signCert(b, role, signingBundle, true, useCSRValues, req, data) + if err != nil { + switch err.(type) { + case certutil.UserError: + return logical.ErrorResponse(err.Error()), nil + case certutil.InternalError: + return nil, err + } + } + + cb, err := parsedBundle.ToCertBundle() + if err != nil { + return nil, fmt.Errorf("Error converting raw cert bundle to cert bundle: %s", err) + } + + resp := b.Secret(SecretCertsType).Response( + map[string]interface{}{ + "expiration": int64(parsedBundle.Certificate.NotAfter.Unix()), + "serial_number": cb.SerialNumber, + }, + map[string]interface{}{ + "serial_number": cb.SerialNumber, + }) + + switch format { + case "pem": + resp.Data["certificate"] = cb.Certificate + resp.Data["issuing_ca"] = cb.IssuingCA + case "der": + resp.Data["certificate"] = base64.StdEncoding.EncodeToString(parsedBundle.CertificateBytes) + resp.Data["issuing_ca"] = base64.StdEncoding.EncodeToString(parsedBundle.IssuingCABytes) + } + + resp.Secret.TTL = parsedBundle.Certificate.NotAfter.Sub(time.Now()) + + err = req.Storage.Put(&logical.StorageEntry{ + Key: "certs/" + cb.SerialNumber, + Value: parsedBundle.CertificateBytes, + }) + if err != nil { + return nil, fmt.Errorf("Unable to store certificate locally") + } + + if parsedBundle.Certificate.MaxPathLen == 0 { + resp.AddWarning("Max path length of the signed certificate is zero. This certificate cannot be used to issue intermediate CA certificates.") + } + + return resp, nil +} + +const pathGenerateRootHelpSyn = ` +Generate a new CA certificate and private key used for signing. +` + +const pathGenerateRootHelpDesc = ` +See the API documentation for more information. +` + +const pathSignIntermediateHelpSyn = ` +Issue an intermediate CA certificate based on the provided CSR. +` + +const pathSignIntermediateHelpDesc = ` +See the API documentation for more information. +` diff --git a/website/source/docs/secrets/pki/index.html.md b/website/source/docs/secrets/pki/index.html.md index 2c1ed219e2..7c1cfc4507 100644 --- a/website/source/docs/secrets/pki/index.html.md +++ b/website/source/docs/secrets/pki/index.html.md @@ -286,17 +286,19 @@ subpath for interactive help output. -### /pki/config/ca/set +### /pki/config/ca #### POST
Description
- Allows submitting the CA information via a PEM file containing the CA - certificate and its private key, concatenated. If you generated an - intermediate CA CSR and received a signed certificate, you do not need to - include the private key in the PEM file.

The information can - be provided from a file via a `curl` command similar to the following:
+ Allows submitting the CA information for the backend via a PEM file + containing the CA certificate and its private key, concatenated. Not needed + if you are generating a self-signed root certificate, and not used if you + have a signed intermediate CA certificate with a generated key (use the + `/pki/intermediate/set-signed` endpoint for that).

The + information can be provided from a file via a `curl` command similar to the + following:
```text $ curl \ @@ -320,7 +322,7 @@ subpath for interactive help output.
POST
URL
-
`/pki/config/ca/set`
+
`/pki/config/ca`
Parameters
@@ -339,66 +341,26 @@ subpath for interactive help output.
-### /pki/config/ca/generate/intermediate -#### POST + +### /pki/config/crl +#### GET
Description
- Generates a new private key and a CSR for signing. If using Vault as a - root, and for many other CAs, the various parameters on the final - certificate are set at signing time and may or may not honor the parameters - set here. If the path ends with `exported`, the private key will be - returned in the response; if it is `internal` the private key will not be - returned and *cannot be retrieved later*.

This is mostly meant - as a helper function, and not all possible parameters that can be set in a - CSR are supported. + Allows getting the duration for which the generated CRL should be marked + valid.
Method
-
POST
+
GET
URL
-
`/pki/config/ca/generate/intermediate/[exported|internal]`
+
`/pki/config/crl`
Parameters
-
    -
  • - common_name - required - The requested CN for the certificate. -
  • -
  • - alt_names - optional - Requested Subject Alternative Names, in a comma-delimited list. These - can be host names or email addresses; they will be parsed into their - respective fields. -
  • -
  • - ip_sans - optional - Requested IP Subject Alternative Names, in a comma-delimited list. -
  • -
  • - format - optional - Format for returned data. Can be `pem` or `der`; defaults to `pem`. If - `der`, the output is base64 encoded. -
  • -
  • - key_type - optional - Desired key type; must be `rsa` or `ec`. Defaults to `rsa`. -
  • -
  • - key_bits - optional - The number of bits to use. Defaults to `2048`. Must be changed to a - valid value if the `key_type` is `ec`. -
  • -
+ None
Returns
@@ -408,9 +370,9 @@ subpath for interactive help output. { "lease_id": "", "renewable": false, - "lease_duration": 21600, + "lease_duration": 0, "data": { - "csr": "-----BEGIN CERTIFICATE REQUEST-----\nMIIDzDCCAragAwIBAgIUOd0ukLcjH43TfTHFG9qE0FtlMVgwCwYJKoZIhvcNAQEL\n...\numkqeYeO30g1uYvDuWLXVA==\n-----END CERTIFICATE REQUEST-----\n", + "expiry": "72h" }, "auth": null } @@ -419,192 +381,36 @@ subpath for interactive help output.
- -### /pki/config/ca/generate/root #### POST
Description
- Generates a new self-signed CA certificate and private key. If the path - ends with `exported`, the private key will be returned in the response; if - it is `internal` the private key will not be returned and *cannot be - retrieved later*. Distribution points use the values set via `config/urls`. + Allows setting the duration for which the generated CRL should be marked + valid.
Method
POST
URL
-
`/pki/config/ca/generate/root/[exported|internal]`
- -
Parameters
-
-
    -
  • - common_name - required - The requested CN for the certificate. -
  • -
  • - alt_names - optional - Requested Subject Alternative Names, in a comma-delimited list. These - can be host names or email addresses; they will be parsed into their - respective fields. -
  • -
  • - ip_sans - optional - Requested IP Subject Alternative Names, in a comma-delimited list. -
  • -
  • - ttl - optional - 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). -
  • -
  • - format - optional - Format for returned data. Can be `pem` or `der`; defaults to `pem`. If - `der`, the output is base64 encoded. -
  • -
  • - key_type - optional - Desired key type; must be `rsa` or `ec`. Defaults to `rsa`. -
  • -
  • - key_bits - optional - The number of bits to use. Defaults to `2048`. Must be changed to a - valid value if the `key_type` is `ec`. -
  • -
  • - max_path_length - optional - If set, the maximum path length to encode in the generated certificate. - Defaults to `-1`, which means no limit. A limit of `0` means a literal - path length of zero. -
  • -
-
- -
Returns
-
- - ```javascript - { - "lease_id": "", - "renewable": false, - "lease_duration": 21600, - "data": { - "certificate": "-----BEGIN CERTIFICATE-----\nMIIDzDCCAragAwIBAgIUOd0ukLcjH43TfTHFG9qE0FtlMVgwCwYJKoZIhvcNAQEL\n...\numkqeYeO30g1uYvDuWLXVA==\n-----END CERTIFICATE-----\n", - "issuing_ca": "-----BEGIN CERTIFICATE-----\nMIIDzDCCAragAwIBAgIUOd0ukLcjH43TfTHFG9qE0FtlMVgwCwYJKoZIhvcNAQEL\n...\numkqeYeO30g1uYvDuWLXVA==\n-----END CERTIFICATE-----\n", - "serial": "39:dd:2e:90:b7:23:1f:8d:d3:7d:31:c5:1b:da:84:d0:5b:65:31:58" - }, - "auth": null - } - ``` - -
-
- -### /pki/config/ca/sign -#### POST - -
-
Description
-
- Uses the configured CA certificate to issue a certificate with appropriate - values for acting as an intermediate CA. Distribution points use the values - set via `config/urls`. Values set in the CSR are ignored unless - `use_csr_values` is set to true, in which case the values from the CSR are - used verbatim. -
- -
Method
-
POST
- -
URL
-
`/pki/config/ca/sign`
+
`/pki/config/crl`
Parameters
  • - csr + expiry required - The PEM-encoded CSR. -
  • - common_name - required - The requested CN for the certificate. - -
  • - alt_names - optional - Requested Subject Alternative Names, in a comma-delimited list. These - can be host names or email addresses; they will be parsed into their - respective fields. -
  • -
  • - ip_sans - optional - Requested IP Subject Alternative Names, in a comma-delimited list. -
  • -
  • - ttl - optional - 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). -
  • -
  • - format - optional - Format for returned data. Can be `pem` or `der`; defaults to `pem`. If - `der`, the output is base64 encoded. -
  • -
  • - max_path_length - optional - If set, the maximum path length to encode in the generated certificate. - Defaults to `-1`, which means no limit. A limit of `0` means a literal - path length of zero. -
  • -
  • - use_csr_values - optional - If set to `true`, then: 1) Subject information, including names and - alternate names, will be preserved from the CSR rather than using the - values provided in the other parameters to this path; 2) Any key usages - (for instance, non-repudiation) requested in the CSR will be added to - the basic set of key usages used for CA certs signed by this path. + The time until expiration. Defaults to `72h`.
Returns
- - ```javascript - { - "lease_id": "pki/config/ca/sign/bc23e3c6-8dcd-48c6-f3af-dd2db7f815c2", - "renewable": false, - "lease_duration": 21600, - "data": { - "certificate": "-----BEGIN CERTIFICATE-----\nMIIDzDCCAragAwIBAgIUOd0ukLcjH43TfTHFG9qE0FtlMVgwCwYJKoZIhvcNAQEL\n...\numkqeYeO30g1uYvDuWLXVA==\n-----END CERTIFICATE-----\n", - "issuing_ca": "-----BEGIN CERTIFICATE-----\nMIIDUTCCAjmgAwIBAgIJAKM+z4MSfw2mMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNV\n...\nG/7g4koczXLoUM3OQXd5Aq2cs4SS1vODrYmgbioFsQ3eDHd1fg==\n-----END CERTIFICATE-----\n", - "serial": "39:dd:2e:90:b7:23:1f:8d:d3:7d:31:c5:1b:da:84:d0:5b:65:31:58" - }, - "auth": null - } - ``` - + A `204` response code.
@@ -693,78 +499,6 @@ subpath for interactive help output. -### /pki/config/crl -#### GET - -
-
Description
-
- Allows getting the duration for which the generated CRL should be marked - valid. -
- -
Method
-
GET
- -
URL
-
`/pki/config/crl`
- -
Parameters
-
- None -
- -
Returns
-
- - ```javascript - { - "lease_id": "", - "renewable": false, - "lease_duration": 0, - "data": { - "expiry": "72h" - }, - "auth": null - } - ``` - -
-
- -#### POST - -
-
Description
-
- Allows setting the duration for which the generated CRL should be marked - valid. -
- -
Method
-
POST
- -
URL
-
`/pki/config/crl`
- -
Parameters
-
-
    -
  • -
  • - expiry - required - The time until expiration. Defaults to `72h`. -
  • -
-
- -
Returns
-
- A `204` response code. -
-
- ### /pki/crl(/pem) #### GET @@ -837,6 +571,118 @@ subpath for interactive help output. +### /pki/intermediate/generate +#### POST + +
+
Description
+
+ Generates a new private key and a CSR for signing. If using Vault as a + root, and for many other CAs, the various parameters on the final + certificate are set at signing time and may or may not honor the parameters + set here. If the path ends with `exported`, the private key will be + returned in the response; if it is `internal` the private key will not be + returned and *cannot be retrieved later*.

This is mostly meant + as a helper function, and not all possible parameters that can be set in a + CSR are supported. +
+ +
Method
+
POST
+ +
URL
+
`/pki/intermediate/generate/[exported|internal]`
+ +
Parameters
+
+
    +
  • + common_name + required + The requested CN for the certificate. +
  • +
  • + alt_names + optional + Requested Subject Alternative Names, in a comma-delimited list. These + can be host names or email addresses; they will be parsed into their + respective fields. +
  • +
  • + ip_sans + optional + Requested IP Subject Alternative Names, in a comma-delimited list. +
  • +
  • + format + optional + Format for returned data. Can be `pem` or `der`; defaults to `pem`. If + `der`, the output is base64 encoded. +
  • +
  • + key_type + optional + Desired key type; must be `rsa` or `ec`. Defaults to `rsa`. +
  • +
  • + key_bits + optional + The number of bits to use. Defaults to `2048`. Must be changed to a + valid value if the `key_type` is `ec`. +
  • +
+
+ +
Returns
+
+ + ```javascript + { + "lease_id": "", + "renewable": false, + "lease_duration": 21600, + "data": { + "csr": "-----BEGIN CERTIFICATE REQUEST-----\nMIIDzDCCAragAwIBAgIUOd0ukLcjH43TfTHFG9qE0FtlMVgwCwYJKoZIhvcNAQEL\n...\numkqeYeO30g1uYvDuWLXVA==\n-----END CERTIFICATE REQUEST-----\n", + }, + "auth": null + } + ``` + +
+
+ +### /pki/intermediate/set-signed +#### POST + +
+
Description
+
+ Allows submitting the signed CA certificate corresponding to a private key generated via `/pki/intermediate/generate`. The certificate should be submitted in PEM format; see the documentation for `/pki/config/ca` for some hints on submitting. +
+ +
Method
+
POST
+ +
URL
+
`/pki/intermediate/set-signed`
+ +
Parameters
+
+
    +
  • + certificate + required + The certificate in PEM format. +
  • +
+
+ +
Returns
+
+ A `204` response code. +
+
+ ### /pki/issue/ #### POST @@ -854,7 +700,7 @@ subpath for interactive help output.
POST
URL
-
`/pki/issue/`
+
`/pki/issue/`
Parameters
@@ -977,7 +823,7 @@ subpath for interactive help output.
POST
URL
-
`/pki/roles/`
+
`/pki/roles/`
Parameters
@@ -1117,7 +963,7 @@ subpath for interactive help output.
GET
URL
-
`/pki/roles/`
+
`/pki/roles/`
Parameters
@@ -1164,7 +1010,7 @@ subpath for interactive help output.
DELETE
URL
-
`/pki/roles/`
+
`/pki/roles/`
Parameters
@@ -1177,6 +1023,200 @@ subpath for interactive help output.
+### /pki/root/generate +#### POST + +
+
Description
+
+ Generates a new self-signed CA certificate and private key. If the path + ends with `exported`, the private key will be returned in the response; if + it is `internal` the private key will not be returned and *cannot be + retrieved later*. Distribution points use the values set via `config/urls`. +
+ +
Method
+
POST
+ +
URL
+
`/pki/root/generate/[exported|internal]`
+ +
Parameters
+
+
    +
  • + common_name + required + The requested CN for the certificate. +
  • +
  • + alt_names + optional + Requested Subject Alternative Names, in a comma-delimited list. These + can be host names or email addresses; they will be parsed into their + respective fields. +
  • +
  • + ip_sans + optional + Requested IP Subject Alternative Names, in a comma-delimited list. +
  • +
  • + ttl + optional + 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). +
  • +
  • + format + optional + Format for returned data. Can be `pem` or `der`; defaults to `pem`. If + `der`, the output is base64 encoded. +
  • +
  • + key_type + optional + Desired key type; must be `rsa` or `ec`. Defaults to `rsa`. +
  • +
  • + key_bits + optional + The number of bits to use. Defaults to `2048`. Must be changed to a + valid value if the `key_type` is `ec`. +
  • +
  • + max_path_length + optional + If set, the maximum path length to encode in the generated certificate. + Defaults to `-1`, which means no limit. unless the signing certificate + has a maximum path length set, in which case the path length is set to + one less than that of the signing certificate. A limit of `0` means a + literal path length of zero. +
  • +
+
+ +
Returns
+
+ + ```javascript + { + "lease_id": "", + "renewable": false, + "lease_duration": 21600, + "data": { + "certificate": "-----BEGIN CERTIFICATE-----\nMIIDzDCCAragAwIBAgIUOd0ukLcjH43TfTHFG9qE0FtlMVgwCwYJKoZIhvcNAQEL\n...\numkqeYeO30g1uYvDuWLXVA==\n-----END CERTIFICATE-----\n", + "issuing_ca": "-----BEGIN CERTIFICATE-----\nMIIDzDCCAragAwIBAgIUOd0ukLcjH43TfTHFG9qE0FtlMVgwCwYJKoZIhvcNAQEL\n...\numkqeYeO30g1uYvDuWLXVA==\n-----END CERTIFICATE-----\n", + "serial": "39:dd:2e:90:b7:23:1f:8d:d3:7d:31:c5:1b:da:84:d0:5b:65:31:58" + }, + "auth": null + } + ``` + +
+
+ +### /pki/root/sign-intermediate +#### POST + +
+
Description
+
+ Uses the configured CA certificate to issue a certificate with appropriate + values for acting as an intermediate CA. Distribution points use the values + set via `config/urls`. Values set in the CSR are ignored unless + `use_csr_values` is set to true, in which case the values from the CSR are + used verbatim. +
+ +
Method
+
POST
+ +
URL
+
`/pki/root/sign-intermediate`
+ +
Parameters
+
+
    +
  • +
  • + csr + required + The PEM-encoded CSR. +
  • + common_name + required + The requested CN for the certificate. + +
  • + alt_names + optional + Requested Subject Alternative Names, in a comma-delimited list. These + can be host names or email addresses; they will be parsed into their + respective fields. +
  • +
  • + ip_sans + optional + Requested IP Subject Alternative Names, in a comma-delimited list. +
  • +
  • + ttl + optional + 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). +
  • +
  • + format + optional + Format for returned data. Can be `pem` or `der`; defaults to `pem`. If + `der`, the output is base64 encoded. +
  • +
  • + max_path_length + optional + If set, the maximum path length to encode in the generated certificate. + Defaults to `-1`, which means no limit. unless the signing certificate + has a maximum path length set, in which case the path length is set to + one less than that of the signing certificate. A limit of `0` means a + literal path length of zero. +
  • +
  • + use_csr_values + optional + If set to `true`, then: 1) Subject information, including names and + alternate names, will be preserved from the CSR rather than using the + values provided in the other parameters to this path; 2) Any key usages + (for instance, non-repudiation) requested in the CSR will be added to + the basic set of key usages used for CA certs signed by this path; 3) + Extensions requested in the CSR will be copied into the issued + certificate. +
  • +
+
+ +
Returns
+
+ + ```javascript + { + "lease_id": "pki/root/sign-intermediate/bc23e3c6-8dcd-48c6-f3af-dd2db7f815c2", + "renewable": false, + "lease_duration": 21600, + "data": { + "certificate": "-----BEGIN CERTIFICATE-----\nMIIDzDCCAragAwIBAgIUOd0ukLcjH43TfTHFG9qE0FtlMVgwCwYJKoZIhvcNAQEL\n...\numkqeYeO30g1uYvDuWLXVA==\n-----END CERTIFICATE-----\n", + "issuing_ca": "-----BEGIN CERTIFICATE-----\nMIIDUTCCAjmgAwIBAgIJAKM+z4MSfw2mMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNV\n...\nG/7g4koczXLoUM3OQXd5Aq2cs4SS1vODrYmgbioFsQ3eDHd1fg==\n-----END CERTIFICATE-----\n", + "serial": "39:dd:2e:90:b7:23:1f:8d:d3:7d:31:c5:1b:da:84:d0:5b:65:31:58" + }, + "auth": null + } + ``` + +
+
+ ### /pki/sign/ #### POST @@ -1193,7 +1233,7 @@ subpath for interactive help output.
POST
URL
-
`/pki/sign/`
+
`/pki/sign/`
Parameters
@@ -1230,6 +1270,7 @@ subpath for interactive help output. value. If not provided, the role's `ttl` value will be used. Note that the role values default to system values if not explicitly set. +
  • format optional Format for returned data. Can be `pem` or `der`; defaults to `pem`. If @@ -1257,3 +1298,67 @@ subpath for interactive help output.
  • + +### /pki/sign-verbatim +#### POST + +
    +
    Description
    +
    + Signs a new certificate based upon the provided CSR. Values are taken + verbatim from the CSR; the _only_ restriction is that this endpoint will + refuse to issue an intermediate CA certificate (see the + `/pki/root/sign-intermediate` endpoint for that functionality.) _This is a + potentially dangerous endpoint and only highly trusted users should + have access._ +
    + +
    Method
    +
    POST
    + +
    URL
    +
    `/pki/sign-verbatim`
    + +
    Parameters
    +
    +
      +
    • + csr + required + The PEM-encoded CSR. +
    • +
    • + ttl + optional + Requested Time To Live. Cannot be greater than the mount's `max_ttl` + value. If not provided, the mount's `ttl` value will be used, which + defaults to system values if not explicitly set. +
    • +
    • + format + optional + Format for returned data. Can be `pem` or `der`; defaults to `pem`. If + `der`, the output is base64 encoded. +
    • +
    +
    + +
    Returns
    +
    + + ```javascript + { + "lease_id": "pki/sign-verbatim/7ad6cfa5-f04f-c62a-d477-f33210475d05", + "renewable": false, + "lease_duration": 21600, + "data": { + "certificate": "-----BEGIN CERTIFICATE-----\nMIIDzDCCAragAwIBAgIUOd0ukLcjH43TfTHFG9qE0FtlMVgwCwYJKoZIhvcNAQEL\n...\numkqeYeO30g1uYvDuWLXVA==\n-----END CERTIFICATE-----\n", + "issuing_ca": "-----BEGIN CERTIFICATE-----\nMIIDUTCCAjmgAwIBAgIJAKM+z4MSfw2mMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNV\n...\nG/7g4koczXLoUM3OQXd5Aq2cs4SS1vODrYmgbioFsQ3eDHd1fg==\n-----END CERTIFICATE-----\n", + "serial": "39:dd:2e:90:b7:23:1f:8d:d3:7d:31:c5:1b:da:84:d0:5b:65:31:58" + }, + "auth": null + } + ``` + +
    +