From da2ff436e90f99488d295cb4ff8d58075cb31138 Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Fri, 27 Oct 2017 13:34:02 -0400 Subject: [PATCH 1/2] Add PKCS8 marshaling to PKI --- builtin/logical/pki/cert_util.go | 66 +++++++++++++ builtin/logical/pki/fields.go | 11 +++ builtin/logical/pki/path_intermediate.go | 7 ++ builtin/logical/pki/path_issue_sign.go | 7 ++ builtin/logical/pki/path_root.go | 7 ++ helper/certutil/pkcs8.go | 119 +++++++++++++++++++++++ helper/certutil/pkcs8_test.go | 110 +++++++++++++++++++++ 7 files changed, 327 insertions(+) create mode 100644 helper/certutil/pkcs8.go create mode 100644 helper/certutil/pkcs8_test.go diff --git a/builtin/logical/pki/cert_util.go b/builtin/logical/pki/cert_util.go index b4bb38173b7b..9356cdde8df6 100644 --- a/builtin/logical/pki/cert_util.go +++ b/builtin/logical/pki/cert_util.go @@ -2,6 +2,7 @@ package pki import ( "bytes" + "crypto" "crypto/ecdsa" "crypto/rand" "crypto/rsa" @@ -9,6 +10,7 @@ import ( "crypto/x509" "crypto/x509/pkix" "encoding/asn1" + "encoding/base64" "encoding/pem" "fmt" "net" @@ -16,6 +18,7 @@ import ( "strings" "time" + "github.com/hashicorp/errwrap" "github.com/hashicorp/vault/helper/certutil" "github.com/hashicorp/vault/helper/errutil" "github.com/hashicorp/vault/helper/parseutil" @@ -1183,3 +1186,66 @@ NameCheck: return fmt.Errorf("name %q disallowed by CA's permitted DNS domains", badName) } + +func convertRespToPKCS8(resp *logical.Response) error { + privRaw, ok := resp.Data["private_key"] + if !ok { + return nil + } + priv, ok := privRaw.(string) + if !ok { + return fmt.Errorf("error converting response to pkcs8: could not parse original value as string") + } + + privKeyTypeRaw, ok := resp.Data["private_key_type"] + if !ok { + return fmt.Errorf("error converting response to pkcs8: %q not found in response", "private_key_type") + } + privKeyType, ok := privKeyTypeRaw.(certutil.PrivateKeyType) + if !ok { + return fmt.Errorf("error converting response to pkcs8: could not parse original type value as string") + } + + var keyData []byte + var pemUsed bool + var err error + var signer crypto.Signer + + block, _ := pem.Decode([]byte(priv)) + if block == nil { + keyData, err = base64.StdEncoding.DecodeString(priv) + if err != nil { + return errwrap.Wrapf("error converting response to pkcs8: error decoding original value: {{err}}", err) + } + } else { + keyData = block.Bytes + pemUsed = true + } + + switch privKeyType { + case certutil.RSAPrivateKey: + signer, err = x509.ParsePKCS1PrivateKey(keyData) + case certutil.ECPrivateKey: + signer, err = x509.ParseECPrivateKey(keyData) + default: + return fmt.Errorf("unknown private key type %q", privKeyType) + } + if err != nil { + return errwrap.Wrapf("error converting response to pkcs8: error parsing previous key: {{err}}", err) + } + + keyData, err = certutil.MarshalPKCS8PrivateKey(signer) + if err != nil { + return errwrap.Wrapf("error converting response to pkcs8: error marshaling pkcs8 key: {{err}}", err) + } + + if pemUsed { + block.Type = "PRIVATE KEY" + block.Bytes = keyData + resp.Data["private_key"] = string(pem.EncodeToMemory(block)) + } else { + resp.Data["private_key"] = base64.StdEncoding.EncodeToString(keyData) + } + + return nil +} diff --git a/builtin/logical/pki/fields.go b/builtin/logical/pki/fields.go index 52adf10eb79e..9aa1d4b556db 100644 --- a/builtin/logical/pki/fields.go +++ b/builtin/logical/pki/fields.go @@ -22,6 +22,17 @@ key and issuing cert will be appended to the certificate pem. Defaults to "pem".`, } + fields["private_key_format"] = &framework.FieldSchema{ + Type: framework.TypeString, + Default: "der", + Description: `Format for the returned private key. +Generally the default will be controlled by the "format" +parameter as either base64-encoded DER or PEM-encoded DER. +However, this can be set to "pkcs8" to have the returned +private key contain base64-encoded pkcs8 or PEM-encoded +pkcs8 instead. Defaults to "der".`, + } + fields["ip_sans"] = &framework.FieldSchema{ Type: framework.TypeString, Description: `The requested IP SANs, if any, in a diff --git a/builtin/logical/pki/path_intermediate.go b/builtin/logical/pki/path_intermediate.go index 2073621d3cbb..f98939379621 100644 --- a/builtin/logical/pki/path_intermediate.go +++ b/builtin/logical/pki/path_intermediate.go @@ -106,6 +106,13 @@ func (b *backend) pathGenerateIntermediate( } } + if data.Get("private_key_format").(string) == "pkcs8" { + err = convertRespToPKCS8(resp) + if err != nil { + return nil, err + } + } + cb := &certutil.CertBundle{} cb.PrivateKey = csrb.PrivateKey cb.PrivateKeyType = csrb.PrivateKeyType diff --git a/builtin/logical/pki/path_issue_sign.go b/builtin/logical/pki/path_issue_sign.go index a297c6ab9a7d..d6e9b7368c21 100644 --- a/builtin/logical/pki/path_issue_sign.go +++ b/builtin/logical/pki/path_issue_sign.go @@ -268,6 +268,13 @@ func (b *backend) pathIssueSignCert( resp.Secret.TTL = parsedBundle.Certificate.NotAfter.Sub(time.Now()) } + if data.Get("private_key_format").(string) == "pkcs8" { + err = convertRespToPKCS8(resp) + if err != nil { + return nil, err + } + } + if !role.NoStore { err = req.Storage.Put(&logical.StorageEntry{ Key: "certs/" + normalizeSerial(cb.SerialNumber), diff --git a/builtin/logical/pki/path_root.go b/builtin/logical/pki/path_root.go index 3f18eecae2cd..0c4c1e74345a 100644 --- a/builtin/logical/pki/path_root.go +++ b/builtin/logical/pki/path_root.go @@ -188,6 +188,13 @@ func (b *backend) pathCAGenerateRoot( } } + if data.Get("private_key_format").(string) == "pkcs8" { + err = convertRespToPKCS8(resp) + if err != nil { + return nil, err + } + } + // Store it as the CA bundle entry, err = logical.StorageEntryJSON("config/ca_bundle", cb) if err != nil { diff --git a/helper/certutil/pkcs8.go b/helper/certutil/pkcs8.go new file mode 100644 index 000000000000..22585de0ccb4 --- /dev/null +++ b/helper/certutil/pkcs8.go @@ -0,0 +1,119 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package certutil + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" + "errors" + "fmt" +) + +var ( + oidNamedCurveP224 = asn1.ObjectIdentifier{1, 3, 132, 0, 33} + oidNamedCurveP256 = asn1.ObjectIdentifier{1, 2, 840, 10045, 3, 1, 7} + oidNamedCurveP384 = asn1.ObjectIdentifier{1, 3, 132, 0, 34} + oidNamedCurveP521 = asn1.ObjectIdentifier{1, 3, 132, 0, 35} + + oidPublicKeyRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 1} + oidPublicKeyDSA = asn1.ObjectIdentifier{1, 2, 840, 10040, 4, 1} + oidPublicKeyECDSA = asn1.ObjectIdentifier{1, 2, 840, 10045, 2, 1} +) + +// pkcs8 reflects an ASN.1, PKCS#8 PrivateKey. See +// ftp://ftp.rsasecurity.com/pub/pkcs/pkcs-8/pkcs-8v1_2.asn +// and RFC 5208. +type pkcs8 struct { + Version int + Algo pkix.AlgorithmIdentifier + PrivateKey []byte + // optional attributes omitted. +} + +type ecPrivateKey struct { + Version int + PrivateKey []byte + NamedCurveOID asn1.ObjectIdentifier `asn1:"optional,explicit,tag:0"` + PublicKey asn1.BitString `asn1:"optional,explicit,tag:1"` +} + +// MarshalPKCS8PrivateKey converts a private key to PKCS#8 encoded form. +// The following key types are supported: *rsa.PrivateKey, *ecdsa.PublicKey. +// Unsupported key types result in an error. +// +// See RFC 5208. +func MarshalPKCS8PrivateKey(key interface{}) ([]byte, error) { + var privKey pkcs8 + + switch k := key.(type) { + case *rsa.PrivateKey: + privKey.Algo = pkix.AlgorithmIdentifier{ + Algorithm: oidPublicKeyRSA, + Parameters: asn1.NullRawValue, + } + privKey.PrivateKey = x509.MarshalPKCS1PrivateKey(k) + + case *ecdsa.PrivateKey: + oid, ok := oidFromNamedCurve(k.Curve) + if !ok { + return nil, errors.New("x509: unknown curve while marshalling to PKCS#8") + } + + oidBytes, err := asn1.Marshal(oid) + if err != nil { + return nil, errors.New("x509: failed to marshal curve OID: " + err.Error()) + } + + privKey.Algo = pkix.AlgorithmIdentifier{ + Algorithm: oidPublicKeyECDSA, + Parameters: asn1.RawValue{ + FullBytes: oidBytes, + }, + } + + if privKey.PrivateKey, err = marshalECPrivateKeyWithOID(k, nil); err != nil { + return nil, errors.New("x509: failed to marshal EC private key while building PKCS#8: " + err.Error()) + } + + default: + return nil, fmt.Errorf("x509: unknown key type while marshalling PKCS#8: %T", key) + } + + return asn1.Marshal(privKey) +} + +func oidFromNamedCurve(curve elliptic.Curve) (asn1.ObjectIdentifier, bool) { + switch curve { + case elliptic.P224(): + return oidNamedCurveP224, true + case elliptic.P256(): + return oidNamedCurveP256, true + case elliptic.P384(): + return oidNamedCurveP384, true + case elliptic.P521(): + return oidNamedCurveP521, true + } + + return nil, false +} + +// marshalECPrivateKey marshals an EC private key into ASN.1, DER format and +// sets the curve ID to the given OID, or omits it if OID is nil. +func marshalECPrivateKeyWithOID(key *ecdsa.PrivateKey, oid asn1.ObjectIdentifier) ([]byte, error) { + privateKeyBytes := key.D.Bytes() + paddedPrivateKey := make([]byte, (key.Curve.Params().N.BitLen()+7)/8) + copy(paddedPrivateKey[len(paddedPrivateKey)-len(privateKeyBytes):], privateKeyBytes) + + return asn1.Marshal(ecPrivateKey{ + Version: 1, + PrivateKey: paddedPrivateKey, + NamedCurveOID: oid, + PublicKey: asn1.BitString{Bytes: elliptic.Marshal(key.Curve, key.X, key.Y)}, + }) +} diff --git a/helper/certutil/pkcs8_test.go b/helper/certutil/pkcs8_test.go new file mode 100644 index 000000000000..df350ead4137 --- /dev/null +++ b/helper/certutil/pkcs8_test.go @@ -0,0 +1,110 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package certutil + +import ( + "bytes" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rsa" + "crypto/x509" + "encoding/hex" + "reflect" + "testing" +) + +// Generated using: +// openssl genrsa 1024 | openssl pkcs8 -topk8 -nocrypt +var pkcs8RSAPrivateKeyHex = `30820278020100300d06092a864886f70d0101010500048202623082025e02010002818100cfb1b5bf9685ffa97b4f99df4ff122b70e59ac9b992f3bc2b3dde17d53c1a34928719b02e8fd17839499bfbd515bd6ef99c7a1c47a239718fe36bfd824c0d96060084b5f67f0273443007a24dfaf5634f7772c9346e10eb294c2306671a5a5e719ae24b4de467291bc571014b0e02dec04534d66a9bb171d644b66b091780e8d020301000102818100b595778383c4afdbab95d2bfed12b3f93bb0a73a7ad952f44d7185fd9ec6c34de8f03a48770f2009c8580bcd275e9632714e9a5e3f32f29dc55474b2329ff0ebc08b3ffcb35bc96e6516b483df80a4a59cceb71918cbabf91564e64a39d7e35dce21cb3031824fdbc845dba6458852ec16af5dddf51a8397a8797ae0337b1439024100ea0eb1b914158c70db39031dd8904d6f18f408c85fbbc592d7d20dee7986969efbda081fdf8bc40e1b1336d6b638110c836bfdc3f314560d2e49cd4fbde1e20b024100e32a4e793b574c9c4a94c8803db5152141e72d03de64e54ef2c8ed104988ca780cd11397bc359630d01b97ebd87067c5451ba777cf045ca23f5912f1031308c702406dfcdbbd5a57c9f85abc4edf9e9e29153507b07ce0a7ef6f52e60dcfebe1b8341babd8b789a837485da6c8d55b29bbb142ace3c24a1f5b54b454d01b51e2ad03024100bd6a2b60dee01e1b3bfcef6a2f09ed027c273cdbbaf6ba55a80f6dcc64e4509ee560f84b4f3e076bd03b11e42fe71a3fdd2dffe7e0902c8584f8cad877cdc945024100aa512fa4ada69881f1d8bb8ad6614f192b83200aef5edf4811313d5ef30a86cbd0a90f7b025c71ea06ec6b34db6306c86b1040670fd8654ad7291d066d06d031` + +// Generated using: +// openssl ecparam -genkey -name secp224r1 | openssl pkcs8 -topk8 -nocrypt +var pkcs8P224PrivateKeyHex = `3078020100301006072a8648ce3d020106052b810400210461305f020101041cca3d72b3e88fed2684576dad9b80a9180363a5424986900e3abcab3fa13c033a0004f8f2a6372872a4e61263ed893afb919576a4cacfecd6c081a2cbc76873cf4ba8530703c6042b3a00e2205087e87d2435d2e339e25702fae1` + +// Generated using: +// openssl ecparam -genkey -name secp256r1 | openssl pkcs8 -topk8 -nocrypt +var pkcs8P256PrivateKeyHex = `308187020100301306072a8648ce3d020106082a8648ce3d030107046d306b0201010420dad6b2f49ca774c36d8ae9517e935226f667c929498f0343d2424d0b9b591b43a14403420004b9c9b90095476afe7b860d8bd43568cab7bcb2eed7b8bf2fa0ce1762dd20b04193f859d2d782b1e4cbfd48492f1f533113a6804903f292258513837f07fda735` + +// Generated using: +// openssl ecparam -genkey -name secp384r1 | openssl pkcs8 -topk8 -nocrypt +var pkcs8P384PrivateKeyHex = `3081b6020100301006072a8648ce3d020106052b8104002204819e30819b02010104309bf832f6aaaeacb78ce47ffb15e6fd0fd48683ae79df6eca39bfb8e33829ac94aa29d08911568684c2264a08a4ceb679a164036200049070ad4ed993c7770d700e9f6dc2baa83f63dd165b5507f98e8ff29b5d2e78ccbe05c8ddc955dbf0f7497e8222cfa49314fe4e269459f8e880147f70d785e530f2939e4bf9f838325bb1a80ad4cf59272ae0e5efe9a9dc33d874492596304bd3` + +// Generated using: +// openssl ecparam -genkey -name secp521r1 | openssl pkcs8 -topk8 -nocrypt +// +// Note that OpenSSL will truncate the private key if it can (i.e. it emits it +// like an integer, even though it's an OCTET STRING field). Thus if you +// regenerate this you may, randomly, find that it's a byte shorter than +// expected and the Go test will fail to recreate it exactly. +var pkcs8P521PrivateKeyHex = `3081ee020100301006072a8648ce3d020106052b810400230481d63081d3020101044200cfe0b87113a205cf291bb9a8cd1a74ac6c7b2ebb8199aaa9a5010d8b8012276fa3c22ac913369fa61beec2a3b8b4516bc049bde4fb3b745ac11b56ab23ac52e361a1818903818600040138f75acdd03fbafa4f047a8e4b272ba9d555c667962b76f6f232911a5786a0964e5edea6bd21a6f8725720958de049c6e3e6661c1c91b227cebee916c0319ed6ca003db0a3206d372229baf9dd25d868bf81140a518114803ce40c1855074d68c4e9dab9e65efba7064c703b400f1767f217dac82715ac1f6d88c74baf47a7971de4ea` + +func TestPKCS8(t *testing.T) { + tests := []struct { + name string + keyHex string + keyType reflect.Type + curve elliptic.Curve + }{ + { + name: "RSA private key", + keyHex: pkcs8RSAPrivateKeyHex, + keyType: reflect.TypeOf(&rsa.PrivateKey{}), + }, + { + name: "P-224 private key", + keyHex: pkcs8P224PrivateKeyHex, + keyType: reflect.TypeOf(&ecdsa.PrivateKey{}), + curve: elliptic.P224(), + }, + { + name: "P-256 private key", + keyHex: pkcs8P256PrivateKeyHex, + keyType: reflect.TypeOf(&ecdsa.PrivateKey{}), + curve: elliptic.P256(), + }, + { + name: "P-384 private key", + keyHex: pkcs8P384PrivateKeyHex, + keyType: reflect.TypeOf(&ecdsa.PrivateKey{}), + curve: elliptic.P384(), + }, + { + name: "P-521 private key", + keyHex: pkcs8P521PrivateKeyHex, + keyType: reflect.TypeOf(&ecdsa.PrivateKey{}), + curve: elliptic.P521(), + }, + } + + for _, test := range tests { + derBytes, err := hex.DecodeString(test.keyHex) + if err != nil { + t.Errorf("%s: failed to decode hex: %s", test.name, err) + continue + } + privKey, err := x509.ParsePKCS8PrivateKey(derBytes) + if err != nil { + t.Errorf("%s: failed to decode PKCS#8: %s", test.name, err) + continue + } + if reflect.TypeOf(privKey) != test.keyType { + t.Errorf("%s: decoded PKCS#8 returned unexpected key type: %T", test.name, privKey) + continue + } + if ecKey, isEC := privKey.(*ecdsa.PrivateKey); isEC && ecKey.Curve != test.curve { + t.Errorf("%s: decoded PKCS#8 returned unexpected curve %#v", test.name, ecKey.Curve) + continue + } + reserialised, err := MarshalPKCS8PrivateKey(privKey) + if err != nil { + t.Errorf("%s: failed to marshal into PKCS#8: %s", test.name, err) + continue + } + if !bytes.Equal(derBytes, reserialised) { + t.Errorf("%s: marshalled PKCS#8 didn't match original: got %x, want %x", test.name, reserialised, derBytes) + continue + } + } +} From 618a7703f0086317f6df3468480978f476664678 Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Thu, 2 Nov 2017 11:56:57 -0400 Subject: [PATCH 2/2] Add format value to docs --- website/source/api/secret/pki/index.html.md | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/website/source/api/secret/pki/index.html.md b/website/source/api/secret/pki/index.html.md index 58fbf83c15d8..87f37c787858 100644 --- a/website/source/api/secret/pki/index.html.md +++ b/website/source/api/secret/pki/index.html.md @@ -296,7 +296,7 @@ $ curl \ "data": { "issuing_certificates": ["", ""], "crl_distribution_points": ["", ""], - "ocsp_servers": ["", ""], + "ocsp_servers": ["", ""] }, "auth": null } @@ -437,6 +437,11 @@ can be set in a CSR are supported. base64 encoded. If `pem_bundle`, the `csr` field will contain the private key (if exported) and CSR, concatenated. +- `private_key_format` `(string: "")` – Specifies the format for marshaling the + private key. Defaults to `der` which will return either base64-encoded DER or + PEM-encoded DER, depending on the value of `format`. The other option is + `pkcs8` which will return the key marshalled as PEM-encoded PKCS8. + - `key_type` `(string: "rsa")` – Specifies the desired key type; must be `rsa` or `ec`. @@ -556,6 +561,11 @@ need to request a new certificate.** private key and certificate, concatenated; if the issuing CA is not a Vault-derived self-signed root, this will be included as well. +- `private_key_format` `(string: "")` – Specifies the format for marshaling the + private key. Defaults to `der` which will return either base64-encoded DER or + PEM-encoded DER, depending on the value of `format`. The other option is + `pkcs8` which will return the key marshalled as PEM-encoded PKCS8. + - `exclude_cn_from_sans` `(bool: false)` – If true, the given `common_name` will not be included in DNS or Email Subject Alternate Names (as appropriate). Useful if the CN is not a hostname or email address, but is instead some @@ -932,6 +942,11 @@ Vault would overwrite the existing cert/key with new values. exported) and certificate, concatenated; if the issuing CA is not a Vault-derived self-signed root, this will be included as well. +- `private_key_format` `(string: "")` – Specifies the format for marshaling the + private key. Defaults to `der` which will return either base64-encoded DER or + PEM-encoded DER, depending on the value of `format`. The other option is + `pkcs8` which will return the key marshalled as PEM-encoded PKCS8. + - `key_type` `(string: "rsa")` – Specifies the desired key type; must be `rsa` or `ec`.