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

Transit backend: Create CSR's from keys in transit and import certificate chains #21081

Merged
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
47aaa74
setup initial boilerplate code for sign csr endpoint
Gabrielopesantos May 23, 2023
aed80d0
add function to sign csr
Gabrielopesantos May 24, 2023
471ce41
working version of sign csr endpoint
Gabrielopesantos May 27, 2023
04f70d3
improving errors for csr create and sign endpoint
Gabrielopesantos May 28, 2023
1be606b
initial implementation for import leaf certificate endpoint
Gabrielopesantos May 30, 2023
a91a604
check if more than one certificate was provided in the ceritificate c…
Gabrielopesantos May 31, 2023
42ee5e3
improve validate cert public key matches transit key
Gabrielopesantos May 31, 2023
996f2c3
convert provided cert chain from PEM to DER so it can be parsed by
Gabrielopesantos May 31, 2023
029ad26
fix creation of csr from csrTemplate
Gabrielopesantos Jun 3, 2023
352fde7
add missing persist of certificate chain after validations in set-cer…
Gabrielopesantos Jun 4, 2023
20f1661
allow exporting a certificate-chain
Gabrielopesantos Jun 4, 2023
1176d3f
Merge branch 'main' into gabrielopesantos-transit-engine-x509-certs
Gabrielopesantos Jun 4, 2023
4c81673
move function declaration to end of page
Gabrielopesantos Jun 4, 2023
dbc75eb
improving variable and function names, removing comments
Gabrielopesantos Jun 4, 2023
02096bf
fix certificate chain parsing - work in progress
Gabrielopesantos Jun 4, 2023
05060af
test for signCsr endpoint
Gabrielopesantos Jun 5, 2023
32cbe24
use Operations instead of Callbacks in framework.Path
Gabrielopesantos Jun 6, 2023
70a73e0
setup test for set-certificate endpoint
Gabrielopesantos Jun 6, 2023
e2212e0
finish set-certificate endpoint test
Gabrielopesantos Jun 7, 2023
bb91683
use public key KeyEntry fields instead of retrieving public key from …
Gabrielopesantos Jun 8, 2023
4e89380
improve error message and make better distinction between client and …
Gabrielopesantos Jun 8, 2023
a624b76
check if private key has been imported for key version selected when …
Gabrielopesantos Jun 8, 2023
a2b45d8
improve errors
Gabrielopesantos Jun 8, 2023
1cfc109
add endpoint description and synopsis
Gabrielopesantos Jun 8, 2023
87426a8
fix functions calls in backend as function names changed
Gabrielopesantos Jun 8, 2023
65dfb80
improve import cert chain test
Gabrielopesantos Jun 8, 2023
ea4a98d
trim whitespaces on export certificate chain
Gabrielopesantos Jun 8, 2023
25c723f
changelog
Gabrielopesantos Jun 8, 2023
9ddb037
pass context from handler function to policy Persist
Gabrielopesantos Jun 8, 2023
ed5b3f1
make fmt run
Gabrielopesantos Jun 9, 2023
6aa09d4
fix: assign returned error from PersistCertificateChain to err so it …
Gabrielopesantos Jun 10, 2023
9520a45
additional validations and improvements to parseCertificateChain func…
Gabrielopesantos Jun 10, 2023
708afbf
add validation to check if there is only one certificate in the certi…
Gabrielopesantos Jun 10, 2023
e222b45
import cert chain test: move creation of cluster to exported test fun…
Gabrielopesantos Jun 11, 2023
a4c2361
move check of end-cert pub key algorithm and key transit algorithm ma…
Gabrielopesantos Jun 12, 2023
66e1168
test export certificate chain
Gabrielopesantos Jun 12, 2023
2eb05e4
Update sdk/helper/keysutil/policy.go
Gabrielopesantos Jun 13, 2023
a43857e
fix validateLeafCertPosition
Gabrielopesantos Jun 13, 2023
b2d2329
reject certificate actions on policies that allow key derivation and …
Gabrielopesantos Jun 13, 2023
aa1dde0
return UserError from CreateCSR SDK function as 400 in transit API ha…
Gabrielopesantos Jul 22, 2023
d0d4bb0
add derived check for ED5519 keys on CreateCSR SDK func
Gabrielopesantos Jul 22, 2023
1ec8147
remove unecessary calls of x509.CreateCertificateRequest
Gabrielopesantos Jul 22, 2023
3d934ff
move validate key type match back into SDK ValidateLeafCertMatch func…
Gabrielopesantos Jul 23, 2023
9570e42
add additional validations (ValidateLeafCertKeyMatch, etc) in SDK Per…
Gabrielopesantos Jul 23, 2023
33c3cbf
remove uncessary call of ValidateLeafCertKeyMatch in parseImportCertC…
Gabrielopesantos Jul 23, 2023
fb3c475
store certificate chain as a [][]byte instead of []*x509.Certificate
Gabrielopesantos Jul 23, 2023
7c836b4
include persisted ca chain in import cert-chain response
Gabrielopesantos Jul 23, 2023
261bf32
remove NOTE comment
Gabrielopesantos Jul 23, 2023
6577441
allow exporting cert-chain even if exportable is set as false
Gabrielopesantos Jul 23, 2023
57ec3f7
remove NOTE comment
Gabrielopesantos Jul 23, 2023
a69604d
add certifcate chain to formatKeyPublic if present
Gabrielopesantos Jul 29, 2023
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
2 changes: 2 additions & 0 deletions builtin/logical/transit/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ func Backend(ctx context.Context, conf *logical.BackendConfig) (*backend, error)
b.pathTrim(),
b.pathCacheConfig(),
b.pathConfigKeys(),
b.pathCreateCsr(),
b.pathImportCertChain(),
},

