diff --git a/builtin/logical/transit/path_import_test.go b/builtin/logical/transit/path_import_test.go index 6e2748833..a532dfdbe 100644 --- a/builtin/logical/transit/path_import_test.go +++ b/builtin/logical/transit/path_import_test.go @@ -45,6 +45,8 @@ var ( keys = map[string]interface{}{} ) +const nssFormattedEd25519Key = "MGcCAQAwFAYHKoZIzj0CAQYJKwYBBAHaRw8BBEwwSgIBAQQgfJm5R+LK4FMwGzOpemTBXksimEVOVCE8QeC+XBBfNU+hIwMhADaif7IhYx46IHcRTy1z8LeyhABep+UB8Da6olMZGx0i" + func generateKeys(t *testing.T) { t.Helper() @@ -78,6 +80,39 @@ func getKey(t *testing.T, keyType string) interface{} { 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) { generateKeys(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 { 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 ephKey, err := uuid.GenerateRandomBytes(32) 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) } - // 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 targetKeyWrapped, err := kwp.Wrap(preppedTargetKey) if err != nil { diff --git a/changelog/15742.txt b/changelog/15742.txt new file mode 100644 index 000000000..c7f69b6f7 --- /dev/null +++ b/changelog/15742.txt @@ -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). +``` diff --git a/sdk/helper/keysutil/policy.go b/sdk/helper/keysutil/policy.go index 59029756d..7e35b050f 100644 --- a/sdk/helper/keysutil/policy.go +++ b/sdk/helper/keysutil/policy.go @@ -1385,7 +1385,17 @@ func (p *Policy) Import(ctx context.Context, storage logical.Storage, key []byte } else { parsedPrivateKey, err := x509.ParsePKCS8PrivateKey(key) if err != nil { - return fmt.Errorf("error parsing asymmetric key: %s", err) + 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) + } } switch parsedPrivateKey.(type) { diff --git a/sdk/helper/keysutil/util.go b/sdk/helper/keysutil/util.go new file mode 100644 index 000000000..063af5914 --- /dev/null +++ b/sdk/helper/keysutil/util.go @@ -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 +}