From 530b67bbb9a551a9188c875f5adf0aed9a1c4366 Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Fri, 15 May 2015 12:13:05 -0400 Subject: [PATCH 01/15] Initial PKI backend implementation. Complete: * Up-to-date API documents * Backend configuration (root certificate and private key) * Highly granular role configuration * Certificate generation * CN checking against role * IP and DNS subject alternative names * Server, client, and code signing usage types * Later certificate (but not private key) retrieval * CRL creation and update * CRL/CA bare endpoints (for cert extensions) * Revocation (both Vault-native and by serial number) * CRL force-rotation endpoint Missing: * OCSP support (can't implement without changes in Vault) * Unit tests Commit contents (C)2015 Akamai Technologies, Inc. --- builtin/logical/pki/backend.go | 67 ++ builtin/logical/pki/cert_util.go | 382 ++++++++++ builtin/logical/pki/crl_util.go | 153 ++++ builtin/logical/pki/path_config_ca.go | 133 ++++ builtin/logical/pki/path_fetch.go | 194 +++++ builtin/logical/pki/path_issue.go | 207 ++++++ builtin/logical/pki/path_revoke.go | 77 ++ builtin/logical/pki/path_roles.go | 261 +++++++ builtin/logical/pki/secret_certs.go | 54 ++ cli/commands.go | 2 + website/source/docs/secrets/pki/index.html.md | 677 ++++++++++++++++++ website/source/layouts/docs.erb | 4 + 12 files changed, 2211 insertions(+) create mode 100644 builtin/logical/pki/backend.go create mode 100644 builtin/logical/pki/cert_util.go create mode 100644 builtin/logical/pki/crl_util.go create mode 100644 builtin/logical/pki/path_config_ca.go create mode 100644 builtin/logical/pki/path_fetch.go create mode 100644 builtin/logical/pki/path_issue.go create mode 100644 builtin/logical/pki/path_revoke.go create mode 100644 builtin/logical/pki/path_roles.go create mode 100644 builtin/logical/pki/secret_certs.go create mode 100644 website/source/docs/secrets/pki/index.html.md diff --git a/builtin/logical/pki/backend.go b/builtin/logical/pki/backend.go new file mode 100644 index 0000000000..4a66e20c80 --- /dev/null +++ b/builtin/logical/pki/backend.go @@ -0,0 +1,67 @@ +package pki + +import ( + "strings" + + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" +) + +// Factory creates a new backend implementing the logical.Backend interface +func Factory(map[string]string) (logical.Backend, error) { + return Backend(), nil +} + +// Backend returns a new Backend framework struct +func Backend() *framework.Backend { + var b backend + b.Backend = &framework.Backend{ + Help: strings.TrimSpace(backendHelp), + + PathsSpecial: &logical.Paths{ + Root: []string{ + "config/*", + "revoked/*", + "revoke/*", + "crl/build", + }, + Unauthenticated: []string{ + "cert/*", + "ca/pem", + "ca", + "crl/pem", + "crl", + }, + }, + + Paths: []*framework.Path{ + pathRoles(&b), + pathConfigCA(&b), + pathIssue(&b), + pathRotateCRL(&b), + pathFetchCA(&b), + pathFetchCRL(&b), + pathFetchCRLViaCertPath(&b), + pathFetchValid(&b), + pathFetchRevoked(&b), + pathRevoke(&b), + }, + + Secrets: []*framework.Secret{ + secretCerts(&b), + }, + } + + return b.Backend +} + +type backend struct { + *framework.Backend +} + +const backendHelp = ` +The PKI backend dynamically generates X509 server and client certificates. + +After mounting this backend, configure the CA using the "ca_bundle" endpoint within +the "config/" path. +` diff --git a/builtin/logical/pki/cert_util.go b/builtin/logical/pki/cert_util.go new file mode 100644 index 0000000000..0e90ff061e --- /dev/null +++ b/builtin/logical/pki/cert_util.go @@ -0,0 +1,382 @@ +package pki + +import ( + "bytes" + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + crand "crypto/rand" + "crypto/rsa" + "crypto/sha1" + "crypto/x509" + "crypto/x509/pkix" + "encoding/base64" + "fmt" + "math/big" + mrand "math/rand" + "net" + "regexp" + "strings" + "time" + + "github.com/hashicorp/vault/logical" +) + +// The type of Private Key, for storage +const ( + UnknownPrivateKeyType = iota + RSAPrivateKeyType + ECDSAPrivateKeyType +) + +type certUsage int + +const ( + serverUsage certUsage = 1 << iota + clientUsage + codeSigningUsage +) + +type certBundle struct { + PrivateKeyType int `json:"private_key_type"` + PrivateKeyString string `json:"private_key_string"` + CertificateString string `json:"certificate_string"` +} + +type rawCertBundle struct { + PrivateKeyType int + PrivateKeyBytes []byte + CertificateBytes []byte + SerialNumber *big.Int +} + +type certCreationBundle struct { + RawSigningBundle *rawCertBundle + CACert *x509.Certificate + CommonNames []string + IPSANs []net.IP + KeyType string + KeyBits int + Lease time.Duration + Usage certUsage +} + +func (c *certBundle) toRawCertBundle() (*rawCertBundle, error) { + decoder := base64.URLEncoding + result := &rawCertBundle{ + PrivateKeyType: c.PrivateKeyType, + } + var err error + if result.PrivateKeyBytes, err = decoder.DecodeString(c.PrivateKeyString); err != nil { + return nil, err + } + if result.CertificateBytes, err = decoder.DecodeString(c.CertificateString); err != nil { + return nil, err + } + + if err := result.populateSerialNumber(); err != nil { + return nil, err + } + + return result, nil +} + +func (r *rawCertBundle) toCertBundle() *certBundle { + encoder := base64.URLEncoding + result := &certBundle{ + PrivateKeyType: r.PrivateKeyType, + PrivateKeyString: encoder.EncodeToString(r.PrivateKeyBytes), + CertificateString: encoder.EncodeToString(r.CertificateBytes), + } + return result +} + +func (r *rawCertBundle) populateSerialNumber() error { + cert, err := x509.ParseCertificate(r.CertificateBytes) + if err != nil { + return fmt.Errorf("Error encountered parsing certificate bytes from raw bundle") + } + r.SerialNumber = cert.SerialNumber + return nil +} + +// "Signer" corresponds to the Go interface that private keys implement +// that provides a Public() function for getting the corresponding public +// key. It can be type converted to private keys. +func (r *rawCertBundle) getSigner() (crypto.Signer, error) { + var signer crypto.Signer + var err error + switch r.PrivateKeyType { + case ECDSAPrivateKeyType: + signer, err = x509.ParseECPrivateKey(r.PrivateKeyBytes) + if err != nil { + return nil, fmt.Errorf("Unable to parse CA's private EC key: %s", err) + } + case RSAPrivateKeyType: + signer, err = x509.ParsePKCS1PrivateKey(r.PrivateKeyBytes) + if err != nil { + return nil, fmt.Errorf("Unable to parse CA's private RSA key: %s", err) + } + default: + return nil, fmt.Errorf("Unable to determine the type of private key") + } + return signer, nil +} + +func (r *rawCertBundle) getSubjKeyID() ([]byte, error) { + privateKey, err := r.getSigner() + if err != nil { + return nil, err + } + + marshaledKey, err := x509.MarshalPKIXPublicKey(privateKey.Public()) + if err != nil { + return nil, fmt.Errorf("Error marshalling public key: %s", err) + } + + subjKeyID := sha1.Sum(marshaledKey) + + return subjKeyID[:], nil +} + +func getCertBundle(s logical.Storage, path string) (*certBundle, error) { + bundle, err := s.Get(path) + if err != nil { + return nil, err + } + if bundle == nil { + return nil, nil + } + + var result certBundle + if err := bundle.DecodeJSON(&result); err != nil { + return nil, err + } + + return &result, nil +} + +func getOctalFormatted(buf []byte, sep string) string { + var ret bytes.Buffer + for _, cur := range buf { + if ret.Len() > 0 { + fmt.Fprintf(&ret, sep) + } + fmt.Fprintf(&ret, "%02x", cur) + } + return ret.String() +} + +func fetchCAInfo(req *logical.Request) (*rawCertBundle, *x509.Certificate, error) { + bundle, err := getCertBundle(req.Storage, "config/ca_bundle") + if err != nil { + return nil, nil, fmt.Errorf("Unable to fetch local CA certificate/key: %s", err) + } + if bundle == nil { + return nil, nil, fmt.Errorf("Backend must be configured with a CA certificate/key") + } + + rawBundle, err := bundle.toRawCertBundle() + if err != nil { + return nil, nil, err + } + + certificates, err := x509.ParseCertificates(rawBundle.CertificateBytes) + switch { + case err != nil: + return nil, nil, err + case len(certificates) != 1: + return nil, nil, fmt.Errorf("Length of CA certificate bundle is wrong") + } + + return rawBundle, certificates[0], nil +} + +func fetchCertBySerial(req *logical.Request, prefix, serial string) (certEntry *logical.StorageEntry, userError, internalError error) { + var path string + var err error + switch { + case serial == "ca": + path = "ca" + case serial == "crl": + path = "crl" + case strings.HasPrefix(prefix, "revoked/"): + path = "revoked/" + strings.Replace(strings.ToLower(serial), "-", ":", -1) + default: + path = "certs/" + strings.Replace(strings.ToLower(serial), "-", ":", -1) + } + + certEntry, err = req.Storage.Get(path) + if err != nil || certEntry == nil { + return nil, fmt.Errorf("Certificate with serial number %s not found (if it has been revoked, the revoked/ endpoint must be used)", serial), nil + } + + if len(certEntry.Value) == 0 { + return nil, nil, fmt.Errorf("Returned certificate bytes for serial %s were empty", serial) + } + + return +} + +func validateCommonNames(req *logical.Request, commonNames []string, role *roleEntry) (string, error) { + // TODO: handle wildcards + hostnameRegex, err := regexp.Compile(`^(([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])$`) + if err != nil { + return "", fmt.Errorf("Error compiling hostname regex: %s", err) + } + subdomainRegex, err := regexp.Compile(`^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9]))*$`) + if err != nil { + return "", fmt.Errorf("Error compiling subdomain regex: %s", err) + } + for _, name := range commonNames { + if role.AllowLocalhost && name == "localhost" { + continue + } + + sanitizedName := name + isWildcard := false + if strings.HasPrefix(name, "*.") { + sanitizedName = name[2:] + isWildcard = true + } + if !hostnameRegex.MatchString(sanitizedName) { + return name, nil + } + if role.AllowAnyName { + continue + } + + if role.AllowTokenDisplayName { + if name == req.DisplayName { + continue + } + + if role.AllowSubdomains { + if strings.HasSuffix(name, "."+req.DisplayName) { + continue + } + } + } + + if len(role.AllowedBaseDomain) != 0 { + if strings.HasSuffix(name, "."+role.AllowedBaseDomain) { + if role.AllowSubdomains { + continue + } + + if subdomainRegex.MatchString(strings.TrimSuffix(name, "."+role.AllowedBaseDomain)) { + continue + } + + if isWildcard && role.AllowedBaseDomain == sanitizedName { + continue + } + } + } + + return name, nil + } + + return "", nil +} + +func createCertificate(creationInfo *certCreationBundle) (rawBundle *rawCertBundle, userErr, intErr error) { + rawBundle = &rawCertBundle{ + SerialNumber: (&big.Int{}).Rand(mrand.New(mrand.NewSource(time.Now().UnixNano())), (&big.Int{}).Exp(big.NewInt(2), big.NewInt(159), nil)), + } + + var clientPrivKey crypto.Signer + var err error + switch creationInfo.KeyType { + case "rsa": + rawBundle.PrivateKeyType = RSAPrivateKeyType + clientPrivKey, err = rsa.GenerateKey(crand.Reader, creationInfo.KeyBits) + if err != nil { + return nil, nil, fmt.Errorf("Error generating RSA private key") + } + rawBundle.PrivateKeyBytes = x509.MarshalPKCS1PrivateKey(clientPrivKey.(*rsa.PrivateKey)) + case "ecdsa": + rawBundle.PrivateKeyType = ECDSAPrivateKeyType + var curve elliptic.Curve + switch creationInfo.KeyBits { + case 224: + curve = elliptic.P224() + case 256: + curve = elliptic.P256() + case 384: + curve = elliptic.P384() + case 521: + curve = elliptic.P521() + default: + return nil, fmt.Errorf("Unsupported bit length for ECDSA key: %d", creationInfo.KeyBits), nil + } + clientPrivKey, err = ecdsa.GenerateKey(curve, crand.Reader) + if err != nil { + return nil, nil, fmt.Errorf("Error generating ECDSA private key") + } + rawBundle.PrivateKeyBytes, err = x509.MarshalECPrivateKey(clientPrivKey.(*ecdsa.PrivateKey)) + if err != nil { + return nil, nil, fmt.Errorf("Error marshalling ECDSA private key") + } + default: + return nil, fmt.Errorf("Unknown key type: %s", creationInfo.KeyType), nil + } + + subjKeyID, err := rawBundle.getSubjKeyID() + if err != nil { + return nil, nil, fmt.Errorf("Error getting subject key ID: %s", err) + } + + subject := pkix.Name{ + Country: creationInfo.CACert.Subject.Country, + Organization: creationInfo.CACert.Subject.Organization, + OrganizationalUnit: creationInfo.CACert.Subject.OrganizationalUnit, + Locality: creationInfo.CACert.Subject.Locality, + Province: creationInfo.CACert.Subject.Province, + StreetAddress: creationInfo.CACert.Subject.StreetAddress, + PostalCode: creationInfo.CACert.Subject.PostalCode, + SerialNumber: rawBundle.SerialNumber.String(), + CommonName: creationInfo.CommonNames[0], + } + + certTemplate := &x509.Certificate{ + SignatureAlgorithm: x509.SHA256WithRSA, + SerialNumber: rawBundle.SerialNumber, + Subject: subject, + NotBefore: time.Now(), + NotAfter: time.Now().Add(creationInfo.Lease), + KeyUsage: x509.KeyUsage(x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement), + BasicConstraintsValid: true, + IsCA: false, + SubjectKeyId: subjKeyID, + DNSNames: creationInfo.CommonNames, + IPAddresses: creationInfo.IPSANs, + PermittedDNSDomainsCritical: false, + PermittedDNSDomains: nil, + CRLDistributionPoints: creationInfo.CACert.CRLDistributionPoints, + } + + if creationInfo.Usage&serverUsage != 0 { + certTemplate.ExtKeyUsage = append(certTemplate.ExtKeyUsage, x509.ExtKeyUsageServerAuth) + } + if creationInfo.Usage&clientUsage != 0 { + certTemplate.ExtKeyUsage = append(certTemplate.ExtKeyUsage, x509.ExtKeyUsageClientAuth) + } + if creationInfo.Usage&codeSigningUsage != 0 { + certTemplate.ExtKeyUsage = append(certTemplate.ExtKeyUsage, x509.ExtKeyUsageCodeSigning) + } + + signingPrivKey, err := creationInfo.RawSigningBundle.getSigner() + if err != nil { + return nil, nil, fmt.Errorf("Unable to get signing private key: %s", err) + } + + cert, err := x509.CreateCertificate(crand.Reader, certTemplate, creationInfo.CACert, clientPrivKey.Public(), signingPrivKey) + if err != nil { + return nil, nil, fmt.Errorf("Unable to create certificate: %s", err) + } + + rawBundle.CertificateBytes = cert + + return +} diff --git a/builtin/logical/pki/crl_util.go b/builtin/logical/pki/crl_util.go new file mode 100644 index 0000000000..5ce2da2355 --- /dev/null +++ b/builtin/logical/pki/crl_util.go @@ -0,0 +1,153 @@ +package pki + +import ( + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "fmt" + "time" + + "github.com/hashicorp/vault/logical" +) + +type revocationInfo struct { + CertificateBytes []byte `json:"certificate_bytes"` + RevocationTime int64 `json:"unix_time"` +} + +func revokeCert(req *logical.Request, serial string) (*logical.Response, error) { + certEntry, userErr, intErr := fetchCertBySerial(req, "revoked/", serial) + if certEntry != nil { + return nil, nil + } + + certEntry, userErr, intErr = fetchCertBySerial(req, "certs/", serial) + switch { + case userErr != nil: + return logical.ErrorResponse(userErr.Error()), nil + case intErr != nil: + return nil, intErr + } + + // Possible TODO: use some kind of transaction log in case of an + // error anywhere along here (so we've validated that we got a + // value back, but want to make sure it not only is deleted from + // certs/ but also shows up in revoked/ and a CRL is generated) + err := req.Storage.Delete("certs/" + serial) + + if err != nil { + return nil, fmt.Errorf("Error deleting cert from valid-certs location") + } + + cert, err := x509.ParseCertificate(certEntry.Value) + if err != nil { + return nil, fmt.Errorf("Error parsing certificate") + } + if cert == nil { + return nil, fmt.Errorf("Got a nil certificate") + } + + if cert.NotAfter.Before(time.Now()) { + return nil, nil + } + + revInfo := revocationInfo{ + CertificateBytes: certEntry.Value, + RevocationTime: time.Now().Unix(), + } + + certEntry, err = logical.StorageEntryJSON("revoked/"+serial, revInfo) + if err != nil { + return nil, fmt.Errorf("Error creating revocation entry") + } + + err = req.Storage.Put(certEntry) + if err != nil { + return nil, fmt.Errorf("Error saving revoked certificate to new location") + } + + err = buildCRL(req) + if err != nil { + return nil, fmt.Errorf("Error encountered during CRL building: %s", err) + } + + return &logical.Response{ + Data: map[string]interface{}{ + "revocation_time": revInfo.RevocationTime, + }, + }, nil +} + +func buildCRL(req *logical.Request) error { + revokedSerials, err := req.Storage.List("revoked/") + if err != nil { + return fmt.Errorf("Error fetching list of revoked certs: %s", err) + } + + revokedCerts := []pkix.RevokedCertificate{} + var revInfo revocationInfo + for _, serial := range revokedSerials { + revokedEntry, err := req.Storage.Get("revoked/" + serial) + if err != nil { + return fmt.Errorf("Unable to fetch revoked cert with serial %s: %s", serial, err) + } + if revokedEntry == nil { + return fmt.Errorf("Revoked certificate entry for serial %s is nil", serial) + } + if revokedEntry.Value == nil || len(revokedEntry.Value) == 0 { + // TODO: In this case, remove it and continue? How likely is this to + // happen? Alternately, could skip it entirely, or could implement a + // delete function so that there is a way to remove these + return fmt.Errorf("Found revoked serial but actual certificate is empty") + } + + err = revokedEntry.DecodeJSON(&revInfo) + if err != nil { + return fmt.Errorf("Error decoding revocation entry for serial %s: %s", serial, err) + } + + revokedCert, err := x509.ParseCertificate(revInfo.CertificateBytes) + if err != nil { + return fmt.Errorf("Unable to parse stored revoked certificate with serial %s: %s", serial, err) + } + + if revokedCert.NotAfter.Before(time.Now()) { + err = req.Storage.Delete(serial) + if err != nil { + return fmt.Errorf("Unable to delete revoked, expired certificate with serial %s: %s", serial, err) + } + continue + } + + revokedCerts = append(revokedCerts, pkix.RevokedCertificate{ + SerialNumber: revokedCert.SerialNumber, + RevocationTime: time.Unix(revInfo.RevocationTime, 0), + }) + } + + rawSigningBundle, caCert, err := fetchCAInfo(req) + if err != nil { + return fmt.Errorf("Could not fetch the CA certificate") + } + + signingPrivKey, err := rawSigningBundle.getSigner() + if err != nil { + return fmt.Errorf("Unable to get signing private key: %s", err) + } + + // TODO: Make expiry configurable + crlBytes, err := caCert.CreateCRL(rand.Reader, signingPrivKey, revokedCerts, time.Now(), time.Now().Add(time.Hour*72)) + if err != nil { + return fmt.Errorf("Error creating new CRL: %s", err) + } + + err = req.Storage.Put(&logical.StorageEntry{ + Key: "crl", + Value: crlBytes, + }) + if err != nil { + return fmt.Errorf("Error storing CRL: %s", err) + } + + return nil +} diff --git a/builtin/logical/pki/path_config_ca.go b/builtin/logical/pki/path_config_ca.go new file mode 100644 index 0000000000..073f074ebd --- /dev/null +++ b/builtin/logical/pki/path_config_ca.go @@ -0,0 +1,133 @@ +package pki + +import ( + "crypto/x509" + "encoding/pem" + + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" +) + +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: "PEM-format, concatenated unencrypted secret key and certificate", + }, + }, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.WriteOperation: b.pathCAWrite, + }, + + HelpSynopsis: pathConfigCAHelpSyn, + HelpDescription: pathConfigCAHelpDesc, + } +} + +func (b *backend) pathCAWrite( + req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + pemBundle := d.Get("pem_bundle").(string) + + if len(pemBundle) == 0 { + return logical.ErrorResponse("Empty PEM bundle"), nil + } + + pemBytes := []byte(pemBundle) + var pemBlock *pem.Block + rawBundle := &rawCertBundle{} + + for { + pemBlock, pemBytes = pem.Decode(pemBytes) + if pemBlock == nil { + return logical.ErrorResponse("No PEM data found"), nil + } + + if _, err := x509.ParseECPrivateKey(pemBlock.Bytes); err == nil { + if rawBundle.PrivateKeyType != UnknownPrivateKeyType { + return logical.ErrorResponse("More than one private key given; provide only one private key in the bundle"), nil + } + rawBundle.PrivateKeyType = ECDSAPrivateKeyType + rawBundle.PrivateKeyBytes = pemBlock.Bytes + // TODO?: CRLs can only be generated with RSA keys right now, in the + // Go standard library. The plubming is here to support non-RSA keys + // if the library gets support + return logical.ErrorResponse("Only RSA keys are supported at this time due to limitations in the Go standard library"), nil + } else if _, err := x509.ParsePKCS1PrivateKey(pemBlock.Bytes); err == nil { + + if rawBundle.PrivateKeyType != UnknownPrivateKeyType { + return logical.ErrorResponse("More than one private key given; provide only one private key in the bundle"), nil + } + rawBundle.PrivateKeyType = RSAPrivateKeyType + rawBundle.PrivateKeyBytes = pemBlock.Bytes + } else if certificates, err := x509.ParseCertificates(pemBlock.Bytes); err == nil { + switch len(certificates) { + case 0: + return logical.ErrorResponse("No certificates found in the bundle"), nil + case 1: + cert := certificates[0] + if !cert.IsCA { + return logical.ErrorResponse("The given certificate is not marked for CA use and cannot be used with this backend"), nil + } + rawBundle.CertificateBytes = pemBlock.Bytes + default: + return logical.ErrorResponse("More than one certificate given; provide only one certificate in the bundle"), nil + } + } + + if len(pemBytes) == 0 { + break + } + } + + switch { + case rawBundle.PrivateKeyType == UnknownPrivateKeyType: + return logical.ErrorResponse("Unable to figure out the private key type; must be RSA or ECDSA"), nil + case len(rawBundle.PrivateKeyBytes) == 0: + return logical.ErrorResponse("Unable to decode the private key from the bundle"), nil + case len(rawBundle.CertificateBytes) == 0: + return logical.ErrorResponse("Unable to decode the certificate from the bundle"), nil + } + + cb := rawBundle.toCertBundle() + 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 blank CRL + entry.Key = "ca" + entry.Value = rawBundle.CertificateBytes + err = req.Storage.Put(entry) + if err != nil { + return nil, err + } + + entry.Key = "crl" + entry.Value = []byte{} + err = req.Storage.Put(entry) + if err != nil { + return nil, err + } + + return nil, nil +} + +const pathConfigCAHelpSyn = ` +Configure the CA certificate and private key used for generated credentials. +` + +const pathConfigCAHelpDesc = ` +This configures the CA information used for credentials +generated by this backend. This must be a PEM-format, concatenated +unencrypted secret key and certificate. + +For security reasons, you can only view the certificate when reading this endpoint. +` diff --git a/builtin/logical/pki/path_fetch.go b/builtin/logical/pki/path_fetch.go new file mode 100644 index 0000000000..6063aedd22 --- /dev/null +++ b/builtin/logical/pki/path_fetch.go @@ -0,0 +1,194 @@ +package pki + +import ( + "encoding/pem" + "fmt" + "strings" + + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" +) + +func pathFetchCA(b *backend) *framework.Path { + return &framework.Path{ + Pattern: `ca(/pem)?`, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.ReadOperation: b.pathFetchRead, + }, + + HelpSynopsis: pathFetchHelpSyn, + HelpDescription: pathFetchHelpDesc, + } +} + +func pathFetchCRL(b *backend) *framework.Path { + return &framework.Path{ + Pattern: `crl(/pem)?`, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.ReadOperation: b.pathFetchRead, + }, + + HelpSynopsis: pathFetchHelpSyn, + HelpDescription: pathFetchHelpDesc, + } +} + +func pathFetchValid(b *backend) *framework.Path { + return &framework.Path{ + Pattern: `cert/(?P[0-9A-Fa-f-:]+)`, + Fields: map[string]*framework.FieldSchema{ + "serial": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "Certificate serial number, in colon- or hyphen-separated octal", + }, + }, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.ReadOperation: b.pathFetchRead, + }, + + HelpSynopsis: pathFetchHelpSyn, + HelpDescription: pathFetchHelpDesc, + } +} + +func pathFetchCRLViaCertPath(b *backend) *framework.Path { + return &framework.Path{ + Pattern: `cert/crl`, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.ReadOperation: b.pathFetchRead, + }, + + HelpSynopsis: pathFetchHelpSyn, + HelpDescription: pathFetchHelpDesc, + } +} + +func pathFetchRevoked(b *backend) *framework.Path { + return &framework.Path{ + Pattern: `revoked/(?P[0-9A-Fa-f-:]+)`, + Fields: map[string]*framework.FieldSchema{ + "serial": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "Certificate serial number, in colon- or hyphen-separated octal", + }, + }, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.ReadOperation: b.pathFetchRead, + }, + + HelpSynopsis: pathFetchHelpSyn, + HelpDescription: pathFetchHelpDesc, + } +} + +func (b *backend) pathFetchRead(req *logical.Request, data *framework.FieldData) (response *logical.Response, retErr error) { + var serial string + var pemType string + var contentType string + var certEntry *logical.StorageEntry + var userErr, intErr, err error + var certificate []byte + response = &logical.Response{ + Data: map[string]interface{}{}, + } + + switch { + case req.Path == "ca" || req.Path == "ca/pem": + serial = "ca" + contentType = "application/pkix-cert" + if req.Path == "ca/pem" { + pemType = "CERTIFICATE" + } + case req.Path == "crl" || req.Path == "crl/pem": + serial = "crl" + contentType = "application/pkix-crl" + if req.Path == "crl/pem" { + pemType = "X509 CRL" + } + case req.Path == "cert/crl": + serial = "crl" + pemType = "X509 CRL" + default: + serial = data.Get("serial").(string) + pemType = "CERTIFICATE" + } + if len(serial) == 0 { + response = logical.ErrorResponse("The serial number must be provided") + goto reply + } + + _, _, err = fetchCAInfo(req) + if err != nil { + response = logical.ErrorResponse("No CA information configured for this backend") + goto reply + } + + certEntry, userErr, intErr = fetchCertBySerial(req, req.Path, serial) + switch { + case userErr != nil: + response = logical.ErrorResponse(userErr.Error()) + goto reply + case intErr != nil: + retErr = intErr + goto reply + } + + switch { + case strings.HasPrefix(req.Path, "revoked/"): + var revInfo revocationInfo + err := certEntry.DecodeJSON(&revInfo) + if err != nil { + retErr = fmt.Errorf("Error decoding revocation entry for serial %s: %s", serial, err) + goto reply + } + certificate = revInfo.CertificateBytes + response.Data["revocation_time"] = revInfo.RevocationTime + default: + certificate = certEntry.Value + } + + if len(pemType) != 0 { + block := pem.Block{ + Type: pemType, + Bytes: certEntry.Value, + } + certificate = pem.EncodeToMemory(&block) + } + +reply: + switch { + case len(contentType) != 0: + response = &logical.Response{ + Data: map[string]interface{}{ + logical.HTTPContentType: contentType, + logical.HTTPRawBody: certificate, + }} + if retErr != nil { + b.Logger().Printf("Possible error, but cannot return in raw response: %s. Note that an empty CA probably means none was configured, and an empty CRL is quite possibly correct", retErr) + } + retErr = nil + response.Data[logical.HTTPStatusCode] = 200 + + case retErr != nil: + response = nil + default: + response.Data["certificate"] = string(certificate) + } + + return +} + +const pathFetchHelpSyn = ` +Fetch a CA, CRL, valid or revoked certificate. +` + +const pathFetchHelpDesc = ` +This allows certificates to be fetched. If using the fetch/ prefix any valid certificate can be fetched; if using the revoked/ prefix, which requires a root token, revoked certificates can also be fetched. + +Using "ca" or "crl" as the value fetches the appropriate information in DER encoding. Add "/pem" to either to get PEM encoding. +` diff --git a/builtin/logical/pki/path_issue.go b/builtin/logical/pki/path_issue.go new file mode 100644 index 0000000000..1f291155f5 --- /dev/null +++ b/builtin/logical/pki/path_issue.go @@ -0,0 +1,207 @@ +package pki + +import ( + "encoding/pem" + "fmt" + "net" + "strings" + "time" + + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" +) + +func pathIssue(b *backend) *framework.Path { + return &framework.Path{ + Pattern: `issue/(?P\w+)`, + Fields: map[string]*framework.FieldSchema{ + "role": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "The desired role with configuration for this request", + }, + "common_name": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "The requested common name; if you want more than one, specify the alternative names in the alt_names map", + }, + "alt_names": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "The requested Subject Alternative Names, if any, in a comma-delimited list", + }, + "ip_sans": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "The requested IP SANs, if any, in a common-delimited list", + }, + "lease": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "The requested lease", + }, + }, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.WriteOperation: b.pathIssueCert, + }, + + HelpSynopsis: pathIssueCertHelpSyn, + HelpDescription: pathIssueCertHelpDesc, + } +} + +func (b *backend) pathIssueCert( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + roleName := data.Get("role").(string) + + // Get the common name(s) + var commonNames []string + cn := data.Get("common_name").(string) + if len(cn) == 0 { + return logical.ErrorResponse("The common_name field is required"), nil + } + commonNames = []string{cn} + + cnAlt := data.Get("alt_names").(string) + if len(cnAlt) != 0 { + for _, v := range strings.Split(cnAlt, ",") { + commonNames = append(commonNames, v) + } + } + + // 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 + } + + // Get any IP SANs + ipSANs := []net.IP{} + + ipAlt := data.Get("ip_sans").(string) + if len(ipAlt) != 0 { + if !role.AllowIPSANs { + return logical.ErrorResponse("IP Subject Alternative Names are not allowed in this role"), nil + } + for _, v := range strings.Split(ipAlt, ",") { + parsedIP := net.ParseIP(v) + if parsedIP == nil { + return logical.ErrorResponse(fmt.Sprintf("The value '%s' is not a valid IP address", v)), nil + } + ipSANs = append(ipSANs, parsedIP) + } + } + + leaseField := data.Get("lease").(string) + if len(leaseField) == 0 { + leaseField = role.Lease + } + + lease, err := time.ParseDuration(leaseField) + if err != nil { + return logical.ErrorResponse(fmt.Sprintf( + "Invalid requested lease: %s", err)), nil + } + leaseMax, err := time.ParseDuration(role.LeaseMax) + if err != nil { + return logical.ErrorResponse(fmt.Sprintf( + "Invalid lease: %s", err)), nil + } + + if time.Now().Add(lease).After(time.Now().Add(leaseMax)) { + return logical.ErrorResponse("Lease expires after maximum allowed by this role"), nil + } + + badName, err := validateCommonNames(req, commonNames, role) + if len(badName) != 0 { + return logical.ErrorResponse(fmt.Sprintf("Name %s not allowed by this role", badName)), nil + } else if err != nil { + return nil, fmt.Errorf("Error validating name %s: %s", badName, err) + } + + rawSigningBundle, caCert, err := fetchCAInfo(req) + if err != nil { + return logical.ErrorResponse("Could not fetch the CA certificate; has it been set?"), nil + } + + if time.Now().Add(lease).After(caCert.NotAfter) { + return logical.ErrorResponse(fmt.Sprintf("Cannot satisfy request, as maximum lease is beyond the expiration of the CA certificate")), nil + } + + var usage certUsage + if role.ServerFlag { + usage = usage | serverUsage + } + if role.ClientFlag { + usage = usage | clientUsage + } + creationBundle := &certCreationBundle{ + RawSigningBundle: rawSigningBundle, + CACert: caCert, + CommonNames: commonNames, + IPSANs: ipSANs, + KeyType: role.KeyType, + KeyBits: role.KeyBits, + Lease: lease, + Usage: usage, + } + + rawBundle, userErr, intErr := createCertificate(creationBundle) + switch { + case userErr != nil: + return logical.ErrorResponse(userErr.Error()), nil + case intErr != nil: + return nil, intErr + } + + serial := strings.ToLower(getOctalFormatted(rawBundle.SerialNumber.Bytes(), ":")) + + resp := b.Secret(SecretCertsType).Response(map[string]interface{}{}, map[string]interface{}{ + "serial": serial, + }) + + resp.Data["serial"] = serial + + block := pem.Block{ + Type: "CERTIFICATE", + Bytes: rawBundle.CertificateBytes, + } + certificateString := string(pem.EncodeToMemory(&block)) + resp.Data["certificate"] = certificateString + + block.Bytes = rawSigningBundle.CertificateBytes + caString := string(pem.EncodeToMemory(&block)) + resp.Data["issuing_ca"] = caString + + block.Bytes = rawBundle.PrivateKeyBytes + switch rawBundle.PrivateKeyType { + case RSAPrivateKeyType: + block.Type = "RSA PRIVATE KEY" + case ECDSAPrivateKeyType: + block.Type = "EC PRIVATE KEY" + default: + return nil, fmt.Errorf("Could not determine private key type when creating block") + } + resp.Data["private_key"] = string(pem.EncodeToMemory(&block)) + + resp.Secret.Lease = lease + + err = req.Storage.Put(&logical.StorageEntry{ + Key: "certs/" + serial, + Value: rawBundle.CertificateBytes, + }) + if err != nil { + return nil, fmt.Errorf("Unable to store certificate locally") + } + + return resp, nil +} + +const pathIssueCertHelpSyn = ` +Request certificates using a certain role with the provided common name. +` + +const pathIssueCertHelpDesc = ` +This path allows requesting certificates to be issued according to the +policy of the given role. The certificate will only be issued if the +requested common name is allowed by the role policy. +` diff --git a/builtin/logical/pki/path_revoke.go b/builtin/logical/pki/path_revoke.go new file mode 100644 index 0000000000..eb89555979 --- /dev/null +++ b/builtin/logical/pki/path_revoke.go @@ -0,0 +1,77 @@ +package pki + +import ( + "fmt" + + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" +) + +func pathRevoke(b *backend) *framework.Path { + return &framework.Path{ + Pattern: `revoke`, + Fields: map[string]*framework.FieldSchema{ + "serial": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "Certificate serial number, in colon- or hyphen-separated octal", + }, + }, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.WriteOperation: b.pathRevokeWrite, + }, + + HelpSynopsis: pathRevokeHelpSyn, + HelpDescription: pathRevokeHelpDesc, + } +} + +func pathRotateCRL(b *backend) *framework.Path { + return &framework.Path{ + Pattern: `crl/rotate`, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.ReadOperation: b.pathRotateCRLRead, + }, + + HelpSynopsis: pathRotateCRLHelpSyn, + HelpDescription: pathRotateCRLHelpDesc, + } +} + +func (b *backend) pathRevokeWrite(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + serial := data.Get("serial").(string) + if len(serial) == 0 { + return logical.ErrorResponse("The serial number must be provided"), nil + } + + return revokeCert(req, serial) +} + +func (b *backend) pathRotateCRLRead(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + err := buildCRL(req) + if err != nil { + return logical.ErrorResponse(fmt.Sprintf("Error building CRL: %s", err)), err + } + return &logical.Response{ + Data: map[string]interface{}{ + "success": true, + }, + }, nil +} + +const pathRevokeHelpSyn = ` +Revoke a certificate by serial number. +` + +const pathRevokeHelpDesc = ` +This allows certificates to be revoked using its serial number. A root token is required. +` + +const pathRotateCRLHelpSyn = ` +Force a rebuild of the CRL. +` + +const pathRotateCRLHelpDesc = ` +Force a rebuild of the CRL. This can be used to remove expired certificates from it if no certificates have been revoked. A root token is required. +` diff --git a/builtin/logical/pki/path_roles.go b/builtin/logical/pki/path_roles.go new file mode 100644 index 0000000000..3a5477cdee --- /dev/null +++ b/builtin/logical/pki/path_roles.go @@ -0,0 +1,261 @@ +package pki + +import ( + "fmt" + "time" + + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" +) + +func pathRoles(b *backend) *framework.Path { + return &framework.Path{ + Pattern: "roles/(?P\\w+)", + Fields: map[string]*framework.FieldSchema{ + "name": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "Name of the role.", + }, + + "lease": &framework.FieldSchema{ + Type: framework.TypeString, + Default: "", + Description: "The lease length if no specific lease length is requested. The lease length controls the expiration of certificates issued by this backend. Defaults to the value of lease_max.", + }, + + "lease_max": &framework.FieldSchema{ + Type: framework.TypeString, + Default: "", + Description: "The maximum allowed lease length.", + }, + + "allow_localhost": &framework.FieldSchema{ + Type: framework.TypeBool, + Default: true, + Description: "Whether to allow \"localhost\" as a valid common name in a request.", + }, + + "allowed_base_domain": &framework.FieldSchema{ + Type: framework.TypeString, + Default: "", + Description: "If set, clients can request certificates for subdomains directly beneath this base domain, including the wildcard subdomain. See the documentation for more information.", + }, + + "allow_token_displayname": &framework.FieldSchema{ + Type: framework.TypeBool, + Default: false, + Description: "If set, clients can request certificates for matching the value of the Display Name on the requesting token. See the documentation for more information.", + }, + + "allow_subdomains": &framework.FieldSchema{ + Type: framework.TypeBool, + Default: false, + Description: "If set, clients can request certificates for subdomains of the CNs allowed by the other role options, including wildcard subdomains. See the documentation for more information.", + }, + + "allow_any_name": &framework.FieldSchema{ + Type: framework.TypeBool, + Default: false, + Description: "If set, clients can request certificates for any CN they like. See the documentation for more information.", + }, + + "allow_ip_sans": &framework.FieldSchema{ + Type: framework.TypeBool, + Default: true, + Description: "If set, IP Subject Alternative Names are allowed. Any valid IP is accepted.", + }, + + "server_flag": &framework.FieldSchema{ + Type: framework.TypeBool, + Default: true, + Description: "If set, certificates are flagged for server use. Defaults to true.", + }, + + "client_flag": &framework.FieldSchema{ + Type: framework.TypeBool, + Default: true, + Description: "If set, certificates are flagged for client use. Defaults to true.", + }, + + "code_signing_flag": &framework.FieldSchema{ + Type: framework.TypeBool, + Default: false, + Description: "If set, certificates are flagged for code signing use. Defaults to false.", + }, + + "key_type": &framework.FieldSchema{ + Type: framework.TypeString, + Default: "rsa", + Description: "The type of key to use; defaults to RSA. \"rsa\" and \"ecdsa\" are the only valid values.", + }, + + "key_bits": &framework.FieldSchema{ + Type: framework.TypeInt, + Default: 2048, + Description: "The number of bits to use. You will almost certainly want to change this if you adjust the key_type.", + }, + }, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.ReadOperation: b.pathRoleRead, + logical.WriteOperation: b.pathRoleCreate, + logical.DeleteOperation: b.pathRoleDelete, + }, + + HelpSynopsis: pathRoleHelpSyn, + HelpDescription: pathRoleHelpDesc, + } +} + +func (b *backend) getRole(s logical.Storage, n string) (*roleEntry, error) { + entry, err := s.Get("role/" + n) + if err != nil { + return nil, err + } + if entry == nil { + return nil, nil + } + + var result roleEntry + if err := entry.DecodeJSON(&result); err != nil { + return nil, err + } + + return &result, nil +} + +func (b *backend) pathRoleDelete( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + err := req.Storage.Delete("role/" + data.Get("name").(string)) + if err != nil { + return nil, err + } + + return nil, nil +} + +func (b *backend) pathRoleRead( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + role, err := b.getRole(req.Storage, data.Get("name").(string)) + if err != nil { + return nil, err + } + if role == nil { + return nil, nil + } + + resp := &logical.Response{ + Data: map[string]interface{}{ + "lease_max": role.LeaseMax, + "lease": role.Lease, + "allow_localhost": role.AllowLocalhost, + "allowed_base_domain": role.AllowedBaseDomain, + "allow_token_displayname": role.AllowTokenDisplayName, + "allow_subdomains": role.AllowSubdomains, + "allow_ip_sans": role.AllowIPSANs, + "allow_any_name": role.AllowAnyName, + "server_flag": role.ServerFlag, + "client_flag": role.ClientFlag, + "code_signing_flag": role.CodeSigningFlag, + "key_type": role.KeyType, + "key_bits": role.KeyBits, + }, + } + + return resp, nil +} + +func (b *backend) pathRoleCreate( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + name := data.Get("name").(string) + + entry := &roleEntry{ + LeaseMax: data.Get("lease_max").(string), + Lease: data.Get("lease").(string), + AllowLocalhost: data.Get("allow_localhost").(bool), + AllowedBaseDomain: data.Get("allowed_base_domain").(string), + AllowTokenDisplayName: data.Get("allow_token_displayname").(bool), + AllowSubdomains: data.Get("allow_subdomains").(bool), + AllowAnyName: data.Get("allow_any_name").(bool), + AllowIPSANs: data.Get("allow_ip_sans").(bool), + ServerFlag: data.Get("server_flag").(bool), + ClientFlag: data.Get("client_flag").(bool), + CodeSigningFlag: data.Get("code_signing_flag").(bool), + KeyType: data.Get("key_type").(string), + KeyBits: data.Get("key_bits").(int), + } + + if len(entry.LeaseMax) == 0 { + return logical.ErrorResponse("\"lease_max\" value must be supplied"), nil + } + + leaseMax, err := time.ParseDuration(entry.LeaseMax) + if err != nil { + return logical.ErrorResponse(fmt.Sprintf( + "Invalid lease: %s", err)), nil + } + + switch len(entry.Lease) { + case 0: + entry.Lease = entry.LeaseMax + default: + lease, err := time.ParseDuration(entry.Lease) + if err != nil { + return logical.ErrorResponse(fmt.Sprintf( + "Invalid lease: %s", err)), nil + } + if lease > leaseMax { + return logical.ErrorResponse("\"lease\" value must be less than \"lease_max\" value"), nil + } + } + + switch entry.KeyType { + case "rsa": + case "ecdsa": + switch entry.KeyBits { + case 224: + case 256: + case 384: + case 521: + default: + return logical.ErrorResponse(fmt.Sprintf("Unsupported bit length for ECDSA key: %d", entry.KeyBits)), nil + } + default: + return logical.ErrorResponse(fmt.Sprintf("Unknown key type %s", entry.KeyType)), nil + } + + // Store it + jsonEntry, err := logical.StorageEntryJSON("role/"+name, entry) + if err != nil { + return nil, err + } + if err := req.Storage.Put(jsonEntry); err != nil { + return nil, err + } + + return nil, nil +} + +type roleEntry struct { + LeaseMax string `json:"lease_max"` + Lease string `json:"lease"` + AllowLocalhost bool `json:"allow_localhost"` + AllowedBaseDomain string `json:"allowed_base_domain"` + AllowTokenDisplayName bool `json:"allow_token_displaynae"` + AllowSubdomains bool `json:"allow_subdomains"` + AllowAnyName bool `json:"allow_any_name"` + AllowIPSANs bool `json:"allow_ip_sans"` + ServerFlag bool `json:"server_flag"` + ClientFlag bool `json:"client_flag"` + CodeSigningFlag bool `json:"code_signing_flag"` + KeyType string `json:"key_type"` + KeyBits int `json:"key_bits"` +} + +const pathRoleHelpSyn = ` +Manage the roles that can be created with this backend. +` + +const pathRoleHelpDesc = ` +This path lets you manage the roles that can be created with this backend. +` diff --git a/builtin/logical/pki/secret_certs.go b/builtin/logical/pki/secret_certs.go new file mode 100644 index 0000000000..d09159a643 --- /dev/null +++ b/builtin/logical/pki/secret_certs.go @@ -0,0 +1,54 @@ +package pki + +import ( + "fmt" + "strings" + "time" + + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" +) + +// SecretCertsType is the name used to identify this type +const SecretCertsType = "pki" + +func secretCerts(b *backend) *framework.Secret { + return &framework.Secret{ + Type: SecretCertsType, + Fields: map[string]*framework.FieldSchema{ + "certificate": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "The PEM-encoded concatenated certificate and issuing certificate authority", + }, + "private_key": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "The PEM-encoded private key for the certificate", + }, + "serial": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "The serial number of the certificate, for handy reference", + }, + }, + + DefaultDuration: 168 * time.Hour, + DefaultGracePeriod: 10 * time.Minute, + + Revoke: b.secretCredsRevoke, + } +} + +func (b *backend) secretCredsRevoke( + req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + if req.Secret == nil { + return nil, fmt.Errorf("Secret is nil in request") + } + + serialInt, ok := req.Secret.InternalData["serial"] + if !ok { + return nil, fmt.Errorf("Could not find serial in internal secret data") + } + + serial := strings.Replace(strings.ToLower(serialInt.(string)), "-", ":", -1) + + return revokeCert(req, serial) +} diff --git a/cli/commands.go b/cli/commands.go index 1a7916c742..d007a96ba9 100644 --- a/cli/commands.go +++ b/cli/commands.go @@ -15,6 +15,7 @@ import ( "github.com/hashicorp/vault/builtin/logical/aws" "github.com/hashicorp/vault/builtin/logical/consul" "github.com/hashicorp/vault/builtin/logical/mysql" + "github.com/hashicorp/vault/builtin/logical/pki" "github.com/hashicorp/vault/builtin/logical/postgresql" "github.com/hashicorp/vault/builtin/logical/transit" @@ -65,6 +66,7 @@ func Commands(metaPtr *command.Meta) map[string]cli.CommandFactory { "aws": aws.Factory, "consul": consul.Factory, "postgresql": postgresql.Factory, + "pki": pki.Factory, "transit": transit.Factory, "mysql": mysql.Factory, }, diff --git a/website/source/docs/secrets/pki/index.html.md b/website/source/docs/secrets/pki/index.html.md new file mode 100644 index 0000000000..2d28f5a231 --- /dev/null +++ b/website/source/docs/secrets/pki/index.html.md @@ -0,0 +1,677 @@ +--- +layout: "docs" +page_title: "Secret Backend: PKI" +sidebar_current: "docs-secrets-pki" +description: |- + The PKI secret backend for Vault generates TLS certificates. +--- + +# PKI Secret Backend + +Name: `pki` + +The PKI secret backend for Vault generates X.509 certificates dynamically based on configured roles. This means services can get certificates needed for both client and server authentication without going through the usual manual process of generating a private key and CSR, submitting to a CA, and waiting for a verification and signing process to complete. Vault's built-in authentication and authorization mechanisms provide the verification functionality. + +By keeping leases relatively short, revocations are less likely to be needed, keeping CRLs short and helping the backend scale to large workloads. This in turn allows each instance of a running application to have a unique certificate, eliminating sharing and the accompanying pain of revocation and rollover. + +In addition, by allowing revocation to mostly be forgone, this backend allows for ephemeral certificates; certificates can be fetched and stored in memory upon application startup and discarded upon shutdown, without ever being written to disk. + +This page will show a quick start for this backend. For detailed documentation on every path, use `vault help` after mounting the backend. + +## Considerations + +To successfully deploy this backend, there are a number of important considerations to be aware of, as well as some preparatory steps that should be undertaken. You should read all of these *before* using this backend or generating the CA to use with this backend. + +### Never use root CAs + +Vault storage is secure, but not as secure as a piece of paper in a bank vault. It is, after all, networked software. Your long-lived self-signed root CA's private key should instead be used to issue a shorter-lived intermediate CA certificate, and this is what you should put into Vault. This aligns with industry best practices. + +### One CA Certificate, One Backend + +In order to vastly simplify both the configuration and codebase of the PKI backend, only one CA certificate is allowed per backend. If you want to issue certificates from multiple CAs, mount the PKI backend at multiple mount points with separate CA certificates in each. + +This also provides a convenient method of switching to a new CA certificate while keeping CRLs valid from the old CA certificate; simply mount a new backend and issue from there. + +### Keep certificate lifetimes short, for CRL's sake + +This backend aligns with Vault's philosophy of short-lived secrets. As such it is not expected that CRLs will grow large; the only place a private key is ever returned is to the requesting client (this backend does *not* store generated private keys). In most cases, if the key is lost, the certificate can simply be ignored, as it will expire shortly. + +If a certificate must truly be revoked, the normal Vault revocation function can be used; alternately a root token can be used to revoke the certificate using the certificate's serial number. Any revocation action will cause the CRL to be regenerated. When the CRL is regenerated, any expired certificates are removed from the CRL (and any revoked, expired certificate are removed from backend storage). + +This backend does not support multiple CRL endpoints with sliding date windows; often such mechanisms will have the transition point a few days apart, but this gets into the expected realm of the actual certificate validity periods issued from this backend. A good rule of thumb for this backend would be to simply not issue certificates with a validity period greater than your maximum comfortable CRL lifetime. Alternately, you can control CRL caching behavior on the client to ensure that checks happen more often. + +Often multiple endpoints are used in case a single CRL endpoint is down so that clients don't have to figure out what to do with a lack of response. Run Vault in HA mode, and the CRL endpoint should be available even if a particular node is down. + +### You must configure CRL information *in advance* + +This backend serves CRLs from a predictable location. That location must be encoded into your CA certificate if you want to allow applications to use the CRL endpoint encoded in certificates to find the CRL. Instructions for doing so are below. If you need to adjust this later, you will have to generate a new CA certificate using the same private key if you want to keep validity for already-issued certificates. + +### No OCSP support, yet + +Vault's architecture does not currently allow for a binary protocol such as OCSP to be supported by a backend. As such, you should configure your software to use CRLs for revocation information, with a caching lifetime that feels good to you. Since you are following the advice above about keeping lifetimes short (right?), CRLs should not grow too large. + +## Quick Start + +### CA certificate + +In order for this backend to serve CRL information at the expected location, you will need to generate your CA certificate with this information. For OpenSSL, this means putting a value in the CA section with the appropriate URL; in this example the PKI backend is mounted at `pki`: + +```text +crlDistributionPoints = URI:https://vault.example.com:8200/v1/pki/crl +``` + +Adjust the URI as appropriate. + +### Vault + +The first step to using the PKI backend is to mount it. Unlike the `generic` backend, the `pki` backend is not mounted by default. + +```text +$ vault mount pki +Successfully mounted 'pki' at 'pki'! +``` + +Next, Vault must be configured with a root certificate and associated private key. This is done by writing the contents of a file or *stdin*: + +```text +$ vault write pki/config/ca value="@ca_bundle.pem" +Success! Data written to: pki/config/ca +``` + +or + +``` +$ cat bundle.pem | vault write pki/config/ca value="-" +Success! Data written to: pki/config/ca +``` + +Although in this example the value being piped into *stdin* could be passed directly into the Vault CLI command, a more complex usage might be to use [Ansible](http://www.ansible.com) to securely store the certificate and private key in an `ansible-vault` file, then have an `ansible-playbook` command decrypt this value and pass it in to Vault. + +The next step is to configure a role. A role is a logical name that maps to a policy used to generated those credentials. For example, let's create an "example-dot-com" role: + +```text +$ vault write pki/roles/example-dot-com \ + allowed_base_domain="example.com" \ + allow_subdomains="true" lease_max="72h" +Success! Data written to: pki/roles/example-dot-com +``` + +By writing to the `roles/example-dot-com` path we are defining the `example-dot-com` role. To generate a new set of credentials, we simply write to the `issue` endpoint with that role name: Vault is now configured to create and manage certificates! + +```text +$ vault write pki/issue/example-dot-com common_name=blah.example.com +Key Value +lease_id pki/issue/example-dot-com/819393b5-e1a1-9efd-b72f-4dc3a1972e31 +lease_duration 259200 +lease_renewable false +certificate -----BEGIN CERTIFICATE----- +MIIECDCCAvKgAwIBAgIUXmLrLkTdBIOOIYg2/BXO7docKfUwCwYJKoZIhvcNAQEL +... +az3gfwlOqVTdgi/ZVAtIzhSEJ0OY136bq4NOaw== +-----END CERTIFICATE----- +issuing_ca -----BEGIN CERTIFICATE----- +MIIDUTCCAjmgAwIBAgIJAKM+z4MSfw2mMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNV +... +-----END CERTIFICATE----- +private_key -----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA0cczc7Y2yIu7aD/IaDi23Io+tvvDS9XaXXDUFW1kqd58P83r +... +3xhCNnZ3CMQaM2I48sloVK/XoikMLb5MZwOUQn/V+TrhWP4Lu7qD +-----END RSA PRIVATE KEY----- +serial 5e:62:eb:2e:44:dd:04:83:8e:21:88:36:fc:15:ce:ed:da:1c:29:f5 +``` + +Note that this is a write, not a read, to allow values to be passed in at request time. + +Vault has now generated a new set of credentials using the `example-dot-com` role configuration. Here we see the dynamically generated private key and certificate. The issuing CA certificate is returned as well. + +Using ACLs, it is possible to restrict using the pki backend such that trusted operators can manage the role definitions, and both users and applications are restricted in the credentials they are allowed to read. + +If you get stuck at any time, simply run `vault help pki` or with a subpath for interactive help output. + +## API + +### /pki/ca(/pem) +#### GET + +
+
Description
+
+ Retrieves the CA certificate *in raw DER-encoded form*. + This is a bare endpoint that does not return a + standard Vault data structure. If `/pem` is added to the + endpoint, the CA certificate is returned in PEM format. +

This is an unauthenticated endpoint. +
+ +
Method
+
GET
+ +
URL
+
`/pki/ca(/pem)`
+ +
Parameters
+
+ None +
+ +
Returns
+
+ + ``` + + ``` + +
+
+ +### /pki/cert/ +#### GET + +
+
Description
+
+ Retrieves one of a selection of certificates. Valid values: `ca` + for the CA certificate, `crl` for the current CRL, or a serial + number in either hyphen-separated or colon-separated octal format. + This endpoint returns the certificate in PEM formatting in the + `certificate` key of the JSON object. +

This is an unauthenticated endpoint. +
+ +
Method
+
GET
+ +
URL
+
`/pki/cert/`
+ +
Parameters
+
+ None +
+ +
Returns
+
+ + ```javascript + { + "data": { + "certificate": "-----BEGIN CERTIFICATE-----\nMIIGmDCCBYCgAwIBAgIHBzEB3fTzhTANBgkqhkiG9w0BAQsFADCBjDELMAkGA1UE\n..." + } + } + ... + ``` + +
+
+ +### /pki/config/ca +#### POST + +
+
Description
+
+ A PEM file containing the issuing CA certificate + and its private key, concatenated. +

This is a root-protected endpoint. +

The information can be provided from a file via a `curl` + command similar to the following:
+ + ```text + curl -X POST --data "@cert_and_key.pem" ... + ``` +
+ +
Method
+
POST
+ +
URL
+
`/pki/config/ca`
+ +
Parameters
+
+
    +
  • + pem_bundle + required + The key and certificate concatenated in PEM format. +
  • +
+
+ +
Returns
+
+ A `204` response code. +
+
+ +### /pki/crl(/pem) +#### GET + +
+
Description
+
+ Retrieves the current CRL *in raw DER-encoded form*. This endpoint + is suitable for usage in the CRL Distribution Points extension in a + CA certificate. This is a bare endpoint that does not return a + standard Vault data structure. If `/pem` is added to the endpoint, + the CRL is returned in PEM format. +

This is an unauthenticated endpoint. +
+ +
Method
+
GET
+ +
URL
+
`/pki/crl(/pem)`
+ +
Parameters
+
+ None +
+ +
Returns
+
+ + ``` + + ``` + +
+
+ +### /pki/crl/rotate +#### GET + +
+
Description
+
+ This endpoint forces a rotation of the CRL. This can be used + by administrators to cut the size of the CRL if it contains + a number of certificates that have now expired, but has + not been rotated due to no further certificates being revoked. +

This is a root-protected endpoint. +
+ +
Method
+
GET
+ +
URL
+
`/pki/crl/rotate`
+ +
Parameters
+
+ None +
+ +
Returns
+
+ + ```javascript + { + "data": { + "success": true + } + } + ``` + +
+
+ +### /pki/issue/ +#### GET + +
+
Description
+
+ Generates a new set of credentials (private key and + certificate) based on the named role. The issuing CA + certificate is returned as well, so that only the root CA + need be in a client's trust store. +

*The private key is _not_ stored. + If you do not save the private key, you will need to + request a new certificate.* +
+ +
Method
+
GET
+ +
URL
+
`/pki/issue/`
+ +
Parameters
+
+
    +
  • + common_name + required + The requested CN for the certificate. If the CN is allowed + by role policy, it will be issued. +
  • +
  • + alt_names + optional + Requested Subject Alternative Names, in a comma-delimited + list. If any requested names do not match role policy, + the entire request will be denied. +
  • + ip_sans + optional + Requested IP Subject Alternative Names, in a comma-delimited + list. Only valid if the role allows IP SANs (which is the + default). + + lease + optional + Requested lease time. Cannot be greater than the role's + `lease_max` parameter. If not provided, the role's `lease` + value will be used. + +
+
+ +
Returns
+
+ + ```javascript + { + "lease_id": "pki/issue/test/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", + "private_key": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAnVHfwoKsUG1GDVyWB1AFroaKl2ImMBO8EnvGLRrmobIkQvh+\n...\nQN351pgTphi6nlCkGPzkDuwvtxSxiCWXQcaxrHAL7MiJpPzkIBq1\n-----END RSA PRIVATE KEY-----\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/revoke +#### POST + +
+
Description
+
+ Revokes a certificate using its serial number. This is an + alternative option to the standard method of revoking + using Vault lease IDs. A successful revocation will + rotate the CRL. +

This is a root-protected endpoint. +
+ +
Method
+
POST
+ +
URL
+
`/pki/revoke`
+ +
Parameters
+
+
    +
  • + serial + required + The serial number of the certificate to revoke, in + hyphen-separated or colon-separated octal. +
  • +
+
+ +
Returns
+
+ ```javascript + { + "data": { + "revocation_time": 1433269787 + } + } + ``` +
+
+ +### /pki/revoked/ +#### GET + +
+
Description
+
+ Retrieves a revoked certificate and its revocation time. The serial + number must be in either hyphen-separated or colon-separated octal format. +

This is a root-protected endpoint. +
+ +
Method
+
GET
+ +
URL
+
`/pki/revoked/`
+ +
Parameters
+
+ None +
+ +
Returns
+
+ + ```javascript + { + "data": { + "revocation_time": 1433269787, + "certificate": "-----BEGIN CERTIFICATE-----\nMIIGmDCCBYCgAwIBAgIHBzEB3fTzhTANBgkqhkiG9w0BAQsFADCBjDELMAkGA1UE\n..." + } + } + ... + ``` + +
+
+ +### /pki/roles/ +#### POST + +
+
Description
+
+ Creates or updates the role definition. Note that + the `allowed_base_domain`, `allow_token_displayname`, + `allow_subdomains`, and `allow_any_name` attributes + are additive; between them nearly and across multiple + roles nearly any issuing policy can be accommodated. + `server_flag`, `client_flag`, and `code_signing_flag` + are additive as well. If a client requests a + certificate that is not allowed by the CN policy in + the role, the request is denied. +
+ +
Method
+
POST
+ +
URL
+
`/pki/roles/`
+ +
Parameters
+
+
    +
  • + lease + optional + The lease value provided as a string duration + with time suffix. Hour is the largest suffix. + If not set, uses the value of `lease_max`. +
  • +
  • + lease_max + required + The maximum lease value provided as a string duration + with time suffix. Hour is the largest suffix. +
  • +
  • + allow_localhost + optional + If set, clients can request certificates for `localhost` + as one of the requested common names. This is useful + for testing and to allow clients on a single host to + talk securely. + Defaults to true. +
  • +
  • + allowed_base_domain + optional + If set, clients can request certificates for subdomains + directly off of this base domain. _This includes the + wildcard subdomain._ For instance, a base_domain of + `example.com` allows clients to request certificates for + `foo.example.com` and `*.example.com`. To allow further + levels of subdomains, enable the `allow_subdomains` option. + There is no default. +
  • +
  • + allow_token_displayname + optional + If set, clients can request certificates matching + the value of Display Name from the requesting token. + Remember, this stacks with the other CN options, + including `allowed_base_domain`. Defaults to `false`. +
  • +
  • + allow_subdomains + optional + If set, clients can request certificates with CNs that + are subdomains of the CNs allowed by the other role + options. _This includes wildcard subdomains._ This is + redundant when using the `allow_any_name` option. + Defaults to `false`. +
  • +
  • + allow_any_name + optional + If set, clients can request any CN. Useful in some + circumstances, but make sure you understand whether it + is appropriate for your installation before enabling it. + Defaults to `false`. +
  • +
  • + allow_ip_sans + optional + If set, clients can request IP Subject Alternative + Names. Unlike CNs, no authorization checking is + performed except to verify that the given values + are valid IP addresses. Defaults to `true`. +
  • + server_flag + optional + If set, certificates are flagged for server use. + Defaults to `true`. +
  • +
  • + client_flag + optional + If set, certificates are flagged for client use. + Defaults to `true`. +
  • +
  • + code_signing_flag + optional + If set, certificates are flagged for code signing + use. Defaults to `false`. +
  • +
  • + key_type + optional + The type of key to generate for generated private + keys. Currently, `rsa` and `ecdsa` are supported. + Defaults to `rsa`. +
  • +
  • + key_bits + optional + The number of bits to use for the generated keys. + Defaults to `2048`; this will need to be changed for + `ecdsa` keys. See https://golang.org/pkg/crypto/elliptic/#Curve + for an overview of allowed bit lengths for `ecdsa`. +
  • +
+
+ +
Returns
+
+ A `204` response code. +
+
+ +#### GET + +
+
Description
+
+ Queries the role definition. +
+ +
Method
+
GET
+ +
URL
+
`/pki/roles/`
+ +
Parameters
+
+ None +
+ +
Returns
+
+ + ```javascript + { + "data": { + "allow_any_name": false, + "allow_ip_sans": true, + "allow_localhost": true, + "allow_subdomains": false, + "allow_token_displayname": false, + "allowed_base_domain": "example.com", + "client_flag": true, + "code_signing_flag": false, + "key_bits": 2048, + "key_type": "rsa", + "lease": "6h", + "lease_max": "12h", + "server_flag": true + } + } + ``` + +
+
+ + +#### DELETE + +
+
Description
+
+ Deletes the role definition. +
+ +
Method
+
DELETE
+ +
URL
+
`/pki/roles/`
+ +
Parameters
+
+ None +
+ +
Returns
+
+ A `204` response code. +
+
diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index ea291a999f..f4a31be867 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -106,6 +106,10 @@ Consul + > + PKI (Certificates) + + > PostgreSQL From 20ac7a46f7914b6a25e6a94637aecb4ea6a0f4d0 Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Fri, 5 Jun 2015 14:03:57 -0400 Subject: [PATCH 02/15] Add acceptance tests * CA bundle uploading * Basic role creation * Common Name restrictions * IP SAN restrictions * EC + RSA keys * Various key usages * Lease times * CA fetching in various formats * DNS SAN handling Also, fix a bug when trying to get code signing certificates. Not tested: * Revocation (I believe this is impossible with the current testing framework) Commit contents (C)2015 Akamai Technologies, Inc. --- Godeps/Godeps.json | 4 + .../src/github.com/fatih/structs/.gitignore | 23 + .../src/github.com/fatih/structs/.travis.yml | 11 + .../src/github.com/fatih/structs/LICENSE | 21 + .../src/github.com/fatih/structs/README.md | 164 ++++ .../src/github.com/fatih/structs/field.go | 126 +++ .../github.com/fatih/structs/field_test.go | 324 +++++++ .../src/github.com/fatih/structs/structs.go | 449 +++++++++ .../fatih/structs/structs_example_test.go | 351 +++++++ .../github.com/fatih/structs/structs_test.go | 898 ++++++++++++++++++ .../src/github.com/fatih/structs/tags.go | 32 + .../src/github.com/fatih/structs/tags_test.go | 46 + builtin/logical/pki/backend_test.go | 440 +++++++++ builtin/logical/pki/cert_util.go | 14 +- builtin/logical/pki/path_config_ca.go | 4 +- builtin/logical/pki/path_issue.go | 8 +- builtin/logical/pki/path_roles.go | 39 +- website/source/docs/secrets/pki/index.html.md | 6 +- 18 files changed, 2930 insertions(+), 30 deletions(-) create mode 100644 Godeps/_workspace/src/github.com/fatih/structs/.gitignore create mode 100644 Godeps/_workspace/src/github.com/fatih/structs/.travis.yml create mode 100644 Godeps/_workspace/src/github.com/fatih/structs/LICENSE create mode 100644 Godeps/_workspace/src/github.com/fatih/structs/README.md create mode 100644 Godeps/_workspace/src/github.com/fatih/structs/field.go create mode 100644 Godeps/_workspace/src/github.com/fatih/structs/field_test.go create mode 100644 Godeps/_workspace/src/github.com/fatih/structs/structs.go create mode 100644 Godeps/_workspace/src/github.com/fatih/structs/structs_example_test.go create mode 100644 Godeps/_workspace/src/github.com/fatih/structs/structs_test.go create mode 100644 Godeps/_workspace/src/github.com/fatih/structs/tags.go create mode 100644 Godeps/_workspace/src/github.com/fatih/structs/tags_test.go create mode 100644 builtin/logical/pki/backend_test.go diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 3033aab6c7..b72705bf9e 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -63,6 +63,10 @@ "Comment": "v2.0.0-7-g73a8ef7", "Rev": "73a8ef737e8ea002281a28b4cb92a1de121ad4c6" }, + { + "ImportPath": "github.com/fatih/structs", + "Rev": "a9f7daa9c2729e97450c2da2feda19130a367d8f" + }, { "ImportPath": "github.com/go-sql-driver/mysql", "Comment": "v1.2-88-ga197e5d", diff --git a/Godeps/_workspace/src/github.com/fatih/structs/.gitignore b/Godeps/_workspace/src/github.com/fatih/structs/.gitignore new file mode 100644 index 0000000000..836562412f --- /dev/null +++ b/Godeps/_workspace/src/github.com/fatih/structs/.gitignore @@ -0,0 +1,23 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test diff --git a/Godeps/_workspace/src/github.com/fatih/structs/.travis.yml b/Godeps/_workspace/src/github.com/fatih/structs/.travis.yml new file mode 100644 index 0000000000..28381ef8e6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/fatih/structs/.travis.yml @@ -0,0 +1,11 @@ +language: go +go: 1.3 +before_install: +- go get github.com/axw/gocov/gocov +- go get github.com/mattn/goveralls +- go get code.google.com/p/go.tools/cmd/cover +script: +- $HOME/gopath/bin/goveralls -repotoken $COVERALLS_TOKEN +env: + global: + - secure: hkc+92KPmMFqIH9n4yWdnH1JpZjahmOyDJwpTh8Yl0JieJNG0XEXpOqNao27eA0cLF+UHdyjFeGcPUJKNmgE46AoQjtovt+ICjCXKR2yF6S2kKJcUOz/Vd6boZF7qHV06jjxyxOebpID5iSoW6UfFr001bFxpd3jaSLFTzSHWRQ= diff --git a/Godeps/_workspace/src/github.com/fatih/structs/LICENSE b/Godeps/_workspace/src/github.com/fatih/structs/LICENSE new file mode 100644 index 0000000000..34504e4b3e --- /dev/null +++ b/Godeps/_workspace/src/github.com/fatih/structs/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Fatih Arslan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/Godeps/_workspace/src/github.com/fatih/structs/README.md b/Godeps/_workspace/src/github.com/fatih/structs/README.md new file mode 100644 index 0000000000..752ba5717c --- /dev/null +++ b/Godeps/_workspace/src/github.com/fatih/structs/README.md @@ -0,0 +1,164 @@ +# Structs [![GoDoc](http://img.shields.io/badge/go-documentation-blue.svg?style=flat-square)](http://godoc.org/github.com/fatih/structs) [![Build Status](http://img.shields.io/travis/fatih/structs.svg?style=flat-square)](https://travis-ci.org/fatih/structs) [![Coverage Status](http://img.shields.io/coveralls/fatih/structs.svg?style=flat-square)](https://coveralls.io/r/fatih/structs) + +Structs contains various utilities to work with Go (Golang) structs. It was +initially used by me to convert a struct into a `map[string]interface{}`. With +time I've added other utilities for structs. It's basically a high level +package based on primitives from the reflect package. Feel free to add new +functions or improve the existing code. + +## Install + +```bash +go get github.com/fatih/structs +``` + +## Usage and Examples + +Just like the standard lib `strings`, `bytes` and co packages, `structs` has +many global functions to manipulate or organize your struct data. Lets define +and declare a struct: + +```go +type Server struct { + Name string `json:"name,omitempty"` + ID int + Enabled bool + users []string // not exported + http.Server // embedded +} + +server := &Server{ + Name: "gopher", + ID: 123456, + Enabled: true, +} +``` + +```go +// Convert a struct to a map[string]interface{} +// => {"Name":"gopher", "ID":123456, "Enabled":true} +m := structs.Map(server) + +// Convert the values of a struct to a []interface{} +// => ["gopher", 123456, true] +v := structs.Values(server) + +// Convert the names of a struct to a []string +// (see "Names methods" for more info about fields) +n := structs.Names(server) + +// Convert the values of a struct to a []*Field +// (see "Field methods" for more info about fields) +f := structs.Fields(server) + +// Return the struct name => "Server" +n := structs.Name(server) + +// Check if any field of a struct is initialized or not. +h := structs.HasZero(server) + +// Check if all fields of a struct is initialized or not. +z := structs.IsZero(server) + +// Check if server is a struct or a pointer to struct +i := structs.IsStruct(server) +``` + +### Struct methods + +The structs functions can be also used as independent methods by creating a new +`*structs.Struct`. This is handy if you want to have more control over the +structs (such as retrieving a single Field). + +```go +// Create a new struct type: +s := structs.New(server) + +m := s.Map() // Get a map[string]interface{} +v := s.Values() // Get a []interface{} +f := s.Fields() // Get a []*Field +n := s.Names() // Get a []string +f := s.Field(name) // Get a *Field based on the given field name +f, ok := s.FieldOk(name) // Get a *Field based on the given field name +n := s.Name() // Get the struct name +h := s.HasZero() // Check if any field is initialized +z := s.IsZero() // Check if all fields are initialized +``` + +### Field methods + +We can easily examine a single Field for more detail. Below you can see how we +get and interact with various field methods: + + +```go +s := structs.New(server) + +// Get the Field struct for the "Name" field +name := s.Field("Name") + +// Get the underlying value, value => "gopher" +value := name.Value().(string) + +// Set the field's value +name.Set("another gopher") + +// Get the field's kind, kind => "string" +name.Kind() + +// Check if the field is exported or not +if name.IsExported() { + fmt.Println("Name field is exported") +} + +// Check if the value is a zero value, such as "" for string, 0 for int +if !name.IsZero() { + fmt.Println("Name is initialized") +} + +// Check if the field is an anonymous (embedded) field +if !name.IsEmbedded() { + fmt.Println("Name is not an embedded field") +} + +// Get the Field's tag value for tag name "json", tag value => "name,omitempty" +tagValue := name.Tag("json") +``` + +Nested structs are supported too: + +```go +addrField := s.Field("Server").Field("Addr") + +// Get the value for addr +a := addrField.Value().(string) + +// Or get all fields +httpServer := s.Field("Server").Fields() +``` + +We can also get a slice of Fields from the Struct type to iterate over all +fields. This is handy if you wish to examine all fields: + +```go +// Convert the fields of a struct to a []*Field +fields := s.Fields() + +for _, f := range fields { + fmt.Printf("field name: %+v\n", f.Name()) + + if f.IsExported() { + fmt.Printf("value : %+v\n", f.Value()) + fmt.Printf("is zero : %+v\n", f.IsZero()) + } +} +``` + +## Credits + + * [Fatih Arslan](https://github.com/fatih) + * [Cihangir Savas](https://github.com/cihangir) + +## License + +The MIT License (MIT) - see LICENSE.md for more details diff --git a/Godeps/_workspace/src/github.com/fatih/structs/field.go b/Godeps/_workspace/src/github.com/fatih/structs/field.go new file mode 100644 index 0000000000..4b5e15b067 --- /dev/null +++ b/Godeps/_workspace/src/github.com/fatih/structs/field.go @@ -0,0 +1,126 @@ +package structs + +import ( + "errors" + "fmt" + "reflect" +) + +var ( + errNotExported = errors.New("field is not exported") + errNotSettable = errors.New("field is not settable") +) + +// Field represents a single struct field that encapsulates high level +// functions around the field. +type Field struct { + value reflect.Value + field reflect.StructField + defaultTag string +} + +// Tag returns the value associated with key in the tag string. If there is no +// such key in the tag, Tag returns the empty string. +func (f *Field) Tag(key string) string { + return f.field.Tag.Get(key) +} + +// Value returns the underlying value of of the field. It panics if the field +// is not exported. +func (f *Field) Value() interface{} { + return f.value.Interface() +} + +// IsEmbedded returns true if the given field is an anonymous field (embedded) +func (f *Field) IsEmbedded() bool { + return f.field.Anonymous +} + +// IsExported returns true if the given field is exported. +func (f *Field) IsExported() bool { + return f.field.PkgPath == "" +} + +// IsZero returns true if the given field is not initalized (has a zero value). +// It panics if the field is not exported. +func (f *Field) IsZero() bool { + zero := reflect.Zero(f.value.Type()).Interface() + current := f.Value() + + return reflect.DeepEqual(current, zero) +} + +// Name returns the name of the given field +func (f *Field) Name() string { + return f.field.Name +} + +// Kind returns the fields kind, such as "string", "map", "bool", etc .. +func (f *Field) Kind() reflect.Kind { + return f.value.Kind() +} + +// Set sets the field to given value v. It retuns an error if the field is not +// settable (not addresable or not exported) or if the given value's type +// doesn't match the fields type. +func (f *Field) Set(val interface{}) error { + // we can't set unexported fields, so be sure this field is exported + if !f.IsExported() { + return errNotExported + } + + // do we get here? not sure... + if !f.value.CanSet() { + return errNotSettable + } + + given := reflect.ValueOf(val) + + if f.value.Kind() != given.Kind() { + return fmt.Errorf("wrong kind. got: %s want: %s", given.Kind(), f.value.Kind()) + } + + f.value.Set(given) + return nil +} + +// Fields returns a slice of Fields. This is particular handy to get the fields +// of a nested struct . A struct tag with the content of "-" ignores the +// checking of that particular field. Example: +// +// // Field is ignored by this package. +// Field *http.Request `structs:"-"` +// +// It panics if field is not exported or if field's kind is not struct +func (f *Field) Fields() []*Field { + return getFields(f.value, f.defaultTag) +} + +// Field returns the field from a nested struct. It panics if the nested struct +// is not exported or if the field was not found. +func (f *Field) Field(name string) *Field { + field, ok := f.FieldOk(name) + if !ok { + panic("field not found") + } + + return field +} + +// Field returns the field from a nested struct. The boolean returns true if +// the field was found. It panics if the nested struct is not exported or if +// the field was not found. +func (f *Field) FieldOk(name string) (*Field, bool) { + v := strctVal(f.value.Interface()) + t := v.Type() + + field, ok := t.FieldByName(name) + if !ok { + return nil, false + } + + return &Field{ + field: field, + value: v.FieldByName(name), + }, true +} diff --git a/Godeps/_workspace/src/github.com/fatih/structs/field_test.go b/Godeps/_workspace/src/github.com/fatih/structs/field_test.go new file mode 100644 index 0000000000..46187d6559 --- /dev/null +++ b/Godeps/_workspace/src/github.com/fatih/structs/field_test.go @@ -0,0 +1,324 @@ +package structs + +import ( + "reflect" + "testing" +) + +// A test struct that defines all cases +type Foo struct { + A string + B int `structs:"y"` + C bool `json:"c"` + d string // not exported + E *Baz + x string `xml:"x"` // not exported, with tag + Y []string + Z map[string]interface{} + *Bar // embedded +} + +type Baz struct { + A string + B int +} + +type Bar struct { + E string + F int + g []string +} + +func newStruct() *Struct { + b := &Bar{ + E: "example", + F: 2, + g: []string{"zeynep", "fatih"}, + } + + // B and x is not initialized for testing + f := &Foo{ + A: "gopher", + C: true, + d: "small", + E: nil, + Y: []string{"example"}, + Z: nil, + } + f.Bar = b + + return New(f) +} + +func TestField_Set(t *testing.T) { + s := newStruct() + + f := s.Field("A") + err := f.Set("fatih") + if err != nil { + t.Error(err) + } + + if f.Value().(string) != "fatih" { + t.Errorf("Setted value is wrong: %s want: %s", f.Value().(string), "fatih") + } + + f = s.Field("Y") + err = f.Set([]string{"override", "with", "this"}) + if err != nil { + t.Error(err) + } + + sliceLen := len(f.Value().([]string)) + if sliceLen != 3 { + t.Errorf("Setted values slice length is wrong: %d, want: %d", sliceLen, 3) + } + + f = s.Field("C") + err = f.Set(false) + if err != nil { + t.Error(err) + } + + if f.Value().(bool) { + t.Errorf("Setted value is wrong: %s want: %s", f.Value().(bool), false) + } + + // let's pass a different type + f = s.Field("A") + err = f.Set(123) // Field A is of type string, but we are going to pass an integer + if err == nil { + t.Error("Setting a field's value with a different type than the field's type should return an error") + } + + // old value should be still there :) + if f.Value().(string) != "fatih" { + t.Errorf("Setted value is wrong: %s want: %s", f.Value().(string), "fatih") + } + + // let's access an unexported field, which should give an error + f = s.Field("d") + err = f.Set("large") + if err != errNotExported { + t.Error(err) + } + + // let's set a pointer to struct + b := &Bar{ + E: "gopher", + F: 2, + } + + f = s.Field("Bar") + err = f.Set(b) + if err != nil { + t.Error(err) + } + + baz := &Baz{ + A: "helloWorld", + B: 42, + } + + f = s.Field("E") + err = f.Set(baz) + if err != nil { + t.Error(err) + } + + ba := s.Field("E").Value().(*Baz) + + if ba.A != "helloWorld" { + t.Errorf("could not set baz. Got: %s Want: helloWorld", ba.A) + } +} + +func TestField(t *testing.T) { + s := newStruct() + + defer func() { + err := recover() + if err == nil { + t.Error("Retrieveing a non existing field from the struct should panic") + } + }() + + _ = s.Field("no-field") +} + +func TestField_Kind(t *testing.T) { + s := newStruct() + + f := s.Field("A") + if f.Kind() != reflect.String { + t.Errorf("Field A has wrong kind: %s want: %s", f.Kind(), reflect.String) + } + + f = s.Field("B") + if f.Kind() != reflect.Int { + t.Errorf("Field B has wrong kind: %s want: %s", f.Kind(), reflect.Int) + } + + // unexported + f = s.Field("d") + if f.Kind() != reflect.String { + t.Errorf("Field d has wrong kind: %s want: %s", f.Kind(), reflect.String) + } +} + +func TestField_Tag(t *testing.T) { + s := newStruct() + + v := s.Field("B").Tag("json") + if v != "" { + t.Errorf("Field's tag value of a non existing tag should return empty, got: %s", v) + } + + v = s.Field("C").Tag("json") + if v != "c" { + t.Errorf("Field's tag value of the existing field C should return 'c', got: %s", v) + } + + v = s.Field("d").Tag("json") + if v != "" { + t.Errorf("Field's tag value of a non exported field should return empty, got: %s", v) + } + + v = s.Field("x").Tag("xml") + if v != "x" { + t.Errorf("Field's tag value of a non exported field with a tag should return 'x', got: %s", v) + } + + v = s.Field("A").Tag("json") + if v != "" { + t.Errorf("Field's tag value of a existing field without a tag should return empty, got: %s", v) + } +} + +func TestField_Value(t *testing.T) { + s := newStruct() + + v := s.Field("A").Value() + val, ok := v.(string) + if !ok { + t.Errorf("Field's value of a A should be string") + } + + if val != "gopher" { + t.Errorf("Field's value of a existing tag should return 'gopher', got: %s", val) + } + + defer func() { + err := recover() + if err == nil { + t.Error("Value of a non exported field from the field should panic") + } + }() + + // should panic + _ = s.Field("d").Value() +} + +func TestField_IsEmbedded(t *testing.T) { + s := newStruct() + + if !s.Field("Bar").IsEmbedded() { + t.Errorf("Fields 'Bar' field is an embedded field") + } + + if s.Field("d").IsEmbedded() { + t.Errorf("Fields 'd' field is not an embedded field") + } +} + +func TestField_IsExported(t *testing.T) { + s := newStruct() + + if !s.Field("Bar").IsExported() { + t.Errorf("Fields 'Bar' field is an exported field") + } + + if !s.Field("A").IsExported() { + t.Errorf("Fields 'A' field is an exported field") + } + + if s.Field("d").IsExported() { + t.Errorf("Fields 'd' field is not an exported field") + } +} + +func TestField_IsZero(t *testing.T) { + s := newStruct() + + if s.Field("A").IsZero() { + t.Errorf("Fields 'A' field is an initialized field") + } + + if !s.Field("B").IsZero() { + t.Errorf("Fields 'B' field is not an initialized field") + } +} + +func TestField_Name(t *testing.T) { + s := newStruct() + + if s.Field("A").Name() != "A" { + t.Errorf("Fields 'A' field should have the name 'A'") + } +} + +func TestField_Field(t *testing.T) { + s := newStruct() + + e := s.Field("Bar").Field("E") + + val, ok := e.Value().(string) + if !ok { + t.Error("The value of the field 'e' inside 'Bar' struct should be string") + } + + if val != "example" { + t.Errorf("The value of 'e' should be 'example, got: %s", val) + } + + defer func() { + err := recover() + if err == nil { + t.Error("Field of a non existing nested struct should panic") + } + }() + + _ = s.Field("Bar").Field("e") +} + +func TestField_Fields(t *testing.T) { + s := newStruct() + fields := s.Field("Bar").Fields() + + if len(fields) != 3 { + t.Errorf("We expect 3 fields in embedded struct, was: %d", len(fields)) + } +} + +func TestField_FieldOk(t *testing.T) { + s := newStruct() + + b, ok := s.FieldOk("Bar") + if !ok { + t.Error("The field 'Bar' should exists.") + } + + e, ok := b.FieldOk("E") + if !ok { + t.Error("The field 'E' should exists.") + } + + val, ok := e.Value().(string) + if !ok { + t.Error("The value of the field 'e' inside 'Bar' struct should be string") + } + + if val != "example" { + t.Errorf("The value of 'e' should be 'example, got: %s", val) + } +} diff --git a/Godeps/_workspace/src/github.com/fatih/structs/structs.go b/Godeps/_workspace/src/github.com/fatih/structs/structs.go new file mode 100644 index 0000000000..a0b77e67dd --- /dev/null +++ b/Godeps/_workspace/src/github.com/fatih/structs/structs.go @@ -0,0 +1,449 @@ +// Package structs contains various utilities functions to work with structs. +package structs + +import "reflect" + +var ( + // DefaultTagName is the default tag name for struct fields which provides + // a more granular to tweak certain structs. Lookup the necessary functions + // for more info. + DefaultTagName = "structs" // struct's field default tag name +) + +// Struct encapsulates a struct type to provide several high level functions +// around the struct. +type Struct struct { + raw interface{} + value reflect.Value + TagName string +} + +// New returns a new *Struct with the struct s. It panics if the s's kind is +// not struct. +func New(s interface{}) *Struct { + return &Struct{ + raw: s, + value: strctVal(s), + TagName: DefaultTagName, + } +} + +// Map converts the given struct to a map[string]interface{}, where the keys +// of the map are the field names and the values of the map the associated +// values of the fields. The default key string is the struct field name but +// can be changed in the struct field's tag value. The "structs" key in the +// struct's field tag value is the key name. Example: +// +// // Field appears in map as key "myName". +// Name string `structs:"myName"` +// +// A tag value with the content of "-" ignores that particular field. Example: +// +// // Field is ignored by this package. +// Field bool `structs:"-"` +// +// A tag value with the option of "omitnested" stops iterating further if the type +// is a struct. Example: +// +// // Field is not processed further by this package. +// Field time.Time `structs:"myName,omitnested"` +// Field *http.Request `structs:",omitnested"` +// +// A tag value with the option of "omitempty" ignores that particular field if +// the field value is empty. Example: +// +// // Field appears in map as key "myName", but the field is +// // skipped if empty. +// Field string `structs:"myName,omitempty"` +// +// // Field appears in map as key "Field" (the default), but +// // the field is skipped if empty. +// Field string `structs:",omitempty"` +// +// Note that only exported fields of a struct can be accessed, non exported +// fields will be neglected. +func (s *Struct) Map() map[string]interface{} { + out := make(map[string]interface{}) + + fields := s.structFields() + + for _, field := range fields { + name := field.Name + val := s.value.FieldByName(name) + + var finalVal interface{} + + tagName, tagOpts := parseTag(field.Tag.Get(s.TagName)) + if tagName != "" { + name = tagName + } + + // if the value is a zero value and the field is marked as omitempty do + // not include + if tagOpts.Has("omitempty") { + zero := reflect.Zero(val.Type()).Interface() + current := val.Interface() + + if reflect.DeepEqual(current, zero) { + continue + } + } + + if IsStruct(val.Interface()) && !tagOpts.Has("omitnested") { + // look out for embedded structs, and convert them to a + // map[string]interface{} too + n := New(val.Interface()) + n.TagName = s.TagName + finalVal = n.Map() + } else { + finalVal = val.Interface() + } + + out[name] = finalVal + } + + return out +} + +// Values converts the given s struct's field values to a []interface{}. A +// struct tag with the content of "-" ignores the that particular field. +// Example: +// +// // Field is ignored by this package. +// Field int `structs:"-"` +// +// A value with the option of "omitnested" stops iterating further if the type +// is a struct. Example: +// +// // Fields is not processed further by this package. +// Field time.Time `structs:",omitnested"` +// Field *http.Request `structs:",omitnested"` +// +// A tag value with the option of "omitempty" ignores that particular field and +// is not added to the values if the field value is empty. Example: +// +// // Field is skipped if empty +// Field string `structs:",omitempty"` +// +// Note that only exported fields of a struct can be accessed, non exported +// fields will be neglected. +func (s *Struct) Values() []interface{} { + fields := s.structFields() + + var t []interface{} + + for _, field := range fields { + val := s.value.FieldByName(field.Name) + + _, tagOpts := parseTag(field.Tag.Get(s.TagName)) + + // if the value is a zero value and the field is marked as omitempty do + // not include + if tagOpts.Has("omitempty") { + zero := reflect.Zero(val.Type()).Interface() + current := val.Interface() + + if reflect.DeepEqual(current, zero) { + continue + } + } + + if IsStruct(val.Interface()) && !tagOpts.Has("omitnested") { + // look out for embedded structs, and convert them to a + // []interface{} to be added to the final values slice + for _, embeddedVal := range Values(val.Interface()) { + t = append(t, embeddedVal) + } + } else { + t = append(t, val.Interface()) + } + } + + return t +} + +// Fields returns a slice of Fields. A struct tag with the content of "-" +// ignores the checking of that particular field. Example: +// +// // Field is ignored by this package. +// Field bool `structs:"-"` +// +// It panics if s's kind is not struct. +func (s *Struct) Fields() []*Field { + return getFields(s.value, s.TagName) +} + +// Names returns a slice of field names. A struct tag with the content of "-" +// ignores the checking of that particular field. Example: +// +// // Field is ignored by this package. +// Field bool `structs:"-"` +// +// It panics if s's kind is not struct. +func (s *Struct) Names() []string { + fields := getFields(s.value, s.TagName) + + names := make([]string, len(fields)) + + for i, field := range fields { + names[i] = field.Name() + } + + return names +} + +func getFields(v reflect.Value, tagName string) []*Field { + if v.Kind() == reflect.Ptr { + v = v.Elem() + } + + t := v.Type() + + var fields []*Field + + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + + if tag := field.Tag.Get(tagName); tag == "-" { + continue + } + + f := &Field{ + field: field, + value: v.FieldByName(field.Name), + } + + fields = append(fields, f) + + } + + return fields +} + +// Field returns a new Field struct that provides several high level functions +// around a single struct field entity. It panics if the field is not found. +func (s *Struct) Field(name string) *Field { + f, ok := s.FieldOk(name) + if !ok { + panic("field not found") + } + + return f +} + +// Field returns a new Field struct that provides several high level functions +// around a single struct field entity. The boolean returns true if the field +// was found. +func (s *Struct) FieldOk(name string) (*Field, bool) { + t := s.value.Type() + + field, ok := t.FieldByName(name) + if !ok { + return nil, false + } + + return &Field{ + field: field, + value: s.value.FieldByName(name), + defaultTag: s.TagName, + }, true +} + +// IsZero returns true if all fields in a struct is a zero value (not +// initialized) A struct tag with the content of "-" ignores the checking of +// that particular field. Example: +// +// // Field is ignored by this package. +// Field bool `structs:"-"` +// +// A value with the option of "omitnested" stops iterating further if the type +// is a struct. Example: +// +// // Field is not processed further by this package. +// Field time.Time `structs:"myName,omitnested"` +// Field *http.Request `structs:",omitnested"` +// +// Note that only exported fields of a struct can be accessed, non exported +// fields will be neglected. It panics if s's kind is not struct. +func (s *Struct) IsZero() bool { + fields := s.structFields() + + for _, field := range fields { + val := s.value.FieldByName(field.Name) + + _, tagOpts := parseTag(field.Tag.Get(s.TagName)) + + if IsStruct(val.Interface()) && !tagOpts.Has("omitnested") { + ok := IsZero(val.Interface()) + if !ok { + return false + } + + continue + } + + // zero value of the given field, such as "" for string, 0 for int + zero := reflect.Zero(val.Type()).Interface() + + // current value of the given field + current := val.Interface() + + if !reflect.DeepEqual(current, zero) { + return false + } + } + + return true +} + +// HasZero returns true if a field in a struct is not initialized (zero value). +// A struct tag with the content of "-" ignores the checking of that particular +// field. Example: +// +// // Field is ignored by this package. +// Field bool `structs:"-"` +// +// A value with the option of "omitnested" stops iterating further if the type +// is a struct. Example: +// +// // Field is not processed further by this package. +// Field time.Time `structs:"myName,omitnested"` +// Field *http.Request `structs:",omitnested"` +// +// Note that only exported fields of a struct can be accessed, non exported +// fields will be neglected. It panics if s's kind is not struct. +func (s *Struct) HasZero() bool { + fields := s.structFields() + + for _, field := range fields { + val := s.value.FieldByName(field.Name) + + _, tagOpts := parseTag(field.Tag.Get(s.TagName)) + + if IsStruct(val.Interface()) && !tagOpts.Has("omitnested") { + ok := HasZero(val.Interface()) + if ok { + return true + } + + continue + } + + // zero value of the given field, such as "" for string, 0 for int + zero := reflect.Zero(val.Type()).Interface() + + // current value of the given field + current := val.Interface() + + if reflect.DeepEqual(current, zero) { + return true + } + } + + return false +} + +// Name returns the structs's type name within its package. For more info refer +// to Name() function. +func (s *Struct) Name() string { + return s.value.Type().Name() +} + +// structFields returns the exported struct fields for a given s struct. This +// is a convenient helper method to avoid duplicate code in some of the +// functions. +func (s *Struct) structFields() []reflect.StructField { + t := s.value.Type() + + var f []reflect.StructField + + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + // we can't access the value of unexported fields + if field.PkgPath != "" { + continue + } + + // don't check if it's omitted + if tag := field.Tag.Get(s.TagName); tag == "-" { + continue + } + + f = append(f, field) + } + + return f +} + +func strctVal(s interface{}) reflect.Value { + v := reflect.ValueOf(s) + + // if pointer get the underlying element≤ + if v.Kind() == reflect.Ptr { + v = v.Elem() + } + + if v.Kind() != reflect.Struct { + panic("not struct") + } + + return v +} + +// Map converts the given struct to a map[string]interface{}. For more info +// refer to Struct types Map() method. It panics if s's kind is not struct. +func Map(s interface{}) map[string]interface{} { + return New(s).Map() +} + +// Values converts the given struct to a []interface{}. For more info refer to +// Struct types Values() method. It panics if s's kind is not struct. +func Values(s interface{}) []interface{} { + return New(s).Values() +} + +// Fields returns a slice of *Field. For more info refer to Struct types +// Fields() method. It panics if s's kind is not struct. +func Fields(s interface{}) []*Field { + return New(s).Fields() +} + +// Names returns a slice of field names. For more info refer to Struct types +// Names() method. It panics if s's kind is not struct. +func Names(s interface{}) []string { + return New(s).Names() +} + +// IsZero returns true if all fields is equal to a zero value. For more info +// refer to Struct types IsZero() method. It panics if s's kind is not struct. +func IsZero(s interface{}) bool { + return New(s).IsZero() +} + +// HasZero returns true if any field is equal to a zero value. For more info +// refer to Struct types HasZero() method. It panics if s's kind is not struct. +func HasZero(s interface{}) bool { + return New(s).HasZero() +} + +// IsStruct returns true if the given variable is a struct or a pointer to +// struct. +func IsStruct(s interface{}) bool { + v := reflect.ValueOf(s) + if v.Kind() == reflect.Ptr { + v = v.Elem() + } + + // uninitialized zero value of a struct + if v.Kind() == reflect.Invalid { + return false + } + + return v.Kind() == reflect.Struct +} + +// Name returns the structs's type name within its package. It returns an +// empty string for unnamed types. It panics if s's kind is not struct. +func Name(s interface{}) string { + return New(s).Name() +} diff --git a/Godeps/_workspace/src/github.com/fatih/structs/structs_example_test.go b/Godeps/_workspace/src/github.com/fatih/structs/structs_example_test.go new file mode 100644 index 0000000000..32bb82937a --- /dev/null +++ b/Godeps/_workspace/src/github.com/fatih/structs/structs_example_test.go @@ -0,0 +1,351 @@ +package structs + +import ( + "fmt" + "time" +) + +func ExampleNew() { + type Server struct { + Name string + ID int32 + Enabled bool + } + + server := &Server{ + Name: "Arslan", + ID: 123456, + Enabled: true, + } + + s := New(server) + + fmt.Printf("Name : %v\n", s.Name()) + fmt.Printf("Values : %v\n", s.Values()) + fmt.Printf("Value of ID : %v\n", s.Field("ID").Value()) + // Output: + // Name : Server + // Values : [Arslan 123456 true] + // Value of ID : 123456 + +} + +func ExampleMap() { + type Server struct { + Name string + ID int32 + Enabled bool + } + + s := &Server{ + Name: "Arslan", + ID: 123456, + Enabled: true, + } + + m := Map(s) + + fmt.Printf("%#v\n", m["Name"]) + fmt.Printf("%#v\n", m["ID"]) + fmt.Printf("%#v\n", m["Enabled"]) + // Output: + // "Arslan" + // 123456 + // true + +} + +func ExampleMap_tags() { + // Custom tags can change the map keys instead of using the fields name + type Server struct { + Name string `structs:"server_name"` + ID int32 `structs:"server_id"` + Enabled bool `structs:"enabled"` + } + + s := &Server{ + Name: "Zeynep", + ID: 789012, + } + + m := Map(s) + + // access them by the custom tags defined above + fmt.Printf("%#v\n", m["server_name"]) + fmt.Printf("%#v\n", m["server_id"]) + fmt.Printf("%#v\n", m["enabled"]) + // Output: + // "Zeynep" + // 789012 + // false + +} + +func ExampleMap_nested() { + // By default field with struct types are processed too. We can stop + // processing them via "omitnested" tag option. + type Server struct { + Name string `structs:"server_name"` + ID int32 `structs:"server_id"` + Time time.Time `structs:"time,omitnested"` // do not convert to map[string]interface{} + } + + const shortForm = "2006-Jan-02" + t, _ := time.Parse("2006-Jan-02", "2013-Feb-03") + + s := &Server{ + Name: "Zeynep", + ID: 789012, + Time: t, + } + + m := Map(s) + + // access them by the custom tags defined above + fmt.Printf("%v\n", m["server_name"]) + fmt.Printf("%v\n", m["server_id"]) + fmt.Printf("%v\n", m["time"].(time.Time)) + // Output: + // Zeynep + // 789012 + // 2013-02-03 00:00:00 +0000 UTC +} + +func ExampleMap_omitEmpty() { + // By default field with struct types of zero values are processed too. We + // can stop processing them via "omitempty" tag option. + type Server struct { + Name string `structs:",omitempty"` + ID int32 `structs:"server_id,omitempty"` + Location string + } + + // Only add location + s := &Server{ + Location: "Tokyo", + } + + m := Map(s) + + // map contains only the Location field + fmt.Printf("%v\n", m) + // Output: + // map[Location:Tokyo] +} + +func ExampleValues() { + type Server struct { + Name string + ID int32 + Enabled bool + } + + s := &Server{ + Name: "Fatih", + ID: 135790, + Enabled: false, + } + + m := Values(s) + + fmt.Printf("Values: %+v\n", m) + // Output: + // Values: [Fatih 135790 false] +} + +func ExampleValues_omitEmpty() { + // By default field with struct types of zero values are processed too. We + // can stop processing them via "omitempty" tag option. + type Server struct { + Name string `structs:",omitempty"` + ID int32 `structs:"server_id,omitempty"` + Location string + } + + // Only add location + s := &Server{ + Location: "Ankara", + } + + m := Values(s) + + // values contains only the Location field + fmt.Printf("Values: %+v\n", m) + // Output: + // Values: [Ankara] +} + +func ExampleValues_tags() { + type Location struct { + City string + Country string + } + + type Server struct { + Name string + ID int32 + Enabled bool + Location Location `structs:"-"` // values from location are not included anymore + } + + s := &Server{ + Name: "Fatih", + ID: 135790, + Enabled: false, + Location: Location{City: "Ankara", Country: "Turkey"}, + } + + // Let get all values from the struct s. Note that we don't include values + // from the Location field + m := Values(s) + + fmt.Printf("Values: %+v\n", m) + // Output: + // Values: [Fatih 135790 false] +} + +func ExampleFields() { + type Access struct { + Name string + LastAccessed time.Time + Number int + } + + s := &Access{ + Name: "Fatih", + LastAccessed: time.Now(), + Number: 1234567, + } + + fields := Fields(s) + + for i, field := range fields { + fmt.Printf("[%d] %+v\n", i, field.Name()) + } + + // Output: + // [0] Name + // [1] LastAccessed + // [2] Number +} + +func ExampleFields_nested() { + type Person struct { + Name string + Number int + } + + type Access struct { + Person Person + HasPermission bool + LastAccessed time.Time + } + + s := &Access{ + Person: Person{Name: "fatih", Number: 1234567}, + LastAccessed: time.Now(), + HasPermission: true, + } + + // Let's get all fields from the struct s. + fields := Fields(s) + + for _, field := range fields { + if field.Name() == "Person" { + fmt.Printf("Access.Person.Name: %+v\n", field.Field("Name").Value()) + } + } + + // Output: + // Access.Person.Name: fatih +} + +func ExampleField() { + type Person struct { + Name string + Number int + } + + type Access struct { + Person Person + HasPermission bool + LastAccessed time.Time + } + + access := &Access{ + Person: Person{Name: "fatih", Number: 1234567}, + LastAccessed: time.Now(), + HasPermission: true, + } + + // Create a new Struct type + s := New(access) + + // Get the Field type for "Person" field + p := s.Field("Person") + + // Get the underlying "Name field" and print the value of it + name := p.Field("Name") + + fmt.Printf("Value of Person.Access.Name: %+v\n", name.Value()) + + // Output: + // Value of Person.Access.Name: fatih + +} + +func ExampleIsZero() { + type Server struct { + Name string + ID int32 + Enabled bool + } + + // Nothing is initalized + a := &Server{} + isZeroA := IsZero(a) + + // Name and Enabled is initialized, but not ID + b := &Server{ + Name: "Golang", + Enabled: true, + } + isZeroB := IsZero(b) + + fmt.Printf("%#v\n", isZeroA) + fmt.Printf("%#v\n", isZeroB) + // Output: + // true + // false +} + +func ExampleHasZero() { + // Let's define an Access struct. Note that the "Enabled" field is not + // going to be checked because we added the "structs" tag to the field. + type Access struct { + Name string + LastAccessed time.Time + Number int + Enabled bool `structs:"-"` + } + + // Name and Number is not initialized. + a := &Access{ + LastAccessed: time.Now(), + } + hasZeroA := HasZero(a) + + // Name and Number is initialized. + b := &Access{ + Name: "Fatih", + LastAccessed: time.Now(), + Number: 12345, + } + hasZeroB := HasZero(b) + + fmt.Printf("%#v\n", hasZeroA) + fmt.Printf("%#v\n", hasZeroB) + // Output: + // true + // false +} diff --git a/Godeps/_workspace/src/github.com/fatih/structs/structs_test.go b/Godeps/_workspace/src/github.com/fatih/structs/structs_test.go new file mode 100644 index 0000000000..14e3de72f1 --- /dev/null +++ b/Godeps/_workspace/src/github.com/fatih/structs/structs_test.go @@ -0,0 +1,898 @@ +package structs + +import ( + "fmt" + "reflect" + "testing" + "time" +) + +func TestMapNonStruct(t *testing.T) { + foo := []string{"foo"} + + defer func() { + err := recover() + if err == nil { + t.Error("Passing a non struct into Map should panic") + } + }() + + // this should panic. We are going to recover and and test it + _ = Map(foo) +} + +func TestStructIndexes(t *testing.T) { + type C struct { + something int + Props map[string]interface{} + } + + defer func() { + err := recover() + if err != nil { + fmt.Printf("err %+v\n", err) + t.Error("Using mixed indexes should not panic") + } + }() + + // They should not panic + _ = Map(&C{}) + _ = Fields(&C{}) + _ = Values(&C{}) + _ = IsZero(&C{}) + _ = HasZero(&C{}) +} + +func TestMap(t *testing.T) { + var T = struct { + A string + B int + C bool + }{ + A: "a-value", + B: 2, + C: true, + } + + a := Map(T) + + if typ := reflect.TypeOf(a).Kind(); typ != reflect.Map { + t.Errorf("Map should return a map type, got: %v", typ) + } + + // we have three fields + if len(a) != 3 { + t.Errorf("Map should return a map of len 3, got: %d", len(a)) + } + + inMap := func(val interface{}) bool { + for _, v := range a { + if reflect.DeepEqual(v, val) { + return true + } + } + + return false + } + + for _, val := range []interface{}{"a-value", 2, true} { + if !inMap(val) { + t.Errorf("Map should have the value %v", val) + } + } + +} + +func TestMap_Tag(t *testing.T) { + var T = struct { + A string `structs:"x"` + B int `structs:"y"` + C bool `structs:"z"` + }{ + A: "a-value", + B: 2, + C: true, + } + + a := Map(T) + + inMap := func(key interface{}) bool { + for k := range a { + if reflect.DeepEqual(k, key) { + return true + } + } + return false + } + + for _, key := range []string{"x", "y", "z"} { + if !inMap(key) { + t.Errorf("Map should have the key %v", key) + } + } + +} + +func TestMap_CustomTag(t *testing.T) { + var T = struct { + A string `json:"x"` + B int `json:"y"` + C bool `json:"z"` + D struct { + E string `json:"jkl"` + } `json:"nested"` + }{ + A: "a-value", + B: 2, + C: true, + } + T.D.E = "e-value" + + s := New(T) + s.TagName = "json" + + a := s.Map() + + inMap := func(key interface{}) bool { + for k := range a { + if reflect.DeepEqual(k, key) { + return true + } + } + return false + } + + for _, key := range []string{"x", "y", "z"} { + if !inMap(key) { + t.Errorf("Map should have the key %v", key) + } + } + + nested, ok := a["nested"].(map[string]interface{}) + if !ok { + t.Fatalf("Map should contain the D field that is tagged as 'nested'") + } + + e, ok := nested["jkl"].(string) + if !ok { + t.Fatalf("Map should contain the D.E field that is tagged as 'jkl'") + } + + if e != "e-value" { + t.Errorf("D.E field should be equal to 'e-value', got: '%v'", e) + } + +} + +func TestMap_MultipleCustomTag(t *testing.T) { + var A = struct { + X string `aa:"ax"` + }{"a_value"} + + aStruct := New(A) + aStruct.TagName = "aa" + + var B = struct { + X string `bb:"bx"` + }{"b_value"} + + bStruct := New(B) + bStruct.TagName = "bb" + + a, b := aStruct.Map(), bStruct.Map() + if !reflect.DeepEqual(a, map[string]interface{}{"ax": "a_value"}) { + t.Error("Map should have field ax with value a_value") + } + + if !reflect.DeepEqual(b, map[string]interface{}{"bx": "b_value"}) { + t.Error("Map should have field bx with value b_value") + } +} + +func TestMap_OmitEmpty(t *testing.T) { + type A struct { + Name string + Value string `structs:",omitempty"` + Time time.Time `structs:",omitempty"` + } + a := A{} + + m := Map(a) + + _, ok := m["Value"].(map[string]interface{}) + if ok { + t.Error("Map should not contain the Value field that is tagged as omitempty") + } + + _, ok = m["Time"].(map[string]interface{}) + if ok { + t.Error("Map should not contain the Time field that is tagged as omitempty") + } +} + +func TestMap_OmitNested(t *testing.T) { + type A struct { + Name string + Value string + Time time.Time `structs:",omitnested"` + } + a := A{Time: time.Now()} + + type B struct { + Desc string + A A + } + b := &B{A: a} + + m := Map(b) + + in, ok := m["A"].(map[string]interface{}) + if !ok { + t.Error("Map nested structs is not available in the map") + } + + // should not happen + if _, ok := in["Time"].(map[string]interface{}); ok { + t.Error("Map nested struct should omit recursiving parsing of Time") + } + + if _, ok := in["Time"].(time.Time); !ok { + t.Error("Map nested struct should stop parsing of Time at is current value") + } +} + +func TestMap_Nested(t *testing.T) { + type A struct { + Name string + } + a := &A{Name: "example"} + + type B struct { + A *A + } + b := &B{A: a} + + m := Map(b) + + if typ := reflect.TypeOf(m).Kind(); typ != reflect.Map { + t.Errorf("Map should return a map type, got: %v", typ) + } + + in, ok := m["A"].(map[string]interface{}) + if !ok { + t.Error("Map nested structs is not available in the map") + } + + if name := in["Name"].(string); name != "example" { + t.Errorf("Map nested struct's name field should give example, got: %s", name) + } +} + +func TestMap_Anonymous(t *testing.T) { + type A struct { + Name string + } + a := &A{Name: "example"} + + type B struct { + *A + } + b := &B{} + b.A = a + + m := Map(b) + + if typ := reflect.TypeOf(m).Kind(); typ != reflect.Map { + t.Errorf("Map should return a map type, got: %v", typ) + } + + in, ok := m["A"].(map[string]interface{}) + if !ok { + t.Error("Embedded structs is not available in the map") + } + + if name := in["Name"].(string); name != "example" { + t.Errorf("Embedded A struct's Name field should give example, got: %s", name) + } +} + +func TestStruct(t *testing.T) { + var T = struct{}{} + + if !IsStruct(T) { + t.Errorf("T should be a struct, got: %T", T) + } + + if !IsStruct(&T) { + t.Errorf("T should be a struct, got: %T", T) + } + +} + +func TestValues(t *testing.T) { + var T = struct { + A string + B int + C bool + }{ + A: "a-value", + B: 2, + C: true, + } + + s := Values(T) + + if typ := reflect.TypeOf(s).Kind(); typ != reflect.Slice { + t.Errorf("Values should return a slice type, got: %v", typ) + } + + inSlice := func(val interface{}) bool { + for _, v := range s { + if reflect.DeepEqual(v, val) { + return true + } + } + return false + } + + for _, val := range []interface{}{"a-value", 2, true} { + if !inSlice(val) { + t.Errorf("Values should have the value %v", val) + } + } +} + +func TestValues_OmitEmpty(t *testing.T) { + type A struct { + Name string + Value int `structs:",omitempty"` + } + + a := A{Name: "example"} + s := Values(a) + + if len(s) != 1 { + t.Errorf("Values of omitted empty fields should be not counted") + } + + if s[0].(string) != "example" { + t.Errorf("Values of omitted empty fields should left the value example") + } +} + +func TestValues_OmitNested(t *testing.T) { + type A struct { + Name string + Value int + } + + a := A{ + Name: "example", + Value: 123, + } + + type B struct { + A A `structs:",omitnested"` + C int + } + b := &B{A: a, C: 123} + + s := Values(b) + + if len(s) != 2 { + t.Errorf("Values of omitted nested struct should be not counted") + } + + inSlice := func(val interface{}) bool { + for _, v := range s { + if reflect.DeepEqual(v, val) { + return true + } + } + return false + } + + for _, val := range []interface{}{123, a} { + if !inSlice(val) { + t.Errorf("Values should have the value %v", val) + } + } +} + +func TestValues_Nested(t *testing.T) { + type A struct { + Name string + } + a := A{Name: "example"} + + type B struct { + A A + C int + } + b := &B{A: a, C: 123} + + s := Values(b) + + inSlice := func(val interface{}) bool { + for _, v := range s { + if reflect.DeepEqual(v, val) { + return true + } + } + return false + } + + for _, val := range []interface{}{"example", 123} { + if !inSlice(val) { + t.Errorf("Values should have the value %v", val) + } + } +} + +func TestValues_Anonymous(t *testing.T) { + type A struct { + Name string + } + a := A{Name: "example"} + + type B struct { + A + C int + } + b := &B{C: 123} + b.A = a + + s := Values(b) + + inSlice := func(val interface{}) bool { + for _, v := range s { + if reflect.DeepEqual(v, val) { + return true + } + } + return false + } + + for _, val := range []interface{}{"example", 123} { + if !inSlice(val) { + t.Errorf("Values should have the value %v", val) + } + } +} + +func TestNames(t *testing.T) { + var T = struct { + A string + B int + C bool + }{ + A: "a-value", + B: 2, + C: true, + } + + s := Names(T) + + if len(s) != 3 { + t.Errorf("Names should return a slice of len 3, got: %d", len(s)) + } + + inSlice := func(val string) bool { + for _, v := range s { + if reflect.DeepEqual(v, val) { + return true + } + } + return false + } + + for _, val := range []string{"A", "B", "C"} { + if !inSlice(val) { + t.Errorf("Names should have the value %v", val) + } + } +} + +func TestFields(t *testing.T) { + var T = struct { + A string + B int + C bool + }{ + A: "a-value", + B: 2, + C: true, + } + + s := Fields(T) + + if len(s) != 3 { + t.Errorf("Fields should return a slice of len 3, got: %d", len(s)) + } + + inSlice := func(val string) bool { + for _, v := range s { + if reflect.DeepEqual(v.Name(), val) { + return true + } + } + return false + } + + for _, val := range []string{"A", "B", "C"} { + if !inSlice(val) { + t.Errorf("Fields should have the value %v", val) + } + } +} + +func TestFields_OmitNested(t *testing.T) { + type A struct { + Name string + Enabled bool + } + a := A{Name: "example"} + + type B struct { + A A + C int + Value string `structs:"-"` + Number int + } + b := &B{A: a, C: 123} + + s := Fields(b) + + if len(s) != 3 { + t.Errorf("Fields should omit nested struct. Expecting 2 got: %d", len(s)) + } + + inSlice := func(val interface{}) bool { + for _, v := range s { + if reflect.DeepEqual(v.Name(), val) { + return true + } + } + return false + } + + for _, val := range []interface{}{"A", "C"} { + if !inSlice(val) { + t.Errorf("Fields should have the value %v", val) + } + } +} + +func TestFields_Anonymous(t *testing.T) { + type A struct { + Name string + } + a := A{Name: "example"} + + type B struct { + A + C int + } + b := &B{C: 123} + b.A = a + + s := Fields(b) + + inSlice := func(val interface{}) bool { + for _, v := range s { + if reflect.DeepEqual(v.Name(), val) { + return true + } + } + return false + } + + for _, val := range []interface{}{"A", "C"} { + if !inSlice(val) { + t.Errorf("Fields should have the value %v", val) + } + } +} + +func TestIsZero(t *testing.T) { + var T = struct { + A string + B int + C bool `structs:"-"` + D []string + }{} + + ok := IsZero(T) + if !ok { + t.Error("IsZero should return true because none of the fields are initialized.") + } + + var X = struct { + A string + F *bool + }{ + A: "a-value", + } + + ok = IsZero(X) + if ok { + t.Error("IsZero should return false because A is initialized") + } + + var Y = struct { + A string + B int + }{ + A: "a-value", + B: 123, + } + + ok = IsZero(Y) + if ok { + t.Error("IsZero should return false because A and B is initialized") + } +} + +func TestIsZero_OmitNested(t *testing.T) { + type A struct { + Name string + D string + } + a := A{Name: "example"} + + type B struct { + A A `structs:",omitnested"` + C int + } + b := &B{A: a, C: 123} + + ok := IsZero(b) + if ok { + t.Error("IsZero should return false because A, B and C are initialized") + } + + aZero := A{} + bZero := &B{A: aZero} + + ok = IsZero(bZero) + if !ok { + t.Error("IsZero should return true because neither A nor B is initialized") + } + +} + +func TestIsZero_Nested(t *testing.T) { + type A struct { + Name string + D string + } + a := A{Name: "example"} + + type B struct { + A A + C int + } + b := &B{A: a, C: 123} + + ok := IsZero(b) + if ok { + t.Error("IsZero should return false because A, B and C are initialized") + } + + aZero := A{} + bZero := &B{A: aZero} + + ok = IsZero(bZero) + if !ok { + t.Error("IsZero should return true because neither A nor B is initialized") + } + +} + +func TestIsZero_Anonymous(t *testing.T) { + type A struct { + Name string + D string + } + a := A{Name: "example"} + + type B struct { + A + C int + } + b := &B{C: 123} + b.A = a + + ok := IsZero(b) + if ok { + t.Error("IsZero should return false because A, B and C are initialized") + } + + aZero := A{} + bZero := &B{} + bZero.A = aZero + + ok = IsZero(bZero) + if !ok { + t.Error("IsZero should return true because neither A nor B is initialized") + } +} + +func TestHasZero(t *testing.T) { + var T = struct { + A string + B int + C bool `structs:"-"` + D []string + }{ + A: "a-value", + B: 2, + } + + ok := HasZero(T) + if !ok { + t.Error("HasZero should return true because A and B are initialized.") + } + + var X = struct { + A string + F *bool + }{ + A: "a-value", + } + + ok = HasZero(X) + if !ok { + t.Error("HasZero should return true because A is initialized") + } + + var Y = struct { + A string + B int + }{ + A: "a-value", + B: 123, + } + + ok = HasZero(Y) + if ok { + t.Error("HasZero should return false because A and B is initialized") + } +} + +func TestHasZero_OmitNested(t *testing.T) { + type A struct { + Name string + D string + } + a := A{Name: "example"} + + type B struct { + A A `structs:",omitnested"` + C int + } + b := &B{A: a, C: 123} + + // Because the Field A inside B is omitted HasZero should return false + // because it will stop iterating deeper andnot going to lookup for D + ok := HasZero(b) + if ok { + t.Error("HasZero should return false because A and C are initialized") + } +} + +func TestHasZero_Nested(t *testing.T) { + type A struct { + Name string + D string + } + a := A{Name: "example"} + + type B struct { + A A + C int + } + b := &B{A: a, C: 123} + + ok := HasZero(b) + if !ok { + t.Error("HasZero should return true because D is not initialized") + } +} + +func TestHasZero_Anonymous(t *testing.T) { + type A struct { + Name string + D string + } + a := A{Name: "example"} + + type B struct { + A + C int + } + b := &B{C: 123} + b.A = a + + ok := HasZero(b) + if !ok { + t.Error("HasZero should return false because D is not initialized") + } +} + +func TestName(t *testing.T) { + type Foo struct { + A string + B bool + } + f := &Foo{} + + n := Name(f) + if n != "Foo" { + t.Errorf("Name should return Foo, got: %s", n) + } + + unnamed := struct{ Name string }{Name: "Cihangir"} + m := Name(unnamed) + if m != "" { + t.Errorf("Name should return empty string for unnamed struct, got: %s", n) + } + + defer func() { + err := recover() + if err == nil { + t.Error("Name should panic if a non struct is passed") + } + }() + + Name([]string{}) +} + +func TestNestedNilPointer(t *testing.T) { + type Collar struct { + Engraving string + } + + type Dog struct { + Name string + Collar *Collar + } + + type Person struct { + Name string + Dog *Dog + } + + person := &Person{ + Name: "John", + } + + personWithDog := &Person{ + Name: "Ron", + Dog: &Dog{ + Name: "Rover", + }, + } + + personWithDogWithCollar := &Person{ + Name: "Kon", + Dog: &Dog{ + Name: "Ruffles", + Collar: &Collar{ + Engraving: "If lost, call Kon", + }, + }, + } + + defer func() { + err := recover() + if err != nil { + fmt.Printf("err %+v\n", err) + t.Error("Internal nil pointer should not panic") + } + }() + + _ = Map(person) // Panics + _ = Map(personWithDog) // Panics + _ = Map(personWithDogWithCollar) // Doesn't panic +} diff --git a/Godeps/_workspace/src/github.com/fatih/structs/tags.go b/Godeps/_workspace/src/github.com/fatih/structs/tags.go new file mode 100644 index 0000000000..8859341c1f --- /dev/null +++ b/Godeps/_workspace/src/github.com/fatih/structs/tags.go @@ -0,0 +1,32 @@ +package structs + +import "strings" + +// tagOptions contains a slice of tag options +type tagOptions []string + +// Has returns true if the given optiton is available in tagOptions +func (t tagOptions) Has(opt string) bool { + for _, tagOpt := range t { + if tagOpt == opt { + return true + } + } + + return false +} + +// parseTag splits a struct field's tag into its name and a list of options +// which comes after a name. A tag is in the form of: "name,option1,option2". +// The name can be neglectected. +func parseTag(tag string) (string, tagOptions) { + // tag is one of followings: + // "" + // "name" + // "name,opt" + // "name,opt,opt2" + // ",opt" + + res := strings.Split(tag, ",") + return res[0], res[1:] +} diff --git a/Godeps/_workspace/src/github.com/fatih/structs/tags_test.go b/Godeps/_workspace/src/github.com/fatih/structs/tags_test.go new file mode 100644 index 0000000000..5d12724f14 --- /dev/null +++ b/Godeps/_workspace/src/github.com/fatih/structs/tags_test.go @@ -0,0 +1,46 @@ +package structs + +import "testing" + +func TestParseTag_Name(t *testing.T) { + tags := []struct { + tag string + has bool + }{ + {"", false}, + {"name", true}, + {"name,opt", true}, + {"name , opt, opt2", false}, // has a single whitespace + {", opt, opt2", false}, + } + + for _, tag := range tags { + name, _ := parseTag(tag.tag) + + if (name != "name") && tag.has { + t.Errorf("Parse tag should return name: %#v", tag) + } + } +} + +func TestParseTag_Opts(t *testing.T) { + tags := []struct { + opts string + has bool + }{ + {"name", false}, + {"name,opt", true}, + {"name , opt, opt2", false}, // has a single whitespace + {",opt, opt2", true}, + {", opt3, opt4", false}, + } + + // search for "opt" + for _, tag := range tags { + _, opts := parseTag(tag.opts) + + if opts.Has("opt") != tag.has { + t.Errorf("Tag opts should have opt: %#v", tag) + } + } +} diff --git a/builtin/logical/pki/backend_test.go b/builtin/logical/pki/backend_test.go new file mode 100644 index 0000000000..4678153014 --- /dev/null +++ b/builtin/logical/pki/backend_test.go @@ -0,0 +1,440 @@ +package pki + +import ( + "crypto" + "crypto/x509" + "encoding/pem" + "fmt" + "math" + "math/rand" + "os" + "testing" + "time" + + "github.com/fatih/structs" + "github.com/hashicorp/vault/logical" + logicaltest "github.com/hashicorp/vault/logical/testing" + "github.com/mitchellh/mapstructure" +) + +type issueEntry struct { + Lease string `structs:"lease" mapstructure:"lease"` + CommonName string `structs:"common_name" mapstructure:"common_name"` + AltNames string `structs:"alt_names" mapstructure:"alt_names"` + IPSANs string `structs:"ip_sans" mapstructure:"ip_sans"` +} + +type issueReply struct { + Certificate string `structs:"certificate" mapstructure:"certificate"` + IssuingCA string `structs:"issuing_ca" mapstructure:"issuing_ca"` + PrivateKey string `structs:"private_key" mapstructure:"private_key"` +} + +var ( + stepCount = 0 +) + +func checkCertsAndPrivateKey(keyType string, usage certUsage, validity time.Duration, issueResp *issueReply) (cert, ca *x509.Certificate, privKey crypto.Signer, err error) { + var pemBlock *pem.Block + + pemBlock, _ = pem.Decode([]byte(issueResp.Certificate)) + if pemBlock == nil { + return nil, nil, nil, fmt.Errorf("No PEM data found for cert") + } + cert, err = x509.ParseCertificate(pemBlock.Bytes) + if err != nil { + return nil, nil, nil, err + } + + pemBlock, _ = pem.Decode([]byte(issueResp.IssuingCA)) + if pemBlock == nil { + return nil, nil, nil, fmt.Errorf("No PEM data found for issuing CA") + } + ca, err = x509.ParseCertificate(pemBlock.Bytes) + if err != nil { + return nil, nil, nil, err + } + + pemBlock, _ = pem.Decode([]byte(issueResp.PrivateKey)) + if pemBlock == nil { + return nil, nil, nil, fmt.Errorf("No PEM data found for private key") + } + switch keyType { + case "rsa": + privKey, err = x509.ParsePKCS1PrivateKey(pemBlock.Bytes) + if err != nil { + return nil, nil, nil, err + } + case "ec": + privKey, err = x509.ParseECPrivateKey(pemBlock.Bytes) + if err != nil { + return nil, nil, nil, fmt.Errorf("Unable to decode EC private key: %s; value was %s", err, issueResp.PrivateKey) + } + default: + return nil, nil, nil, fmt.Errorf("Unknown private key type %s", keyType) + } + + // There should only be one usage type, because only one is requested + // in the tests + if len(cert.ExtKeyUsage) != 1 { + return cert, nil, nil, fmt.Errorf("Got wrong size key usage in generated cert") + } + switch usage { + case serverUsage: + if cert.ExtKeyUsage[0] != x509.ExtKeyUsageServerAuth { + return cert, nil, nil, fmt.Errorf("Bad key usage") + } + case clientUsage: + if cert.ExtKeyUsage[0] != x509.ExtKeyUsageClientAuth { + return cert, nil, nil, fmt.Errorf("Bad key usage") + } + case codeSigningUsage: + if cert.ExtKeyUsage[0] != x509.ExtKeyUsageCodeSigning { + return cert, nil, nil, fmt.Errorf("Bad key usage") + } + } + + if math.Abs(float64(time.Now().Unix()-cert.NotBefore.Unix())) > 10 { + return cert, nil, nil, fmt.Errorf("Validity period starts out of range") + } + + if math.Abs(float64(time.Now().Add(validity).Unix()-cert.NotAfter.Unix())) > 10 { + return cert, nil, nil, fmt.Errorf("Validity period too large") + } + + return +} + +func TestBackend_basic(t *testing.T) { + b := Backend() + + testCase := logicaltest.TestCase{ + Backend: b, + Steps: []logicaltest.TestStep{}, + } + + stepCount += len(testCase.Steps) + + testCase.Steps = append(testCase.Steps, generateCASteps(t)...) + + logicaltest.Test(t, testCase) +} + +func TestBackend_roles(t *testing.T) { + b := Backend() + + testCase := logicaltest.TestCase{ + Backend: b, + Steps: []logicaltest.TestStep{}, + } + + testCase.Steps = append(testCase.Steps, generateCASteps(t)...) + testCase.Steps = append(testCase.Steps, generateRoleSteps(t)...) + if len(os.Getenv("VAULT_VERBOSE_PKITESTS")) > 0 { + for i, v := range testCase.Steps { + fmt.Printf("Step %d:\n%+v\n\n", i+stepCount, v) + } + } + + stepCount += len(testCase.Steps) + + logicaltest.Test(t, testCase) +} + +func generateCASteps(t *testing.T) []logicaltest.TestStep { + ret := []logicaltest.TestStep{ + logicaltest.TestStep{ + Operation: logical.WriteOperation, + Path: "config/ca", + Data: map[string]interface{}{ + "pem_bundle": caKey + caCert, + }, + }, + + // Ensure we can fetch it back via unauthenticated means, in various formats + logicaltest.TestStep{ + Operation: logical.ReadOperation, + Path: "cert/ca", + Unauthenticated: true, + Check: func(resp *logical.Response) error { + if resp.Data["certificate"].(string) != caCert { + return fmt.Errorf("CA certificate:\n%s\ndoes not match original:\n%s\n", resp.Data["certificate"].(string), caCert) + } + return nil + }, + }, + + logicaltest.TestStep{ + Operation: logical.ReadOperation, + Path: "ca/pem", + Unauthenticated: true, + Check: func(resp *logical.Response) error { + rawBytes := resp.Data["http_raw_body"].([]byte) + if string(rawBytes) != caCert { + return fmt.Errorf("CA certificate:\n%s\ndoes not match original:\n%s\n", string(rawBytes), caCert) + } + if resp.Data["http_content_type"].(string) != "application/pkix-cert" { + return fmt.Errorf("Expected application/pkix-cert as content-type, but got %s", resp.Data["http_content_type"].(string)) + } + return nil + }, + }, + + logicaltest.TestStep{ + Operation: logical.ReadOperation, + Path: "ca", + Unauthenticated: true, + Check: func(resp *logical.Response) error { + rawBytes := resp.Data["http_raw_body"].([]byte) + pemBytes := pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: rawBytes, + }) + if string(pemBytes) != caCert { + return fmt.Errorf("CA certificate:\n%s\ndoes not match original:\n%s\n", string(pemBytes), caCert) + } + if resp.Data["http_content_type"].(string) != "application/pkix-cert" { + return fmt.Errorf("Expected application/pkix-cert as content-type, but got %s", resp.Data["http_content_type"].(string)) + } + return nil + }, + }, + } + + return ret +} + +func generateRoleSteps(t *testing.T) []logicaltest.TestStep { + roleVals := roleEntry{ + LeaseMax: "12h", + } + issueVals := issueEntry{} + ret := []logicaltest.TestStep{} + + roleTestStep := logicaltest.TestStep{ + Operation: logical.WriteOperation, + Path: "roles/test", + } + issueTestStep := logicaltest.TestStep{ + Operation: logical.WriteOperation, + Path: "issue/test", + } + + genericErrorOkCheck := func(resp *logical.Response) error { + if resp.IsError() { + return nil + } + return fmt.Errorf("Expected an error, but did not seem to get one") + } + + addTests := func(testCheck logicaltest.TestCheckFunc) { + //fmt.Printf("role vals: %#v\n", roleVals) + //fmt.Printf("issue vals: %#v\n", issueTestStep) + roleTestStep.Data = structs.New(roleVals).Map() + ret = append(ret, roleTestStep) + issueTestStep.Data = structs.New(issueVals).Map() + switch { + case issueTestStep.ErrorOk: + issueTestStep.Check = genericErrorOkCheck + case testCheck != nil: + issueTestStep.Check = testCheck + default: + issueTestStep.Check = nil + } + ret = append(ret, issueTestStep) + } + + getCnCheck := func(name, keyType string, usage certUsage, validity time.Duration) logicaltest.TestCheckFunc { + var issueResp issueReply + return func(resp *logical.Response) error { + err := mapstructure.Decode(resp.Data, &issueResp) + if err != nil { + return err + } + cert, _, _, err := checkCertsAndPrivateKey(keyType, usage, validity, &issueResp) + if err != nil { + return fmt.Errorf("Error checking generated certificate: %s", err) + } + if cert.Subject.CommonName != name { + return fmt.Errorf("Error: returned certificate has CN of %s but %s was requested", cert.Subject.CommonName, name) + } + if len(cert.DNSNames) != 1 { + return fmt.Errorf("Error: found more than one DNS SAN but only one was requested") + } + if cert.DNSNames[0] != name { + return fmt.Errorf("Error: returned certificate has a DNS SAN of %s but %s was requested", cert.DNSNames[0], name) + } + return nil + } + } + + var commonNames struct { + Localhost bool `structs:"localhost"` + BaseDomain bool `structs:"foo.example.com"` + Wildcard bool `structs:"*.example.com"` + Subdomain bool `structs:"foo.bar.example.com"` + SubdomainWildcard bool `structs:"*.bar.example.com"` + AnyHost bool `structs:"porkslap.beer"` + } + + addCnTests := func() { + cnMap := structs.New(commonNames).Map() + // For the number of tests being run, this is known to hit all + // of the various values below + mathRand := rand.New(rand.NewSource(1)) + for name, allowedInt := range cnMap { + roleVals.KeyType = "rsa" + roleVals.KeyBits = 2048 + if mathRand.Int()%2 == 1 { + roleVals.KeyType = "ec" + roleVals.KeyBits = 224 + } + + roleVals.ServerFlag = false + roleVals.ClientFlag = false + roleVals.CodeSigningFlag = false + var usage certUsage + i := mathRand.Int() + switch { + case i%3 == 0: + usage = serverUsage + roleVals.ServerFlag = true + case i%2 == 0: + usage = clientUsage + roleVals.ClientFlag = true + default: + usage = codeSigningUsage + roleVals.CodeSigningFlag = true + } + + allowed := allowedInt.(bool) + issueVals.CommonName = name + if allowed { + issueTestStep.ErrorOk = false + } else { + issueTestStep.ErrorOk = true + } + + validity, _ := time.ParseDuration(roleVals.LeaseMax) + addTests(getCnCheck(name, roleVals.KeyType, usage, validity)) + } + } + + // Common Name tests + { + // common_name not provided + issueVals.CommonName = "" + issueTestStep.ErrorOk = true + addTests(nil) + + // Nothing is allowed + addCnTests() + + roleVals.AllowLocalhost = true + commonNames.Localhost = true + addCnTests() + + roleVals.AllowedBaseDomain = "foobar.com" + addCnTests() + + roleVals.AllowedBaseDomain = "example.com" + commonNames.BaseDomain = true + commonNames.Wildcard = true + addCnTests() + + roleVals.AllowSubdomains = true + commonNames.Subdomain = true + commonNames.SubdomainWildcard = true + addCnTests() + + roleVals.AllowAnyName = true + commonNames.AnyHost = true + addCnTests() + } + + // IP SAN tests + { + issueVals.IPSANs = "127.0.0.1,::1" + issueTestStep.ErrorOk = true + addTests(nil) + + roleVals.AllowIPSANs = true + issueTestStep.ErrorOk = false + addTests(nil) + + issueVals.IPSANs = "foobar" + issueTestStep.ErrorOk = true + addTests(nil) + + issueTestStep.ErrorOk = false + issueVals.IPSANs = "" + } + + // Lease tests + { + roleTestStep.ErrorOk = true + roleVals.Lease = "" + roleVals.LeaseMax = "" + addTests(nil) + + roleVals.Lease = "12h" + roleVals.LeaseMax = "6h" + addTests(nil) + + roleTestStep.ErrorOk = false + } + + return ret +} + +const ( + caKey string = `-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA1eKB2nFbRqTFs7KyZjbzB5VRCBbnLZfEXVP1c3bHe+YGjlfl +34cy52dmancUzOf1/Jfo+VglocjTLVy5wHSGJwQYs8b6pEuuvAVo/6wUL5Z7ZlQD +R4kDe5Q+xgoRT6Bi/Bs57E+fNYgyUq/YAUY5WLuC+ZliCbJkLnb15ItuP1yVUTDX +TYORRE3qJS5RRol8D3QvteG9LyPEc7C+jsm5iBCagyxluzU0dnEOib5q7xwZncoM +bQz+rZH3QnwOij41FOGRPazrD5Mv6xLBkFnE5VAJ+GIgvd4bpOwvYMuofvF4PS7S +FzxkGssMLlICap6PFpKz86DpAoDxPuoZeOhU4QIDAQABAoIBAQCp6VIdFdZcDYPd +WIVuvBJfINiJo6AtURa2yX8BJggdPkRRCjTcWUwwFq1+wHDuwwtgidGTW9oxZxeU +Psh1wlvcXN2+28C7ikAar/WUvsAeed44EV+1kXwJzV/89XyBFDnuazadqzcgUL0h +gP4JLR9bhULsRFRkvanmW6zFzZpcjBzi/UoFuWkFRRqZ0euM2Lpz8L75PFfW9s9M +kNglZpcV6ZmvR9c1JkEMUs/mrB8ZgCd1uvmcVosQ+u7sE8Yk/xAurHXuNJQlGXx4 +azrLW0XY1CLO2Tm4l4MwPjmhH0WytXNjOSKycBCXVnBIfZsI128DsP5YyA/fW9qA +BAqFSzABAoGBAPcBNk9sf3cnZ5w6qwlE2ysDwGIGR+I1fb09YjRI6vjwwdWZgGR0 +EE4UB1Pp+KIehXaTJHcEgvBBErM2NLS4qKzh25O30C2EwK6o//3jEAribuYutBhJ +ihu1qKzqcPbKClG+34kjX6nmtux2wlYM05f5v3ALki5Is7W/RrfceBuBAoGBAN2s +hdt4TcgIcZymPG2931qCBGF3E8AaA8bUl9TKaZHuFikOMFKA/KM5O5mznPGnQP2d +kXYKXuqdYhVLwp32FTbIbozGZZ8XliO5oS7J3vIID+sLWQhrvyFO7d0lpSjv41HH +yJ2DrykHRg8hxsbh2D4By7olBx6Q2m+B8lPzHmlhAoGACHUeKvIIG0haH9tSZ+rX +pk1mlPSqGXDDcWtcpXWptgRoXqv23Xmr5UCCT7k/Li3lW/4FzZ117kwMG97LRzTb +ca/6GMC+fBCDmHdo7ISN1BGUwoTu3bYG6JP7xo/wdkLMv6fNd6CicerYcJhQZynh +RN7kUy3SP4t1u89k2H7QDgECgYBpU0bKr8+tQq3Qs3+02OmeFHbGZJDCztmKiIqX +tZERoGFxIme9W8IuP8xczGW+wCx2FH7/6g+NRDhNTBDtgvYzcGpugvnX7JoO4W1/ +ULWYpFID6QFlqeRHjDwivndKCykkO1vL07zPLsCQAglzh+16ENpe2KcYU9Ul9EVS +tAp4IQKBgQDrb/NpiVx7NI6PyTCm6ctuUAYm3ihAiQNV4Bmr0liPDp9PozbqkhcF +udNtivO4LlRb/PJ+DK6afDyH8aJQdDqe3NpDvyrmKiMSYOY3iVFvan4tbIiofxdQ +flwiZUzox814fzXbxheO9Cs6pXz7PUBVU4fN0Y/hXJCfRO4Ns9152A== +-----END RSA PRIVATE KEY----- +` + caCert string = `-----BEGIN CERTIFICATE----- +MIIDUTCCAjmgAwIBAgIJAKM+z4MSfw2mMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNV +BAMMEFZhdWx0IFRlc3RpbmcgQ0EwHhcNMTUwNjAxMjA1MTUzWhcNMjUwNTI5MjA1 +MTUzWjAbMRkwFwYDVQQDDBBWYXVsdCBUZXN0aW5nIENBMIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEA1eKB2nFbRqTFs7KyZjbzB5VRCBbnLZfEXVP1c3bH +e+YGjlfl34cy52dmancUzOf1/Jfo+VglocjTLVy5wHSGJwQYs8b6pEuuvAVo/6wU +L5Z7ZlQDR4kDe5Q+xgoRT6Bi/Bs57E+fNYgyUq/YAUY5WLuC+ZliCbJkLnb15Itu +P1yVUTDXTYORRE3qJS5RRol8D3QvteG9LyPEc7C+jsm5iBCagyxluzU0dnEOib5q +7xwZncoMbQz+rZH3QnwOij41FOGRPazrD5Mv6xLBkFnE5VAJ+GIgvd4bpOwvYMuo +fvF4PS7SFzxkGssMLlICap6PFpKz86DpAoDxPuoZeOhU4QIDAQABo4GXMIGUMB0G +A1UdDgQWBBTknN5eFxxo5aTlfq+G4ZXs3AsxWTAfBgNVHSMEGDAWgBTknN5eFxxo +5aTlfq+G4ZXs3AsxWTAxBgNVHR8EKjAoMCagJKAihiBodHRwOi8vbG9jYWxob3N0 +OjgyMDAvdjEvcGtpL2NybDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjANBgkqhkiG9w0BAQsFAAOCAQEAsINcA4PZm+OyldgNrwRVgxoSrhV1I9zszhc9 +VV340ZWlpTTxFKVb/K5Hg+jMF9tv70X1HwlYdlutE6KdrsA3gks5zanh4/3zlrYk +ABNBmSD6SSU2HKX1bFCBAAS3YHONE5o1K5tzwLsMl5uilNf+Wid3NjFnQ4KfuYI5 +loN/opnM6+a/O3Zua8RAuMMAv9wyqwn88aVuLvVzDNSMe5qC5kkuLGmRkNgY06rI +S/fXIHIOldeQxgYCqhdVmcDWJ1PtVaDfBsKVpRg1GRU8LUGw2E4AY+twd+J2FBfa +G/7g4koczXLoUM3OQXd5Aq2cs4SS1vODrYmgbioFsQ3eDHd1fg== +-----END CERTIFICATE----- +` +) diff --git a/builtin/logical/pki/cert_util.go b/builtin/logical/pki/cert_util.go index 0e90ff061e..bbdc3d5d87 100644 --- a/builtin/logical/pki/cert_util.go +++ b/builtin/logical/pki/cert_util.go @@ -26,7 +26,7 @@ import ( const ( UnknownPrivateKeyType = iota RSAPrivateKeyType - ECDSAPrivateKeyType + ECPrivateKeyType ) type certUsage int @@ -107,7 +107,7 @@ func (r *rawCertBundle) getSigner() (crypto.Signer, error) { var signer crypto.Signer var err error switch r.PrivateKeyType { - case ECDSAPrivateKeyType: + case ECPrivateKeyType: signer, err = x509.ParseECPrivateKey(r.PrivateKeyBytes) if err != nil { return nil, fmt.Errorf("Unable to parse CA's private EC key: %s", err) @@ -295,8 +295,8 @@ func createCertificate(creationInfo *certCreationBundle) (rawBundle *rawCertBund return nil, nil, fmt.Errorf("Error generating RSA private key") } rawBundle.PrivateKeyBytes = x509.MarshalPKCS1PrivateKey(clientPrivKey.(*rsa.PrivateKey)) - case "ecdsa": - rawBundle.PrivateKeyType = ECDSAPrivateKeyType + case "ec": + rawBundle.PrivateKeyType = ECPrivateKeyType var curve elliptic.Curve switch creationInfo.KeyBits { case 224: @@ -308,15 +308,15 @@ func createCertificate(creationInfo *certCreationBundle) (rawBundle *rawCertBund case 521: curve = elliptic.P521() default: - return nil, fmt.Errorf("Unsupported bit length for ECDSA key: %d", creationInfo.KeyBits), nil + return nil, fmt.Errorf("Unsupported bit length for EC key: %d", creationInfo.KeyBits), nil } clientPrivKey, err = ecdsa.GenerateKey(curve, crand.Reader) if err != nil { - return nil, nil, fmt.Errorf("Error generating ECDSA private key") + return nil, nil, fmt.Errorf("Error generating EC private key") } rawBundle.PrivateKeyBytes, err = x509.MarshalECPrivateKey(clientPrivKey.(*ecdsa.PrivateKey)) if err != nil { - return nil, nil, fmt.Errorf("Error marshalling ECDSA private key") + return nil, nil, fmt.Errorf("Error marshalling EC private key") } default: return nil, fmt.Errorf("Unknown key type: %s", creationInfo.KeyType), nil diff --git a/builtin/logical/pki/path_config_ca.go b/builtin/logical/pki/path_config_ca.go index 073f074ebd..3f2310bfe9 100644 --- a/builtin/logical/pki/path_config_ca.go +++ b/builtin/logical/pki/path_config_ca.go @@ -49,7 +49,7 @@ func (b *backend) pathCAWrite( if rawBundle.PrivateKeyType != UnknownPrivateKeyType { return logical.ErrorResponse("More than one private key given; provide only one private key in the bundle"), nil } - rawBundle.PrivateKeyType = ECDSAPrivateKeyType + rawBundle.PrivateKeyType = ECPrivateKeyType rawBundle.PrivateKeyBytes = pemBlock.Bytes // TODO?: CRLs can only be generated with RSA keys right now, in the // Go standard library. The plubming is here to support non-RSA keys @@ -84,7 +84,7 @@ func (b *backend) pathCAWrite( switch { case rawBundle.PrivateKeyType == UnknownPrivateKeyType: - return logical.ErrorResponse("Unable to figure out the private key type; must be RSA or ECDSA"), nil + return logical.ErrorResponse("Unable to figure out the private key type; must be RSA or EC"), nil case len(rawBundle.PrivateKeyBytes) == 0: return logical.ErrorResponse("Unable to decode the private key from the bundle"), nil case len(rawBundle.CertificateBytes) == 0: diff --git a/builtin/logical/pki/path_issue.go b/builtin/logical/pki/path_issue.go index 1f291155f5..6844a1a911 100644 --- a/builtin/logical/pki/path_issue.go +++ b/builtin/logical/pki/path_issue.go @@ -80,7 +80,7 @@ func (b *backend) pathIssueCert( ipAlt := data.Get("ip_sans").(string) if len(ipAlt) != 0 { if !role.AllowIPSANs { - return logical.ErrorResponse("IP Subject Alternative Names are not allowed in this role"), nil + return logical.ErrorResponse(fmt.Sprintf("IP Subject Alternative Names are not allowed in this role, but was provided %s", ipAlt)), nil } for _, v := range strings.Split(ipAlt, ",") { parsedIP := net.ParseIP(v) @@ -134,6 +134,10 @@ func (b *backend) pathIssueCert( if role.ClientFlag { usage = usage | clientUsage } + if role.CodeSigningFlag { + usage = usage | codeSigningUsage + } + creationBundle := &certCreationBundle{ RawSigningBundle: rawSigningBundle, CACert: caCert, @@ -176,7 +180,7 @@ func (b *backend) pathIssueCert( switch rawBundle.PrivateKeyType { case RSAPrivateKeyType: block.Type = "RSA PRIVATE KEY" - case ECDSAPrivateKeyType: + case ECPrivateKeyType: block.Type = "EC PRIVATE KEY" default: return nil, fmt.Errorf("Could not determine private key type when creating block") diff --git a/builtin/logical/pki/path_roles.go b/builtin/logical/pki/path_roles.go index 3a5477cdee..399550a785 100644 --- a/builtin/logical/pki/path_roles.go +++ b/builtin/logical/pki/path_roles.go @@ -86,7 +86,7 @@ func pathRoles(b *backend) *framework.Path { "key_type": &framework.FieldSchema{ Type: framework.TypeString, Default: "rsa", - Description: "The type of key to use; defaults to RSA. \"rsa\" and \"ecdsa\" are the only valid values.", + Description: "The type of key to use; defaults to RSA. \"rsa\" and \"ec\" are the only valid values.", }, "key_bits": &framework.FieldSchema{ @@ -209,16 +209,23 @@ func (b *backend) pathRoleCreate( } } + if len(entry.KeyType) == 0 { + entry.KeyType = "rsa" + } + if entry.KeyBits == 0 { + entry.KeyBits = 2048 + } + switch entry.KeyType { case "rsa": - case "ecdsa": + case "ec": switch entry.KeyBits { case 224: case 256: case 384: case 521: default: - return logical.ErrorResponse(fmt.Sprintf("Unsupported bit length for ECDSA key: %d", entry.KeyBits)), nil + return logical.ErrorResponse(fmt.Sprintf("Unsupported bit length for EC key: %d", entry.KeyBits)), nil } default: return logical.ErrorResponse(fmt.Sprintf("Unknown key type %s", entry.KeyType)), nil @@ -237,19 +244,19 @@ func (b *backend) pathRoleCreate( } type roleEntry struct { - LeaseMax string `json:"lease_max"` - Lease string `json:"lease"` - AllowLocalhost bool `json:"allow_localhost"` - AllowedBaseDomain string `json:"allowed_base_domain"` - AllowTokenDisplayName bool `json:"allow_token_displaynae"` - AllowSubdomains bool `json:"allow_subdomains"` - AllowAnyName bool `json:"allow_any_name"` - AllowIPSANs bool `json:"allow_ip_sans"` - ServerFlag bool `json:"server_flag"` - ClientFlag bool `json:"client_flag"` - CodeSigningFlag bool `json:"code_signing_flag"` - KeyType string `json:"key_type"` - KeyBits int `json:"key_bits"` + LeaseMax string `json:"lease_max" structs:"lease_max" mapstructure:"lease_max"` + Lease string `json:"lease" structs:"lease" mapstructure:"lease"` + AllowLocalhost bool `json:"allow_localhost" structs:"allow_localhost" mapstructure:"allow_localhost"` + AllowedBaseDomain string `json:"allowed_base_domain" structs:"allowed_base_domain" mapstructure:"allowed_base_domain"` + AllowTokenDisplayName bool `json:"allow_token_displayname" structs:"allow_token_displayname" mapstructure:"allow_token_displayname"` + AllowSubdomains bool `json:"allow_subdomains" structs:"allow_subdomains" mapstructure:"allow_subdomains"` + AllowAnyName bool `json:"allow_any_name" structs:"allow_any_name" mapstructure:"allow_any_name"` + AllowIPSANs bool `json:"allow_ip_sans" structs:"allow_ip_sans" mapstructure:"allow_ip_sans"` + ServerFlag bool `json:"server_flag" structs:"server_flag" mapstructure:"server_flag"` + ClientFlag bool `json:"client_flag" structs:"client_flag" mapstructure:"client_flag"` + CodeSigningFlag bool `json:"code_signing_flag" structs:"code_signing_flag" mapstructure:"code_signing_flag"` + KeyType string `json:"key_type" structs:"key_type" mapstructure:"key_type"` + KeyBits int `json:"key_bits" structs:"key_bits" mapstructure:"key_bits"` } const pathRoleHelpSyn = ` diff --git a/website/source/docs/secrets/pki/index.html.md b/website/source/docs/secrets/pki/index.html.md index 2d28f5a231..fbad7e1f67 100644 --- a/website/source/docs/secrets/pki/index.html.md +++ b/website/source/docs/secrets/pki/index.html.md @@ -585,7 +585,7 @@ If you get stuck at any time, simply run `vault help pki` or with a subpath for key_type optional The type of key to generate for generated private - keys. Currently, `rsa` and `ecdsa` are supported. + keys. Currently, `rsa` and `ec` are supported. Defaults to `rsa`.
  • @@ -593,8 +593,8 @@ If you get stuck at any time, simply run `vault help pki` or with a subpath for optional The number of bits to use for the generated keys. Defaults to `2048`; this will need to be changed for - `ecdsa` keys. See https://golang.org/pkg/crypto/elliptic/#Curve - for an overview of allowed bit lengths for `ecdsa`. + `ec` keys. See https://golang.org/pkg/crypto/elliptic/#Curve + for an overview of allowed bit lengths for `ec`.
  • From 0ee9735a5af1efaf42588552d51207a4d0d4f2af Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Thu, 11 Jun 2015 21:16:36 -0400 Subject: [PATCH 03/15] Fix some out-of-date examples. Commit contents (C)2015 Akamai Technologies, Inc. --- website/source/docs/secrets/pki/index.html.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/website/source/docs/secrets/pki/index.html.md b/website/source/docs/secrets/pki/index.html.md index fbad7e1f67..50320b69a9 100644 --- a/website/source/docs/secrets/pki/index.html.md +++ b/website/source/docs/secrets/pki/index.html.md @@ -74,14 +74,14 @@ Successfully mounted 'pki' at 'pki'! Next, Vault must be configured with a root certificate and associated private key. This is done by writing the contents of a file or *stdin*: ```text -$ vault write pki/config/ca value="@ca_bundle.pem" +$ vault write pki/config/ca pem_bundle="@ca_bundle.pem" Success! Data written to: pki/config/ca ``` or ``` -$ cat bundle.pem | vault write pki/config/ca value="-" +$ cat bundle.pem | vault write pki/config/ca pem_bundle="-" Success! Data written to: pki/config/ca ``` @@ -218,7 +218,14 @@ If you get stuck at any time, simply run `vault help pki` or with a subpath for command similar to the following:
    ```text - curl -X POST --data "@cert_and_key.pem" ... + curl -X POST --data "@cabundle.json" http://127.0.0.1:8200/v1/pki/config/ca -H X-Vault-Token:06b9d... + ``` + + Note that if you provide the data through the HTTP API it must be + JSON-formatted, with newlines replaced with `\n`, like so: + + ```text + { "pem_bundle": "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END CERTIFICATE-----" } ``` From 722eca13670c5b445022e262883fd6db69f572c5 Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Thu, 11 Jun 2015 21:57:05 -0400 Subject: [PATCH 04/15] Address most of Armon's initial feedback. Commit contents (C)2015 Akamai Technologies, Inc. --- builtin/logical/pki/backend.go | 2 +- builtin/logical/pki/cert_util.go | 20 +++--- builtin/logical/pki/crl_util.go | 100 ++++++++++++++++++------------ builtin/logical/pki/path_issue.go | 8 +-- 4 files changed, 77 insertions(+), 53 deletions(-) diff --git a/builtin/logical/pki/backend.go b/builtin/logical/pki/backend.go index 4a66e20c80..aba5a157d4 100644 --- a/builtin/logical/pki/backend.go +++ b/builtin/logical/pki/backend.go @@ -23,7 +23,7 @@ func Backend() *framework.Backend { "config/*", "revoked/*", "revoke/*", - "crl/build", + "crl/rotate", }, Unauthenticated: []string{ "cert/*", diff --git a/builtin/logical/pki/cert_util.go b/builtin/logical/pki/cert_util.go index bbdc3d5d87..6a017cf0ec 100644 --- a/builtin/logical/pki/cert_util.go +++ b/builtin/logical/pki/cert_util.go @@ -5,7 +5,7 @@ import ( "crypto" "crypto/ecdsa" "crypto/elliptic" - crand "crypto/rand" + "crypto/rand" "crypto/rsa" "crypto/sha1" "crypto/x509" @@ -13,7 +13,6 @@ import ( "encoding/base64" "fmt" "math/big" - mrand "math/rand" "net" "regexp" "strings" @@ -281,16 +280,19 @@ func validateCommonNames(req *logical.Request, commonNames []string, role *roleE } func createCertificate(creationInfo *certCreationBundle) (rawBundle *rawCertBundle, userErr, intErr error) { - rawBundle = &rawCertBundle{ - SerialNumber: (&big.Int{}).Rand(mrand.New(mrand.NewSource(time.Now().UnixNano())), (&big.Int{}).Exp(big.NewInt(2), big.NewInt(159), nil)), - } - var clientPrivKey crypto.Signer var err error + rawBundle = &rawCertBundle{} + + rawBundle.SerialNumber, err = rand.Int(rand.Reader, (&big.Int{}).Exp(big.NewInt(2), big.NewInt(159), nil)) + if err != nil { + return nil, nil, fmt.Errorf("Error getting random serial number") + } + switch creationInfo.KeyType { case "rsa": rawBundle.PrivateKeyType = RSAPrivateKeyType - clientPrivKey, err = rsa.GenerateKey(crand.Reader, creationInfo.KeyBits) + clientPrivKey, err = rsa.GenerateKey(rand.Reader, creationInfo.KeyBits) if err != nil { return nil, nil, fmt.Errorf("Error generating RSA private key") } @@ -310,7 +312,7 @@ func createCertificate(creationInfo *certCreationBundle) (rawBundle *rawCertBund default: return nil, fmt.Errorf("Unsupported bit length for EC key: %d", creationInfo.KeyBits), nil } - clientPrivKey, err = ecdsa.GenerateKey(curve, crand.Reader) + clientPrivKey, err = ecdsa.GenerateKey(curve, rand.Reader) if err != nil { return nil, nil, fmt.Errorf("Error generating EC private key") } @@ -371,7 +373,7 @@ func createCertificate(creationInfo *certCreationBundle) (rawBundle *rawCertBund return nil, nil, fmt.Errorf("Unable to get signing private key: %s", err) } - cert, err := x509.CreateCertificate(crand.Reader, certTemplate, creationInfo.CACert, clientPrivKey.Public(), signingPrivKey) + cert, err := x509.CreateCertificate(rand.Reader, certTemplate, creationInfo.CACert, clientPrivKey.Public(), signingPrivKey) if err != nil { return nil, nil, fmt.Errorf("Unable to create certificate: %s", err) } diff --git a/builtin/logical/pki/crl_util.go b/builtin/logical/pki/crl_util.go index 5ce2da2355..1fc00d4653 100644 --- a/builtin/logical/pki/crl_util.go +++ b/builtin/logical/pki/crl_util.go @@ -15,55 +15,71 @@ type revocationInfo struct { RevocationTime int64 `json:"unix_time"` } +var ( + crlLifetime = time.Hour * 72 +) + func revokeCert(req *logical.Request, serial string) (*logical.Response, error) { + alreadyRevoked := false + var err error + + revInfo := revocationInfo{} + certEntry, userErr, intErr := fetchCertBySerial(req, "revoked/", serial) if certEntry != nil { - return nil, nil + // Verify that it is also deleted from certs/ + // in case of partial failure from an earlier run. + certEntry, _, _ = fetchCertBySerial(req, "certs/", serial) + if certEntry != nil { + alreadyRevoked = true + + revEntry, err := req.Storage.Get("revoked/" + serial) + if err != nil { + return nil, fmt.Errorf("Error getting existing revocation info") + } + err = revEntry.DecodeJSON(&revInfo) + if err != nil { + return nil, fmt.Errorf("Error decoding existing revocation info") + } + } else { + return nil, nil + } } - certEntry, userErr, intErr = fetchCertBySerial(req, "certs/", serial) - switch { - case userErr != nil: - return logical.ErrorResponse(userErr.Error()), nil - case intErr != nil: - return nil, intErr - } + if !alreadyRevoked { + certEntry, userErr, intErr = fetchCertBySerial(req, "certs/", serial) + switch { + case userErr != nil: + return logical.ErrorResponse(userErr.Error()), nil + case intErr != nil: + return nil, intErr + } - // Possible TODO: use some kind of transaction log in case of an - // error anywhere along here (so we've validated that we got a - // value back, but want to make sure it not only is deleted from - // certs/ but also shows up in revoked/ and a CRL is generated) - err := req.Storage.Delete("certs/" + serial) + cert, err := x509.ParseCertificate(certEntry.Value) + if err != nil { + return nil, fmt.Errorf("Error parsing certificate") + } + if cert == nil { + return nil, fmt.Errorf("Got a nil certificate") + } - if err != nil { - return nil, fmt.Errorf("Error deleting cert from valid-certs location") - } + if cert.NotAfter.Before(time.Now()) { + return nil, nil + } - cert, err := x509.ParseCertificate(certEntry.Value) - if err != nil { - return nil, fmt.Errorf("Error parsing certificate") - } - if cert == nil { - return nil, fmt.Errorf("Got a nil certificate") - } + revInfo.CertificateBytes = certEntry.Value + revInfo.RevocationTime = time.Now().Unix() - if cert.NotAfter.Before(time.Now()) { - return nil, nil - } + certEntry, err = logical.StorageEntryJSON("revoked/"+serial, revInfo) + if err != nil { + return nil, fmt.Errorf("Error creating revocation entry") + } - revInfo := revocationInfo{ - CertificateBytes: certEntry.Value, - RevocationTime: time.Now().Unix(), - } + err = req.Storage.Put(certEntry) + if err != nil { + return nil, fmt.Errorf("Error saving revoked certificate to new location") + } - certEntry, err = logical.StorageEntryJSON("revoked/"+serial, revInfo) - if err != nil { - return nil, fmt.Errorf("Error creating revocation entry") - } - - err = req.Storage.Put(certEntry) - if err != nil { - return nil, fmt.Errorf("Error saving revoked certificate to new location") } err = buildCRL(req) @@ -71,6 +87,12 @@ func revokeCert(req *logical.Request, serial string) (*logical.Response, error) return nil, fmt.Errorf("Error encountered during CRL building: %s", err) } + err = req.Storage.Delete("certs/" + serial) + + if err != nil { + return nil, fmt.Errorf("Error deleting cert from valid-certs location") + } + return &logical.Response{ Data: map[string]interface{}{ "revocation_time": revInfo.RevocationTime, @@ -136,7 +158,7 @@ func buildCRL(req *logical.Request) error { } // TODO: Make expiry configurable - crlBytes, err := caCert.CreateCRL(rand.Reader, signingPrivKey, revokedCerts, time.Now(), time.Now().Add(time.Hour*72)) + crlBytes, err := caCert.CreateCRL(rand.Reader, signingPrivKey, revokedCerts, time.Now(), time.Now().Add(crlLifetime)) if err != nil { return fmt.Errorf("Error creating new CRL: %s", err) } diff --git a/builtin/logical/pki/path_issue.go b/builtin/logical/pki/path_issue.go index 6844a1a911..15aaa885fb 100644 --- a/builtin/logical/pki/path_issue.go +++ b/builtin/logical/pki/path_issue.go @@ -107,7 +107,7 @@ func (b *backend) pathIssueCert( "Invalid lease: %s", err)), nil } - if time.Now().Add(lease).After(time.Now().Add(leaseMax)) { + if lease > leaseMax { return logical.ErrorResponse("Lease expires after maximum allowed by this role"), nil } @@ -159,12 +159,12 @@ func (b *backend) pathIssueCert( serial := strings.ToLower(getOctalFormatted(rawBundle.SerialNumber.Bytes(), ":")) - resp := b.Secret(SecretCertsType).Response(map[string]interface{}{}, map[string]interface{}{ + resp := b.Secret(SecretCertsType).Response(map[string]interface{}{ + "serial": serial, + }, map[string]interface{}{ "serial": serial, }) - resp.Data["serial"] = serial - block := pem.Block{ Type: "CERTIFICATE", Bytes: rawBundle.CertificateBytes, From 64c8a437e9f296628e7a176259d0df77c746ab1d Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Thu, 11 Jun 2015 22:28:13 -0400 Subject: [PATCH 05/15] Add locking for revocation/CRL generation. I originally was going to use an RWMutex but punted, because it's not worth trying to save some milliseconds with the possibility of getting something wrong. So the entire operations are now wrapped, which is minimally slower but very safe. Commit contents (C)2015 Akamai Technologies, Inc. --- builtin/logical/pki/crl_util.go | 5 ++++- builtin/logical/pki/path_revoke.go | 6 ++++++ builtin/logical/pki/secret_certs.go | 3 +++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/builtin/logical/pki/crl_util.go b/builtin/logical/pki/crl_util.go index 1fc00d4653..eb4c58da32 100644 --- a/builtin/logical/pki/crl_util.go +++ b/builtin/logical/pki/crl_util.go @@ -5,6 +5,7 @@ import ( "crypto/x509" "crypto/x509/pkix" "fmt" + "sync" "time" "github.com/hashicorp/vault/logical" @@ -16,7 +17,8 @@ type revocationInfo struct { } var ( - crlLifetime = time.Hour * 72 + crlLifetime = time.Hour * 72 + revokeStorageLock = &sync.Mutex{} ) func revokeCert(req *logical.Request, serial string) (*logical.Response, error) { @@ -37,6 +39,7 @@ func revokeCert(req *logical.Request, serial string) (*logical.Response, error) if err != nil { return nil, fmt.Errorf("Error getting existing revocation info") } + err = revEntry.DecodeJSON(&revInfo) if err != nil { return nil, fmt.Errorf("Error decoding existing revocation info") diff --git a/builtin/logical/pki/path_revoke.go b/builtin/logical/pki/path_revoke.go index eb89555979..3c2c9ec5aa 100644 --- a/builtin/logical/pki/path_revoke.go +++ b/builtin/logical/pki/path_revoke.go @@ -45,10 +45,16 @@ func (b *backend) pathRevokeWrite(req *logical.Request, data *framework.FieldDat return logical.ErrorResponse("The serial number must be provided"), nil } + revokeStorageLock.Lock() + defer revokeStorageLock.Unlock() + return revokeCert(req, serial) } func (b *backend) pathRotateCRLRead(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + revokeStorageLock.Lock() + defer revokeStorageLock.Unlock() + err := buildCRL(req) if err != nil { return logical.ErrorResponse(fmt.Sprintf("Error building CRL: %s", err)), err diff --git a/builtin/logical/pki/secret_certs.go b/builtin/logical/pki/secret_certs.go index d09159a643..28c8a2dc49 100644 --- a/builtin/logical/pki/secret_certs.go +++ b/builtin/logical/pki/secret_certs.go @@ -50,5 +50,8 @@ func (b *backend) secretCredsRevoke( serial := strings.Replace(strings.ToLower(serialInt.(string)), "-", ":", -1) + revokeStorageLock.Lock() + defer revokeStorageLock.Unlock() + return revokeCert(req, serial) } From 8b55d33722d739e12767f2495fd701b1a0d81ac1 Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Thu, 11 Jun 2015 23:16:13 -0400 Subject: [PATCH 06/15] Erp, forgot this feedback... Commit contents (C)2015 Akamai Technologies, Inc. --- builtin/logical/pki/backend.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/builtin/logical/pki/backend.go b/builtin/logical/pki/backend.go index aba5a157d4..7dfb30443d 100644 --- a/builtin/logical/pki/backend.go +++ b/builtin/logical/pki/backend.go @@ -62,6 +62,6 @@ type backend struct { const backendHelp = ` The PKI backend dynamically generates X509 server and client certificates. -After mounting this backend, configure the CA using the "ca_bundle" endpoint within +After mounting this backend, configure the CA using the "pem_bundle" endpoint within the "config/" path. ` From 067fbc90785a164b1782363c15e4f0f67a8485d9 Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Fri, 12 Jun 2015 16:33:00 -0400 Subject: [PATCH 07/15] Fix a docs-out-of-date bug. Commit contents (C)2015 Akamai Technologies, Inc. --- website/source/docs/secrets/pki/index.html.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/source/docs/secrets/pki/index.html.md b/website/source/docs/secrets/pki/index.html.md index 50320b69a9..32d8e0127b 100644 --- a/website/source/docs/secrets/pki/index.html.md +++ b/website/source/docs/secrets/pki/index.html.md @@ -326,7 +326,7 @@ If you get stuck at any time, simply run `vault help pki` or with a subpath for ### /pki/issue/ -#### GET +#### POST
    Description
    @@ -341,7 +341,7 @@ If you get stuck at any time, simply run `vault help pki` or with a subpath for
    Method
    -
    GET
    +
    POST
    URL
    `/pki/issue/`
    From a2b3e1302a6e00ecc5d75e4c6c24a509258734ab Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Mon, 15 Jun 2015 13:33:23 -0400 Subject: [PATCH 08/15] A bunch of cleanup and moving around. logical/certutil is a package that now has helper functions useful for other parts of Vault (including the API) to take advantage of. Commit contents (C)2015 Akamai Technologies, Inc. --- builtin/logical/pki/backend_test.go | 32 ++--- builtin/logical/pki/cert_util.go | 148 +++------------------ builtin/logical/pki/crl_util.go | 46 ++++--- builtin/logical/pki/path_config_ca.go | 20 ++- builtin/logical/pki/path_fetch.go | 13 +- builtin/logical/pki/path_issue.go | 48 +++---- builtin/logical/pki/path_revoke.go | 14 +- builtin/logical/pki/path_roles.go | 17 +-- builtin/logical/pki/secret_certs.go | 2 +- logical/certutil/helpers.go | 35 +++++ logical/certutil/types.go | 183 ++++++++++++++++++++++++++ 11 files changed, 322 insertions(+), 236 deletions(-) create mode 100644 logical/certutil/helpers.go create mode 100644 logical/certutil/types.go diff --git a/builtin/logical/pki/backend_test.go b/builtin/logical/pki/backend_test.go index 4678153014..f57adbd60d 100644 --- a/builtin/logical/pki/backend_test.go +++ b/builtin/logical/pki/backend_test.go @@ -13,31 +13,19 @@ import ( "github.com/fatih/structs" "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/certutil" logicaltest "github.com/hashicorp/vault/logical/testing" "github.com/mitchellh/mapstructure" ) -type issueEntry struct { - Lease string `structs:"lease" mapstructure:"lease"` - CommonName string `structs:"common_name" mapstructure:"common_name"` - AltNames string `structs:"alt_names" mapstructure:"alt_names"` - IPSANs string `structs:"ip_sans" mapstructure:"ip_sans"` -} - -type issueReply struct { - Certificate string `structs:"certificate" mapstructure:"certificate"` - IssuingCA string `structs:"issuing_ca" mapstructure:"issuing_ca"` - PrivateKey string `structs:"private_key" mapstructure:"private_key"` -} - var ( stepCount = 0 ) -func checkCertsAndPrivateKey(keyType string, usage certUsage, validity time.Duration, issueResp *issueReply) (cert, ca *x509.Certificate, privKey crypto.Signer, err error) { +func checkCertsAndPrivateKey(keyType string, usage certUsage, validity time.Duration, certBundle *certutil.CertBundle) (cert, ca *x509.Certificate, privKey crypto.Signer, err error) { var pemBlock *pem.Block - pemBlock, _ = pem.Decode([]byte(issueResp.Certificate)) + pemBlock, _ = pem.Decode([]byte(certBundle.Certificate)) if pemBlock == nil { return nil, nil, nil, fmt.Errorf("No PEM data found for cert") } @@ -46,7 +34,7 @@ func checkCertsAndPrivateKey(keyType string, usage certUsage, validity time.Dura return nil, nil, nil, err } - pemBlock, _ = pem.Decode([]byte(issueResp.IssuingCA)) + pemBlock, _ = pem.Decode([]byte(certBundle.IssuingCA)) if pemBlock == nil { return nil, nil, nil, fmt.Errorf("No PEM data found for issuing CA") } @@ -55,7 +43,7 @@ func checkCertsAndPrivateKey(keyType string, usage certUsage, validity time.Dura return nil, nil, nil, err } - pemBlock, _ = pem.Decode([]byte(issueResp.PrivateKey)) + pemBlock, _ = pem.Decode([]byte(certBundle.PrivateKey)) if pemBlock == nil { return nil, nil, nil, fmt.Errorf("No PEM data found for private key") } @@ -68,7 +56,7 @@ func checkCertsAndPrivateKey(keyType string, usage certUsage, validity time.Dura case "ec": privKey, err = x509.ParseECPrivateKey(pemBlock.Bytes) if err != nil { - return nil, nil, nil, fmt.Errorf("Unable to decode EC private key: %s; value was %s", err, issueResp.PrivateKey) + return nil, nil, nil, fmt.Errorf("Unable to decode EC private key: %s; value was %s", err, certBundle.PrivateKey) } default: return nil, nil, nil, fmt.Errorf("Unknown private key type %s", keyType) @@ -208,7 +196,7 @@ func generateRoleSteps(t *testing.T) []logicaltest.TestStep { roleVals := roleEntry{ LeaseMax: "12h", } - issueVals := issueEntry{} + issueVals := certutil.IssueData{} ret := []logicaltest.TestStep{} roleTestStep := logicaltest.TestStep{ @@ -245,13 +233,13 @@ func generateRoleSteps(t *testing.T) []logicaltest.TestStep { } getCnCheck := func(name, keyType string, usage certUsage, validity time.Duration) logicaltest.TestCheckFunc { - var issueResp issueReply + var certBundle certutil.CertBundle return func(resp *logical.Response) error { - err := mapstructure.Decode(resp.Data, &issueResp) + err := mapstructure.Decode(resp.Data, &certBundle) if err != nil { return err } - cert, _, _, err := checkCertsAndPrivateKey(keyType, usage, validity, &issueResp) + cert, _, _, err := checkCertsAndPrivateKey(keyType, usage, validity, &certBundle) if err != nil { return fmt.Errorf("Error checking generated certificate: %s", err) } diff --git a/builtin/logical/pki/cert_util.go b/builtin/logical/pki/cert_util.go index 6a017cf0ec..7d8bd2b23d 100644 --- a/builtin/logical/pki/cert_util.go +++ b/builtin/logical/pki/cert_util.go @@ -1,16 +1,13 @@ package pki import ( - "bytes" "crypto" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/rsa" - "crypto/sha1" "crypto/x509" "crypto/x509/pkix" - "encoding/base64" "fmt" "math/big" "net" @@ -19,13 +16,7 @@ import ( "time" "github.com/hashicorp/vault/logical" -) - -// The type of Private Key, for storage -const ( - UnknownPrivateKeyType = iota - RSAPrivateKeyType - ECPrivateKeyType + "github.com/hashicorp/vault/logical/certutil" ) type certUsage int @@ -36,21 +27,8 @@ const ( codeSigningUsage ) -type certBundle struct { - PrivateKeyType int `json:"private_key_type"` - PrivateKeyString string `json:"private_key_string"` - CertificateString string `json:"certificate_string"` -} - -type rawCertBundle struct { - PrivateKeyType int - PrivateKeyBytes []byte - CertificateBytes []byte - SerialNumber *big.Int -} - type certCreationBundle struct { - RawSigningBundle *rawCertBundle + RawSigningBundle *certutil.RawCertBundle CACert *x509.Certificate CommonNames []string IPSANs []net.IP @@ -60,85 +38,7 @@ type certCreationBundle struct { Usage certUsage } -func (c *certBundle) toRawCertBundle() (*rawCertBundle, error) { - decoder := base64.URLEncoding - result := &rawCertBundle{ - PrivateKeyType: c.PrivateKeyType, - } - var err error - if result.PrivateKeyBytes, err = decoder.DecodeString(c.PrivateKeyString); err != nil { - return nil, err - } - if result.CertificateBytes, err = decoder.DecodeString(c.CertificateString); err != nil { - return nil, err - } - - if err := result.populateSerialNumber(); err != nil { - return nil, err - } - - return result, nil -} - -func (r *rawCertBundle) toCertBundle() *certBundle { - encoder := base64.URLEncoding - result := &certBundle{ - PrivateKeyType: r.PrivateKeyType, - PrivateKeyString: encoder.EncodeToString(r.PrivateKeyBytes), - CertificateString: encoder.EncodeToString(r.CertificateBytes), - } - return result -} - -func (r *rawCertBundle) populateSerialNumber() error { - cert, err := x509.ParseCertificate(r.CertificateBytes) - if err != nil { - return fmt.Errorf("Error encountered parsing certificate bytes from raw bundle") - } - r.SerialNumber = cert.SerialNumber - return nil -} - -// "Signer" corresponds to the Go interface that private keys implement -// that provides a Public() function for getting the corresponding public -// key. It can be type converted to private keys. -func (r *rawCertBundle) getSigner() (crypto.Signer, error) { - var signer crypto.Signer - var err error - switch r.PrivateKeyType { - case ECPrivateKeyType: - signer, err = x509.ParseECPrivateKey(r.PrivateKeyBytes) - if err != nil { - return nil, fmt.Errorf("Unable to parse CA's private EC key: %s", err) - } - case RSAPrivateKeyType: - signer, err = x509.ParsePKCS1PrivateKey(r.PrivateKeyBytes) - if err != nil { - return nil, fmt.Errorf("Unable to parse CA's private RSA key: %s", err) - } - default: - return nil, fmt.Errorf("Unable to determine the type of private key") - } - return signer, nil -} - -func (r *rawCertBundle) getSubjKeyID() ([]byte, error) { - privateKey, err := r.getSigner() - if err != nil { - return nil, err - } - - marshaledKey, err := x509.MarshalPKIXPublicKey(privateKey.Public()) - if err != nil { - return nil, fmt.Errorf("Error marshalling public key: %s", err) - } - - subjKeyID := sha1.Sum(marshaledKey) - - return subjKeyID[:], nil -} - -func getCertBundle(s logical.Storage, path string) (*certBundle, error) { +func getCertBundle(s logical.Storage, path string) (*certutil.CertBundle, error) { bundle, err := s.Get(path) if err != nil { return nil, err @@ -147,7 +47,7 @@ func getCertBundle(s logical.Storage, path string) (*certBundle, error) { return nil, nil } - var result certBundle + var result certutil.CertBundle if err := bundle.DecodeJSON(&result); err != nil { return nil, err } @@ -155,40 +55,29 @@ func getCertBundle(s logical.Storage, path string) (*certBundle, error) { return &result, nil } -func getOctalFormatted(buf []byte, sep string) string { - var ret bytes.Buffer - for _, cur := range buf { - if ret.Len() > 0 { - fmt.Fprintf(&ret, sep) - } - fmt.Fprintf(&ret, "%02x", cur) - } - return ret.String() -} - -func fetchCAInfo(req *logical.Request) (*rawCertBundle, *x509.Certificate, error) { +func fetchCAInfo(req *logical.Request) (*certutil.RawCertBundle, *x509.Certificate, error, error) { bundle, err := getCertBundle(req.Storage, "config/ca_bundle") if err != nil { - return nil, nil, fmt.Errorf("Unable to fetch local CA certificate/key: %s", err) + return nil, nil, nil, fmt.Errorf("Unable to fetch local CA certificate/key: %s", err) } if bundle == nil { - return nil, nil, fmt.Errorf("Backend must be configured with a CA certificate/key") + return nil, nil, fmt.Errorf("Backend must be configured with a CA certificate/key"), nil } - rawBundle, err := bundle.toRawCertBundle() + rawBundle, err := bundle.ToRawCertBundle() if err != nil { - return nil, nil, err + return nil, nil, nil, err } certificates, err := x509.ParseCertificates(rawBundle.CertificateBytes) switch { case err != nil: - return nil, nil, err + return nil, nil, nil, err case len(certificates) != 1: - return nil, nil, fmt.Errorf("Length of CA certificate bundle is wrong") + return nil, nil, nil, fmt.Errorf("Length of CA certificate bundle is wrong") } - return rawBundle, certificates[0], nil + return rawBundle, certificates[0], nil, nil } func fetchCertBySerial(req *logical.Request, prefix, serial string) (certEntry *logical.StorageEntry, userError, internalError error) { @@ -279,10 +168,10 @@ func validateCommonNames(req *logical.Request, commonNames []string, role *roleE return "", nil } -func createCertificate(creationInfo *certCreationBundle) (rawBundle *rawCertBundle, userErr, intErr error) { +func createCertificate(creationInfo *certCreationBundle) (rawBundle *certutil.RawCertBundle, userErr, intErr error) { var clientPrivKey crypto.Signer var err error - rawBundle = &rawCertBundle{} + rawBundle = &certutil.RawCertBundle{} rawBundle.SerialNumber, err = rand.Int(rand.Reader, (&big.Int{}).Exp(big.NewInt(2), big.NewInt(159), nil)) if err != nil { @@ -291,14 +180,14 @@ func createCertificate(creationInfo *certCreationBundle) (rawBundle *rawCertBund switch creationInfo.KeyType { case "rsa": - rawBundle.PrivateKeyType = RSAPrivateKeyType + rawBundle.PrivateKeyType = certutil.RSAPrivateKeyType clientPrivKey, err = rsa.GenerateKey(rand.Reader, creationInfo.KeyBits) if err != nil { return nil, nil, fmt.Errorf("Error generating RSA private key") } rawBundle.PrivateKeyBytes = x509.MarshalPKCS1PrivateKey(clientPrivKey.(*rsa.PrivateKey)) case "ec": - rawBundle.PrivateKeyType = ECPrivateKeyType + rawBundle.PrivateKeyType = certutil.ECPrivateKeyType var curve elliptic.Curve switch creationInfo.KeyBits { case 224: @@ -324,7 +213,7 @@ func createCertificate(creationInfo *certCreationBundle) (rawBundle *rawCertBund return nil, fmt.Errorf("Unknown key type: %s", creationInfo.KeyType), nil } - subjKeyID, err := rawBundle.getSubjKeyID() + subjKeyID, err := rawBundle.GetSubjKeyID() if err != nil { return nil, nil, fmt.Errorf("Error getting subject key ID: %s", err) } @@ -368,7 +257,7 @@ func createCertificate(creationInfo *certCreationBundle) (rawBundle *rawCertBund certTemplate.ExtKeyUsage = append(certTemplate.ExtKeyUsage, x509.ExtKeyUsageCodeSigning) } - signingPrivKey, err := creationInfo.RawSigningBundle.getSigner() + signingPrivKey, err := creationInfo.RawSigningBundle.GetSigner() if err != nil { return nil, nil, fmt.Errorf("Unable to get signing private key: %s", err) } @@ -379,6 +268,7 @@ func createCertificate(creationInfo *certCreationBundle) (rawBundle *rawCertBund } rawBundle.CertificateBytes = cert + rawBundle.IssuingCABytes = creationInfo.RawSigningBundle.CertificateBytes return } diff --git a/builtin/logical/pki/crl_util.go b/builtin/logical/pki/crl_util.go index eb4c58da32..307594785d 100644 --- a/builtin/logical/pki/crl_util.go +++ b/builtin/logical/pki/crl_util.go @@ -13,7 +13,7 @@ import ( type revocationInfo struct { CertificateBytes []byte `json:"certificate_bytes"` - RevocationTime int64 `json:"unix_time"` + RevocationTime int64 `json:"revocation_time"` } var ( @@ -85,9 +85,12 @@ func revokeCert(req *logical.Request, serial string) (*logical.Response, error) } - err = buildCRL(req) - if err != nil { - return nil, fmt.Errorf("Error encountered during CRL building: %s", err) + userErr, intErr = buildCRL(req) + switch { + case userErr != nil: + return logical.ErrorResponse(fmt.Sprintf("Error during CRL building: %s", userErr)), nil + case intErr != nil: + return nil, fmt.Errorf("Error encountered during CRL building: %s", intErr) } err = req.Storage.Delete("certs/" + serial) @@ -103,10 +106,10 @@ func revokeCert(req *logical.Request, serial string) (*logical.Response, error) }, nil } -func buildCRL(req *logical.Request) error { +func buildCRL(req *logical.Request) (error, error) { revokedSerials, err := req.Storage.List("revoked/") if err != nil { - return fmt.Errorf("Error fetching list of revoked certs: %s", err) + return nil, fmt.Errorf("Error fetching list of revoked certs: %s", err) } revokedCerts := []pkix.RevokedCertificate{} @@ -114,32 +117,32 @@ func buildCRL(req *logical.Request) error { for _, serial := range revokedSerials { revokedEntry, err := req.Storage.Get("revoked/" + serial) if err != nil { - return fmt.Errorf("Unable to fetch revoked cert with serial %s: %s", serial, err) + return nil, fmt.Errorf("Unable to fetch revoked cert with serial %s: %s", serial, err) } if revokedEntry == nil { - return fmt.Errorf("Revoked certificate entry for serial %s is nil", serial) + return nil, fmt.Errorf("Revoked certificate entry for serial %s is nil", serial) } if revokedEntry.Value == nil || len(revokedEntry.Value) == 0 { // TODO: In this case, remove it and continue? How likely is this to // happen? Alternately, could skip it entirely, or could implement a // delete function so that there is a way to remove these - return fmt.Errorf("Found revoked serial but actual certificate is empty") + return nil, fmt.Errorf("Found revoked serial but actual certificate is empty") } err = revokedEntry.DecodeJSON(&revInfo) if err != nil { - return fmt.Errorf("Error decoding revocation entry for serial %s: %s", serial, err) + return nil, fmt.Errorf("Error decoding revocation entry for serial %s: %s", serial, err) } revokedCert, err := x509.ParseCertificate(revInfo.CertificateBytes) if err != nil { - return fmt.Errorf("Unable to parse stored revoked certificate with serial %s: %s", serial, err) + return nil, fmt.Errorf("Unable to parse stored revoked certificate with serial %s: %s", serial, err) } if revokedCert.NotAfter.Before(time.Now()) { err = req.Storage.Delete(serial) if err != nil { - return fmt.Errorf("Unable to delete revoked, expired certificate with serial %s: %s", serial, err) + return nil, fmt.Errorf("Unable to delete revoked, expired certificate with serial %s: %s", serial, err) } continue } @@ -150,20 +153,23 @@ func buildCRL(req *logical.Request) error { }) } - rawSigningBundle, caCert, err := fetchCAInfo(req) - if err != nil { - return fmt.Errorf("Could not fetch the CA certificate") + rawSigningBundle, caCert, userErr, intErr := fetchCAInfo(req) + switch { + case userErr != nil: + return fmt.Errorf("Could not fetch the CA certificate: %s", userErr), nil + case intErr != nil: + return nil, fmt.Errorf("Error fetching CA certificate: %s", intErr) } - signingPrivKey, err := rawSigningBundle.getSigner() + signingPrivKey, err := rawSigningBundle.GetSigner() if err != nil { - return fmt.Errorf("Unable to get signing private key: %s", err) + return nil, fmt.Errorf("Unable to get signing private key: %s", err) } // TODO: Make expiry configurable crlBytes, err := caCert.CreateCRL(rand.Reader, signingPrivKey, revokedCerts, time.Now(), time.Now().Add(crlLifetime)) if err != nil { - return fmt.Errorf("Error creating new CRL: %s", err) + return nil, fmt.Errorf("Error creating new CRL: %s", err) } err = req.Storage.Put(&logical.StorageEntry{ @@ -171,8 +177,8 @@ func buildCRL(req *logical.Request) error { Value: crlBytes, }) if err != nil { - return fmt.Errorf("Error storing CRL: %s", err) + return nil, fmt.Errorf("Error storing CRL: %s", err) } - return nil + return nil, nil } diff --git a/builtin/logical/pki/path_config_ca.go b/builtin/logical/pki/path_config_ca.go index 3f2310bfe9..2c029ff9ae 100644 --- a/builtin/logical/pki/path_config_ca.go +++ b/builtin/logical/pki/path_config_ca.go @@ -3,8 +3,10 @@ package pki import ( "crypto/x509" "encoding/pem" + "fmt" "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/certutil" "github.com/hashicorp/vault/logical/framework" ) @@ -37,7 +39,7 @@ func (b *backend) pathCAWrite( pemBytes := []byte(pemBundle) var pemBlock *pem.Block - rawBundle := &rawCertBundle{} + rawBundle := &certutil.RawCertBundle{} for { pemBlock, pemBytes = pem.Decode(pemBytes) @@ -46,10 +48,10 @@ func (b *backend) pathCAWrite( } if _, err := x509.ParseECPrivateKey(pemBlock.Bytes); err == nil { - if rawBundle.PrivateKeyType != UnknownPrivateKeyType { + if rawBundle.PrivateKeyType != certutil.UnknownPrivateKeyType { return logical.ErrorResponse("More than one private key given; provide only one private key in the bundle"), nil } - rawBundle.PrivateKeyType = ECPrivateKeyType + rawBundle.PrivateKeyType = certutil.ECPrivateKeyType rawBundle.PrivateKeyBytes = pemBlock.Bytes // TODO?: CRLs can only be generated with RSA keys right now, in the // Go standard library. The plubming is here to support non-RSA keys @@ -57,10 +59,10 @@ func (b *backend) pathCAWrite( return logical.ErrorResponse("Only RSA keys are supported at this time due to limitations in the Go standard library"), nil } else if _, err := x509.ParsePKCS1PrivateKey(pemBlock.Bytes); err == nil { - if rawBundle.PrivateKeyType != UnknownPrivateKeyType { + if rawBundle.PrivateKeyType != certutil.UnknownPrivateKeyType { return logical.ErrorResponse("More than one private key given; provide only one private key in the bundle"), nil } - rawBundle.PrivateKeyType = RSAPrivateKeyType + rawBundle.PrivateKeyType = certutil.RSAPrivateKeyType rawBundle.PrivateKeyBytes = pemBlock.Bytes } else if certificates, err := x509.ParseCertificates(pemBlock.Bytes); err == nil { switch len(certificates) { @@ -72,6 +74,7 @@ func (b *backend) pathCAWrite( return logical.ErrorResponse("The given certificate is not marked for CA use and cannot be used with this backend"), nil } rawBundle.CertificateBytes = pemBlock.Bytes + rawBundle.SerialNumber = cert.SerialNumber default: return logical.ErrorResponse("More than one certificate given; provide only one certificate in the bundle"), nil } @@ -83,7 +86,7 @@ func (b *backend) pathCAWrite( } switch { - case rawBundle.PrivateKeyType == UnknownPrivateKeyType: + case rawBundle.PrivateKeyType == certutil.UnknownPrivateKeyType: return logical.ErrorResponse("Unable to figure out the private key type; must be RSA or EC"), nil case len(rawBundle.PrivateKeyBytes) == 0: return logical.ErrorResponse("Unable to decode the private key from the bundle"), nil @@ -91,7 +94,10 @@ func (b *backend) pathCAWrite( return logical.ErrorResponse("Unable to decode the certificate from the bundle"), nil } - cb := rawBundle.toCertBundle() + cb, err := rawBundle.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 diff --git a/builtin/logical/pki/path_fetch.go b/builtin/logical/pki/path_fetch.go index 6063aedd22..3a4d5e6d13 100644 --- a/builtin/logical/pki/path_fetch.go +++ b/builtin/logical/pki/path_fetch.go @@ -91,7 +91,7 @@ func (b *backend) pathFetchRead(req *logical.Request, data *framework.FieldData) var pemType string var contentType string var certEntry *logical.StorageEntry - var userErr, intErr, err error + var userErr, intErr error var certificate []byte response = &logical.Response{ Data: map[string]interface{}{}, @@ -122,9 +122,13 @@ func (b *backend) pathFetchRead(req *logical.Request, data *framework.FieldData) goto reply } - _, _, err = fetchCAInfo(req) - if err != nil { - response = logical.ErrorResponse("No CA information configured for this backend") + _, _, userErr, intErr = fetchCAInfo(req) + switch { + case userErr != nil: + response = logical.ErrorResponse(fmt.Sprintf("%s", userErr)) + goto reply + case intErr != nil: + retErr = intErr goto reply } @@ -173,7 +177,6 @@ reply: } retErr = nil response.Data[logical.HTTPStatusCode] = 200 - case retErr != nil: response = nil default: diff --git a/builtin/logical/pki/path_issue.go b/builtin/logical/pki/path_issue.go index 15aaa885fb..024eae88de 100644 --- a/builtin/logical/pki/path_issue.go +++ b/builtin/logical/pki/path_issue.go @@ -1,12 +1,12 @@ package pki import ( - "encoding/pem" "fmt" "net" "strings" "time" + "github.com/fatih/structs" "github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/logical/framework" ) @@ -118,9 +118,12 @@ func (b *backend) pathIssueCert( return nil, fmt.Errorf("Error validating name %s: %s", badName, err) } - rawSigningBundle, caCert, err := fetchCAInfo(req) - if err != nil { - return logical.ErrorResponse("Could not fetch the CA certificate; has it been set?"), nil + rawSigningBundle, caCert, userErr, intErr := fetchCAInfo(req) + switch { + case userErr != nil: + return logical.ErrorResponse(fmt.Sprintf("Could not fetch the CA certificate: %s", userErr)), nil + case intErr != nil: + return nil, fmt.Errorf("Error fetching CA certificate: %s", intErr) } if time.Now().Add(lease).After(caCert.NotAfter) { @@ -157,40 +160,21 @@ func (b *backend) pathIssueCert( return nil, intErr } - serial := strings.ToLower(getOctalFormatted(rawBundle.SerialNumber.Bytes(), ":")) - - resp := b.Secret(SecretCertsType).Response(map[string]interface{}{ - "serial": serial, - }, map[string]interface{}{ - "serial": serial, - }) - - block := pem.Block{ - Type: "CERTIFICATE", - Bytes: rawBundle.CertificateBytes, + cb, err := rawBundle.ToCertBundle() + if err != nil { + return nil, fmt.Errorf("Error converting raw cert bundle to cert bundle: %s", err) } - certificateString := string(pem.EncodeToMemory(&block)) - resp.Data["certificate"] = certificateString - block.Bytes = rawSigningBundle.CertificateBytes - caString := string(pem.EncodeToMemory(&block)) - resp.Data["issuing_ca"] = caString - - block.Bytes = rawBundle.PrivateKeyBytes - switch rawBundle.PrivateKeyType { - case RSAPrivateKeyType: - block.Type = "RSA PRIVATE KEY" - case ECPrivateKeyType: - block.Type = "EC PRIVATE KEY" - default: - return nil, fmt.Errorf("Could not determine private key type when creating block") - } - resp.Data["private_key"] = string(pem.EncodeToMemory(&block)) + resp := b.Secret(SecretCertsType).Response( + structs.New(cb).Map(), + map[string]interface{}{ + "serial_number": cb.SerialNumber, + }) resp.Secret.Lease = lease err = req.Storage.Put(&logical.StorageEntry{ - Key: "certs/" + serial, + Key: "certs/" + cb.SerialNumber, Value: rawBundle.CertificateBytes, }) if err != nil { diff --git a/builtin/logical/pki/path_revoke.go b/builtin/logical/pki/path_revoke.go index 3c2c9ec5aa..9e6a88cf8a 100644 --- a/builtin/logical/pki/path_revoke.go +++ b/builtin/logical/pki/path_revoke.go @@ -11,7 +11,7 @@ func pathRevoke(b *backend) *framework.Path { return &framework.Path{ Pattern: `revoke`, Fields: map[string]*framework.FieldSchema{ - "serial": &framework.FieldSchema{ + "serial_number": &framework.FieldSchema{ Type: framework.TypeString, Description: "Certificate serial number, in colon- or hyphen-separated octal", }, @@ -40,7 +40,7 @@ func pathRotateCRL(b *backend) *framework.Path { } func (b *backend) pathRevokeWrite(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { - serial := data.Get("serial").(string) + serial := data.Get("serial_number").(string) if len(serial) == 0 { return logical.ErrorResponse("The serial number must be provided"), nil } @@ -55,10 +55,14 @@ func (b *backend) pathRotateCRLRead(req *logical.Request, data *framework.FieldD revokeStorageLock.Lock() defer revokeStorageLock.Unlock() - err := buildCRL(req) - if err != nil { - return logical.ErrorResponse(fmt.Sprintf("Error building CRL: %s", err)), err + userErr, intErr := buildCRL(req) + switch { + case userErr != nil: + return logical.ErrorResponse(fmt.Sprintf("Error during CRL building: %s", userErr)), nil + case intErr != nil: + return nil, fmt.Errorf("Error encountered during CRL building: %s", intErr) } + return &logical.Response{ Data: map[string]interface{}{ "success": true, diff --git a/builtin/logical/pki/path_roles.go b/builtin/logical/pki/path_roles.go index 399550a785..ce838c30fc 100644 --- a/builtin/logical/pki/path_roles.go +++ b/builtin/logical/pki/path_roles.go @@ -4,6 +4,7 @@ import ( "fmt" "time" + "github.com/fatih/structs" "github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/logical/framework" ) @@ -145,21 +146,7 @@ func (b *backend) pathRoleRead( } resp := &logical.Response{ - Data: map[string]interface{}{ - "lease_max": role.LeaseMax, - "lease": role.Lease, - "allow_localhost": role.AllowLocalhost, - "allowed_base_domain": role.AllowedBaseDomain, - "allow_token_displayname": role.AllowTokenDisplayName, - "allow_subdomains": role.AllowSubdomains, - "allow_ip_sans": role.AllowIPSANs, - "allow_any_name": role.AllowAnyName, - "server_flag": role.ServerFlag, - "client_flag": role.ClientFlag, - "code_signing_flag": role.CodeSigningFlag, - "key_type": role.KeyType, - "key_bits": role.KeyBits, - }, + Data: structs.New(role).Map(), } return resp, nil diff --git a/builtin/logical/pki/secret_certs.go b/builtin/logical/pki/secret_certs.go index 28c8a2dc49..4a6681ac24 100644 --- a/builtin/logical/pki/secret_certs.go +++ b/builtin/logical/pki/secret_certs.go @@ -43,7 +43,7 @@ func (b *backend) secretCredsRevoke( return nil, fmt.Errorf("Secret is nil in request") } - serialInt, ok := req.Secret.InternalData["serial"] + serialInt, ok := req.Secret.InternalData["serial_number"] if !ok { return nil, fmt.Errorf("Could not find serial in internal secret data") } diff --git a/logical/certutil/helpers.go b/logical/certutil/helpers.go new file mode 100644 index 0000000000..974ddbd753 --- /dev/null +++ b/logical/certutil/helpers.go @@ -0,0 +1,35 @@ +package certutil + +import ( + "bytes" + "fmt" + + "github.com/hashicorp/vault/api" + "github.com/mitchellh/mapstructure" +) + +// GetOctalFormatted returns the byte buffer formatted in octal with +// the specified separator between bytes +func GetOctalFormatted(buf []byte, sep string) string { + var ret bytes.Buffer + for _, cur := range buf { + if ret.Len() > 0 { + fmt.Fprintf(&ret, sep) + } + fmt.Fprintf(&ret, "%02x", cur) + } + return ret.String() +} + +// ParsePKISecret takes a Secret returned from the PKI backend +// and returns a CertBundle for further processing (espcially +// by converting to a RawCertBundle) +func ParsePKISecret(secret *api.Secret) (*CertBundle, error) { + var result CertBundle + err := mapstructure.Decode(secret.Data, &result) + if err != nil { + return nil, err + } + + return &result, nil +} diff --git a/logical/certutil/types.go b/logical/certutil/types.go new file mode 100644 index 0000000000..2ccc0f8afa --- /dev/null +++ b/logical/certutil/types.go @@ -0,0 +1,183 @@ +// Package certutil contains helper functions that are mostly used +// with the PKI backend but can be generally useful. Functionality +// includes helpers for converting a certificate/private key bundle +// between DER and PEM, printing certificate serial numbers, and more. +// +// Functionality specific to the PKI backend includes some types +// and helper methods to make requesting certificates from the +// backend easy. +package certutil + +import ( + "crypto" + "crypto/sha1" + "crypto/x509" + "encoding/pem" + "fmt" + "math/big" +) + +// The type of of the Private Key referenced in CertBundle +// and RawCertBundle. This uses colloquial names rather than +// official names, to eliminate confusion +const ( + UnknownPrivateKeyType = iota + RSAPrivateKeyType + ECPrivateKeyType +) + +// CertBundle contains a key type, a private key, +// a certificate, and a string-encoded serial number, +// returned from a successful Issue request +type CertBundle struct { + PrivateKeyType string `json:"private_key_type" structs:"private_key_type" mapstructure:"private_key_type"` + Certificate string `json:"certificate" structs:"certificate" mapstructure:"certificate"` + IssuingCA string `json:"issuing_ca" structs:"issuing_ca" mapstructure:"issuing_ca"` + PrivateKey string `json:"private_key" structs:"private_key" mapstructure:"private_key"` + SerialNumber string `json:"serial_number" structs:"serial_number" mapstructure:"serial_number"` +} + +// ToRawCertBundle converts a string-based certificate bundle +// to a byte-based raw certificate bundle +func (c *CertBundle) ToRawCertBundle() (*RawCertBundle, error) { + result := &RawCertBundle{} + switch c.PrivateKeyType { + case "ec": + result.PrivateKeyType = ECPrivateKeyType + case "rsa": + result.PrivateKeyType = RSAPrivateKeyType + default: + return nil, fmt.Errorf("Unknown private key type in bundle: %s", c.PrivateKeyType) + } + + var pemBlock *pem.Block + pemBlock, _ = pem.Decode([]byte(c.PrivateKey)) + if pemBlock == nil { + return nil, fmt.Errorf("Error decoding private key from cert bundle") + } + result.PrivateKeyBytes = pemBlock.Bytes + + pemBlock, _ = pem.Decode([]byte(c.Certificate)) + if pemBlock == nil { + return nil, fmt.Errorf("Error decoding certificate from cert bundle") + } + result.CertificateBytes = pemBlock.Bytes + + if len(c.IssuingCA) != 0 { + pemBlock, _ = pem.Decode([]byte(c.IssuingCA)) + if pemBlock == nil { + return nil, fmt.Errorf("Error decoding issuing CA from cert bundle") + } + result.IssuingCABytes = pemBlock.Bytes + } + + if err := result.populateSerialNumber(); err != nil { + return nil, err + } + + return result, nil +} + +// RawCertBundle contains a key type, a DER-encoded private key, +// a DER-encoded certificate, and a big.Int serial number +type RawCertBundle struct { + PrivateKeyType int + PrivateKeyBytes []byte + IssuingCABytes []byte + CertificateBytes []byte + SerialNumber *big.Int +} + +// ToCertBundle converts a byte-based raw DER certificate bundle +// to a PEM-based string certificate bundle +func (r *RawCertBundle) ToCertBundle() (*CertBundle, error) { + result := &CertBundle{ + SerialNumber: GetOctalFormatted(r.SerialNumber.Bytes(), ":"), + } + + block := pem.Block{ + Type: "CERTIFICATE", + Bytes: r.CertificateBytes, + } + result.Certificate = string(pem.EncodeToMemory(&block)) + + if len(r.IssuingCABytes) != 0 { + block.Bytes = r.IssuingCABytes + result.IssuingCA = string(pem.EncodeToMemory(&block)) + } + + block.Bytes = r.PrivateKeyBytes + switch r.PrivateKeyType { + case RSAPrivateKeyType: + result.PrivateKeyType = "rsa" + block.Type = "RSA PRIVATE KEY" + case ECPrivateKeyType: + result.PrivateKeyType = "ec" + block.Type = "EC PRIVATE KEY" + default: + return nil, fmt.Errorf("Could not determine private key type when creating block") + } + result.PrivateKey = string(pem.EncodeToMemory(&block)) + + return result, nil +} + +func (r *RawCertBundle) populateSerialNumber() error { + cert, err := x509.ParseCertificate(r.CertificateBytes) + if err != nil { + return fmt.Errorf("Error encountered parsing certificate bytes from raw bundle") + } + r.SerialNumber = cert.SerialNumber + return nil +} + +// GetSigner returns a crypto.Signer corresponding to the private key +// contained in this RawCertBundle. The Signer contains a Public() function +// for getting the corresponding public. The Signer can also be +// type-converted to private keys +func (r *RawCertBundle) GetSigner() (crypto.Signer, error) { + var signer crypto.Signer + var err error + switch r.PrivateKeyType { + case ECPrivateKeyType: + signer, err = x509.ParseECPrivateKey(r.PrivateKeyBytes) + if err != nil { + return nil, fmt.Errorf("Unable to parse CA's private EC key: %s", err) + } + case RSAPrivateKeyType: + signer, err = x509.ParsePKCS1PrivateKey(r.PrivateKeyBytes) + if err != nil { + return nil, fmt.Errorf("Unable to parse CA's private RSA key: %s", err) + } + default: + return nil, fmt.Errorf("Unable to determine the type of private key") + } + return signer, nil +} + +// GetSubjKeyID returns the subject key ID, e.g. the SHA1 sum +// of the marshaled public key +func (r *RawCertBundle) GetSubjKeyID() ([]byte, error) { + privateKey, err := r.GetSigner() + if err != nil { + return nil, err + } + + marshaledKey, err := x509.MarshalPKIXPublicKey(privateKey.Public()) + if err != nil { + return nil, fmt.Errorf("Error marshalling public key: %s", err) + } + + subjKeyID := sha1.Sum(marshaledKey) + + return subjKeyID[:], nil +} + +// IssueData is a structure that is suitable for marshaling into a request; +// either via JSON, or into a map[string]interface{} via the structs package +type IssueData struct { + Lease string `json:"lease" structs:"lease" mapstructure:"lease"` + CommonName string `json:"common_name" structs:"common_name" mapstructure:"common_name"` + AltNames string `json:"alt_names" structs:"alt_names" mapstructure:"alt_names"` + IPSANs string `json:"ip_sans" structs:"ip_sans" mapstructure:"ip_sans"` +} From 31e680048e11e0bb5c176cb0e1306c6c910c07ed Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Wed, 17 Jun 2015 12:43:36 -0400 Subject: [PATCH 09/15] A lot of refactoring: move PEM bundle parsing into helper/certutil, so that it is usable by other backends that want to use it to get the necessary data for TLS auth. Also, enhance the raw cert bundle => parsed cert bundle to make it more useful and perform more validation checks. More refactoring could be done within the PKI backend itself, but that can wait. Commit contents (C)2015 Akamai Technologies, Inc. --- builtin/logical/pki/backend_test.go | 2 +- builtin/logical/pki/cert_util.go | 66 +++---- builtin/logical/pki/crl_util.go | 9 +- builtin/logical/pki/path_config_ca.go | 82 +++------ builtin/logical/pki/path_issue.go | 24 +-- helper/certutil/helpers.go | 167 +++++++++++++++++ helper/certutil/types.go | 255 ++++++++++++++++++++++++++ logical/certutil/helpers.go | 35 ---- logical/certutil/types.go | 183 ------------------ 9 files changed, 493 insertions(+), 330 deletions(-) create mode 100644 helper/certutil/helpers.go create mode 100644 helper/certutil/types.go delete mode 100644 logical/certutil/helpers.go delete mode 100644 logical/certutil/types.go diff --git a/builtin/logical/pki/backend_test.go b/builtin/logical/pki/backend_test.go index f57adbd60d..3c85973305 100644 --- a/builtin/logical/pki/backend_test.go +++ b/builtin/logical/pki/backend_test.go @@ -12,8 +12,8 @@ import ( "time" "github.com/fatih/structs" + "github.com/hashicorp/vault/helper/certutil" "github.com/hashicorp/vault/logical" - "github.com/hashicorp/vault/logical/certutil" logicaltest "github.com/hashicorp/vault/logical/testing" "github.com/mitchellh/mapstructure" ) diff --git a/builtin/logical/pki/cert_util.go b/builtin/logical/pki/cert_util.go index 7d8bd2b23d..23af3de38e 100644 --- a/builtin/logical/pki/cert_util.go +++ b/builtin/logical/pki/cert_util.go @@ -15,8 +15,8 @@ import ( "strings" "time" + "github.com/hashicorp/vault/helper/certutil" "github.com/hashicorp/vault/logical" - "github.com/hashicorp/vault/logical/certutil" ) type certUsage int @@ -28,14 +28,14 @@ const ( ) type certCreationBundle struct { - RawSigningBundle *certutil.RawCertBundle - CACert *x509.Certificate - CommonNames []string - IPSANs []net.IP - KeyType string - KeyBits int - Lease time.Duration - Usage certUsage + SigningBundle *certutil.ParsedCertBundle + CACert *x509.Certificate + CommonNames []string + IPSANs []net.IP + KeyType string + KeyBits int + Lease time.Duration + Usage certUsage } func getCertBundle(s logical.Storage, path string) (*certutil.CertBundle, error) { @@ -55,7 +55,7 @@ func getCertBundle(s logical.Storage, path string) (*certutil.CertBundle, error) return &result, nil } -func fetchCAInfo(req *logical.Request) (*certutil.RawCertBundle, *x509.Certificate, error, error) { +func fetchCAInfo(req *logical.Request) (*certutil.ParsedCertBundle, *x509.Certificate, error, error) { bundle, err := getCertBundle(req.Storage, "config/ca_bundle") if err != nil { return nil, nil, nil, fmt.Errorf("Unable to fetch local CA certificate/key: %s", err) @@ -64,12 +64,12 @@ func fetchCAInfo(req *logical.Request) (*certutil.RawCertBundle, *x509.Certifica return nil, nil, fmt.Errorf("Backend must be configured with a CA certificate/key"), nil } - rawBundle, err := bundle.ToRawCertBundle() + parsedBundle, err := bundle.ToParsedCertBundle() if err != nil { return nil, nil, nil, err } - certificates, err := x509.ParseCertificates(rawBundle.CertificateBytes) + certificates, err := x509.ParseCertificates(parsedBundle.CertificateBytes) switch { case err != nil: return nil, nil, nil, err @@ -77,7 +77,7 @@ func fetchCAInfo(req *logical.Request) (*certutil.RawCertBundle, *x509.Certifica return nil, nil, nil, fmt.Errorf("Length of CA certificate bundle is wrong") } - return rawBundle, certificates[0], nil, nil + return parsedBundle, certificates[0], nil, nil } func fetchCertBySerial(req *logical.Request, prefix, serial string) (certEntry *logical.StorageEntry, userError, internalError error) { @@ -168,26 +168,28 @@ func validateCommonNames(req *logical.Request, commonNames []string, role *roleE return "", nil } -func createCertificate(creationInfo *certCreationBundle) (rawBundle *certutil.RawCertBundle, userErr, intErr error) { +func createCertificate(creationInfo *certCreationBundle) (parsedBundle *certutil.ParsedCertBundle, userErr, intErr error) { var clientPrivKey crypto.Signer var err error - rawBundle = &certutil.RawCertBundle{} + parsedBundle = &certutil.ParsedCertBundle{} - rawBundle.SerialNumber, err = rand.Int(rand.Reader, (&big.Int{}).Exp(big.NewInt(2), big.NewInt(159), nil)) + var serialNumber *big.Int + serialNumber, err = rand.Int(rand.Reader, (&big.Int{}).Exp(big.NewInt(2), big.NewInt(159), nil)) if err != nil { return nil, nil, fmt.Errorf("Error getting random serial number") } switch creationInfo.KeyType { case "rsa": - rawBundle.PrivateKeyType = certutil.RSAPrivateKeyType + parsedBundle.PrivateKeyType = certutil.RSAPrivateKey clientPrivKey, err = rsa.GenerateKey(rand.Reader, creationInfo.KeyBits) if err != nil { return nil, nil, fmt.Errorf("Error generating RSA private key") } - rawBundle.PrivateKeyBytes = x509.MarshalPKCS1PrivateKey(clientPrivKey.(*rsa.PrivateKey)) + parsedBundle.PrivateKey = clientPrivKey + parsedBundle.PrivateKeyBytes = x509.MarshalPKCS1PrivateKey(clientPrivKey.(*rsa.PrivateKey)) case "ec": - rawBundle.PrivateKeyType = certutil.ECPrivateKeyType + parsedBundle.PrivateKeyType = certutil.ECPrivateKey var curve elliptic.Curve switch creationInfo.KeyBits { case 224: @@ -205,7 +207,8 @@ func createCertificate(creationInfo *certCreationBundle) (rawBundle *certutil.Ra if err != nil { return nil, nil, fmt.Errorf("Error generating EC private key") } - rawBundle.PrivateKeyBytes, err = x509.MarshalECPrivateKey(clientPrivKey.(*ecdsa.PrivateKey)) + parsedBundle.PrivateKey = clientPrivKey + parsedBundle.PrivateKeyBytes, err = x509.MarshalECPrivateKey(clientPrivKey.(*ecdsa.PrivateKey)) if err != nil { return nil, nil, fmt.Errorf("Error marshalling EC private key") } @@ -213,7 +216,7 @@ func createCertificate(creationInfo *certCreationBundle) (rawBundle *certutil.Ra return nil, fmt.Errorf("Unknown key type: %s", creationInfo.KeyType), nil } - subjKeyID, err := rawBundle.GetSubjKeyID() + subjKeyID, err := certutil.GetSubjKeyID(parsedBundle.PrivateKey) if err != nil { return nil, nil, fmt.Errorf("Error getting subject key ID: %s", err) } @@ -226,13 +229,13 @@ func createCertificate(creationInfo *certCreationBundle) (rawBundle *certutil.Ra Province: creationInfo.CACert.Subject.Province, StreetAddress: creationInfo.CACert.Subject.StreetAddress, PostalCode: creationInfo.CACert.Subject.PostalCode, - SerialNumber: rawBundle.SerialNumber.String(), + SerialNumber: serialNumber.String(), CommonName: creationInfo.CommonNames[0], } certTemplate := &x509.Certificate{ SignatureAlgorithm: x509.SHA256WithRSA, - SerialNumber: rawBundle.SerialNumber, + SerialNumber: serialNumber, Subject: subject, NotBefore: time.Now(), NotAfter: time.Now().Add(creationInfo.Lease), @@ -257,18 +260,19 @@ func createCertificate(creationInfo *certCreationBundle) (rawBundle *certutil.Ra certTemplate.ExtKeyUsage = append(certTemplate.ExtKeyUsage, x509.ExtKeyUsageCodeSigning) } - signingPrivKey, err := creationInfo.RawSigningBundle.GetSigner() - if err != nil { - return nil, nil, fmt.Errorf("Unable to get signing private key: %s", err) - } - - cert, err := x509.CreateCertificate(rand.Reader, certTemplate, creationInfo.CACert, clientPrivKey.Public(), signingPrivKey) + cert, err := x509.CreateCertificate(rand.Reader, certTemplate, creationInfo.CACert, clientPrivKey.Public(), creationInfo.SigningBundle.PrivateKey) if err != nil { return nil, nil, fmt.Errorf("Unable to create certificate: %s", err) } - rawBundle.CertificateBytes = cert - rawBundle.IssuingCABytes = creationInfo.RawSigningBundle.CertificateBytes + parsedBundle.CertificateBytes = cert + parsedBundle.Certificate, err = x509.ParseCertificate(cert) + if err != nil { + return nil, nil, fmt.Errorf("Unable to parse created certificate: %s", err) + } + + parsedBundle.IssuingCABytes = creationInfo.SigningBundle.CertificateBytes + parsedBundle.IssuingCA = creationInfo.SigningBundle.Certificate return } diff --git a/builtin/logical/pki/crl_util.go b/builtin/logical/pki/crl_util.go index 307594785d..2ca33d5391 100644 --- a/builtin/logical/pki/crl_util.go +++ b/builtin/logical/pki/crl_util.go @@ -153,7 +153,7 @@ func buildCRL(req *logical.Request) (error, error) { }) } - rawSigningBundle, caCert, userErr, intErr := fetchCAInfo(req) + signingBundle, caCert, userErr, intErr := fetchCAInfo(req) switch { case userErr != nil: return fmt.Errorf("Could not fetch the CA certificate: %s", userErr), nil @@ -161,13 +161,8 @@ func buildCRL(req *logical.Request) (error, error) { return nil, fmt.Errorf("Error fetching CA certificate: %s", intErr) } - signingPrivKey, err := rawSigningBundle.GetSigner() - if err != nil { - return nil, fmt.Errorf("Unable to get signing private key: %s", err) - } - // TODO: Make expiry configurable - crlBytes, err := caCert.CreateCRL(rand.Reader, signingPrivKey, revokedCerts, time.Now(), time.Now().Add(crlLifetime)) + crlBytes, err := caCert.CreateCRL(rand.Reader, signingBundle.PrivateKey, revokedCerts, time.Now(), time.Now().Add(crlLifetime)) if err != nil { return nil, fmt.Errorf("Error creating new CRL: %s", err) } diff --git a/builtin/logical/pki/path_config_ca.go b/builtin/logical/pki/path_config_ca.go index 2c029ff9ae..836e1166a2 100644 --- a/builtin/logical/pki/path_config_ca.go +++ b/builtin/logical/pki/path_config_ca.go @@ -1,12 +1,10 @@ package pki import ( - "crypto/x509" - "encoding/pem" "fmt" + "github.com/hashicorp/vault/helper/certutil" "github.com/hashicorp/vault/logical" - "github.com/hashicorp/vault/logical/certutil" "github.com/hashicorp/vault/logical/framework" ) @@ -33,71 +31,33 @@ func (b *backend) pathCAWrite( req *logical.Request, d *framework.FieldData) (*logical.Response, error) { pemBundle := d.Get("pem_bundle").(string) - if len(pemBundle) == 0 { - return logical.ErrorResponse("Empty PEM bundle"), nil - } - - pemBytes := []byte(pemBundle) - var pemBlock *pem.Block - rawBundle := &certutil.RawCertBundle{} - - for { - pemBlock, pemBytes = pem.Decode(pemBytes) - if pemBlock == nil { - return logical.ErrorResponse("No PEM data found"), nil - } - - if _, err := x509.ParseECPrivateKey(pemBlock.Bytes); err == nil { - if rawBundle.PrivateKeyType != certutil.UnknownPrivateKeyType { - return logical.ErrorResponse("More than one private key given; provide only one private key in the bundle"), nil - } - rawBundle.PrivateKeyType = certutil.ECPrivateKeyType - rawBundle.PrivateKeyBytes = pemBlock.Bytes - // TODO?: CRLs can only be generated with RSA keys right now, in the - // Go standard library. The plubming is here to support non-RSA keys - // if the library gets support - return logical.ErrorResponse("Only RSA keys are supported at this time due to limitations in the Go standard library"), nil - } else if _, err := x509.ParsePKCS1PrivateKey(pemBlock.Bytes); err == nil { - - if rawBundle.PrivateKeyType != certutil.UnknownPrivateKeyType { - return logical.ErrorResponse("More than one private key given; provide only one private key in the bundle"), nil - } - rawBundle.PrivateKeyType = certutil.RSAPrivateKeyType - rawBundle.PrivateKeyBytes = pemBlock.Bytes - } else if certificates, err := x509.ParseCertificates(pemBlock.Bytes); err == nil { - switch len(certificates) { - case 0: - return logical.ErrorResponse("No certificates found in the bundle"), nil - case 1: - cert := certificates[0] - if !cert.IsCA { - return logical.ErrorResponse("The given certificate is not marked for CA use and cannot be used with this backend"), nil - } - rawBundle.CertificateBytes = pemBlock.Bytes - rawBundle.SerialNumber = cert.SerialNumber - default: - return logical.ErrorResponse("More than one certificate given; provide only one certificate in the bundle"), nil - } - } - - if len(pemBytes) == 0 { - break + parsedBundle, err := certutil.ParsePEMBundle(pemBundle) + if err != nil { + switch err.(type) { + case certutil.InternalError: + return nil, err + default: + return logical.ErrorResponse(err.Error()), nil } } - switch { - case rawBundle.PrivateKeyType == certutil.UnknownPrivateKeyType: - return logical.ErrorResponse("Unable to figure out the private key type; must be RSA or EC"), nil - case len(rawBundle.PrivateKeyBytes) == 0: - return logical.ErrorResponse("Unable to decode the private key from the bundle"), nil - case len(rawBundle.CertificateBytes) == 0: - return logical.ErrorResponse("Unable to decode the certificate from the bundle"), nil + // TODO?: CRLs can only be generated with RSA keys right now, in the + // Go standard library. The plubming is here to support non-RSA keys + // if the library gets support + + if parsedBundle.PrivateKeyType != certutil.RSAPrivateKey { + return logical.ErrorResponse("Currently, only RSA keys are supported for the CA certificate"), nil } - cb, err := rawBundle.ToCertBundle() + if !parsedBundle.Certificate.IsCA { + return logical.ErrorResponse("The given certificate is not marked for CA use and cannot be used with this backend"), nil + } + + cb, err := parsedBundle.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 @@ -110,7 +70,7 @@ func (b *backend) pathCAWrite( // For ease of later use, also store just the certificate at a known // location, plus a blank CRL entry.Key = "ca" - entry.Value = rawBundle.CertificateBytes + entry.Value = parsedBundle.CertificateBytes err = req.Storage.Put(entry) if err != nil { return nil, err diff --git a/builtin/logical/pki/path_issue.go b/builtin/logical/pki/path_issue.go index 024eae88de..a4fd3308a6 100644 --- a/builtin/logical/pki/path_issue.go +++ b/builtin/logical/pki/path_issue.go @@ -118,7 +118,7 @@ func (b *backend) pathIssueCert( return nil, fmt.Errorf("Error validating name %s: %s", badName, err) } - rawSigningBundle, caCert, userErr, intErr := fetchCAInfo(req) + signingBundle, caCert, userErr, intErr := fetchCAInfo(req) switch { case userErr != nil: return logical.ErrorResponse(fmt.Sprintf("Could not fetch the CA certificate: %s", userErr)), nil @@ -142,17 +142,17 @@ func (b *backend) pathIssueCert( } creationBundle := &certCreationBundle{ - RawSigningBundle: rawSigningBundle, - CACert: caCert, - CommonNames: commonNames, - IPSANs: ipSANs, - KeyType: role.KeyType, - KeyBits: role.KeyBits, - Lease: lease, - Usage: usage, + SigningBundle: signingBundle, + CACert: caCert, + CommonNames: commonNames, + IPSANs: ipSANs, + KeyType: role.KeyType, + KeyBits: role.KeyBits, + Lease: lease, + Usage: usage, } - rawBundle, userErr, intErr := createCertificate(creationBundle) + parsedBundle, userErr, intErr := createCertificate(creationBundle) switch { case userErr != nil: return logical.ErrorResponse(userErr.Error()), nil @@ -160,7 +160,7 @@ func (b *backend) pathIssueCert( return nil, intErr } - cb, err := rawBundle.ToCertBundle() + cb, err := parsedBundle.ToCertBundle() if err != nil { return nil, fmt.Errorf("Error converting raw cert bundle to cert bundle: %s", err) } @@ -175,7 +175,7 @@ func (b *backend) pathIssueCert( err = req.Storage.Put(&logical.StorageEntry{ Key: "certs/" + cb.SerialNumber, - Value: rawBundle.CertificateBytes, + Value: parsedBundle.CertificateBytes, }) if err != nil { return nil, fmt.Errorf("Unable to store certificate locally") diff --git a/helper/certutil/helpers.go b/helper/certutil/helpers.go new file mode 100644 index 0000000000..267ba721fb --- /dev/null +++ b/helper/certutil/helpers.go @@ -0,0 +1,167 @@ +package certutil + +import ( + "bytes" + "crypto" + "crypto/sha1" + "crypto/x509" + "encoding/json" + "encoding/pem" + "fmt" + + "github.com/hashicorp/vault/api" + "github.com/mitchellh/mapstructure" +) + +// GetOctalFormatted returns the byte buffer formatted in octal with +// the specified separator between bytes. +func GetOctalFormatted(buf []byte, sep string) string { + var ret bytes.Buffer + for _, cur := range buf { + if ret.Len() > 0 { + fmt.Fprintf(&ret, sep) + } + fmt.Fprintf(&ret, "%02x", cur) + } + return ret.String() +} + +// GetSubjKeyID returns the subject key ID, e.g. the SHA1 sum +// of the marshaled public key +func GetSubjKeyID(privateKey crypto.Signer) ([]byte, error) { + if privateKey == nil { + return nil, InternalError{"Passed-in private key is nil"} + } + marshaledKey, err := x509.MarshalPKIXPublicKey(privateKey.Public()) + if err != nil { + return nil, InternalError{fmt.Sprintf("Error marshalling public key: %s", err)} + } + + subjKeyID := sha1.Sum(marshaledKey) + + return subjKeyID[:], nil +} + +// ParsePKISecret takes an api.Secret returned from the PKI backend) +// and returns a ParsedCertBundle. +func ParsePKISecret(secret *api.Secret) (*ParsedCertBundle, error) { + return ParsePKIMap(secret.Data) +} + +// ParsePKIMap takes a map (for instance, the Secret.Data +// returned from the PKI backend) and returns a ParsedCertBundle. +func ParsePKIMap(data map[string]interface{}) (*ParsedCertBundle, error) { + result := &CertBundle{} + err := mapstructure.Decode(data, result) + if err != nil { + return nil, UserError{err.Error()} + } + + return result.ToParsedCertBundle() +} + +// ParsePKIJSON takes a JSON-encoded string and returns a CertBundle +// ParsedCertBundle. +// +// This can be either the output of an +// issue call from the PKI backend or just its data member; or, +// JSON not coming from the PKI backend. +func ParsePKIJSON(input []byte) (*ParsedCertBundle, error) { + result := &CertBundle{} + err := json.Unmarshal(input, &result) + + if err == nil { + return result.ToParsedCertBundle() + } + + var secret api.Secret + err = json.Unmarshal(input, &secret) + + if err == nil { + return ParsePKIMap(secret.Data) + } + + return nil, UserError{"Unable to parse out of either secret data or a secret object"} +} + +// ParsePEMBundle takes a string of concatenated PEM-format certificate +// and private key values and decodes/parses them, checking validity along +// the way. There must be at max two certificates (a certificate and its +// issuing certificate) and one private key. +func ParsePEMBundle(pemBundle string) (*ParsedCertBundle, error) { + if len(pemBundle) == 0 { + return nil, UserError{"Empty PEM bundle"} + } + + pemBytes := []byte(pemBundle) + var pemBlock *pem.Block + parsedBundle := &ParsedCertBundle{} + + for { + pemBlock, pemBytes = pem.Decode(pemBytes) + if pemBlock == nil { + return nil, UserError{"No data found"} + } + + if signer, err := x509.ParseECPrivateKey(pemBlock.Bytes); err == nil { + if parsedBundle.PrivateKeyType != UnknownPrivateKey { + return nil, UserError{"More than one private key given; provide only one private key in the bundle"} + } + parsedBundle.PrivateKeyType = ECPrivateKey + parsedBundle.PrivateKeyBytes = pemBlock.Bytes + parsedBundle.PrivateKey = signer + + } else if signer, err := x509.ParsePKCS1PrivateKey(pemBlock.Bytes); err == nil { + if parsedBundle.PrivateKeyType != UnknownPrivateKey { + return nil, UserError{"More than one private key given; provide only one private key in the bundle"} + } + parsedBundle.PrivateKeyType = RSAPrivateKey + parsedBundle.PrivateKeyBytes = pemBlock.Bytes + parsedBundle.PrivateKey = signer + + } else if certificates, err := x509.ParseCertificates(pemBlock.Bytes); err == nil { + switch len(certificates) { + case 0: + return nil, UserError{"PEM block cannot be decoded to a private key or certificate"} + + case 1: + if parsedBundle.Certificate != nil { + switch { + // We just found the issuing CA + case bytes.Equal(parsedBundle.Certificate.AuthorityKeyId, certificates[0].SubjectKeyId): + parsedBundle.IssuingCABytes = pemBlock.Bytes + parsedBundle.IssuingCA = certificates[0] + + // Our saved certificate is actually the issuing CA + case bytes.Equal(parsedBundle.Certificate.SubjectKeyId, certificates[0].AuthorityKeyId): + parsedBundle.IssuingCA = parsedBundle.Certificate + parsedBundle.IssuingCABytes = parsedBundle.CertificateBytes + parsedBundle.CertificateBytes = pemBlock.Bytes + parsedBundle.Certificate = certificates[0] + } + } else { + parsedBundle.CertificateBytes = pemBlock.Bytes + parsedBundle.Certificate = certificates[0] + } + + default: + return nil, UserError{"Too many certificates given; provide a maximum of two certificates in the bundle"} + } + } + + if len(pemBytes) == 0 { + break + } + } + + switch { + case parsedBundle.PrivateKeyType == UnknownPrivateKey: + return nil, UserError{"Unable to figure out the private key type; must be RSA or EC"} + case len(parsedBundle.PrivateKeyBytes) == 0: + return nil, UserError{"Unable to decode the private key from the bundle"} + case len(parsedBundle.CertificateBytes) == 0: + return nil, UserError{"Unable to decode the certificate from the bundle"} + } + + return parsedBundle, nil +} diff --git a/helper/certutil/types.go b/helper/certutil/types.go new file mode 100644 index 0000000000..3bca7fa387 --- /dev/null +++ b/helper/certutil/types.go @@ -0,0 +1,255 @@ +// Package certutil contains helper functions that are mostly used +// with the PKI backend but can be generally useful. Functionality +// includes helpers for converting a certificate/private key bundle +// between DER and PEM, printing certificate serial numbers, and more. +// +// Functionality specific to the PKI backend includes some types +// and helper methods to make requesting certificates from the +// backend easy. +package certutil + +import ( + "crypto" + "crypto/tls" + "crypto/x509" + "encoding/pem" + "fmt" +) + +// TLSUsage controls whether the intended usage of a *tls.Config +// returned from ParsedCertBundle.GetTLSConfig is for server use, +// client use, or both, which affects which values are set +type TLSUsage int + +// The type of of the Private Key referenced in CertBundle +// and ParsedCertBundle. This uses colloquial names rather than +// official names, to eliminate confusion +const ( + UnknownPrivateKey = iota + RSAPrivateKey + ECPrivateKey + + TLSServer TLSUsage = 1 << iota + TLSClient +) + +// UserError represents an error generated due to invalid user input +type UserError struct { + s string +} + +func (e UserError) Error() string { + return e.s +} + +// InternalError represents an error generated internally, +// presumably not due to invalid user input +type InternalError struct { + s string +} + +func (e InternalError) Error() string { + return e.s +} + +// CertBundle contains a key type, a PEM-encoded private key, +// a PEM-encoded certificate, and a string-encoded serial number, +// returned from a successful Issue request +type CertBundle struct { + PrivateKeyType string `json:"private_key_type" structs:"private_key_type" mapstructure:"private_key_type"` + Certificate string `json:"certificate" structs:"certificate" mapstructure:"certificate"` + IssuingCA string `json:"issuing_ca" structs:"issuing_ca" mapstructure:"issuing_ca"` + PrivateKey string `json:"private_key" structs:"private_key" mapstructure:"private_key"` + SerialNumber string `json:"serial_number" structs:"serial_number" mapstructure:"serial_number"` +} + +// ParsedCertBundle contains a key type, a DER-encoded private key, +// a DER-encoded certificate, and a big.Int serial number +type ParsedCertBundle struct { + PrivateKeyType int + PrivateKeyBytes []byte + PrivateKey crypto.Signer + IssuingCABytes []byte + IssuingCA *x509.Certificate + CertificateBytes []byte + Certificate *x509.Certificate +} + +// ToParsedCertBundle converts a string-based certificate bundle +// to a byte-based raw certificate bundle +func (c *CertBundle) ToParsedCertBundle() (*ParsedCertBundle, error) { + result := &ParsedCertBundle{} + var err error + var pemBlock *pem.Block + + pemBlock, _ = pem.Decode([]byte(c.PrivateKey)) + if pemBlock == nil { + return nil, UserError{"Error decoding private key from cert bundle"} + } + result.PrivateKeyBytes = pemBlock.Bytes + + switch c.PrivateKeyType { + case "ec": + result.PrivateKeyType = ECPrivateKey + case "rsa": + result.PrivateKeyType = RSAPrivateKey + default: + // Try to figure it out and correct + if _, err := x509.ParseECPrivateKey(pemBlock.Bytes); err == nil { + result.PrivateKeyType = ECPrivateKey + c.PrivateKeyType = "ec" + } else if _, err := x509.ParsePKCS1PrivateKey(pemBlock.Bytes); err == nil { + result.PrivateKeyType = RSAPrivateKey + c.PrivateKeyType = "rsa" + } else { + return nil, UserError{fmt.Sprintf("Unknown private key type in bundle: %s", c.PrivateKeyType)} + } + } + + result.PrivateKey, err = result.getSigner() + if err != nil { + return nil, UserError{fmt.Sprintf("Error getting signer: %s", err)} + } + + pemBlock, _ = pem.Decode([]byte(c.Certificate)) + if pemBlock == nil { + return nil, UserError{"Error decoding certificate from cert bundle"} + } + result.CertificateBytes = pemBlock.Bytes + result.Certificate, err = x509.ParseCertificate(result.CertificateBytes) + if err != nil { + return nil, UserError{"Error encountered parsing certificate bytes from raw bundle"} + } + + if len(c.IssuingCA) != 0 { + pemBlock, _ = pem.Decode([]byte(c.IssuingCA)) + if pemBlock == nil { + return nil, UserError{"Error decoding issuing CA from cert bundle"} + } + result.IssuingCABytes = pemBlock.Bytes + result.IssuingCA, err = x509.ParseCertificate(result.IssuingCABytes) + if err != nil { + return nil, UserError{fmt.Sprintf("Error parsing CA certificate: %s", err)} + } + } + + if len(c.SerialNumber) == 0 { + c.SerialNumber = GetOctalFormatted(result.Certificate.SerialNumber.Bytes(), ":") + } + + return result, nil +} + +// ToCertBundle converts a byte-based raw DER certificate bundle +// to a PEM-based string certificate bundle +func (p *ParsedCertBundle) ToCertBundle() (*CertBundle, error) { + result := &CertBundle{ + SerialNumber: GetOctalFormatted(p.Certificate.SerialNumber.Bytes(), ":"), + } + + block := pem.Block{ + Type: "CERTIFICATE", + Bytes: p.CertificateBytes, + } + result.Certificate = string(pem.EncodeToMemory(&block)) + + if len(p.IssuingCABytes) != 0 { + block.Bytes = p.IssuingCABytes + result.IssuingCA = string(pem.EncodeToMemory(&block)) + } + + block.Bytes = p.PrivateKeyBytes + switch p.PrivateKeyType { + case RSAPrivateKey: + result.PrivateKeyType = "rsa" + block.Type = "RSA PRIVATE KEY" + case ECPrivateKey: + result.PrivateKeyType = "ec" + block.Type = "EC PRIVATE KEY" + default: + return nil, InternalError{"Could not determine private key type when creating block"} + } + result.PrivateKey = string(pem.EncodeToMemory(&block)) + + return result, nil +} + +// GetSigner returns a crypto.Signer corresponding to the private key +// contained in this ParsedCertBundle. The Signer contains a Public() function +// for getting the corresponding public. The Signer can also be +// type-converted to private keys +func (p *ParsedCertBundle) getSigner() (crypto.Signer, error) { + var signer crypto.Signer + var err error + switch p.PrivateKeyType { + case ECPrivateKey: + signer, err = x509.ParseECPrivateKey(p.PrivateKeyBytes) + if err != nil { + return nil, UserError{fmt.Sprintf("Unable to parse CA's private EC key: %s", err)} + } + case RSAPrivateKey: + signer, err = x509.ParsePKCS1PrivateKey(p.PrivateKeyBytes) + if err != nil { + return nil, UserError{fmt.Sprintf("Unable to parse CA's private RSA key: %s", err)} + } + default: + return nil, UserError{"Unable to determine type of private key; only RSA and EC are supported"} + } + return signer, nil +} + +// GetTLSConfig returns a TLS config generally suitable for client +// authentiation. The returned TLS config can be modified slightly +// to be made suitable for a server requiring client authentication; +// specifically, you should set the value of ClientAuth in the returned +// config to match your needs. +func (p *ParsedCertBundle) GetTLSConfig(usage TLSUsage) (*tls.Config, error) { + tlsCert := &tls.Certificate{ + Certificate: [][]byte{ + p.CertificateBytes, + }, + PrivateKey: p.PrivateKey, + Leaf: p.Certificate, + } + + tlsConfig := &tls.Config{ + NextProtos: []string{"http/1.1"}, + Certificates: []tls.Certificate{*tlsCert}, + } + + if len(p.IssuingCABytes) > 0 { + tlsCert.Certificate = append(tlsCert.Certificate, p.IssuingCABytes) + + // Technically we only need one cert, but this doesn't duplicate code + certBundle, err := p.ToCertBundle() + if err != nil { + return nil, fmt.Errorf("Error converting parsed bundle to string bundle when getting TLS config: %s", err) + } + + caPool := x509.NewCertPool() + ok := caPool.AppendCertsFromPEM([]byte(certBundle.IssuingCA)) + if !ok { + return nil, fmt.Errorf("Could not append CA certificate") + } + + if usage&TLSServer != 0 { + tlsConfig.ClientCAs = caPool + } + if usage&TLSClient != 0 { + tlsConfig.RootCAs = caPool + } + } + + tlsConfig.BuildNameToCertificate() + + return tlsConfig, nil +} + +// IssueData is a structure that is suitable for marshaling into a request; +// either via JSON, or into a map[string]interface{} via the structs package +type IssueData struct { + Lease string `json:"lease" structs:"lease" mapstructure:"lease"` + CommonName string `json:"common_name" structs:"common_name" mapstructure:"common_name"` + AltNames string `json:"alt_names" structs:"alt_names" mapstructure:"alt_names"` + IPSANs string `json:"ip_sans" structs:"ip_sans" mapstructure:"ip_sans"` +} diff --git a/logical/certutil/helpers.go b/logical/certutil/helpers.go deleted file mode 100644 index 974ddbd753..0000000000 --- a/logical/certutil/helpers.go +++ /dev/null @@ -1,35 +0,0 @@ -package certutil - -import ( - "bytes" - "fmt" - - "github.com/hashicorp/vault/api" - "github.com/mitchellh/mapstructure" -) - -// GetOctalFormatted returns the byte buffer formatted in octal with -// the specified separator between bytes -func GetOctalFormatted(buf []byte, sep string) string { - var ret bytes.Buffer - for _, cur := range buf { - if ret.Len() > 0 { - fmt.Fprintf(&ret, sep) - } - fmt.Fprintf(&ret, "%02x", cur) - } - return ret.String() -} - -// ParsePKISecret takes a Secret returned from the PKI backend -// and returns a CertBundle for further processing (espcially -// by converting to a RawCertBundle) -func ParsePKISecret(secret *api.Secret) (*CertBundle, error) { - var result CertBundle - err := mapstructure.Decode(secret.Data, &result) - if err != nil { - return nil, err - } - - return &result, nil -} diff --git a/logical/certutil/types.go b/logical/certutil/types.go deleted file mode 100644 index 2ccc0f8afa..0000000000 --- a/logical/certutil/types.go +++ /dev/null @@ -1,183 +0,0 @@ -// Package certutil contains helper functions that are mostly used -// with the PKI backend but can be generally useful. Functionality -// includes helpers for converting a certificate/private key bundle -// between DER and PEM, printing certificate serial numbers, and more. -// -// Functionality specific to the PKI backend includes some types -// and helper methods to make requesting certificates from the -// backend easy. -package certutil - -import ( - "crypto" - "crypto/sha1" - "crypto/x509" - "encoding/pem" - "fmt" - "math/big" -) - -// The type of of the Private Key referenced in CertBundle -// and RawCertBundle. This uses colloquial names rather than -// official names, to eliminate confusion -const ( - UnknownPrivateKeyType = iota - RSAPrivateKeyType - ECPrivateKeyType -) - -// CertBundle contains a key type, a private key, -// a certificate, and a string-encoded serial number, -// returned from a successful Issue request -type CertBundle struct { - PrivateKeyType string `json:"private_key_type" structs:"private_key_type" mapstructure:"private_key_type"` - Certificate string `json:"certificate" structs:"certificate" mapstructure:"certificate"` - IssuingCA string `json:"issuing_ca" structs:"issuing_ca" mapstructure:"issuing_ca"` - PrivateKey string `json:"private_key" structs:"private_key" mapstructure:"private_key"` - SerialNumber string `json:"serial_number" structs:"serial_number" mapstructure:"serial_number"` -} - -// ToRawCertBundle converts a string-based certificate bundle -// to a byte-based raw certificate bundle -func (c *CertBundle) ToRawCertBundle() (*RawCertBundle, error) { - result := &RawCertBundle{} - switch c.PrivateKeyType { - case "ec": - result.PrivateKeyType = ECPrivateKeyType - case "rsa": - result.PrivateKeyType = RSAPrivateKeyType - default: - return nil, fmt.Errorf("Unknown private key type in bundle: %s", c.PrivateKeyType) - } - - var pemBlock *pem.Block - pemBlock, _ = pem.Decode([]byte(c.PrivateKey)) - if pemBlock == nil { - return nil, fmt.Errorf("Error decoding private key from cert bundle") - } - result.PrivateKeyBytes = pemBlock.Bytes - - pemBlock, _ = pem.Decode([]byte(c.Certificate)) - if pemBlock == nil { - return nil, fmt.Errorf("Error decoding certificate from cert bundle") - } - result.CertificateBytes = pemBlock.Bytes - - if len(c.IssuingCA) != 0 { - pemBlock, _ = pem.Decode([]byte(c.IssuingCA)) - if pemBlock == nil { - return nil, fmt.Errorf("Error decoding issuing CA from cert bundle") - } - result.IssuingCABytes = pemBlock.Bytes - } - - if err := result.populateSerialNumber(); err != nil { - return nil, err - } - - return result, nil -} - -// RawCertBundle contains a key type, a DER-encoded private key, -// a DER-encoded certificate, and a big.Int serial number -type RawCertBundle struct { - PrivateKeyType int - PrivateKeyBytes []byte - IssuingCABytes []byte - CertificateBytes []byte - SerialNumber *big.Int -} - -// ToCertBundle converts a byte-based raw DER certificate bundle -// to a PEM-based string certificate bundle -func (r *RawCertBundle) ToCertBundle() (*CertBundle, error) { - result := &CertBundle{ - SerialNumber: GetOctalFormatted(r.SerialNumber.Bytes(), ":"), - } - - block := pem.Block{ - Type: "CERTIFICATE", - Bytes: r.CertificateBytes, - } - result.Certificate = string(pem.EncodeToMemory(&block)) - - if len(r.IssuingCABytes) != 0 { - block.Bytes = r.IssuingCABytes - result.IssuingCA = string(pem.EncodeToMemory(&block)) - } - - block.Bytes = r.PrivateKeyBytes - switch r.PrivateKeyType { - case RSAPrivateKeyType: - result.PrivateKeyType = "rsa" - block.Type = "RSA PRIVATE KEY" - case ECPrivateKeyType: - result.PrivateKeyType = "ec" - block.Type = "EC PRIVATE KEY" - default: - return nil, fmt.Errorf("Could not determine private key type when creating block") - } - result.PrivateKey = string(pem.EncodeToMemory(&block)) - - return result, nil -} - -func (r *RawCertBundle) populateSerialNumber() error { - cert, err := x509.ParseCertificate(r.CertificateBytes) - if err != nil { - return fmt.Errorf("Error encountered parsing certificate bytes from raw bundle") - } - r.SerialNumber = cert.SerialNumber - return nil -} - -// GetSigner returns a crypto.Signer corresponding to the private key -// contained in this RawCertBundle. The Signer contains a Public() function -// for getting the corresponding public. The Signer can also be -// type-converted to private keys -func (r *RawCertBundle) GetSigner() (crypto.Signer, error) { - var signer crypto.Signer - var err error - switch r.PrivateKeyType { - case ECPrivateKeyType: - signer, err = x509.ParseECPrivateKey(r.PrivateKeyBytes) - if err != nil { - return nil, fmt.Errorf("Unable to parse CA's private EC key: %s", err) - } - case RSAPrivateKeyType: - signer, err = x509.ParsePKCS1PrivateKey(r.PrivateKeyBytes) - if err != nil { - return nil, fmt.Errorf("Unable to parse CA's private RSA key: %s", err) - } - default: - return nil, fmt.Errorf("Unable to determine the type of private key") - } - return signer, nil -} - -// GetSubjKeyID returns the subject key ID, e.g. the SHA1 sum -// of the marshaled public key -func (r *RawCertBundle) GetSubjKeyID() ([]byte, error) { - privateKey, err := r.GetSigner() - if err != nil { - return nil, err - } - - marshaledKey, err := x509.MarshalPKIXPublicKey(privateKey.Public()) - if err != nil { - return nil, fmt.Errorf("Error marshalling public key: %s", err) - } - - subjKeyID := sha1.Sum(marshaledKey) - - return subjKeyID[:], nil -} - -// IssueData is a structure that is suitable for marshaling into a request; -// either via JSON, or into a map[string]interface{} via the structs package -type IssueData struct { - Lease string `json:"lease" structs:"lease" mapstructure:"lease"` - CommonName string `json:"common_name" structs:"common_name" mapstructure:"common_name"` - AltNames string `json:"alt_names" structs:"alt_names" mapstructure:"alt_names"` - IPSANs string `json:"ip_sans" structs:"ip_sans" mapstructure:"ip_sans"` -} From c4256601f234924fd725d96506d05a4b6e512d41 Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Thu, 18 Jun 2015 06:42:57 -0400 Subject: [PATCH 10/15] Restructure a little bit to make the helper library fully standalone. This makes it easier to move around later if desired, and for use by external programs. Commit contents (C)2015 Akamai Technologies, Inc. --- helper/certutil/helpers.go | 9 +-------- helper/certutil/types.go | 7 +++++++ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/helper/certutil/helpers.go b/helper/certutil/helpers.go index 267ba721fb..93040e724f 100644 --- a/helper/certutil/helpers.go +++ b/helper/certutil/helpers.go @@ -9,7 +9,6 @@ import ( "encoding/pem" "fmt" - "github.com/hashicorp/vault/api" "github.com/mitchellh/mapstructure" ) @@ -42,12 +41,6 @@ func GetSubjKeyID(privateKey crypto.Signer) ([]byte, error) { return subjKeyID[:], nil } -// ParsePKISecret takes an api.Secret returned from the PKI backend) -// and returns a ParsedCertBundle. -func ParsePKISecret(secret *api.Secret) (*ParsedCertBundle, error) { - return ParsePKIMap(secret.Data) -} - // ParsePKIMap takes a map (for instance, the Secret.Data // returned from the PKI backend) and returns a ParsedCertBundle. func ParsePKIMap(data map[string]interface{}) (*ParsedCertBundle, error) { @@ -74,7 +67,7 @@ func ParsePKIJSON(input []byte) (*ParsedCertBundle, error) { return result.ToParsedCertBundle() } - var secret api.Secret + var secret Secret err = json.Unmarshal(input, &secret) if err == nil { diff --git a/helper/certutil/types.go b/helper/certutil/types.go index 3bca7fa387..41eaf3bfa6 100644 --- a/helper/certutil/types.go +++ b/helper/certutil/types.go @@ -16,6 +16,12 @@ import ( "fmt" ) +// Secret is used to attempt to unmarshal a Vault secret +// JSON response, as a convenience +type Secret struct { + Data map[string]interface{} `json:"data"` +} + // TLSUsage controls whether the intended usage of a *tls.Config // returned from ParsedCertBundle.GetTLSConfig is for server use, // client use, or both, which affects which values are set @@ -234,6 +240,7 @@ func (p *ParsedCertBundle) GetTLSConfig(usage TLSUsage) (*tls.Config, error) { if usage&TLSServer != 0 { tlsConfig.ClientCAs = caPool + tlsConfig.ClientAuth = tls.VerifyClientCertIfGiven } if usage&TLSClient != 0 { tlsConfig.RootCAs = caPool From 23ba605068d440fe83b1e65762d1dc4ae556299e Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Thu, 18 Jun 2015 10:44:02 -0400 Subject: [PATCH 11/15] Refactor to allow only issuing CAs to be set and not have things blow up. This is useful/important for e.g. the Cassandra backend, where you may want to do TLS with a specific CA cert for server validation, but not actually do client authentication with a client cert. Commit contents (C)2015 Akamai Technologies, Inc. --- builtin/logical/pki/path_config_ca.go | 11 +- builtin/logical/pki/path_fetch.go | 5 +- builtin/logical/pki/path_issue.go | 21 ++- builtin/logical/pki/path_revoke.go | 5 +- builtin/logical/pki/path_roles.go | 97 ++++++----- builtin/logical/pki/secret_certs.go | 10 +- helper/certutil/helpers.go | 28 ++-- helper/certutil/types.go | 155 ++++++++++-------- website/source/docs/secrets/pki/index.html.md | 2 + 9 files changed, 201 insertions(+), 133 deletions(-) diff --git a/builtin/logical/pki/path_config_ca.go b/builtin/logical/pki/path_config_ca.go index 836e1166a2..a21b1991a7 100644 --- a/builtin/logical/pki/path_config_ca.go +++ b/builtin/logical/pki/path_config_ca.go @@ -13,8 +13,9 @@ func pathConfigCA(b *backend) *framework.Path { Pattern: "config/ca", Fields: map[string]*framework.FieldSchema{ "pem_bundle": &framework.FieldSchema{ - Type: framework.TypeString, - Description: "PEM-format, concatenated unencrypted secret key and certificate", + Type: framework.TypeString, + Description: `PEM-format, concatenated unencrypted secret key +and certificate`, }, }, @@ -41,6 +42,12 @@ func (b *backend) pathCAWrite( } } + // Handle the case of a self-signed certificate + if parsedBundle.Certificate == nil && parsedBundle.IssuingCA != nil { + parsedBundle.Certificate = parsedBundle.IssuingCA + parsedBundle.CertificateBytes = parsedBundle.IssuingCABytes + } + // TODO?: CRLs can only be generated with RSA keys right now, in the // Go standard library. The plubming is here to support non-RSA keys // if the library gets support diff --git a/builtin/logical/pki/path_fetch.go b/builtin/logical/pki/path_fetch.go index 3a4d5e6d13..1e95d60d56 100644 --- a/builtin/logical/pki/path_fetch.go +++ b/builtin/logical/pki/path_fetch.go @@ -40,8 +40,9 @@ func pathFetchValid(b *backend) *framework.Path { Pattern: `cert/(?P[0-9A-Fa-f-:]+)`, Fields: map[string]*framework.FieldSchema{ "serial": &framework.FieldSchema{ - Type: framework.TypeString, - Description: "Certificate serial number, in colon- or hyphen-separated octal", + Type: framework.TypeString, + Description: `Certificate serial number, in colon- or +hyphen-separated octal`, }, }, diff --git a/builtin/logical/pki/path_issue.go b/builtin/logical/pki/path_issue.go index a4fd3308a6..844df4952a 100644 --- a/builtin/logical/pki/path_issue.go +++ b/builtin/logical/pki/path_issue.go @@ -16,20 +16,25 @@ func pathIssue(b *backend) *framework.Path { Pattern: `issue/(?P\w+)`, Fields: map[string]*framework.FieldSchema{ "role": &framework.FieldSchema{ - Type: framework.TypeString, - Description: "The desired role with configuration for this request", + Type: framework.TypeString, + Description: `The desired role with configuration for this +request`, }, "common_name": &framework.FieldSchema{ - Type: framework.TypeString, - Description: "The requested common name; if you want more than one, specify the alternative names in the alt_names map", + Type: framework.TypeString, + Description: `The requested common name; if you want more than +one, specify the alternative names in the +alt_names map`, }, "alt_names": &framework.FieldSchema{ - Type: framework.TypeString, - Description: "The requested Subject Alternative Names, if any, in a comma-delimited list", + Type: framework.TypeString, + Description: `The requested Subject Alternative Names, if any, +in a comma-delimited list`, }, "ip_sans": &framework.FieldSchema{ - Type: framework.TypeString, - Description: "The requested IP SANs, if any, in a common-delimited list", + Type: framework.TypeString, + Description: `The requested IP SANs, if any, in a +common-delimited list`, }, "lease": &framework.FieldSchema{ Type: framework.TypeString, diff --git a/builtin/logical/pki/path_revoke.go b/builtin/logical/pki/path_revoke.go index 9e6a88cf8a..0982a5c715 100644 --- a/builtin/logical/pki/path_revoke.go +++ b/builtin/logical/pki/path_revoke.go @@ -12,8 +12,9 @@ func pathRevoke(b *backend) *framework.Path { Pattern: `revoke`, Fields: map[string]*framework.FieldSchema{ "serial_number": &framework.FieldSchema{ - Type: framework.TypeString, - Description: "Certificate serial number, in colon- or hyphen-separated octal", + Type: framework.TypeString, + Description: `Certificate serial number, in colon- or +hyphen-separated octal`, }, }, diff --git a/builtin/logical/pki/path_roles.go b/builtin/logical/pki/path_roles.go index ce838c30fc..524e01cfce 100644 --- a/builtin/logical/pki/path_roles.go +++ b/builtin/logical/pki/path_roles.go @@ -15,85 +15,106 @@ func pathRoles(b *backend) *framework.Path { Fields: map[string]*framework.FieldSchema{ "name": &framework.FieldSchema{ Type: framework.TypeString, - Description: "Name of the role.", + Description: "Name of the role", }, "lease": &framework.FieldSchema{ - Type: framework.TypeString, - Default: "", - Description: "The lease length if no specific lease length is requested. The lease length controls the expiration of certificates issued by this backend. Defaults to the value of lease_max.", + Type: framework.TypeString, + Default: "", + Description: `The lease length if no specific lease length is +requested. The lease length controls the expiration +of certificates issued by this backend. Defaults to +the value of lease_max.`, }, "lease_max": &framework.FieldSchema{ Type: framework.TypeString, Default: "", - Description: "The maximum allowed lease length.", + Description: "The maximum allowed lease length", }, "allow_localhost": &framework.FieldSchema{ - Type: framework.TypeBool, - Default: true, - Description: "Whether to allow \"localhost\" as a valid common name in a request.", + Type: framework.TypeBool, + Default: true, + Description: `Whether to allow "localhost" as a valid common +name in a request`, }, "allowed_base_domain": &framework.FieldSchema{ - Type: framework.TypeString, - Default: "", - Description: "If set, clients can request certificates for subdomains directly beneath this base domain, including the wildcard subdomain. See the documentation for more information.", + Type: framework.TypeString, + Default: "", + Description: `If set, clients can request certificates for +subdomains directly beneath this base domain, including +the wildcard subdomain. See the documentation for more +information.`, }, "allow_token_displayname": &framework.FieldSchema{ - Type: framework.TypeBool, - Default: false, - Description: "If set, clients can request certificates for matching the value of the Display Name on the requesting token. See the documentation for more information.", + Type: framework.TypeBool, + Default: false, + Description: `If set, clients can request certificates for +matching the value of the Display Name on the requesting +token. See the documentation for more information.`, }, "allow_subdomains": &framework.FieldSchema{ - Type: framework.TypeBool, - Default: false, - Description: "If set, clients can request certificates for subdomains of the CNs allowed by the other role options, including wildcard subdomains. See the documentation for more information.", + Type: framework.TypeBool, + Default: false, + Description: `If set, clients can request certificates for +subdomains of the CNs allowed by the other role options, +including wildcard subdomains. See the documentation for +more information.`, }, "allow_any_name": &framework.FieldSchema{ - Type: framework.TypeBool, - Default: false, - Description: "If set, clients can request certificates for any CN they like. See the documentation for more information.", + Type: framework.TypeBool, + Default: false, + Description: `If set, clients can request certificates for +any CN they like. See the documentation for more +information.`, }, "allow_ip_sans": &framework.FieldSchema{ - Type: framework.TypeBool, - Default: true, - Description: "If set, IP Subject Alternative Names are allowed. Any valid IP is accepted.", + Type: framework.TypeBool, + Default: true, + Description: `If set, IP Subject Alternative Names are allowed. +Any valid IP is accepted.`, }, "server_flag": &framework.FieldSchema{ - Type: framework.TypeBool, - Default: true, - Description: "If set, certificates are flagged for server use. Defaults to true.", + Type: framework.TypeBool, + Default: true, + Description: `If set, certificates are flagged for server use. +Defaults to true.`, }, "client_flag": &framework.FieldSchema{ - Type: framework.TypeBool, - Default: true, - Description: "If set, certificates are flagged for client use. Defaults to true.", + Type: framework.TypeBool, + Default: true, + Description: `If set, certificates are flagged for client use. +Defaults to true.`, }, "code_signing_flag": &framework.FieldSchema{ - Type: framework.TypeBool, - Default: false, - Description: "If set, certificates are flagged for code signing use. Defaults to false.", + Type: framework.TypeBool, + Default: false, + Description: `If set, certificates are flagged for code signing +use. Defaults to false.`, }, "key_type": &framework.FieldSchema{ - Type: framework.TypeString, - Default: "rsa", - Description: "The type of key to use; defaults to RSA. \"rsa\" and \"ec\" are the only valid values.", + Type: framework.TypeString, + Default: "rsa", + Description: `The type of key to use; defaults to RSA. "rsa" +and "ec" are the only valid values.`, }, "key_bits": &framework.FieldSchema{ - Type: framework.TypeInt, - Default: 2048, - Description: "The number of bits to use. You will almost certainly want to change this if you adjust the key_type.", + Type: framework.TypeInt, + Default: 2048, + Description: `The number of bits to use. You will almost +certainly want to change this if you adjust +the key_type.`, }, }, diff --git a/builtin/logical/pki/secret_certs.go b/builtin/logical/pki/secret_certs.go index 4a6681ac24..f9dd54c76f 100644 --- a/builtin/logical/pki/secret_certs.go +++ b/builtin/logical/pki/secret_certs.go @@ -17,16 +17,18 @@ func secretCerts(b *backend) *framework.Secret { Type: SecretCertsType, Fields: map[string]*framework.FieldSchema{ "certificate": &framework.FieldSchema{ - Type: framework.TypeString, - Description: "The PEM-encoded concatenated certificate and issuing certificate authority", + Type: framework.TypeString, + Description: `The PEM-encoded concatenated certificate and +issuing certificate authority`, }, "private_key": &framework.FieldSchema{ Type: framework.TypeString, Description: "The PEM-encoded private key for the certificate", }, "serial": &framework.FieldSchema{ - Type: framework.TypeString, - Description: "The serial number of the certificate, for handy reference", + Type: framework.TypeString, + Description: `The serial number of the certificate, for handy +reference`, }, }, diff --git a/helper/certutil/helpers.go b/helper/certutil/helpers.go index 93040e724f..dfce043ff4 100644 --- a/helper/certutil/helpers.go +++ b/helper/certutil/helpers.go @@ -31,6 +31,7 @@ func GetSubjKeyID(privateKey crypto.Signer) ([]byte, error) { if privateKey == nil { return nil, InternalError{"Passed-in private key is nil"} } + marshaledKey, err := x509.MarshalPKIXPublicKey(privateKey.Public()) if err != nil { return nil, InternalError{fmt.Sprintf("Error marshalling public key: %s", err)} @@ -121,20 +122,30 @@ func ParsePEMBundle(pemBundle string) (*ParsedCertBundle, error) { if parsedBundle.Certificate != nil { switch { // We just found the issuing CA - case bytes.Equal(parsedBundle.Certificate.AuthorityKeyId, certificates[0].SubjectKeyId): + case bytes.Equal(parsedBundle.Certificate.AuthorityKeyId, certificates[0].SubjectKeyId) && certificates[0].IsCA: parsedBundle.IssuingCABytes = pemBlock.Bytes parsedBundle.IssuingCA = certificates[0] // Our saved certificate is actually the issuing CA - case bytes.Equal(parsedBundle.Certificate.SubjectKeyId, certificates[0].AuthorityKeyId): + case bytes.Equal(parsedBundle.Certificate.SubjectKeyId, certificates[0].AuthorityKeyId) && parsedBundle.Certificate.IsCA: parsedBundle.IssuingCA = parsedBundle.Certificate parsedBundle.IssuingCABytes = parsedBundle.CertificateBytes parsedBundle.CertificateBytes = pemBlock.Bytes parsedBundle.Certificate = certificates[0] } } else { - parsedBundle.CertificateBytes = pemBlock.Bytes - parsedBundle.Certificate = certificates[0] + switch { + // If this case isn't correct, the caller needs to assign + // the values to Certificate/CertificateBytes; assumptions + // made here will not be valid for all cases. + case certificates[0].IsCA: + parsedBundle.IssuingCABytes = pemBlock.Bytes + parsedBundle.IssuingCA = certificates[0] + + default: + parsedBundle.CertificateBytes = pemBlock.Bytes + parsedBundle.Certificate = certificates[0] + } } default: @@ -147,14 +158,5 @@ func ParsePEMBundle(pemBundle string) (*ParsedCertBundle, error) { } } - switch { - case parsedBundle.PrivateKeyType == UnknownPrivateKey: - return nil, UserError{"Unable to figure out the private key type; must be RSA or EC"} - case len(parsedBundle.PrivateKeyBytes) == 0: - return nil, UserError{"Unable to decode the private key from the bundle"} - case len(parsedBundle.CertificateBytes) == 0: - return nil, UserError{"Unable to decode the certificate from the bundle"} - } - return parsedBundle, nil } diff --git a/helper/certutil/types.go b/helper/certutil/types.go index 41eaf3bfa6..437c0a4c17 100644 --- a/helper/certutil/types.go +++ b/helper/certutil/types.go @@ -88,46 +88,50 @@ func (c *CertBundle) ToParsedCertBundle() (*ParsedCertBundle, error) { var err error var pemBlock *pem.Block - pemBlock, _ = pem.Decode([]byte(c.PrivateKey)) - if pemBlock == nil { - return nil, UserError{"Error decoding private key from cert bundle"} - } - result.PrivateKeyBytes = pemBlock.Bytes + if len(c.PrivateKey) > 0 { + pemBlock, _ = pem.Decode([]byte(c.PrivateKey)) + if pemBlock == nil { + return nil, UserError{"Error decoding private key from cert bundle"} + } + result.PrivateKeyBytes = pemBlock.Bytes - switch c.PrivateKeyType { - case "ec": - result.PrivateKeyType = ECPrivateKey - case "rsa": - result.PrivateKeyType = RSAPrivateKey - default: - // Try to figure it out and correct - if _, err := x509.ParseECPrivateKey(pemBlock.Bytes); err == nil { + switch c.PrivateKeyType { + case "ec": result.PrivateKeyType = ECPrivateKey - c.PrivateKeyType = "ec" - } else if _, err := x509.ParsePKCS1PrivateKey(pemBlock.Bytes); err == nil { + case "rsa": result.PrivateKeyType = RSAPrivateKey - c.PrivateKeyType = "rsa" - } else { - return nil, UserError{fmt.Sprintf("Unknown private key type in bundle: %s", c.PrivateKeyType)} + default: + // Try to figure it out and correct + if _, err := x509.ParseECPrivateKey(pemBlock.Bytes); err == nil { + result.PrivateKeyType = ECPrivateKey + c.PrivateKeyType = "ec" + } else if _, err := x509.ParsePKCS1PrivateKey(pemBlock.Bytes); err == nil { + result.PrivateKeyType = RSAPrivateKey + c.PrivateKeyType = "rsa" + } else { + return nil, UserError{fmt.Sprintf("Unknown private key type in bundle: %s", c.PrivateKeyType)} + } + } + + result.PrivateKey, err = result.getSigner() + if err != nil { + return nil, UserError{fmt.Sprintf("Error getting signer: %s", err)} } } - result.PrivateKey, err = result.getSigner() - if err != nil { - return nil, UserError{fmt.Sprintf("Error getting signer: %s", err)} + if len(c.Certificate) > 0 { + pemBlock, _ = pem.Decode([]byte(c.Certificate)) + if pemBlock == nil { + return nil, UserError{"Error decoding certificate from cert bundle"} + } + result.CertificateBytes = pemBlock.Bytes + result.Certificate, err = x509.ParseCertificate(result.CertificateBytes) + if err != nil { + return nil, UserError{"Error encountered parsing certificate bytes from raw bundle"} + } } - pemBlock, _ = pem.Decode([]byte(c.Certificate)) - if pemBlock == nil { - return nil, UserError{"Error decoding certificate from cert bundle"} - } - result.CertificateBytes = pemBlock.Bytes - result.Certificate, err = x509.ParseCertificate(result.CertificateBytes) - if err != nil { - return nil, UserError{"Error encountered parsing certificate bytes from raw bundle"} - } - - if len(c.IssuingCA) != 0 { + if len(c.IssuingCA) > 0 { pemBlock, _ = pem.Decode([]byte(c.IssuingCA)) if pemBlock == nil { return nil, UserError{"Error decoding issuing CA from cert bundle"} @@ -139,7 +143,7 @@ func (c *CertBundle) ToParsedCertBundle() (*ParsedCertBundle, error) { } } - if len(c.SerialNumber) == 0 { + if len(c.SerialNumber) == 0 && len(c.Certificate) > 0 { c.SerialNumber = GetOctalFormatted(result.Certificate.SerialNumber.Bytes(), ":") } @@ -149,33 +153,39 @@ func (c *CertBundle) ToParsedCertBundle() (*ParsedCertBundle, error) { // ToCertBundle converts a byte-based raw DER certificate bundle // to a PEM-based string certificate bundle func (p *ParsedCertBundle) ToCertBundle() (*CertBundle, error) { - result := &CertBundle{ - SerialNumber: GetOctalFormatted(p.Certificate.SerialNumber.Bytes(), ":"), - } - + result := &CertBundle{} block := pem.Block{ - Type: "CERTIFICATE", - Bytes: p.CertificateBytes, + Type: "CERTIFICATE", } - result.Certificate = string(pem.EncodeToMemory(&block)) - if len(p.IssuingCABytes) != 0 { + if p.Certificate != nil { + result.SerialNumber = GetOctalFormatted(p.Certificate.SerialNumber.Bytes(), ":") + } + + if p.CertificateBytes != nil && len(p.CertificateBytes) > 0 { + block.Bytes = p.CertificateBytes + result.Certificate = string(pem.EncodeToMemory(&block)) + } + + if p.IssuingCABytes != nil && len(p.IssuingCABytes) > 0 { block.Bytes = p.IssuingCABytes result.IssuingCA = string(pem.EncodeToMemory(&block)) } - block.Bytes = p.PrivateKeyBytes - switch p.PrivateKeyType { - case RSAPrivateKey: - result.PrivateKeyType = "rsa" - block.Type = "RSA PRIVATE KEY" - case ECPrivateKey: - result.PrivateKeyType = "ec" - block.Type = "EC PRIVATE KEY" - default: - return nil, InternalError{"Could not determine private key type when creating block"} + if p.PrivateKeyBytes != nil && len(p.PrivateKeyBytes) > 0 { + block.Bytes = p.PrivateKeyBytes + switch p.PrivateKeyType { + case RSAPrivateKey: + result.PrivateKeyType = "rsa" + block.Type = "RSA PRIVATE KEY" + case ECPrivateKey: + result.PrivateKeyType = "ec" + block.Type = "EC PRIVATE KEY" + default: + return nil, InternalError{"Could not determine private key type when creating block"} + } + result.PrivateKey = string(pem.EncodeToMemory(&block)) } - result.PrivateKey = string(pem.EncodeToMemory(&block)) return result, nil } @@ -187,17 +197,24 @@ func (p *ParsedCertBundle) ToCertBundle() (*CertBundle, error) { func (p *ParsedCertBundle) getSigner() (crypto.Signer, error) { var signer crypto.Signer var err error + + if p.PrivateKeyBytes == nil || len(p.PrivateKeyBytes) == 0 { + return nil, UserError{"Given parsed cert bundle does not have private key information"} + } + switch p.PrivateKeyType { case ECPrivateKey: signer, err = x509.ParseECPrivateKey(p.PrivateKeyBytes) if err != nil { return nil, UserError{fmt.Sprintf("Unable to parse CA's private EC key: %s", err)} } + case RSAPrivateKey: signer, err = x509.ParsePKCS1PrivateKey(p.PrivateKeyBytes) if err != nil { return nil, UserError{fmt.Sprintf("Unable to parse CA's private RSA key: %s", err)} } + default: return nil, UserError{"Unable to determine type of private key; only RSA and EC are supported"} } @@ -210,20 +227,27 @@ func (p *ParsedCertBundle) getSigner() (crypto.Signer, error) { // specifically, you should set the value of ClientAuth in the returned // config to match your needs. func (p *ParsedCertBundle) GetTLSConfig(usage TLSUsage) (*tls.Config, error) { - tlsCert := &tls.Certificate{ - Certificate: [][]byte{ - p.CertificateBytes, - }, - PrivateKey: p.PrivateKey, - Leaf: p.Certificate, + tlsCert := tls.Certificate{ + Certificate: [][]byte{}, } tlsConfig := &tls.Config{ - NextProtos: []string{"http/1.1"}, - Certificates: []tls.Certificate{*tlsCert}, + NextProtos: []string{"http/1.1"}, } - if len(p.IssuingCABytes) > 0 { + if p.Certificate != nil { + tlsCert.Leaf = p.Certificate + } + + if p.PrivateKey != nil { + tlsCert.PrivateKey = p.PrivateKey + } + + if p.CertificateBytes != nil && len(p.CertificateBytes) > 0 { + tlsCert.Certificate = append(tlsCert.Certificate, p.CertificateBytes) + } + + if p.IssuingCABytes != nil && len(p.IssuingCABytes) > 0 { tlsCert.Certificate = append(tlsCert.Certificate, p.IssuingCABytes) // Technically we only need one cert, but this doesn't duplicate code @@ -238,16 +262,19 @@ func (p *ParsedCertBundle) GetTLSConfig(usage TLSUsage) (*tls.Config, error) { return nil, fmt.Errorf("Could not append CA certificate") } - if usage&TLSServer != 0 { + if usage&TLSServer > 0 { tlsConfig.ClientCAs = caPool tlsConfig.ClientAuth = tls.VerifyClientCertIfGiven } - if usage&TLSClient != 0 { + if usage&TLSClient > 0 { tlsConfig.RootCAs = caPool } } - tlsConfig.BuildNameToCertificate() + if tlsCert.Certificate != nil && len(tlsCert.Certificate) > 0 { + tlsConfig.Certificates = []tls.Certificate{tlsCert} + tlsConfig.BuildNameToCertificate() + } return tlsConfig, nil } diff --git a/website/source/docs/secrets/pki/index.html.md b/website/source/docs/secrets/pki/index.html.md index 32d8e0127b..c6ff6fa931 100644 --- a/website/source/docs/secrets/pki/index.html.md +++ b/website/source/docs/secrets/pki/index.html.md @@ -362,12 +362,14 @@ If you get stuck at any time, simply run `vault help pki` or with a subpath for list. If any requested names do not match role policy, the entire request will be denied. +
  • ip_sans optional Requested IP Subject Alternative Names, in a comma-delimited list. Only valid if the role allows IP SANs (which is the default).
  • +
  • lease optional Requested lease time. Cannot be greater than the role's From 435aefc07262cddd87bc4aa4784966a03d8eb645 Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Fri, 19 Jun 2015 12:48:18 -0400 Subject: [PATCH 12/15] A few things: * Add comments to every non-obvious (e.g. not basic read/write handler type) function * Remove revoked/ endpoint, at least for now * Add configurable CRL lifetime * Cleanup * Address some comments from code review Commit contents (C)2015 Akamai Technologies, Inc. --- builtin/logical/pki/backend.go | 11 +- builtin/logical/pki/backend_test.go | 166 ++++++++++-------- builtin/logical/pki/cert_util.go | 110 ++++++------ builtin/logical/pki/crl_util.go | 128 ++++++++------ builtin/logical/pki/path_config_crl.go | 102 +++++++++++ builtin/logical/pki/path_fetch.go | 75 +++----- builtin/logical/pki/path_issue.go | 29 +-- builtin/logical/pki/path_revoke.go | 35 ++-- builtin/logical/pki/secret_certs.go | 6 +- helper/certutil/types.go | 8 +- website/source/docs/secrets/pki/index.html.md | 41 +---- 11 files changed, 396 insertions(+), 315 deletions(-) create mode 100644 builtin/logical/pki/path_config_crl.go diff --git a/builtin/logical/pki/backend.go b/builtin/logical/pki/backend.go index 7dfb30443d..cd16ac0d35 100644 --- a/builtin/logical/pki/backend.go +++ b/builtin/logical/pki/backend.go @@ -2,6 +2,8 @@ package pki import ( "strings" + "sync" + "time" "github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/logical/framework" @@ -21,7 +23,6 @@ func Backend() *framework.Backend { PathsSpecial: &logical.Paths{ Root: []string{ "config/*", - "revoked/*", "revoke/*", "crl/rotate", }, @@ -37,13 +38,13 @@ func Backend() *framework.Backend { Paths: []*framework.Path{ pathRoles(&b), pathConfigCA(&b), + pathConfigCRL(&b), pathIssue(&b), pathRotateCRL(&b), pathFetchCA(&b), pathFetchCRL(&b), pathFetchCRLViaCertPath(&b), pathFetchValid(&b), - pathFetchRevoked(&b), pathRevoke(&b), }, @@ -52,11 +53,17 @@ func Backend() *framework.Backend { }, } + b.crlLifetime = time.Hour * 72 + b.revokeStorageLock = &sync.Mutex{} + return b.Backend } type backend struct { *framework.Backend + + crlLifetime time.Duration + revokeStorageLock *sync.Mutex } const backendHelp = ` diff --git a/builtin/logical/pki/backend_test.go b/builtin/logical/pki/backend_test.go index 3c85973305..53c3005aab 100644 --- a/builtin/logical/pki/backend_test.go +++ b/builtin/logical/pki/backend_test.go @@ -1,7 +1,6 @@ package pki import ( - "crypto" "crypto/x509" "encoding/pem" "fmt" @@ -22,77 +21,7 @@ var ( stepCount = 0 ) -func checkCertsAndPrivateKey(keyType string, usage certUsage, validity time.Duration, certBundle *certutil.CertBundle) (cert, ca *x509.Certificate, privKey crypto.Signer, err error) { - var pemBlock *pem.Block - - pemBlock, _ = pem.Decode([]byte(certBundle.Certificate)) - if pemBlock == nil { - return nil, nil, nil, fmt.Errorf("No PEM data found for cert") - } - cert, err = x509.ParseCertificate(pemBlock.Bytes) - if err != nil { - return nil, nil, nil, err - } - - pemBlock, _ = pem.Decode([]byte(certBundle.IssuingCA)) - if pemBlock == nil { - return nil, nil, nil, fmt.Errorf("No PEM data found for issuing CA") - } - ca, err = x509.ParseCertificate(pemBlock.Bytes) - if err != nil { - return nil, nil, nil, err - } - - pemBlock, _ = pem.Decode([]byte(certBundle.PrivateKey)) - if pemBlock == nil { - return nil, nil, nil, fmt.Errorf("No PEM data found for private key") - } - switch keyType { - case "rsa": - privKey, err = x509.ParsePKCS1PrivateKey(pemBlock.Bytes) - if err != nil { - return nil, nil, nil, err - } - case "ec": - privKey, err = x509.ParseECPrivateKey(pemBlock.Bytes) - if err != nil { - return nil, nil, nil, fmt.Errorf("Unable to decode EC private key: %s; value was %s", err, certBundle.PrivateKey) - } - default: - return nil, nil, nil, fmt.Errorf("Unknown private key type %s", keyType) - } - - // There should only be one usage type, because only one is requested - // in the tests - if len(cert.ExtKeyUsage) != 1 { - return cert, nil, nil, fmt.Errorf("Got wrong size key usage in generated cert") - } - switch usage { - case serverUsage: - if cert.ExtKeyUsage[0] != x509.ExtKeyUsageServerAuth { - return cert, nil, nil, fmt.Errorf("Bad key usage") - } - case clientUsage: - if cert.ExtKeyUsage[0] != x509.ExtKeyUsageClientAuth { - return cert, nil, nil, fmt.Errorf("Bad key usage") - } - case codeSigningUsage: - if cert.ExtKeyUsage[0] != x509.ExtKeyUsageCodeSigning { - return cert, nil, nil, fmt.Errorf("Bad key usage") - } - } - - if math.Abs(float64(time.Now().Unix()-cert.NotBefore.Unix())) > 10 { - return cert, nil, nil, fmt.Errorf("Validity period starts out of range") - } - - if math.Abs(float64(time.Now().Add(validity).Unix()-cert.NotAfter.Unix())) > 10 { - return cert, nil, nil, fmt.Errorf("Validity period too large") - } - - return -} - +// Performs basic tests on CA functionality func TestBackend_basic(t *testing.T) { b := Backend() @@ -108,6 +37,8 @@ func TestBackend_basic(t *testing.T) { logicaltest.Test(t, testCase) } +// Generates and tests steps that walk through the various possibilities +// of role flags to ensure that they are properly restricted func TestBackend_roles(t *testing.T) { b := Backend() @@ -129,6 +60,65 @@ func TestBackend_roles(t *testing.T) { logicaltest.Test(t, testCase) } +// Performs some validity checking on the returned bundles +func checkCertsAndPrivateKey(keyType string, usage certUsage, validity time.Duration, certBundle *certutil.CertBundle) (*certutil.ParsedCertBundle, error) { + parsedCertBundle, err := certBundle.ToParsedCertBundle() + if err != nil { + return nil, fmt.Errorf("Error parsing cert bundle: %s", err) + } + + switch { + case parsedCertBundle.Certificate == nil: + return nil, fmt.Errorf("Did not find a certificate in the cert bundle") + case parsedCertBundle.IssuingCA == nil: + return nil, fmt.Errorf("Did not find a CA in the cert bundle") + case parsedCertBundle.PrivateKey == nil: + return nil, fmt.Errorf("Did not find a private key in the cert bundle") + case parsedCertBundle.PrivateKeyType == certutil.UnknownPrivateKey: + return nil, fmt.Errorf("Could not figure out type of private key") + } + + switch { + case parsedCertBundle.PrivateKeyType == certutil.RSAPrivateKey && keyType != "rsa": + fallthrough + case parsedCertBundle.PrivateKeyType == certutil.ECPrivateKey && keyType != "ec": + return nil, fmt.Errorf("Given key type does not match type found in bundle") + } + + cert := parsedCertBundle.Certificate + // There should only be one usage type, because only one is requested + // in the tests + if len(cert.ExtKeyUsage) != 1 { + return nil, fmt.Errorf("Got wrong size key usage in generated cert") + } + switch usage { + case serverUsage: + if cert.ExtKeyUsage[0] != x509.ExtKeyUsageServerAuth { + return nil, fmt.Errorf("Bad key usage") + } + case clientUsage: + if cert.ExtKeyUsage[0] != x509.ExtKeyUsageClientAuth { + return nil, fmt.Errorf("Bad key usage") + } + case codeSigningUsage: + if cert.ExtKeyUsage[0] != x509.ExtKeyUsageCodeSigning { + return nil, fmt.Errorf("Bad key usage") + } + } + + if math.Abs(float64(time.Now().Unix()-cert.NotBefore.Unix())) > 10 { + return nil, fmt.Errorf("Validity period starts out of range") + } + + if math.Abs(float64(time.Now().Add(validity).Unix()-cert.NotAfter.Unix())) > 10 { + return nil, fmt.Errorf("Validity period too large") + } + + return parsedCertBundle, nil +} + +// Generates steps to test out CA configuration -- certificates + CRL expiry, +// and ensure that the certificates are readable after storing them func generateCASteps(t *testing.T) []logicaltest.TestStep { ret := []logicaltest.TestStep{ logicaltest.TestStep{ @@ -139,6 +129,14 @@ func generateCASteps(t *testing.T) []logicaltest.TestStep { }, }, + logicaltest.TestStep{ + Operation: logical.WriteOperation, + Path: "config/crl", + Data: map[string]interface{}{ + "expiry": "16h", + }, + }, + // Ensure we can fetch it back via unauthenticated means, in various formats logicaltest.TestStep{ Operation: logical.ReadOperation, @@ -187,11 +185,23 @@ func generateCASteps(t *testing.T) []logicaltest.TestStep { return nil }, }, + + logicaltest.TestStep{ + Operation: logical.ReadOperation, + Path: "config/crl", + Check: func(resp *logical.Response) error { + if resp.Data["expiry"].(string) != "16h" { + return fmt.Errorf("CRL lifetimes do not match (got %s)", resp.Data["expiry"].(string)) + } + return nil + }, + }, } return ret } +// Generates steps to test out various role permutations func generateRoleSteps(t *testing.T) []logicaltest.TestStep { roleVals := roleEntry{ LeaseMax: "12h", @@ -215,6 +225,7 @@ func generateRoleSteps(t *testing.T) []logicaltest.TestStep { return fmt.Errorf("Expected an error, but did not seem to get one") } + // Adds tests with the currently configured issue/role information addTests := func(testCheck logicaltest.TestCheckFunc) { //fmt.Printf("role vals: %#v\n", roleVals) //fmt.Printf("issue vals: %#v\n", issueTestStep) @@ -232,6 +243,8 @@ func generateRoleSteps(t *testing.T) []logicaltest.TestStep { ret = append(ret, issueTestStep) } + // Returns a TestCheckFunc that performs various validity checks on the + // returned certificate information, mostly within checkCertsAndPrivateKey getCnCheck := func(name, keyType string, usage certUsage, validity time.Duration) logicaltest.TestCheckFunc { var certBundle certutil.CertBundle return func(resp *logical.Response) error { @@ -239,10 +252,11 @@ func generateRoleSteps(t *testing.T) []logicaltest.TestStep { if err != nil { return err } - cert, _, _, err := checkCertsAndPrivateKey(keyType, usage, validity, &certBundle) + parsedCertBundle, err := checkCertsAndPrivateKey(keyType, usage, validity, &certBundle) if err != nil { return fmt.Errorf("Error checking generated certificate: %s", err) } + cert := parsedCertBundle.Certificate if cert.Subject.CommonName != name { return fmt.Errorf("Error: returned certificate has CN of %s but %s was requested", cert.Subject.CommonName, name) } @@ -256,6 +270,7 @@ func generateRoleSteps(t *testing.T) []logicaltest.TestStep { } } + // Common names to test with the various role flags toggled var commonNames struct { Localhost bool `structs:"localhost"` BaseDomain bool `structs:"foo.example.com"` @@ -265,6 +280,11 @@ func generateRoleSteps(t *testing.T) []logicaltest.TestStep { AnyHost bool `structs:"porkslap.beer"` } + // Adds a series of tests based on the current selection of + // allowed common names; contains some (seeded) randomness + // + // This allows for a variety of common names to be tested in various + // combinations with allowed toggles of the role addCnTests := func() { cnMap := structs.New(commonNames).Map() // For the number of tests being run, this is known to hit all diff --git a/builtin/logical/pki/cert_util.go b/builtin/logical/pki/cert_util.go index 23af3de38e..be0a851cc1 100644 --- a/builtin/logical/pki/cert_util.go +++ b/builtin/logical/pki/cert_util.go @@ -38,51 +38,39 @@ type certCreationBundle struct { Usage certUsage } -func getCertBundle(s logical.Storage, path string) (*certutil.CertBundle, error) { - bundle, err := s.Get(path) +// Fetches the CA info. Unlike other certificates, the CA info is stored +// in the backend as a CertBundle, because we are storing its private key +func fetchCAInfo(req *logical.Request) (*certutil.ParsedCertBundle, error) { + bundleEntry, err := req.Storage.Get("config/ca_bundle") if err != nil { - return nil, err + return nil, certutil.InternalError{Err: fmt.Sprintf("Unable to fetch local CA certificate/key: %s", err)} } - if bundle == nil { - return nil, nil + if bundleEntry == nil { + return nil, certutil.UserError{Err: fmt.Sprintf("Backend must be configured with a CA certificate/key")} } - var result certutil.CertBundle - if err := bundle.DecodeJSON(&result); err != nil { - return nil, err - } - - return &result, nil -} - -func fetchCAInfo(req *logical.Request) (*certutil.ParsedCertBundle, *x509.Certificate, error, error) { - bundle, err := getCertBundle(req.Storage, "config/ca_bundle") - if err != nil { - return nil, nil, nil, fmt.Errorf("Unable to fetch local CA certificate/key: %s", err) - } - if bundle == nil { - return nil, nil, fmt.Errorf("Backend must be configured with a CA certificate/key"), nil + var bundle certutil.CertBundle + if err := bundleEntry.DecodeJSON(&bundle); err != nil { + return nil, certutil.InternalError{Err: fmt.Sprintf("Unable to decode local CA certificate/key: %s", err)} } parsedBundle, err := bundle.ToParsedCertBundle() if err != nil { - return nil, nil, nil, err + return nil, certutil.InternalError{Err: err.Error()} } - certificates, err := x509.ParseCertificates(parsedBundle.CertificateBytes) - switch { - case err != nil: - return nil, nil, nil, err - case len(certificates) != 1: - return nil, nil, nil, fmt.Errorf("Length of CA certificate bundle is wrong") + if parsedBundle.Certificate == nil { + return nil, certutil.InternalError{Err: "Stored CA information not able to be parsed"} } - return parsedBundle, certificates[0], nil, nil + return parsedBundle, nil } -func fetchCertBySerial(req *logical.Request, prefix, serial string) (certEntry *logical.StorageEntry, userError, internalError error) { +// Allows fetching certificates from the backend; it handles the slightly +// separate pathing for CA, CRL, and revoked certificates. +func fetchCertBySerial(req *logical.Request, prefix, serial string) (*logical.StorageEntry, error) { var path string - var err error + switch { case serial == "ca": path = "ca" @@ -94,20 +82,22 @@ func fetchCertBySerial(req *logical.Request, prefix, serial string) (certEntry * path = "certs/" + strings.Replace(strings.ToLower(serial), "-", ":", -1) } - certEntry, err = req.Storage.Get(path) + certEntry, err := req.Storage.Get(path) if err != nil || certEntry == nil { - return nil, fmt.Errorf("Certificate with serial number %s not found (if it has been revoked, the revoked/ endpoint must be used)", serial), nil + return nil, certutil.InternalError{Err: fmt.Sprintf("Certificate with serial number %s not found", serial)} } - if len(certEntry.Value) == 0 { - return nil, nil, fmt.Errorf("Returned certificate bytes for serial %s were empty", serial) + if certEntry.Value == nil || len(certEntry.Value) == 0 { + return nil, certutil.InternalError{Err: fmt.Sprintf("Returned certificate bytes for serial %s were empty", serial)} } - return + return certEntry, nil } +// Given a set of requested names for a certificate, verifies that all of them +// match the various toggles set in the role for controlling issuance. +// If one does not pass, it is returned in the string argument. func validateCommonNames(req *logical.Request, commonNames []string, role *roleEntry) (string, error) { - // TODO: handle wildcards hostnameRegex, err := regexp.Compile(`^(([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])$`) if err != nil { return "", fmt.Errorf("Error compiling hostname regex: %s", err) @@ -168,28 +158,30 @@ func validateCommonNames(req *logical.Request, commonNames []string, role *roleE return "", nil } -func createCertificate(creationInfo *certCreationBundle) (parsedBundle *certutil.ParsedCertBundle, userErr, intErr error) { +// Performs the heavy lifting of creating a certificate. Returns +// a fully-filled-in ParsedCertBundle. +func createCertificate(creationInfo *certCreationBundle) (*certutil.ParsedCertBundle, error) { var clientPrivKey crypto.Signer var err error - parsedBundle = &certutil.ParsedCertBundle{} + result := &certutil.ParsedCertBundle{} var serialNumber *big.Int serialNumber, err = rand.Int(rand.Reader, (&big.Int{}).Exp(big.NewInt(2), big.NewInt(159), nil)) if err != nil { - return nil, nil, fmt.Errorf("Error getting random serial number") + return nil, certutil.InternalError{Err: fmt.Sprintf("Error getting random serial number")} } switch creationInfo.KeyType { case "rsa": - parsedBundle.PrivateKeyType = certutil.RSAPrivateKey + result.PrivateKeyType = certutil.RSAPrivateKey clientPrivKey, err = rsa.GenerateKey(rand.Reader, creationInfo.KeyBits) if err != nil { - return nil, nil, fmt.Errorf("Error generating RSA private key") + return nil, certutil.InternalError{Err: fmt.Sprintf("Error generating RSA private key")} } - parsedBundle.PrivateKey = clientPrivKey - parsedBundle.PrivateKeyBytes = x509.MarshalPKCS1PrivateKey(clientPrivKey.(*rsa.PrivateKey)) + result.PrivateKey = clientPrivKey + result.PrivateKeyBytes = x509.MarshalPKCS1PrivateKey(clientPrivKey.(*rsa.PrivateKey)) case "ec": - parsedBundle.PrivateKeyType = certutil.ECPrivateKey + result.PrivateKeyType = certutil.ECPrivateKey var curve elliptic.Curve switch creationInfo.KeyBits { case 224: @@ -201,24 +193,24 @@ func createCertificate(creationInfo *certCreationBundle) (parsedBundle *certutil case 521: curve = elliptic.P521() default: - return nil, fmt.Errorf("Unsupported bit length for EC key: %d", creationInfo.KeyBits), nil + return nil, certutil.UserError{Err: fmt.Sprintf("Unsupported bit length for EC key: %d", creationInfo.KeyBits)} } clientPrivKey, err = ecdsa.GenerateKey(curve, rand.Reader) if err != nil { - return nil, nil, fmt.Errorf("Error generating EC private key") + return nil, certutil.InternalError{Err: fmt.Sprintf("Error generating EC private key")} } - parsedBundle.PrivateKey = clientPrivKey - parsedBundle.PrivateKeyBytes, err = x509.MarshalECPrivateKey(clientPrivKey.(*ecdsa.PrivateKey)) + result.PrivateKey = clientPrivKey + result.PrivateKeyBytes, err = x509.MarshalECPrivateKey(clientPrivKey.(*ecdsa.PrivateKey)) if err != nil { - return nil, nil, fmt.Errorf("Error marshalling EC private key") + return nil, certutil.InternalError{Err: fmt.Sprintf("Error marshalling EC private key")} } default: - return nil, fmt.Errorf("Unknown key type: %s", creationInfo.KeyType), nil + return nil, certutil.UserError{Err: fmt.Sprintf("Unknown key type: %s", creationInfo.KeyType)} } - subjKeyID, err := certutil.GetSubjKeyID(parsedBundle.PrivateKey) + subjKeyID, err := certutil.GetSubjKeyID(result.PrivateKey) if err != nil { - return nil, nil, fmt.Errorf("Error getting subject key ID: %s", err) + return nil, certutil.InternalError{Err: fmt.Sprintf("Error getting subject key ID: %s", err)} } subject := pkix.Name{ @@ -262,17 +254,17 @@ func createCertificate(creationInfo *certCreationBundle) (parsedBundle *certutil cert, err := x509.CreateCertificate(rand.Reader, certTemplate, creationInfo.CACert, clientPrivKey.Public(), creationInfo.SigningBundle.PrivateKey) if err != nil { - return nil, nil, fmt.Errorf("Unable to create certificate: %s", err) + return nil, certutil.InternalError{Err: fmt.Sprintf("Unable to create certificate: %s", err)} } - parsedBundle.CertificateBytes = cert - parsedBundle.Certificate, err = x509.ParseCertificate(cert) + result.CertificateBytes = cert + result.Certificate, err = x509.ParseCertificate(cert) if err != nil { - return nil, nil, fmt.Errorf("Unable to parse created certificate: %s", err) + return nil, certutil.InternalError{Err: fmt.Sprintf("Unable to parse created certificate: %s", err)} } - parsedBundle.IssuingCABytes = creationInfo.SigningBundle.CertificateBytes - parsedBundle.IssuingCA = creationInfo.SigningBundle.Certificate + result.IssuingCABytes = creationInfo.SigningBundle.CertificateBytes + result.IssuingCA = creationInfo.SigningBundle.Certificate - return + return result, nil } diff --git a/builtin/logical/pki/crl_util.go b/builtin/logical/pki/crl_util.go index 2ca33d5391..b433e2512b 100644 --- a/builtin/logical/pki/crl_util.go +++ b/builtin/logical/pki/crl_util.go @@ -5,9 +5,9 @@ import ( "crypto/x509" "crypto/x509/pkix" "fmt" - "sync" "time" + "github.com/hashicorp/vault/helper/certutil" "github.com/hashicorp/vault/logical" ) @@ -16,46 +16,45 @@ type revocationInfo struct { RevocationTime int64 `json:"revocation_time"` } -var ( - crlLifetime = time.Hour * 72 - revokeStorageLock = &sync.Mutex{} -) - -func revokeCert(req *logical.Request, serial string) (*logical.Response, error) { +// Revokes a cert, and tries to be smart about error recovery +func revokeCert(b *backend, req *logical.Request, serial string) (*logical.Response, error) { alreadyRevoked := false - var err error + var revInfo revocationInfo - revInfo := revocationInfo{} - - certEntry, userErr, intErr := fetchCertBySerial(req, "revoked/", serial) + certEntry, err := fetchCertBySerial(req, "revoked/", serial) + // Don't check error because it's expected that it may fail here; + // just check for existence if certEntry != nil { // Verify that it is also deleted from certs/ // in case of partial failure from an earlier run. - certEntry, _, _ = fetchCertBySerial(req, "certs/", serial) - if certEntry != nil { - alreadyRevoked = true - - revEntry, err := req.Storage.Get("revoked/" + serial) - if err != nil { - return nil, fmt.Errorf("Error getting existing revocation info") - } - - err = revEntry.DecodeJSON(&revInfo) - if err != nil { - return nil, fmt.Errorf("Error decoding existing revocation info") - } - } else { + certEntry, _ = fetchCertBySerial(req, "certs/", serial) + if certEntry == nil { + // Everything seems sane, so don't rebuild the CRL return nil, nil } + + // Still exists in certs/; set the revocation info, below it will + // be removed from certs/ and the CRL rotated + alreadyRevoked = true + + revEntry, err := req.Storage.Get("revoked/" + serial) + if revEntry == nil || err != nil { + return nil, fmt.Errorf("Error getting existing revocation info") + } + + err = revEntry.DecodeJSON(&revInfo) + if err != nil { + return nil, fmt.Errorf("Error decoding existing revocation info") + } } if !alreadyRevoked { - certEntry, userErr, intErr = fetchCertBySerial(req, "certs/", serial) - switch { - case userErr != nil: - return logical.ErrorResponse(userErr.Error()), nil - case intErr != nil: - return nil, intErr + certEntry, err = fetchCertBySerial(req, "certs/", serial) + switch err.(type) { + case certutil.UserError: + return logical.ErrorResponse(err.Error()), nil + case certutil.InternalError: + return nil, err } cert, err := x509.ParseCertificate(certEntry.Value) @@ -85,12 +84,12 @@ func revokeCert(req *logical.Request, serial string) (*logical.Response, error) } - userErr, intErr = buildCRL(req) - switch { - case userErr != nil: - return logical.ErrorResponse(fmt.Sprintf("Error during CRL building: %s", userErr)), nil - case intErr != nil: - return nil, fmt.Errorf("Error encountered during CRL building: %s", intErr) + crlErr := buildCRL(b, req) + switch crlErr.(type) { + case certutil.UserError: + return logical.ErrorResponse(fmt.Sprintf("Error during CRL building: %s", crlErr)), nil + case certutil.InternalError: + return nil, fmt.Errorf("Error encountered during CRL building: %s", crlErr) } err = req.Storage.Delete("certs/" + serial) @@ -106,10 +105,15 @@ func revokeCert(req *logical.Request, serial string) (*logical.Response, error) }, nil } -func buildCRL(req *logical.Request) (error, error) { +// Builds a CRL by going through the list of revoked certificates and building +// a new CRL with the stored revocation times and serial numbers. +// +// If a certificate has already expired, it will be removed entirely rather than +// become part of the new CRL. +func buildCRL(b *backend, req *logical.Request) error { revokedSerials, err := req.Storage.List("revoked/") if err != nil { - return nil, fmt.Errorf("Error fetching list of revoked certs: %s", err) + return certutil.InternalError{Err: fmt.Sprintf("Error fetching list of revoked certs: %s", err)} } revokedCerts := []pkix.RevokedCertificate{} @@ -117,32 +121,32 @@ func buildCRL(req *logical.Request) (error, error) { for _, serial := range revokedSerials { revokedEntry, err := req.Storage.Get("revoked/" + serial) if err != nil { - return nil, fmt.Errorf("Unable to fetch revoked cert with serial %s: %s", serial, err) + return certutil.InternalError{Err: fmt.Sprintf("Unable to fetch revoked cert with serial %s: %s", serial, err)} } if revokedEntry == nil { - return nil, fmt.Errorf("Revoked certificate entry for serial %s is nil", serial) + return certutil.InternalError{Err: fmt.Sprintf("Revoked certificate entry for serial %s is nil", serial)} } if revokedEntry.Value == nil || len(revokedEntry.Value) == 0 { // TODO: In this case, remove it and continue? How likely is this to // happen? Alternately, could skip it entirely, or could implement a // delete function so that there is a way to remove these - return nil, fmt.Errorf("Found revoked serial but actual certificate is empty") + return certutil.InternalError{Err: fmt.Sprintf("Found revoked serial but actual certificate is empty")} } err = revokedEntry.DecodeJSON(&revInfo) if err != nil { - return nil, fmt.Errorf("Error decoding revocation entry for serial %s: %s", serial, err) + return certutil.InternalError{Err: fmt.Sprintf("Error decoding revocation entry for serial %s: %s", serial, err)} } revokedCert, err := x509.ParseCertificate(revInfo.CertificateBytes) if err != nil { - return nil, fmt.Errorf("Unable to parse stored revoked certificate with serial %s: %s", serial, err) + return certutil.InternalError{Err: fmt.Sprintf("Unable to parse stored revoked certificate with serial %s: %s", serial, err)} } if revokedCert.NotAfter.Before(time.Now()) { err = req.Storage.Delete(serial) if err != nil { - return nil, fmt.Errorf("Unable to delete revoked, expired certificate with serial %s: %s", serial, err) + return certutil.InternalError{Err: fmt.Sprintf("Unable to delete revoked, expired certificate with serial %s: %s", serial, err)} } continue } @@ -153,18 +157,30 @@ func buildCRL(req *logical.Request) (error, error) { }) } - signingBundle, caCert, userErr, intErr := fetchCAInfo(req) - switch { - case userErr != nil: - return fmt.Errorf("Could not fetch the CA certificate: %s", userErr), nil - case intErr != nil: - return nil, fmt.Errorf("Error fetching CA certificate: %s", intErr) + signingBundle, caErr := fetchCAInfo(req) + switch caErr.(type) { + case certutil.UserError: + return certutil.UserError{Err: fmt.Sprintf("Could not fetch the CA certificate: %s", caErr)} + case certutil.InternalError: + return certutil.InternalError{Err: fmt.Sprintf("Error fetching CA certificate: %s", caErr)} } - // TODO: Make expiry configurable - crlBytes, err := caCert.CreateCRL(rand.Reader, signingBundle.PrivateKey, revokedCerts, time.Now(), time.Now().Add(crlLifetime)) + crlLifetime := b.crlLifetime + crlInfo, err := b.CRL(req.Storage) if err != nil { - return nil, fmt.Errorf("Error creating new CRL: %s", err) + return certutil.InternalError{Err: fmt.Sprintf("Error fetching CRL config information: %s", err)} + } + if crlInfo != nil { + crlDur, err := time.ParseDuration(crlInfo.Expiry) + if err != nil { + return certutil.InternalError{Err: fmt.Sprintf("Error parsing CRL duration of %s", crlInfo.Expiry)} + } + crlLifetime = crlDur + } + + crlBytes, err := signingBundle.Certificate.CreateCRL(rand.Reader, signingBundle.PrivateKey, revokedCerts, time.Now(), time.Now().Add(crlLifetime)) + if err != nil { + return certutil.InternalError{Err: fmt.Sprintf("Error creating new CRL: %s", err)} } err = req.Storage.Put(&logical.StorageEntry{ @@ -172,8 +188,8 @@ func buildCRL(req *logical.Request) (error, error) { Value: crlBytes, }) if err != nil { - return nil, fmt.Errorf("Error storing CRL: %s", err) + return certutil.InternalError{Err: fmt.Sprintf("Error storing CRL: %s", err)} } - return nil, nil + return nil } diff --git a/builtin/logical/pki/path_config_crl.go b/builtin/logical/pki/path_config_crl.go new file mode 100644 index 0000000000..616864ec27 --- /dev/null +++ b/builtin/logical/pki/path_config_crl.go @@ -0,0 +1,102 @@ +package pki + +import ( + "fmt" + "time" + + "github.com/fatih/structs" + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" +) + +// CRLConfig holds basic CRL configuration information +type crlConfig struct { + Expiry string `json:"expiry" mapstructure:"expiry" structs:"expiry"` +} + +func pathConfigCRL(b *backend) *framework.Path { + return &framework.Path{ + Pattern: "config/crl", + Fields: map[string]*framework.FieldSchema{ + "expiry": &framework.FieldSchema{ + Type: framework.TypeString, + Description: `The amount of time the generated CRL should be +valid; defaults to 72 hours`, + Default: "72h", + }, + }, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.ReadOperation: b.pathCRLRead, + logical.WriteOperation: b.pathCRLWrite, + }, + + HelpSynopsis: pathConfigCRLHelpSyn, + HelpDescription: pathConfigCRLHelpDesc, + } +} + +func (b *backend) CRL(s logical.Storage) (*crlConfig, error) { + entry, err := s.Get("config/crl") + if err != nil { + return nil, err + } + if entry == nil { + return nil, nil + } + + var result crlConfig + if err := entry.DecodeJSON(&result); err != nil { + return nil, err + } + + return &result, nil +} + +func (b *backend) pathCRLRead( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + config, err := b.CRL(req.Storage) + if err != nil { + return nil, err + } + if config == nil { + return nil, nil + } + + return &logical.Response{ + Data: structs.New(config).Map(), + }, nil +} + +func (b *backend) pathCRLWrite( + req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + expiry := d.Get("expiry").(string) + + _, err := time.ParseDuration(expiry) + if err != nil { + return logical.ErrorResponse(fmt.Sprintf("Given expiry could not be decoded: %s", err)), nil + } + + config := &crlConfig{ + Expiry: expiry, + } + + entry, err := logical.StorageEntryJSON("config/crl", config) + if err != nil { + return nil, err + } + err = req.Storage.Put(entry) + if err != nil { + return nil, err + } + + return nil, nil +} + +const pathConfigCRLHelpSyn = ` +Configure the CRL expiration. +` + +const pathConfigCRLHelpDesc = ` +This endpoint allows configuration of the CRL lifetime. +` diff --git a/builtin/logical/pki/path_fetch.go b/builtin/logical/pki/path_fetch.go index 1e95d60d56..889d8824ed 100644 --- a/builtin/logical/pki/path_fetch.go +++ b/builtin/logical/pki/path_fetch.go @@ -3,12 +3,13 @@ package pki import ( "encoding/pem" "fmt" - "strings" + "github.com/hashicorp/vault/helper/certutil" "github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/logical/framework" ) +// Returns the CA in raw format func pathFetchCA(b *backend) *framework.Path { return &framework.Path{ Pattern: `ca(/pem)?`, @@ -22,6 +23,7 @@ func pathFetchCA(b *backend) *framework.Path { } } +// Returns the CRL in raw format func pathFetchCRL(b *backend) *framework.Path { return &framework.Path{ Pattern: `crl(/pem)?`, @@ -35,6 +37,8 @@ func pathFetchCRL(b *backend) *framework.Path { } } +// Returns any valid (non-revoked) cert. Since "ca" fits the pattern, this path +// also handles returning the CA cert in a non-raw format. func pathFetchValid(b *backend) *framework.Path { return &framework.Path{ Pattern: `cert/(?P[0-9A-Fa-f-:]+)`, @@ -55,6 +59,7 @@ hyphen-separated octal`, } } +// This returns the CRL in a non-raw format func pathFetchCRLViaCertPath(b *backend) *framework.Path { return &framework.Path{ Pattern: `cert/crl`, @@ -68,36 +73,22 @@ func pathFetchCRLViaCertPath(b *backend) *framework.Path { } } -func pathFetchRevoked(b *backend) *framework.Path { - return &framework.Path{ - Pattern: `revoked/(?P[0-9A-Fa-f-:]+)`, - Fields: map[string]*framework.FieldSchema{ - "serial": &framework.FieldSchema{ - Type: framework.TypeString, - Description: "Certificate serial number, in colon- or hyphen-separated octal", - }, - }, - - Callbacks: map[logical.Operation]framework.OperationFunc{ - logical.ReadOperation: b.pathFetchRead, - }, - - HelpSynopsis: pathFetchHelpSyn, - HelpDescription: pathFetchHelpDesc, - } -} - func (b *backend) pathFetchRead(req *logical.Request, data *framework.FieldData) (response *logical.Response, retErr error) { var serial string var pemType string var contentType string var certEntry *logical.StorageEntry - var userErr, intErr error + var funcErr error var certificate []byte response = &logical.Response{ Data: map[string]interface{}{}, } + // Some of these need to return raw and some non-raw; + // this is basically handled by setting contentType or not. + // Errors don't cause an immediate exit, because the raw + // paths still need to return raw output. + switch { case req.Path == "ca" || req.Path == "ca/pem": serial = "ca" @@ -123,39 +114,27 @@ func (b *backend) pathFetchRead(req *logical.Request, data *framework.FieldData) goto reply } - _, _, userErr, intErr = fetchCAInfo(req) - switch { - case userErr != nil: - response = logical.ErrorResponse(fmt.Sprintf("%s", userErr)) + _, funcErr = fetchCAInfo(req) + switch funcErr.(type) { + case certutil.UserError: + response = logical.ErrorResponse(fmt.Sprintf("%s", funcErr)) goto reply - case intErr != nil: - retErr = intErr + case certutil.InternalError: + retErr = funcErr goto reply } - certEntry, userErr, intErr = fetchCertBySerial(req, req.Path, serial) - switch { - case userErr != nil: - response = logical.ErrorResponse(userErr.Error()) + certEntry, funcErr = fetchCertBySerial(req, req.Path, serial) + switch funcErr.(type) { + case certutil.UserError: + response = logical.ErrorResponse(funcErr.Error()) goto reply - case intErr != nil: - retErr = intErr + case certutil.InternalError: + retErr = funcErr goto reply } - switch { - case strings.HasPrefix(req.Path, "revoked/"): - var revInfo revocationInfo - err := certEntry.DecodeJSON(&revInfo) - if err != nil { - retErr = fmt.Errorf("Error decoding revocation entry for serial %s: %s", serial, err) - goto reply - } - certificate = revInfo.CertificateBytes - response.Data["revocation_time"] = revInfo.RevocationTime - default: - certificate = certEntry.Value - } + certificate = certEntry.Value if len(pemType) != 0 { block := pem.Block{ @@ -188,11 +167,11 @@ reply: } const pathFetchHelpSyn = ` -Fetch a CA, CRL, valid or revoked certificate. +Fetch a CA, CRL, or non-revoked certificate. ` const pathFetchHelpDesc = ` -This allows certificates to be fetched. If using the fetch/ prefix any valid certificate can be fetched; if using the revoked/ prefix, which requires a root token, revoked certificates can also be fetched. +This allows certificates to be fetched. If using the fetch/ prefix any non-revoked certificate can be fetched. Using "ca" or "crl" as the value fetches the appropriate information in DER encoding. Add "/pem" to either to get PEM encoding. ` diff --git a/builtin/logical/pki/path_issue.go b/builtin/logical/pki/path_issue.go index 844df4952a..f6c8ca505f 100644 --- a/builtin/logical/pki/path_issue.go +++ b/builtin/logical/pki/path_issue.go @@ -7,6 +7,7 @@ import ( "time" "github.com/fatih/structs" + "github.com/hashicorp/vault/helper/certutil" "github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/logical/framework" ) @@ -123,15 +124,15 @@ func (b *backend) pathIssueCert( return nil, fmt.Errorf("Error validating name %s: %s", badName, err) } - signingBundle, caCert, userErr, intErr := fetchCAInfo(req) - switch { - case userErr != nil: - return logical.ErrorResponse(fmt.Sprintf("Could not fetch the CA certificate: %s", userErr)), nil - case intErr != nil: - return nil, fmt.Errorf("Error fetching CA certificate: %s", intErr) + signingBundle, caErr := fetchCAInfo(req) + switch caErr.(type) { + case certutil.UserError: + return logical.ErrorResponse(fmt.Sprintf("Could not fetch the CA certificate: %s", caErr)), nil + case certutil.InternalError: + return nil, fmt.Errorf("Error fetching CA certificate: %s", caErr) } - if time.Now().Add(lease).After(caCert.NotAfter) { + if time.Now().Add(lease).After(signingBundle.Certificate.NotAfter) { return logical.ErrorResponse(fmt.Sprintf("Cannot satisfy request, as maximum lease is beyond the expiration of the CA certificate")), nil } @@ -148,7 +149,7 @@ func (b *backend) pathIssueCert( creationBundle := &certCreationBundle{ SigningBundle: signingBundle, - CACert: caCert, + CACert: signingBundle.Certificate, CommonNames: commonNames, IPSANs: ipSANs, KeyType: role.KeyType, @@ -157,12 +158,12 @@ func (b *backend) pathIssueCert( Usage: usage, } - parsedBundle, userErr, intErr := createCertificate(creationBundle) - switch { - case userErr != nil: - return logical.ErrorResponse(userErr.Error()), nil - case intErr != nil: - return nil, intErr + parsedBundle, err := createCertificate(creationBundle) + switch err.(type) { + case certutil.UserError: + return logical.ErrorResponse(err.Error()), nil + case certutil.InternalError: + return nil, err } cb, err := parsedBundle.ToCertBundle() diff --git a/builtin/logical/pki/path_revoke.go b/builtin/logical/pki/path_revoke.go index 0982a5c715..d8ad223b87 100644 --- a/builtin/logical/pki/path_revoke.go +++ b/builtin/logical/pki/path_revoke.go @@ -3,6 +3,7 @@ package pki import ( "fmt" + "github.com/hashicorp/vault/helper/certutil" "github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/logical/framework" ) @@ -46,29 +47,29 @@ func (b *backend) pathRevokeWrite(req *logical.Request, data *framework.FieldDat return logical.ErrorResponse("The serial number must be provided"), nil } - revokeStorageLock.Lock() - defer revokeStorageLock.Unlock() + b.revokeStorageLock.Lock() + defer b.revokeStorageLock.Unlock() - return revokeCert(req, serial) + return revokeCert(b, req, serial) } func (b *backend) pathRotateCRLRead(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { - revokeStorageLock.Lock() - defer revokeStorageLock.Unlock() + b.revokeStorageLock.Lock() + defer b.revokeStorageLock.Unlock() - userErr, intErr := buildCRL(req) - switch { - case userErr != nil: - return logical.ErrorResponse(fmt.Sprintf("Error during CRL building: %s", userErr)), nil - case intErr != nil: - return nil, fmt.Errorf("Error encountered during CRL building: %s", intErr) + crlErr := buildCRL(b, req) + switch crlErr.(type) { + case certutil.UserError: + return logical.ErrorResponse(fmt.Sprintf("Error during CRL building: %s", crlErr)), nil + case certutil.InternalError: + return nil, fmt.Errorf("Error encountered during CRL building: %s", crlErr) + default: + return &logical.Response{ + Data: map[string]interface{}{ + "success": true, + }, + }, nil } - - return &logical.Response{ - Data: map[string]interface{}{ - "success": true, - }, - }, nil } const pathRevokeHelpSyn = ` diff --git a/builtin/logical/pki/secret_certs.go b/builtin/logical/pki/secret_certs.go index f9dd54c76f..06be0caa52 100644 --- a/builtin/logical/pki/secret_certs.go +++ b/builtin/logical/pki/secret_certs.go @@ -52,8 +52,8 @@ func (b *backend) secretCredsRevoke( serial := strings.Replace(strings.ToLower(serialInt.(string)), "-", ":", -1) - revokeStorageLock.Lock() - defer revokeStorageLock.Unlock() + b.revokeStorageLock.Lock() + defer b.revokeStorageLock.Unlock() - return revokeCert(req, serial) + return revokeCert(b, req, serial) } diff --git a/helper/certutil/types.go b/helper/certutil/types.go index 437c0a4c17..f863ea7052 100644 --- a/helper/certutil/types.go +++ b/helper/certutil/types.go @@ -41,21 +41,21 @@ const ( // UserError represents an error generated due to invalid user input type UserError struct { - s string + Err string } func (e UserError) Error() string { - return e.s + return e.Err } // InternalError represents an error generated internally, // presumably not due to invalid user input type InternalError struct { - s string + Err string } func (e InternalError) Error() string { - return e.s + return e.Err } // CertBundle contains a key type, a PEM-encoded private key, diff --git a/website/source/docs/secrets/pki/index.html.md b/website/source/docs/secrets/pki/index.html.md index c6ff6fa931..eccac5835d 100644 --- a/website/source/docs/secrets/pki/index.html.md +++ b/website/source/docs/secrets/pki/index.html.md @@ -443,44 +443,6 @@ If you get stuck at any time, simply run `vault help pki` or with a subpath for
  • -### /pki/revoked/ -#### GET - -
    -
    Description
    -
    - Retrieves a revoked certificate and its revocation time. The serial - number must be in either hyphen-separated or colon-separated octal format. -

    This is a root-protected endpoint. -
    - -
    Method
    -
    GET
    - -
    URL
    -
    `/pki/revoked/`
    - -
    Parameters
    -
    - None -
    - -
    Returns
    -
    - - ```javascript - { - "data": { - "revocation_time": 1433269787, - "certificate": "-----BEGIN CERTIFICATE-----\nMIIGmDCCBYCgAwIBAgIHBzEB3fTzhTANBgkqhkiG9w0BAQsFADCBjDELMAkGA1UE\n..." - } - } - ... - ``` - -
    -
    - ### /pki/roles/ #### POST @@ -665,7 +627,8 @@ If you get stuck at any time, simply run `vault help pki` or with a subpath for
    Description
    - Deletes the role definition. + Deletes the role definition. Deleting a role does not revoke + certificates previously issued under this role.
    Method
    From 15594561ab7a092b9322e1c63c8f34e6a35cd3d6 Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Fri, 19 Jun 2015 16:06:56 -0400 Subject: [PATCH 13/15] Add unit tests for certutil, and fix a whitespace stripping issue. Commit contents (C)2015 Akamai Technologies, Inc. --- helper/certutil/certutil_test.go | 344 +++++++++++++++++++++++++++++++ helper/certutil/types.go | 12 +- 2 files changed, 351 insertions(+), 5 deletions(-) create mode 100644 helper/certutil/certutil_test.go diff --git a/helper/certutil/certutil_test.go b/helper/certutil/certutil_test.go new file mode 100644 index 0000000000..5d8bac1b75 --- /dev/null +++ b/helper/certutil/certutil_test.go @@ -0,0 +1,344 @@ +package certutil + +import ( + "bytes" + "encoding/json" + "fmt" + "strings" + "testing" + + "github.com/fatih/structs" + "github.com/hashicorp/vault/api" +) + +// Tests converting back and forth between a CertBundle and a ParsedCertBundle. +// +// Also tests the GetSubjKeyID, GetOctalFormatted, and +// ParsedCertBundle.getSigner functions. +func TestCertBundleConversion(t *testing.T) { + cbuts := []*CertBundle{ + refreshRSACertBundle(), + refreshECCertBundle(), + } + + for _, cbut := range cbuts { + pcbut, err := cbut.ToParsedCertBundle() + if err != nil { + t.Fatalf("Error converting to parsed cert bundle: %s", err) + } + + err = compareCertBundleToParsedCertBundle(cbut, pcbut) + if err != nil { + t.Fatalf(err.Error()) + } + } +} + +func TestTLSConfig(t *testing.T) { + cbut := refreshRSACertBundle() + + pcbut, err := cbut.ToParsedCertBundle() + if err != nil { + t.Fatalf("Error getting parsed cert bundle: %s", err) + } + + usages := []TLSUsage{ + TLSUnknown, + TLSClient, + TLSServer, + TLSClient | TLSServer, + } + + for _, usage := range usages { + tlsConfig, err := pcbut.GetTLSConfig(usage) + if err != nil { + t.Fatalf("Error getting tls config: %s", err) + } + if tlsConfig == nil { + t.Fatalf("Got nil tls.Config") + } + + if len(tlsConfig.Certificates) != 1 { + t.Fatalf("Unexpected length in config.Certificates") + } + + // Length should be 2, since we passed in a CA + if len(tlsConfig.Certificates[0].Certificate) != 2 { + t.Fatalf("Did not find both certificates in config.Certificates.Certificate") + } + + if tlsConfig.Certificates[0].Leaf != pcbut.Certificate { + t.Fatalf("Leaf certificate does not match parsed bundle's certificate") + } + + if tlsConfig.Certificates[0].PrivateKey != pcbut.PrivateKey { + t.Fatalf("Config's private key does not match parsed bundle's private key") + } + + switch usage { + case TLSServer | TLSClient: + if len(tlsConfig.ClientCAs.Subjects()) != 1 || bytes.Compare(tlsConfig.ClientCAs.Subjects()[0], pcbut.IssuingCA.RawSubject) != 0 { + t.Fatalf("CA certificate not in client cert pool as expected") + } + if len(tlsConfig.RootCAs.Subjects()) != 1 || bytes.Compare(tlsConfig.RootCAs.Subjects()[0], pcbut.IssuingCA.RawSubject) != 0 { + t.Fatalf("CA certificate not in root cert pool as expected") + } + case TLSServer: + if len(tlsConfig.ClientCAs.Subjects()) != 1 || bytes.Compare(tlsConfig.ClientCAs.Subjects()[0], pcbut.IssuingCA.RawSubject) != 0 { + t.Fatalf("CA certificate not in client cert pool as expected") + } + if tlsConfig.RootCAs != nil { + t.Fatalf("Found root pools in config object when not expected") + } + case TLSClient: + if len(tlsConfig.RootCAs.Subjects()) != 1 || bytes.Compare(tlsConfig.RootCAs.Subjects()[0], pcbut.IssuingCA.RawSubject) != 0 { + t.Fatalf("CA certificate not in root cert pool as expected") + } + if tlsConfig.ClientCAs != nil { + t.Fatalf("Found root pools in config object when not expected") + } + default: + if tlsConfig.RootCAs != nil || tlsConfig.ClientCAs != nil { + t.Fatalf("Found root pools in config object when not expected") + } + } + } +} + +func TestCertBundleParsing(t *testing.T) { + jsonBundle := refreshRSACertBundle() + jsonString, err := json.Marshal(jsonBundle) + if err != nil { + t.Fatalf("Error marshaling testing certbundle to JSON: %s", err) + } + pcbut, err := ParsePKIJSON(jsonString) + if err != nil { + t.Fatalf("Error during JSON bundle handling: %s", err) + } + err = compareCertBundleToParsedCertBundle(jsonBundle, pcbut) + if err != nil { + t.Fatalf(err.Error()) + } + + secret := &api.Secret{ + Data: structs.New(jsonBundle).Map(), + } + pcbut, err = ParsePKIMap(secret.Data) + if err != nil { + t.Fatalf("Error during JSON bundle handling: %s", err) + } + err = compareCertBundleToParsedCertBundle(jsonBundle, pcbut) + if err != nil { + t.Fatalf(err.Error()) + } + + pemBundle := strings.Join([]string{ + jsonBundle.Certificate, + jsonBundle.IssuingCA, + jsonBundle.PrivateKey, + }, "\n") + pcbut, err = ParsePEMBundle(pemBundle) + if err != nil { + t.Fatalf("Error during JSON bundle handling: %s", err) + } + err = compareCertBundleToParsedCertBundle(jsonBundle, pcbut) + if err != nil { + t.Fatalf(err.Error()) + } +} + +func compareCertBundleToParsedCertBundle(cbut *CertBundle, pcbut *ParsedCertBundle) error { + if cbut == nil { + return fmt.Errorf("Got nil bundle") + } + if pcbut == nil { + return fmt.Errorf("Got nil parsed bundle") + } + + switch { + case pcbut.Certificate == nil: + return fmt.Errorf("Parsed bundle has nil certificate") + case pcbut.PrivateKey == nil: + return fmt.Errorf("Parsed bundle has nil private key") + case pcbut.IssuingCA == nil: + return fmt.Errorf("Parsed bundle has nil issuing CA") + } + + switch cbut.PrivateKey { + case privRSAKeyPem: + if pcbut.PrivateKeyType != RSAPrivateKey { + return fmt.Errorf("Parsed bundle has wrong private key type") + } + case privECKeyPem: + if pcbut.PrivateKeyType != ECPrivateKey { + return fmt.Errorf("Parsed bundle has wrong private key type") + } + default: + return fmt.Errorf("Parsed bundle has unknown private key type") + } + + subjKeyID, err := GetSubjKeyID(pcbut.PrivateKey) + if err != nil { + return fmt.Errorf("Error when getting subject key id: %s", err) + } + if bytes.Compare(subjKeyID, pcbut.Certificate.SubjectKeyId) != 0 { + return fmt.Errorf("Parsed bundle private key does not match subject key id") + } + + cb, err := pcbut.ToCertBundle() + if err != nil { + return fmt.Errorf("Thrown error during parsed bundle conversion: %s\n\nInput was: %#v", err, *pcbut) + } + + switch { + case len(cb.Certificate) == 0: + return fmt.Errorf("Bundle has nil certificate") + case len(cb.PrivateKey) == 0: + return fmt.Errorf("Bundle has nil private key") + case len(cb.IssuingCA) == 0: + return fmt.Errorf("Bundle has nil issuing CA") + } + + switch cb.PrivateKeyType { + case "rsa": + if pcbut.PrivateKeyType != RSAPrivateKey { + return fmt.Errorf("Bundle has wrong private key type") + } + if cb.PrivateKey != privRSAKeyPem { + return fmt.Errorf("Bundle private key does not match") + } + case "ec": + if pcbut.PrivateKeyType != ECPrivateKey { + return fmt.Errorf("Bundle has wrong private key type") + } + if cb.PrivateKey != privECKeyPem { + return fmt.Errorf("Bundle private key does not match") + } + default: + return fmt.Errorf("Bundle has unknown private key type") + } + + if cb.SerialNumber != GetOctalFormatted(pcbut.Certificate.SerialNumber.Bytes(), ":") { + return fmt.Errorf("Bundle serial number does not match") + } + + return nil +} + +func refreshRSACertBundle() *CertBundle { + return &CertBundle{ + Certificate: certRSAPem, + PrivateKey: privRSAKeyPem, + IssuingCA: issuingCaPem, + } +} + +func refreshECCertBundle() *CertBundle { + return &CertBundle{ + Certificate: certECPem, + PrivateKey: privECKeyPem, + IssuingCA: issuingCaPem, + } +} + +const ( + privRSAKeyPem = `-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAt3ZJUaztCRiVg87P0y8T7QMNFQi61BCSIKepxXXWc7zi5JJS +MfQAstXJEqBYiShsSpYm6soiT6hX074t7wQAHGS3+u7qNogWpmTAUTUnNIM+QCxH +2Nc/kzYxaWajupVzgGvLeiqU3d4tIUk/ZkftvWJryr2hZc8zEN3C4pGS/2F+RQ+z +Ov+BpAI1BdbQGhF7m92vn6KS/iWsqmwHG9oChgvWeBHjWUI8qGauBc+it4S5RxfN +8JJIBXUIZtbaqFZzgjv8kUDyqoQGvkY/4Ce1K0bFJsM7wmMPv+5QscIBF4KWgN0k +TSMPPXfnn/QIAfhaYQkT9MjwGr6+B3SODNGCTQIDAQABAoIBAHtSwhprGbNhmS/P +F5ioLsbFpEedZKkksnXM/qxDd/K45/Qp/6KgmM+eMdmZe6pHR/QjVunBEqtlSBSH +5KykjcaIVbwSWdJqTH9xfm2YQ1BjYLcWjP1QQ+YbKb/mRO0phUiwLUlj0koKDWAw +srN4anFB9Z+FNTcQvwz5ZQWUQbH0neQtWO1nDvLsScgu1kchoEzJEJaFOQ1+HfGe +WxD766fZyqZQi5+cLrhOqHOGSlO+IFVe0hguiEHFr9LEPTXXkZtOR4wTf7j1Us8s +1KQ/jv01sx9S7HEbZJurzIjS23OywEUdJd1EsIE2lJV2QUwSiAsPYZOSQZlgOGzP +VRKVkGkCgYEA1u+pVP2r+xSxYy8KcdcRCdGGBh00VLx1yJRHWZ5YjF56hp0R0cG+ +xGLar5KCdBpr4jJnQGIrx8lw3SDCt4EXlxgJxitXlBtiKByM7/mYRRfURr9WMRr4 +88GQlWDbo2Xalnuac0qlkFqVIg0BaW+Z15A/E1L69aUxaR0ozlA9Jl8CgYEA2oNA +5F2otqzo9eNYucNAjihVhATd11DECQvbIQp/0bEJe0Znnzq/QIGIOVapC0VKGBwB +P5DuLL1P/nTPjjE/ZhjFuhMNM5PzC6obAjBh+gCpc+c+21Qerv7RKUTi2sGTzRHu +lpccRDfuF8bhzD6lAo50FpSmPE/ovZzb9+IsXtMCgYBVnUdM9HKh47846870Q5+k +0pHZM57ZtewQxoeZOgq5dxTFNCGZ9NvBLENBtlFCYBfjFQKt0azwutu7KUaGg+Ra +qheSmUccVsAFjEHTgQ9XTkOfHq39h2ns5ohqCBfVAUhNstR14iEK3BoVYyrRzcNw +6yNE1kPivzdsUFIlxC5nbwKBgDUUjT7sQX+eoTiZ8YOumo/t3Fglln4ncHeCGcj8 +8+/MQbFgeOuFKdBRpvXGx2mle0pAA02dtz3G/xeg6IpyDCSQ//cjiaFt3yyGNeli +N2qznnY5RluhI5L+83BC+5iITY8TPBH4wzUPIRdFiLREw3DLigeyNG+SOcdVw1mD +56NhAoGBALFh3sGkhvPiI/G/i/5tGZVA/dS/4DVXOoHW43+ZDHWEwqiN6vTf/VVi +cm+8kcfLY1E5fSf/4e7mIQq7o5qVn9Y3HWsajS1FFeznJjPj4Jaa1HvegNcycAzs +XOQ7xy23/8wUupgNeD1mFdSFCXQ3UedsJuVBHsElPc5W74q4F4+F +-----END RSA PRIVATE KEY-----` + + certRSAPem = `-----BEGIN CERTIFICATE----- +MIID+jCCAuSgAwIBAgIUcFCL9ESWTKLE6RqSYV7iZ78f1KcwCwYJKoZIhvcNAQEL +MBsxGTAXBgNVBAMMEFZhdWx0IFRlc3RpbmcgQ0EwHhcNMTUwNjE5MTcyMzA0WhcN +MTUwNzAzMTcyMzA0WjBPMRIwEAYDVQQDEwlsb2NhbGhvc3QxOTA3BgNVBAUTMDY0 +MTIwMzIxNzY3NTk2MjQyMjU0OTg5MTUxMzAyMjg1NzQ0NTc0OTkzMjY3NjI2MzCC +ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALd2SVGs7QkYlYPOz9MvE+0D +DRUIutQQkiCnqcV11nO84uSSUjH0ALLVyRKgWIkobEqWJurKIk+oV9O+Le8EABxk +t/ru6jaIFqZkwFE1JzSDPkAsR9jXP5M2MWlmo7qVc4Bry3oqlN3eLSFJP2ZH7b1i +a8q9oWXPMxDdwuKRkv9hfkUPszr/gaQCNQXW0BoRe5vdr5+ikv4lrKpsBxvaAoYL +1ngR41lCPKhmrgXPoreEuUcXzfCSSAV1CGbW2qhWc4I7/JFA8qqEBr5GP+AntStG +xSbDO8JjD7/uULHCAReCloDdJE0jDz1355/0CAH4WmEJE/TI8Bq+vgd0jgzRgk0C +AwEAAaOCAQQwggEAMA4GA1UdDwEB/wQEAwIAqDAdBgNVHSUEFjAUBggrBgEFBQcD +AQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUZHtkxSX5GVAYo3h8 +B8TGJ36vTH4wHwYDVR0jBBgwFoAU5JzeXhccaOWk5X6vhuGV7NwLMVkwTgYDVR0R +BEcwRYIJbG9jYWxob3N0gg9mb28uZXhhbXBsZS5jb22CD2Jhci5leGFtcGxlLmNv +bYcEgAMFBocQ/gEAAAAAAAAAAAAAAAAAATAxBgNVHR8EKjAoMCagJKAihiBodHRw +Oi8vbG9jYWxob3N0OjgyMDAvdjEvcGtpL2NybDALBgkqhkiG9w0BAQsDggEBAAps +W2ZDOAfwWufclmGPHt+YRXXSTWvPfF/cBeg5Oq/F8qUCVMHqdE/+EDWzh+Kz8jp0 +ggklnh76frROvHxygbVD2Hs9ACzgpnHPy8FYOdN+OblvAMtGlMyTq/5XheasmWdY +FFH/ft6tReG7BjGgfdyH8yL/R6b/RtU/qPlowfrZgAzOv7/ou6yRlfjIhsWbne/S +SQuGASRxRp3Txp7Cf3RcdCwVuiQhFLVeVHH+atTc8v2DO/CLfi9enQo96qUku8Bd +b5QPKIV0sQdtwGV5fo2JGd25rWpCo6TkAM9EeNkcVze8wgArSRk8zLkvM/5z+5sn +Qaka08px4wljGQ2Wc88= +-----END CERTIFICATE-----` + + privECKeyPem = `-----BEGIN EC PRIVATE KEY----- +MGgCAQEEHM3nuYLlrvawBN9hGVcu9mpaCEr7LMe44a7oQOygBwYFK4EEACGhPAM6 +AATBZ3VXwBE9oeSREpM5b25PW6WiuLb4EXWpKZyjj552QYKYe7QBuGe9wvvgOeCB +ovN3tSuGKzTiUA== +-----END EC PRIVATE KEY-----` + + certECPem = `-----BEGIN CERTIFICATE----- +MIIDJDCCAg6gAwIBAgIUM3J02tw0ZvpHUVHv6t8kcoft2/MwCwYJKoZIhvcNAQEL +MBsxGTAXBgNVBAMMEFZhdWx0IFRlc3RpbmcgQ0EwHhcNMTUwNjE5MTcyODQyWhcN +MTUwNzAzMTcyODQyWjBPMRIwEAYDVQQDEwlsb2NhbGhvc3QxOTA3BgNVBAUTMDI5 +MzcxMDk5Mzc2NDA3NDYyNjg3MTQzODcwMjc3Njg1OTkzMTkyMzkxNjM4MTE3MTBO +MBAGByqGSM49AgEGBSuBBAAhAzoABMFndVfAET2h5JESkzlvbk9bpaK4tvgRdakp +nKOPnnZBgph7tAG4Z73C++A54IGi83e1K4YrNOJQo4IBBDCCAQAwDgYDVR0PAQH/ +BAQDAgCoMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAMBgNVHRMBAf8E +AjAAMB0GA1UdDgQWBBQiFoWDvInznUGjdJPjBAyoxIkQITAfBgNVHSMEGDAWgBTk +nN5eFxxo5aTlfq+G4ZXs3AsxWTBOBgNVHREERzBFgglsb2NhbGhvc3SCD2Zvby5l +eGFtcGxlLmNvbYIPYmFyLmV4YW1wbGUuY29thwSAAwUGhxD+AQAAAAAAAAAAAAAA +AAABMDEGA1UdHwQqMCgwJqAkoCKGIGh0dHA6Ly9sb2NhbGhvc3Q6ODIwMC92MS9w +a2kvY3JsMAsGCSqGSIb3DQEBCwOCAQEA0RU18OdSdt2k4FKWyUS7EhVFOybiUHof +1n9EeBoxd7fEP/IuQnJGr3CPV5LRFdHRxkihf4N5bRjsst7cqczaIZZLWkAj+P/2 +JxBqv2Hm57dwaw2gtwt3GcYN/5j76fYaoZOgPMqas72vYgnBgdKQs8GYSoy7BVpC +x3nTYHwlOF+sM4wuVSi78lwkcgADF5GIWXrM3tYilmcT9fNbUgSvcVWdNTRJ0W+m +S2AF+4eby5PC9U8eIoCnZPRNmH0jZbNWzZyD0hDhBrDlaEbS2QXKRURPHzht/SqN +nWWcpQG3B8EI7p749dP5L+idi3ajHIH8vm/PK+o5TRrcHB585MlErQ== +-----END CERTIFICATE-----` + + issuingCaPem = `-----BEGIN CERTIFICATE----- +MIIDUTCCAjmgAwIBAgIJAKM+z4MSfw2mMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNV +BAMMEFZhdWx0IFRlc3RpbmcgQ0EwHhcNMTUwNjAxMjA1MTUzWhcNMjUwNTI5MjA1 +MTUzWjAbMRkwFwYDVQQDDBBWYXVsdCBUZXN0aW5nIENBMIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEA1eKB2nFbRqTFs7KyZjbzB5VRCBbnLZfEXVP1c3bH +e+YGjlfl34cy52dmancUzOf1/Jfo+VglocjTLVy5wHSGJwQYs8b6pEuuvAVo/6wU +L5Z7ZlQDR4kDe5Q+xgoRT6Bi/Bs57E+fNYgyUq/YAUY5WLuC+ZliCbJkLnb15Itu +P1yVUTDXTYORRE3qJS5RRol8D3QvteG9LyPEc7C+jsm5iBCagyxluzU0dnEOib5q +7xwZncoMbQz+rZH3QnwOij41FOGRPazrD5Mv6xLBkFnE5VAJ+GIgvd4bpOwvYMuo +fvF4PS7SFzxkGssMLlICap6PFpKz86DpAoDxPuoZeOhU4QIDAQABo4GXMIGUMB0G +A1UdDgQWBBTknN5eFxxo5aTlfq+G4ZXs3AsxWTAfBgNVHSMEGDAWgBTknN5eFxxo +5aTlfq+G4ZXs3AsxWTAxBgNVHR8EKjAoMCagJKAihiBodHRwOi8vbG9jYWxob3N0 +OjgyMDAvdjEvcGtpL2NybDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjANBgkqhkiG9w0BAQsFAAOCAQEAsINcA4PZm+OyldgNrwRVgxoSrhV1I9zszhc9 +VV340ZWlpTTxFKVb/K5Hg+jMF9tv70X1HwlYdlutE6KdrsA3gks5zanh4/3zlrYk +ABNBmSD6SSU2HKX1bFCBAAS3YHONE5o1K5tzwLsMl5uilNf+Wid3NjFnQ4KfuYI5 +loN/opnM6+a/O3Zua8RAuMMAv9wyqwn88aVuLvVzDNSMe5qC5kkuLGmRkNgY06rI +S/fXIHIOldeQxgYCqhdVmcDWJ1PtVaDfBsKVpRg1GRU8LUGw2E4AY+twd+J2FBfa +G/7g4koczXLoUM3OQXd5Aq2cs4SS1vODrYmgbioFsQ3eDHd1fg== +-----END CERTIFICATE-----` +) diff --git a/helper/certutil/types.go b/helper/certutil/types.go index f863ea7052..8ece20c2ba 100644 --- a/helper/certutil/types.go +++ b/helper/certutil/types.go @@ -14,6 +14,7 @@ import ( "crypto/x509" "encoding/pem" "fmt" + "strings" ) // Secret is used to attempt to unmarshal a Vault secret @@ -35,7 +36,8 @@ const ( RSAPrivateKey ECPrivateKey - TLSServer TLSUsage = 1 << iota + TLSUnknown TLSUsage = 0 + TLSServer TLSUsage = 1 << iota TLSClient ) @@ -159,17 +161,17 @@ func (p *ParsedCertBundle) ToCertBundle() (*CertBundle, error) { } if p.Certificate != nil { - result.SerialNumber = GetOctalFormatted(p.Certificate.SerialNumber.Bytes(), ":") + result.SerialNumber = strings.TrimSpace(GetOctalFormatted(p.Certificate.SerialNumber.Bytes(), ":")) } if p.CertificateBytes != nil && len(p.CertificateBytes) > 0 { block.Bytes = p.CertificateBytes - result.Certificate = string(pem.EncodeToMemory(&block)) + result.Certificate = strings.TrimSpace(string(pem.EncodeToMemory(&block))) } if p.IssuingCABytes != nil && len(p.IssuingCABytes) > 0 { block.Bytes = p.IssuingCABytes - result.IssuingCA = string(pem.EncodeToMemory(&block)) + result.IssuingCA = strings.TrimSpace(string(pem.EncodeToMemory(&block))) } if p.PrivateKeyBytes != nil && len(p.PrivateKeyBytes) > 0 { @@ -184,7 +186,7 @@ func (p *ParsedCertBundle) ToCertBundle() (*CertBundle, error) { default: return nil, InternalError{"Could not determine private key type when creating block"} } - result.PrivateKey = string(pem.EncodeToMemory(&block)) + result.PrivateKey = strings.TrimSpace(string(pem.EncodeToMemory(&block))) } return result, nil From fce09c296266325a50d483bc587e4dd1edb7d480 Mon Sep 17 00:00:00 2001 From: Steve Wills Date: Fri, 19 Jun 2015 16:59:24 -0400 Subject: [PATCH 14/15] allow building on FreeBSD Allow this file to build on FreeBSD --- helper/password/password_unix.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helper/password/password_unix.go b/helper/password/password_unix.go index c2bbe13222..f529ed3dfb 100644 --- a/helper/password/password_unix.go +++ b/helper/password/password_unix.go @@ -1,4 +1,4 @@ -// +build linux darwin +// +build linux darwin freebsd package password From cc52aaf5858f11d294f2ab0e682a6a4a32996f8c Mon Sep 17 00:00:00 2001 From: sergiopatino Date: Sun, 21 Jun 2015 02:41:26 -0700 Subject: [PATCH 15/15] Fix typo in link to Atlas URL. Missing a colon after https! --- website/source/community.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/source/community.html.erb b/website/source/community.html.erb index 95f1a56a42..57d64c39a0 100644 --- a/website/source/community.html.erb +++ b/website/source/community.html.erb @@ -74,7 +74,7 @@ employees actively contribute to Vault. Jack Pearkes is the creator of the online interactive demo of Vault. He maintains this demo as well as the design and interaction of the Vault website. Jack is an employee of HashiCorp and a primary engineer - behind Atlas. + behind Atlas. He is also a core committer to Packer, Consul, and