Secrets: []*framework.Secret{},
Expand Down
332 changes: 332 additions & 0 deletions builtin/logical/transit/path_certificates.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,332 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package transit

import (
"context"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"strings"

"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/helper/keysutil"
"github.com/hashicorp/vault/sdk/logical"
)

func (b *backend) pathCreateCsr() *framework.Path {
return &framework.Path{
Pattern: "keys/" + framework.GenericNameRegex("name") + "/csr",
Fields: map[string]*framework.FieldSchema{
"name": {
Type: framework.TypeString,
Required: true,
Description: "Name of the key",
},
"version": {
Type: framework.TypeInt,
Required: false,
Description: "Optional version of key, 'latest' if not set",
},
"csr": {
Type: framework.TypeString,
Required: false,
Description: `PEM encoded CSR template. The information attributes
will be used as a basis for the CSR with the key in transit. If not set, an empty CSR is returned.`,
},
},
Operations: map[logical.Operation]framework.OperationHandler{
// NOTE: Create and Update?
logical.CreateOperation: &framework.PathOperation{
Callback: b.pathCreateCsrWrite,
DisplayAttrs: &framework.DisplayAttributes{
OperationVerb: "create",
},
},
logical.UpdateOperation: &framework.PathOperation{
Callback: b.pathCreateCsrWrite,
DisplayAttrs: &framework.DisplayAttributes{
OperationVerb: "update",
},
},
},
HelpSynopsis: pathCreateCsrHelpSyn,
HelpDescription: pathCreateCsrHelpDesc,
}
}

func (b *backend) pathImportCertChain() *framework.Path {
return &framework.Path{
// NOTE: `set-certificate` or `set_certificate`? Paths seem to use different
// case, such as `transit/wrapping_key` and `transit/cache-config`.
Pattern: "keys/" + framework.GenericNameRegex("name") + "/set-certificate",
Fields: map[string]*framework.FieldSchema{
"name": {
Type: framework.TypeString,
Required: true,
Description: "Name of the key",
},
"version": {
Type: framework.TypeInt,
Required: false,
Description: "Optional version of key, 'latest' if not set",
},
"certificate_chain": {
Type: framework.TypeString,
Required: true,
Description: `PEM encoded certificate chain. It should be composed
by one or more concatenated PEM blocks and ordered starting from the end-entity certificate.`,
},
},
Operations: map[logical.Operation]framework.OperationHandler{
// NOTE: Create and Update?
logical.CreateOperation: &framework.PathOperation{
Callback: b.pathImportCertChainWrite,
DisplayAttrs: &framework.DisplayAttributes{
OperationVerb: "create",
},
},
logical.UpdateOperation: &framework.PathOperation{
Callback: b.pathImportCertChainWrite,
DisplayAttrs: &framework.DisplayAttributes{
OperationVerb: "update",
},
},
},
HelpSynopsis: pathImportCertChainHelpSyn,
HelpDescription: pathImportCertChainHelpDesc,
}
}

