From 6d9e181cf3ea93c3f7368d89e395632ac18337ec Mon Sep 17 00:00:00 2001 From: Alexander Scheel Date: Thu, 20 Jul 2023 08:11:08 -0500 Subject: [PATCH] Add SDK CIEPS changes (#21974) * OSS: Add standard CIEPS request/response structs Signed-off-by: Alexander Scheel * OSS: Add support for parsing TLS-related values Signed-off-by: Alexander Scheel --------- Signed-off-by: Alexander Scheel --- sdk/helper/certutil/cieps.go | 161 +++++++++++++++++++++++++++++++++ sdk/helper/certutil/helpers.go | 19 +++- 2 files changed, 177 insertions(+), 3 deletions(-) create mode 100644 sdk/helper/certutil/cieps.go diff --git a/sdk/helper/certutil/cieps.go b/sdk/helper/certutil/cieps.go new file mode 100644 index 0000000000..059944f104 --- /dev/null +++ b/sdk/helper/certutil/cieps.go @@ -0,0 +1,161 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package certutil + +import ( + "crypto/x509" + "encoding/pem" + "fmt" +) + +// Source of the issuance request: sign implies that the key material was +// generated by the user and submitted via a CSR request but only ACL level +// validation was applied; issue implies that Vault created the key material +// on behalf of the user with ACL level validation occurring; ACME implies +// that the user submitted a CSR and that additional ACME validation has +// occurred before sending the request to the external service for +// construction. +type CIEPSIssuanceMode string + +const ( + SignCIEPSMode = "sign" + IssueCIEPSMode = "issue" + ACMECIEPSMode = "acme" + ICACIEPSMOde = "ica" +) + +// Configuration of the issuer and mount at the time of this request; +// states the issuer's templated AIA information (falling back to the +// mount-global config if no per-issuer AIA info is set, the issuer's +// leaf_not_after_behavior (permit/truncate/err) for TTLs exceeding the +// issuer's validity period, and the mount's default and max TTL. +type CIEPSIssuanceConfig struct { + AIAValues *URLEntries `json:"aia_values"` + LeafNotAfterBehavior NotAfterBehavior `json:"leaf_not_after_behavior"` + MountDefaultTTL string `json:"mount_default_ttl"` + MountMaxTTL string `json:"mount_max_ttl"` +} + +// Structured parameters sent by Vault or explicitly validated by Vault +// prior to sending. +type CIEPSVaultParams struct { + PolicyName string `json:"policy_name,omitempty"` + Mount string `json:"mount"` + Namespace string `json:"ns"` + + // These indicate the type of the cluster node talking to the CIEPS + // service. When IsPerfStandby=true, setting StoreCert=true in the + // response will result in Vault forwarding the client's request + // up to the Performance Secondary's active node and re-trying the + // operation (including re-submitting the request to the CIEPS + // service). + // + // Any response returned by the CIEPS service in this case will be + // ignored and not signed by the CA's keys. + // + // IsPRSecondary is set to false when a local mount is used on a + // PR Secondary; in this scenario, PR Secondary nodes behave like + // PR Primary nodes. From a CIEPS service perspective, no behavior + // difference is expected between PR Primary and PR Secondary nodes; + // both will issue and store certificates on their active nodes. + // This information is included for audit tracking purposes. + IsPerfStandby bool `json:"vault_is_performance_standby"` + IsPRSecondary bool `json:"vault_is_performance_secondary"` + IsDRSecondary bool `json:"vault_is_disaster_secondary"` + + IssuanceMode CIEPSIssuanceMode `json:"issuance_mode"` + + GeneratedKey bool `json:"vault_generated_private_key"` + + IssuerName string `json:"requested_issuer_name"` + IssuerID string `json:"requested_issuer_id"` + IssuerCert string `json:"requested_issuer_cert"` + + Config CIEPSIssuanceConfig `json:"requested_issuance_config"` +} + +// Outer request object sent by Vault to the external CIEPS service. +// +// The top-level fields denote properties about the CIEPS request, +// with various request fields containing untrusted and trusted input +// respectively. +type CIEPSRequest struct { + Version int `json:"request_version"` + UUID string `json:"request_uuid"` + Sync bool `json:"synchronous"` + + UserRequestKV map[string]interface{} `json:"user_request_key_values"` + IdentityRequestKV map[string]interface{} `json:"identity_request_key_values,omitempty"` + ACMERequestKV map[string]interface{} `json:"acme_request_key_values,omitempty"` + VaultRequestKV CIEPSVaultParams `json:"vault_request_values"` + + // Vault guarantees that UserRequestKV will contain a csr parameter + // for all request types; this field is useful for engine implementations + // to have in parsed format. We assume that this is sent in PEM format, + // aligning with other Vault requests. + ParsedCSR *x509.CertificateRequest `json:"-"` +} + +func (req *CIEPSRequest) ParseUserCSR() error { + csrValueRaw, present := req.UserRequestKV["csr"] + if !present { + return fmt.Errorf("missing expected 'csr' attribute on the request") + } + + csrValue, ok := csrValueRaw.(string) + if !ok { + return fmt.Errorf("unexpected type of 'csr' attribute: %T", csrValueRaw) + } + + if csrValue == "" { + return fmt.Errorf("unexpectedly empty 'csr' attribute on the request") + } + + block, rest := pem.Decode([]byte(csrValue)) + if len(rest) > 0 { + return fmt.Errorf("failed to decode 'csr': %v bytes of trailing data after PEM block", len(rest)) + } + if block == nil { + return fmt.Errorf("failed to decode 'csr' PEM block") + } + + csr, err := x509.ParseCertificateRequest(block.Bytes) + if err != nil { + return fmt.Errorf("failed to parse certificate request: %w", err) + } + + req.ParsedCSR = csr + return nil +} + +// Expected response object from the external CIEPS service. +// +// When parsing, Vault will disallow unknown fields, failing the +// parse if unknown fields are sent. +type CIEPSResponse struct { + UUID string `json:"request_uuid"` + Error string `json:"error"` + Warnings []string `json:"warnings"` + Certificate string `json:"certificate"` + ParsedCertificate *x509.Certificate `json:"-"` + IssuerRef string `json:"issuer_ref,omitempty"` + StoreCert bool `json:"store_certificate"` +} + +func (c *CIEPSResponse) MarshalCertificate() error { + if c.ParsedCertificate == nil || len(c.ParsedCertificate.Raw) == 0 { + return fmt.Errorf("no certificate present") + } + + pem := pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: c.ParsedCertificate.Raw, + }) + if len(pem) == 0 { + return fmt.Errorf("failed to generate PEM: no body") + } + c.Certificate = string(pem) + + return nil +} diff --git a/sdk/helper/certutil/helpers.go b/sdk/helper/certutil/helpers.go index 28472027f8..9d7ac540b5 100644 --- a/sdk/helper/certutil/helpers.go +++ b/sdk/helper/certutil/helpers.go @@ -13,6 +13,7 @@ import ( "crypto/rand" "crypto/rsa" "crypto/sha1" + "crypto/tls" "crypto/x509" "crypto/x509/pkix" "encoding/asn1" @@ -304,6 +305,18 @@ func ParsePEMBundle(pemBundle string) (*ParsedCertBundle, error) { return parsedBundle, nil } +func (p *ParsedCertBundle) ToTLSCertificate() tls.Certificate { + var cert tls.Certificate + cert.Certificate = append(cert.Certificate, p.CertificateBytes) + cert.Leaf = p.Certificate + cert.PrivateKey = p.PrivateKey + for _, ca := range p.CAChain { + cert.Certificate = append(cert.Certificate, ca.Bytes) + } + + return cert +} + // GeneratePrivateKey generates a private key with the specified type and key bits. func GeneratePrivateKey(keyType string, keyBits int, container ParsedPrivateKeyContainer) error { return generatePrivateKey(keyType, keyBits, container, nil) @@ -1292,7 +1305,7 @@ func NewCertPool(reader io.Reader) (*x509.CertPool, error) { if err != nil { return nil, err } - certs, err := parseCertsPEM(pemBlock) + certs, err := ParseCertsPEM(pemBlock) if err != nil { return nil, fmt.Errorf("error reading certs: %s", err) } @@ -1303,9 +1316,9 @@ func NewCertPool(reader io.Reader) (*x509.CertPool, error) { return pool, nil } -// parseCertsPEM returns the x509.Certificates contained in the given PEM-encoded byte array +// ParseCertsPEM returns the x509.Certificates contained in the given PEM-encoded byte array // Returns an error if a certificate could not be parsed, or if the data does not contain any certificates -func parseCertsPEM(pemCerts []byte) ([]*x509.Certificate, error) { +func ParseCertsPEM(pemCerts []byte) ([]*x509.Certificate, error) { ok := false certs := []*x509.Certificate{} for len(pemCerts) > 0 {