Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add PKCS8 marshaling to PKI #3518

Merged
merged 4 commits into from
Nov 6, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions builtin/logical/pki/cert_util.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,23 @@ package pki

import (
"bytes"
"crypto"
"crypto/ecdsa"
"crypto/rand"
"crypto/rsa"
"crypto/sha1"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/base64"
"encoding/pem"
"fmt"
"net"
"regexp"
"strings"
"time"

"github.com/hashicorp/errwrap"
"github.com/hashicorp/vault/helper/certutil"
"github.com/hashicorp/vault/helper/errutil"
"github.com/hashicorp/vault/helper/parseutil"
Expand Down Expand Up @@ -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")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we not use %q here since its not taking any variable as input? Or was there a reason for doing it this way?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did it this way instead of \". Just felt nicer than escapes.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I see. 👍

}
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
}
11 changes: 11 additions & 0 deletions builtin/logical/pki/fields.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,17 @@ key and issuing cert will be appended to the
certificate pem. Defaults to "pem".`,
}

fields["private_key_format"] = &framework.FieldSchema{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add this param to the docs page as well.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

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
Expand Down
7 changes: 7 additions & 0 deletions builtin/logical/pki/path_intermediate.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,13 @@ func (b *backend) pathGenerateIntermediate(
}
}

if data.Get("private_key_format").(string) == "pkcs8" {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fine as-is, but noticed that the other params were being retrieved with b.getGenerationParams(data), so this could also be moved there as well

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went down that path originally but there lay madness. The problem is that it's not called by all functions that can generate certs and what is then done with the generation params depends on the path.

After going down that path and then one other, I realized the easiest way by far was to just transform the output once it's already generated.

err = convertRespToPKCS8(resp)
if err != nil {
return nil, err
}
}

cb := &certutil.CertBundle{}
cb.PrivateKey = csrb.PrivateKey
cb.PrivateKeyType = csrb.PrivateKeyType
Expand Down
7 changes: 7 additions & 0 deletions builtin/logical/pki/path_issue_sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
7 changes: 7 additions & 0 deletions builtin/logical/pki/path_root.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
119 changes: 119 additions & 0 deletions helper/certutil/pkcs8.go
Original file line number Diff line number Diff line change
@@ -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)},
})
}
110 changes: 110 additions & 0 deletions helper/certutil/pkcs8_test.go
Original file line number Diff line number Diff line change
@@ -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
}
}
}
Loading