func (b *backend) pathCreateCsrWrite(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
name := d.Get("name").(string)
pemCsrTemplate := d.Get("csr").(string)

p, _, err := b.GetPolicy(ctx, keysutil.PolicyRequest{
Storage: req.Storage,
Name: name,
}, b.GetRandomReader())
if err != nil {
return nil, err
}
if p == nil {
return logical.ErrorResponse(fmt.Sprintf("key with provided name '%s' not found", name)), logical.ErrInvalidRequest
}
if !b.System().CachingDisabled() {
p.Lock(false) // NOTE: No lock on "read" operations?
}
defer p.Unlock()

// Transit key version
signingKeyVersion := p.LatestVersion
// NOTE: BYOK endpoints seem to remove "v" prefix from version,
// are versions like that also supported?
if version, ok := d.GetOk("version"); ok {
signingKeyVersion = version.(int)
}

// Check if transit key supports signing
if !p.Type.SigningSupported() {
return logical.ErrorResponse(fmt.Sprintf("key type '%s' does not support signing", p.Type)), logical.ErrInvalidRequest
}

// Read and parse CSR template
csrTemplate, err := parseCsr(pemCsrTemplate)
if err != nil {
return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest
}

pemCsr, err := p.CreateCsr(signingKeyVersion, csrTemplate)
if err != nil {
return nil, err
}

resp := &logical.Response{
Data: map[string]interface{}{
"name": p.Name,
"type": p.Type.String(),
"csr": string(pemCsr),
},
}

return resp, nil
}

func (b *backend) pathImportCertChainWrite(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
name := d.Get("name").(string)

p, _, err := b.GetPolicy(ctx, keysutil.PolicyRequest{
Storage: req.Storage,
Name: name,
}, b.GetRandomReader())
if err != nil {
return nil, err
}
if p == nil {
return logical.ErrorResponse(fmt.Sprintf("key with provided name '%s' not found", name)), logical.ErrInvalidRequest
}
if !b.System().CachingDisabled() {
p.Lock(true) // NOTE: Lock as we are might write to the policy
}
defer p.Unlock()

// Transit key version
keyVersion := p.LatestVersion
if version, ok := d.GetOk("version"); ok {
keyVersion = version.(int)
}

// Check if transit key supports signing
// NOTE: A key type that doesn't support signing cannot possible (?) have
// a certificate, so does it make sense to have this check?
if !p.Type.SigningSupported() {
return logical.ErrorResponse(fmt.Sprintf("key type %s does not support signing", p.Type)), logical.ErrInvalidRequest
}

// Get certificate chain
pemCertChain := d.Get("certificate_chain").(string)
certChain, err := parseCertificateChain(pemCertChain)
if err != nil {
return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest
}

err = validateLeafCertPosition(certChain)
if err != nil {
return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest
}

leafCertPublicKeyAlgorithm := certChain[0].PublicKeyAlgorithm

// Check if end-entity public key algorithm matches transit key
err = validateKeyTypeMatch(p, leafCertPublicKeyAlgorithm)
if err != nil {
return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest
}

// Validate if leaf cert key matches with transit key
valid, err := p.ValidateLeafCertKeyMatch(keyVersion, leafCertPublicKeyAlgorithm, certChain[0].PublicKey)
if err != nil {
return nil, fmt.Errorf("could not validate key match between leaf certificate key and key version in transit: %s", err.Error())
}
if !valid {
return logical.ErrorResponse("leaf certificate public key does match the key version selected"), logical.ErrInvalidRequest
}

err = p.PersistCertificateChain(ctx, keyVersion, certChain, req.Storage)
if err != nil {
return nil, fmt.Errorf("failed to persist certificate chain: %s", err.Error())
}

return nil, nil
}

