Add parsing for NSS-wrapped Ed25519 keys (#15742)
* Add parsing for NSS-wrapped Ed25519 keys NSS wraps Ed25519 using the PKCS#8 standard structure. The Go standard library as of Go 1.18.x doesn't support parsing this key type with the OID used by NSS; it requires the 1.3.101.112/RFC 8410 format, rather than the RFC 5915-esque structure supported here. Co-authored-by: Rachel Culpepper <84159930+rculpepper@users.noreply.github.com> Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Add integration test with NSS-created wrapped key Co-authored-by: Rachel Culpepper <84159930+rculpepper@users.noreply.github.com> Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Add changelog Co-authored-by: Rachel Culpepper <84159930+rculpepper@users.noreply.github.com> Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> Co-authored-by: Rachel Culpepper <84159930+rculpepper@users.noreply.github.com>
This commit is contained in:
parent
6490a9c1f7
commit
ea6452757f
|
@ -45,6 +45,8 @@ var (
|
||||||
keys = map[string]interface{}{}
|
keys = map[string]interface{}{}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const nssFormattedEd25519Key = "MGcCAQAwFAYHKoZIzj0CAQYJKwYBBAHaRw8BBEwwSgIBAQQgfJm5R+LK4FMwGzOpemTBXksimEVOVCE8QeC+XBBfNU+hIwMhADaif7IhYx46IHcRTy1z8LeyhABep+UB8Da6olMZGx0i"
|
||||||
|
|
||||||
func generateKeys(t *testing.T) {
|
func generateKeys(t *testing.T) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
|
@ -78,6 +80,39 @@ func getKey(t *testing.T, keyType string) interface{} {
|
||||||
return key
|
return key
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTransit_ImportNSSEd25519Key(t *testing.T) {
|
||||||
|
generateKeys(t)
|
||||||
|
b, s := createBackendWithStorage(t)
|
||||||
|
|
||||||
|
wrappingKey, err := b.getWrappingKey(context.Background(), s)
|
||||||
|
if err != nil || wrappingKey == nil {
|
||||||
|
t.Fatalf("failed to retrieve public wrapping key: %s", err)
|
||||||
|
}
|
||||||
|
privWrappingKey := wrappingKey.Keys[strconv.Itoa(wrappingKey.LatestVersion)].RSAKey
|
||||||
|
pubWrappingKey := &privWrappingKey.PublicKey
|
||||||
|
|
||||||
|
rawPKCS8, err := base64.StdEncoding.DecodeString(nssFormattedEd25519Key)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to parse nss base64: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
blob := wrapTargetPKCS8ForImport(t, pubWrappingKey, rawPKCS8, "SHA256")
|
||||||
|
req := &logical.Request{
|
||||||
|
Storage: s,
|
||||||
|
Operation: logical.UpdateOperation,
|
||||||
|
Path: "keys/nss-ed25519/import",
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"ciphertext": blob,
|
||||||
|
"type": "ed25519",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = b.HandleRequest(context.Background(), req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to import NSS-formatted Ed25519 key: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestTransit_Import(t *testing.T) {
|
func TestTransit_Import(t *testing.T) {
|
||||||
generateKeys(t)
|
generateKeys(t)
|
||||||
b, s := createBackendWithStorage(t)
|
b, s := createBackendWithStorage(t)
|
||||||
|
@ -503,6 +538,29 @@ func TestTransit_ImportVersion(t *testing.T) {
|
||||||
func wrapTargetKeyForImport(t *testing.T, wrappingKey *rsa.PublicKey, targetKey interface{}, targetKeyType string, hashFnName string) string {
|
func wrapTargetKeyForImport(t *testing.T, wrappingKey *rsa.PublicKey, targetKey interface{}, targetKeyType string, hashFnName string) string {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
|
// Format target key for wrapping
|
||||||
|
var preppedTargetKey []byte
|
||||||
|
var ok bool
|
||||||
|
var err error
|
||||||
|
switch targetKeyType {
|
||||||
|
case "aes128-gcm96", "aes256-gcm96", "chacha20-poly1305":
|
||||||
|
preppedTargetKey, ok = targetKey.([]byte)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("failed to wrap target key for import: symmetric key not provided in byte format")
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
preppedTargetKey, err = x509.MarshalPKCS8PrivateKey(targetKey)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to wrap target key for import: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return wrapTargetPKCS8ForImport(t, wrappingKey, preppedTargetKey, hashFnName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func wrapTargetPKCS8ForImport(t *testing.T, wrappingKey *rsa.PublicKey, preppedTargetKey []byte, hashFnName string) string {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
// Generate an ephemeral AES-256 key
|
// Generate an ephemeral AES-256 key
|
||||||
ephKey, err := uuid.GenerateRandomBytes(32)
|
ephKey, err := uuid.GenerateRandomBytes(32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -527,22 +585,6 @@ func wrapTargetKeyForImport(t *testing.T, wrappingKey *rsa.PublicKey, targetKey
|
||||||
t.Fatalf("failed to wrap target key for import: %s", err)
|
t.Fatalf("failed to wrap target key for import: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format target key for wrapping
|
|
||||||
var preppedTargetKey []byte
|
|
||||||
var ok bool
|
|
||||||
switch targetKeyType {
|
|
||||||
case "aes128-gcm96", "aes256-gcm96", "chacha20-poly1305":
|
|
||||||
preppedTargetKey, ok = targetKey.([]byte)
|
|
||||||
if !ok {
|
|
||||||
t.Fatal("failed to wrap target key for import: symmetric key not provided in byte format")
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
preppedTargetKey, err = x509.MarshalPKCS8PrivateKey(targetKey)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to wrap target key for import: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wrap target key with KWP
|
// Wrap target key with KWP
|
||||||
targetKeyWrapped, err := kwp.Wrap(preppedTargetKey)
|
targetKeyWrapped, err := kwp.Wrap(preppedTargetKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:improvement
|
||||||
|
secret/transit: Allow importing Ed25519 keys from PKCS#8 with inner RFC 5915 ECPrivateKey blobs (NSS-wrapped keys).
|
||||||
|
```
|
|
@ -1385,8 +1385,18 @@ func (p *Policy) Import(ctx context.Context, storage logical.Storage, key []byte
|
||||||
} else {
|
} else {
|
||||||
parsedPrivateKey, err := x509.ParsePKCS8PrivateKey(key)
|
parsedPrivateKey, err := x509.ParsePKCS8PrivateKey(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), "unknown elliptic curve") {
|
||||||
|
var edErr error
|
||||||
|
parsedPrivateKey, edErr = ParsePKCS8Ed25519PrivateKey(key)
|
||||||
|
if edErr != nil {
|
||||||
|
return fmt.Errorf("error parsing asymmetric key:\n - assuming contents are an ed25519 private key: %s\n - original error: %v", edErr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parsing as Ed25519-in-PKCS8-ECPrivateKey succeeded!
|
||||||
|
} else {
|
||||||
return fmt.Errorf("error parsing asymmetric key: %s", err)
|
return fmt.Errorf("error parsing asymmetric key: %s", err)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
switch parsedPrivateKey.(type) {
|
switch parsedPrivateKey.(type) {
|
||||||
case *ecdsa.PrivateKey:
|
case *ecdsa.PrivateKey:
|
||||||
|
|
|
@ -0,0 +1,115 @@
|
||||||
|
package keysutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/x509/pkix"
|
||||||
|
"encoding/asn1"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/ed25519"
|
||||||
|
)
|
||||||
|
|
||||||
|
// pkcs8 reflects an ASN.1, PKCS #8 PrivateKey. See
|
||||||
|
// ftp://ftp.rsasecurity.com/pub/pkcs/pkcs-8/pkcs-8v1_2.asn
|
||||||
|
// and RFC 5208.
|
||||||
|
//
|
||||||
|
// Copied from Go: https://github.com/golang/go/blob/master/src/crypto/x509/pkcs8.go#L17-L80
|
||||||
|
type pkcs8 struct {
|
||||||
|
Version int
|
||||||
|
Algo pkix.AlgorithmIdentifier
|
||||||
|
PrivateKey []byte
|
||||||
|
// optional attributes omitted.
|
||||||
|
}
|
||||||
|
|
||||||
|
// ecPrivateKey reflects an ASN.1 Elliptic Curve Private Key Structure.
|
||||||
|
// References:
|
||||||
|
//
|
||||||
|
// RFC 5915
|
||||||
|
// SEC1 - http://www.secg.org/sec1-v2.pdf
|
||||||
|
//
|
||||||
|
// Per RFC 5915 the NamedCurveOID is marked as ASN.1 OPTIONAL, however in
|
||||||
|
// most cases it is not.
|
||||||
|
//
|
||||||
|
// Copied from Go: https://github.com/golang/go/blob/master/src/crypto/x509/sec1.go#L18-L31
|
||||||
|
type ecPrivateKey struct {
|
||||||
|
Version int
|
||||||
|
PrivateKey []byte
|
||||||
|
NamedCurveOID asn1.ObjectIdentifier `asn1:"optional,explicit,tag:0"`
|
||||||
|
|
||||||
|
// Because the PKCS8/RFC 5915 encoding of the Ed25519 key uses the
|
||||||
|
// RFC 8032 Ed25519 seed format, we can ignore the public key parameter
|
||||||
|
// and infer it later.
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
// See crypto/x509/x509.go in the Go toolchain source distribution.
|
||||||
|
oidPublicKeyECDSA = asn1.ObjectIdentifier{1, 2, 840, 10045, 2, 1}
|
||||||
|
|
||||||
|
// NSS encodes Ed25519 private keys with the OID 1.3.6.1.4.1.11591.15.1
|
||||||
|
// from https://tools.ietf.org/html/draft-josefsson-pkix-newcurves-01.
|
||||||
|
// See https://github.com/nss-dev/nss/blob/NSS_3_79_BRANCH/lib/util/secoid.c#L600-L603.
|
||||||
|
oidNSSPKIXEd25519 = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 11591, 15, 1}
|
||||||
|
|
||||||
|
// Other implementations may use the OID 1.3.101.110 from
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc8410.
|
||||||
|
oidRFC8410Ed25519 = asn1.ObjectIdentifier{1, 3, 101, 110}
|
||||||
|
)
|
||||||
|
|
||||||
|
func isEd25519OID(oid asn1.ObjectIdentifier) bool {
|
||||||
|
return oidNSSPKIXEd25519.Equal(oid) || oidRFC8410Ed25519.Equal(oid)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParsePKCS8PrivateKey parses an unencrypted private key in PKCS #8, ASN.1 DER form.
|
||||||
|
//
|
||||||
|
// It returns a *rsa.PrivateKey, a *ecdsa.PrivateKey, or a ed25519.PrivateKey.
|
||||||
|
// More types might be supported in the future.
|
||||||
|
//
|
||||||
|
// This kind of key is commonly encoded in PEM blocks of type "PRIVATE KEY".
|
||||||
|
func ParsePKCS8Ed25519PrivateKey(der []byte) (key interface{}, err error) {
|
||||||
|
var privKey pkcs8
|
||||||
|
var ed25519Key ecPrivateKey
|
||||||
|
|
||||||
|
var checkedOID bool
|
||||||
|
|
||||||
|
// If this err is nil, we assume we directly have a ECPrivateKey structure
|
||||||
|
// with explicit OID; ignore this error for now and return the latter err
|
||||||
|
// instead if neither parse correctly.
|
||||||
|
if _, err := asn1.Unmarshal(der, &privKey); err == nil {
|
||||||
|
switch {
|
||||||
|
case privKey.Algo.Algorithm.Equal(oidPublicKeyECDSA):
|
||||||
|
bytes := privKey.Algo.Parameters.FullBytes
|
||||||
|
namedCurveOID := new(asn1.ObjectIdentifier)
|
||||||
|
if _, err := asn1.Unmarshal(bytes, namedCurveOID); err != nil {
|
||||||
|
namedCurveOID = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if namedCurveOID == nil || !isEd25519OID(*namedCurveOID) {
|
||||||
|
return nil, errors.New("keysutil: failed to parse private key (invalid, non-ed25519 curve parameter OID)")
|
||||||
|
}
|
||||||
|
|
||||||
|
der = privKey.PrivateKey
|
||||||
|
checkedOID = true
|
||||||
|
default:
|
||||||
|
// The Go standard library already parses RFC 8410 keys; the
|
||||||
|
// inclusion of the OID here is in case it is used with the
|
||||||
|
// regular ECDSA PrivateKey structure, rather than the struct
|
||||||
|
// recognized by the Go standard library.
|
||||||
|
return nil, errors.New("keysutil: failed to parse key as ed25519 private key")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = asn1.Unmarshal(der, &ed25519Key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("keysutil: failed to parse private key (inner Ed25519 ECPrivateKey format was incorrect): %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !checkedOID && !isEd25519OID(ed25519Key.NamedCurveOID) {
|
||||||
|
return nil, errors.New("keysutil: failed to parse private key (invalid, non-ed25519 curve parameter OID)")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ed25519Key.PrivateKey) != 32 {
|
||||||
|
return nil, fmt.Errorf("keysutil: failed to parse private key as ed25519 private key: got %v bytes but expected %v byte RFC 8032 seed", len(ed25519Key.PrivateKey), ed25519.SeedSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ed25519.NewKeyFromSeed(ed25519Key.PrivateKey), nil
|
||||||
|
}
|
Loading…
Reference in New Issue