func parseCsr(csrStr string) (*x509.CertificateRequest, error) {
if csrStr == "" {
return &x509.CertificateRequest{}, nil
}

block, _ := pem.Decode([]byte(csrStr))
if block == nil {
return nil, errors.New("could not decode PEM certificate request")
}

csr, err := x509.ParseCertificateRequest(block.Bytes)
if err != nil {
return nil, err
}

return csr, nil
}

func parseCertificateChain(certChainString string) ([]*x509.Certificate, error) {
var certificates []*x509.Certificate

var pemCertBlocks []*pem.Block
pemBytes := []byte(strings.TrimSpace(certChainString))
for len(pemBytes) > 0 {
var pemCertBlock *pem.Block
pemCertBlock, pemBytes = pem.Decode(pemBytes)
if pemCertBlock == nil {
return nil, errors.New("could not decode PEM block in certificate chain")
}

switch pemCertBlock.Type {
case "CERTIFICATE", "X05 CERTIFICATE":
pemCertBlocks = append(pemCertBlocks, pemCertBlock)
default:
// Ignore any other entries
}
}

if len(pemCertBlocks) == 0 {
return nil, errors.New("provided certificate chain did not contain any valid PEM certificate")
}

for _, certBlock := range pemCertBlocks {
cert, err := x509.ParseCertificate(certBlock.Bytes)
if err != nil {
return nil, fmt.Errorf("failed to parse certificate in certificate chain: %s", err.Error())
}

certificates = append(certificates, cert)
}

return certificates, nil
}

func validateLeafCertPosition(certChain []*x509.Certificate) error {
// NOTE: Validate if Basic Constraints are valid in different condition and return different error message?
if len(certChain) > 0 && (certChain[0].BasicConstraintsValid && certChain[0].IsCA) {
return errors.New("leaf certificate not found in the first position of the certificate chain")
}

for _, cert := range certChain[1:] {
if cert.BasicConstraintsValid && !cert.IsCA {
return errors.New("provided certificate chain contains more than one leaf certificate")
}
}

return nil
}

func validateKeyTypeMatch(p *keysutil.Policy, leafCertPublicKeyAlgorithm x509.PublicKeyAlgorithm) error {
var keyTypeMatches bool
switch p.Type {
case keysutil.KeyType_ECDSA_P256, keysutil.KeyType_ECDSA_P384, keysutil.KeyType_ECDSA_P521:
if leafCertPublicKeyAlgorithm == x509.ECDSA {
keyTypeMatches = true
}
case keysutil.KeyType_ED25519:
if leafCertPublicKeyAlgorithm == x509.Ed25519 {
keyTypeMatches = true
}
case keysutil.KeyType_RSA2048, keysutil.KeyType_RSA3072, keysutil.KeyType_RSA4096:
if leafCertPublicKeyAlgorithm == x509.RSA {
keyTypeMatches = true
}
}
if !keyTypeMatches {
// NOTE: Different type "names" might lead to confusion.
return fmt.Errorf("provided leaf certificate public key type '%s' does not match the transit key type '%s'",
leafCertPublicKeyAlgorithm.String(), p.Type.String())
}

return nil
}

const pathCreateCsrHelpSyn = `Create a CSR from a key in transit`

const pathCreateCsrHelpDesc = `This path is used to create a CSR from a key in
transit. If a CSR template is provided, its significant information, expect key
related data, are included in the CSR otherwise an empty CSR is returned.
`

const pathImportCertChainHelpSyn = `Imports an externally-signed certificate
chain into an existing key version`

const pathImportCertChainHelpDesc = `This path is used to import an externally-
signed certificate chain into a key in transit. The leaf certificate key has to
match the selected key in transit.
`
Loading