From f7a59ccf55c0023b9501a2e32998d0ad153e122f Mon Sep 17 00:00:00 2001 From: Lucas Molas Date: Thu, 16 Dec 2021 12:03:12 -0300 Subject: [PATCH 1/9] feat(cmds): add PEM/PKCS8 for key import/export --- core/commands/keystore.go | 163 +++++++++++++++++++++++++++++--- test/sharness/t0165-keystore.sh | 55 +++++++---- 2 files changed, 188 insertions(+), 30 deletions(-) diff --git a/core/commands/keystore.go b/core/commands/keystore.go index bd3146ca57c..3729292222f 100644 --- a/core/commands/keystore.go +++ b/core/commands/keystore.go @@ -2,6 +2,10 @@ package commands import ( "bytes" + "crypto/ed25519" + "crypto/rand" + "crypto/x509" + "encoding/pem" "fmt" "io" "io/ioutil" @@ -135,6 +139,15 @@ var keyGenCmd = &cmds.Command{ Type: KeyOutput{}, } +const ( + // Key format options used both for importing and exporting. + keyFormatOptionName = "format" + keyFormatPemEncryptedOption = "pem-pkcs8-encrypted" + keyFormatPemCleartextOption = "pem-pkcs8-cleartext" + keyFormatLibp2pCleartextOption = "libp2p-protobuf-cleartext" + keyEncryptionPasswordOptionName = "password" +) + var keyExportCmd = &cmds.Command{ Helptext: cmds.HelpText{ Tagline: "Export a keypair", @@ -150,6 +163,10 @@ path can be specified with '--output=' or '-o='. }, Options: []cmds.Option{ cmds.StringOption(outputOptionName, "o", "The path where the output should be stored."), + cmds.StringOption(keyFormatOptionName, "f", "The format of the exported private key.").WithDefault(keyFormatLibp2pCleartextOption), + cmds.StringOption(keyEncryptionPasswordOptionName, "p", "The password to encrypt the exported key with (for the encrypted variant only)."), + // FIXME(BLOCKING): change default to keyFormatPemEncryptedOption once it + // is implemented and the sharness tests (if any) are adapted. }, NoRemote: true, Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error { @@ -186,12 +203,38 @@ path can be specified with '--output=' or '-o='. return fmt.Errorf("key with name '%s' doesn't exist", name) } - encoded, err := crypto.MarshalPrivateKey(sk) - if err != nil { - return err + exportFormat, _ := req.Options[keyFormatOptionName].(string) + var formattedKey []byte + switch exportFormat { + case keyFormatPemEncryptedOption, keyFormatPemCleartextOption: + stdKey, err := crypto.PrivKeyToStdKey(sk) + if err != nil { + return fmt.Errorf("converting libp2p private key to std Go key: %w", err) + + } + // For some reason the ed25519.PrivateKey does not use pointer + // receivers, so we need to convert it for MarshalPKCS8PrivateKey. + // (We should probably change this upstream in PrivKeyToStdKey). + if ed25519KeyPointer, ok := stdKey.(*ed25519.PrivateKey); ok { + stdKey = *ed25519KeyPointer + } + // This function supports a restricted list of public key algorithms, + // but we generate and use only the RSA and ed25519 types that are on that list. + formattedKey, err = x509.MarshalPKCS8PrivateKey(stdKey) + if err != nil { + return fmt.Errorf("marshalling key to PKCS8 format: %w", err) + } + + case keyFormatLibp2pCleartextOption: + formattedKey, err = crypto.MarshalPrivateKey(sk) + if err != nil { + return err + } + default: + return fmt.Errorf("unrecognized export format: %s", exportFormat) } - return res.Emit(bytes.NewReader(encoded)) + return res.Emit(bytes.NewReader(formattedKey)) }, PostRun: cmds.PostRunMap{ cmds.CLI: func(res cmds.Response, re cmds.ResponseEmitter) error { @@ -208,8 +251,16 @@ path can be specified with '--output=' or '-o='. } outPath, _ := req.Options[outputOptionName].(string) + exportFormat, _ := req.Options[keyFormatOptionName].(string) if outPath == "" { - trimmed := strings.TrimRight(fmt.Sprintf("%s.key", req.Arguments[0]), "/") + var fileExtension string + switch exportFormat { + case keyFormatPemEncryptedOption, keyFormatPemCleartextOption: + fileExtension = "pem" + case keyFormatLibp2pCleartextOption: + fileExtension = "key" + } + trimmed := strings.TrimRight(fmt.Sprintf("%s.%s", req.Arguments[0], fileExtension), "/") _, outPath = filepath.Split(trimmed) outPath = filepath.Clean(outPath) } @@ -221,9 +272,46 @@ path can be specified with '--output=' or '-o='. } defer file.Close() - _, err = io.Copy(file, outReader) - if err != nil { - return err + switch exportFormat { + case keyFormatPemEncryptedOption, keyFormatPemCleartextOption: + privKeyBytes, err := ioutil.ReadAll(outReader) + if err != nil { + return err + } + + var pemBlock *pem.Block + if exportFormat == keyFormatPemEncryptedOption { + keyEncPassword, ok := req.Options[keyEncryptionPasswordOptionName].(string) + if !ok { + return fmt.Errorf("missing password to encrypt the key with, set it with --%s", + keyEncryptionPasswordOptionName) + } + // FIXME(BLOCKING): Using deprecated security function. + pemBlock, err = x509.EncryptPEMBlock(rand.Reader, + "ENCRYPTED PRIVATE KEY", + privKeyBytes, + []byte(keyEncPassword), + x509.PEMCipherAES256) + if err != nil { + return fmt.Errorf("encrypting PEM block: %w", err) + } + } else { // cleartext + pemBlock = &pem.Block{ + Type: "PRIVATE KEY", + Bytes: privKeyBytes, + } + } + + err = pem.Encode(file, pemBlock) + if err != nil { + return fmt.Errorf("encoding PEM block: %w", err) + } + + case keyFormatLibp2pCleartextOption: + _, err = io.Copy(file, outReader) + if err != nil { + return err + } } return nil @@ -237,6 +325,9 @@ var keyImportCmd = &cmds.Command{ }, Options: []cmds.Option{ ke.OptionIPNSBase, + cmds.StringOption(keyFormatOptionName, "f", "The format of the private key to import.").WithDefault(keyFormatLibp2pCleartextOption), + // FIXME: Attempt to figure out the import format. + cmds.StringOption(keyEncryptionPasswordOptionName, "p", "The password to decrypt the imported key with (for the encrypted variant only)."), }, Arguments: []cmds.Argument{ cmds.StringArg("name", true, false, "name to associate with key in keychain"), @@ -265,9 +356,59 @@ var keyImportCmd = &cmds.Command{ return err } - sk, err := crypto.UnmarshalPrivateKey(data) - if err != nil { - return err + importFormat, _ := req.Options[keyFormatOptionName].(string) + var sk crypto.PrivKey + switch importFormat { + case keyFormatPemEncryptedOption, keyFormatPemCleartextOption: + pemBlock, rest := pem.Decode(data) + if pemBlock == nil { + return fmt.Errorf("PEM block not found in input data:\n%s", rest) + } + + if pemBlock.Type != "PRIVATE KEY" && pemBlock.Type != "ENCRYPTED PRIVATE KEY" { + return fmt.Errorf("expected [ENCRYPTED] PRIVATE KEY type in PEM block but got: %s", pemBlock.Type) + } + + var privKeyBytes []byte + if importFormat == keyFormatPemEncryptedOption { + keyDecPassword, ok := req.Options[keyEncryptionPasswordOptionName].(string) + if !ok { + return fmt.Errorf("missing password to decrypt the key with, set it with --%s", + keyEncryptionPasswordOptionName) + } + privKeyBytes, err = x509.DecryptPEMBlock(pemBlock, + []byte(keyDecPassword)) + if err != nil { + return fmt.Errorf("decrypting PEM block: %w", err) + } + } else { // cleartext + privKeyBytes = pemBlock.Bytes + } + + stdKey, err := x509.ParsePKCS8PrivateKey(privKeyBytes) + if err != nil { + return fmt.Errorf("parsing PKCS8 format: %w", err) + } + + // In case ed25519.PrivateKey is returned we need the pointer for + // conversion to libp2p (see export command for more details). + if ed25519KeyPointer, ok := stdKey.(ed25519.PrivateKey); ok { + stdKey = &ed25519KeyPointer + } + + sk, _, err = crypto.KeyPairFromStdKey(stdKey) + if err != nil { + return fmt.Errorf("converting std Go key to libp2p key : %w", err) + + } + case keyFormatLibp2pCleartextOption: + sk, err = crypto.UnmarshalPrivateKey(data) + if err != nil { + return err + } + + default: + return fmt.Errorf("unrecognized import format: %s", importFormat) } cfgRoot, err := cmdenv.GetConfigRoot(env) diff --git a/test/sharness/t0165-keystore.sh b/test/sharness/t0165-keystore.sh index ad4b6a6c7c7..dad93dd2913 100755 --- a/test/sharness/t0165-keystore.sh +++ b/test/sharness/t0165-keystore.sh @@ -63,24 +63,14 @@ ipfs key rm key_ed25519 echo $rsahash > rsa_key_id ' + test_key_import_export_all_formats rsa_key + test_expect_success "create a new ed25519 key" ' edhash=$(ipfs key gen generated_ed25519_key --type=ed25519) echo $edhash > ed25519_key_id ' - test_expect_success "export and import rsa key" ' - ipfs key export generated_rsa_key && - ipfs key rm generated_rsa_key && - ipfs key import generated_rsa_key generated_rsa_key.key > roundtrip_rsa_key_id && - test_cmp rsa_key_id roundtrip_rsa_key_id - ' - - test_expect_success "export and import ed25519 key" ' - ipfs key export generated_ed25519_key && - ipfs key rm generated_ed25519_key && - ipfs key import generated_ed25519_key generated_ed25519_key.key > roundtrip_ed25519_key_id && - test_cmp ed25519_key_id roundtrip_ed25519_key_id - ' + test_key_import_export_all_formats ed25519_key test_expect_success "test export file option" ' ipfs key export generated_rsa_key -o=named_rsa_export_file && @@ -176,15 +166,13 @@ ipfs key rm key_ed25519 ' # export works directly on the keystore present in IPFS_PATH - test_expect_success "export and import ed25519 key while daemon is running" ' - edhash=$(ipfs key gen exported_ed25519_key --type=ed25519) + test_expect_success "prepare ed25519 key while daemon is running" ' + edhash=$(ipfs key gen generated_ed25519_key --type=ed25519) echo $edhash > ed25519_key_id - ipfs key export exported_ed25519_key && - ipfs key rm exported_ed25519_key && - ipfs key import exported_ed25519_key exported_ed25519_key.key > roundtrip_ed25519_key_id && - test_cmp ed25519_key_id roundtrip_ed25519_key_id ' + test_key_import_export_all_formats ed25519_key + test_expect_success "key export over HTTP /api/v0/key/export is not possible" ' ipfs key gen nohttpexporttest_key --type=ed25519 && curl -X POST -sI "http://$API_ADDR/api/v0/key/export&arg=nohttpexporttest_key" | grep -q "^HTTP/1.1 404 Not Found" @@ -214,6 +202,35 @@ test_check_ed25519_sk() { } } +test_key_import_export_all_formats() { + KEY_NAME=$1 + test_key_import_export $KEY_NAME pem-pkcs8-cleartext + test_key_import_export $KEY_NAME pem-pkcs8-encrypted + test_key_import_export $KEY_NAME libp2p-protobuf-cleartext +} + +test_key_import_export() { + local KEY_NAME FORMAT + KEY_NAME=$1 + FORMAT=$2 + ORIG_KEY="generated_$KEY_NAME" + if [ $FORMAT == "pem-pkcs8-encrypted" ]; then + KEY_PASSWORD="--password=fake-test-password" + fi + if [ $FORMAT == "libp2p-protobuf-cleartext" ]; then + FILE_EXT="key" + else + FILE_EXT="pem" + fi + + test_expect_success "export and import $KEY_NAME with format $FORMAT" ' + ipfs key export $ORIG_KEY --format=$FORMAT $KEY_PASSWORD && + ipfs key rm $ORIG_KEY && + ipfs key import $ORIG_KEY $ORIG_KEY.$FILE_EXT --format=$FORMAT $KEY_PASSWORD > imported_key_id && + test_cmp ${KEY_NAME}_id imported_key_id + ' +} + test_key_cmd test_done From 53dd92de857293d394dcbf28284499c2e9288767 Mon Sep 17 00:00:00 2001 From: Lucas Molas Date: Wed, 19 Jan 2022 10:18:13 -0300 Subject: [PATCH 2/9] SQUASH: review: remove encrypted variant --- core/commands/keystore.go | 66 ++++++--------------------------- test/sharness/t0165-keystore.sh | 8 +--- 2 files changed, 13 insertions(+), 61 deletions(-) diff --git a/core/commands/keystore.go b/core/commands/keystore.go index 3729292222f..f33f9de3c13 100644 --- a/core/commands/keystore.go +++ b/core/commands/keystore.go @@ -3,7 +3,6 @@ package commands import ( "bytes" "crypto/ed25519" - "crypto/rand" "crypto/x509" "encoding/pem" "fmt" @@ -142,10 +141,8 @@ var keyGenCmd = &cmds.Command{ const ( // Key format options used both for importing and exporting. keyFormatOptionName = "format" - keyFormatPemEncryptedOption = "pem-pkcs8-encrypted" keyFormatPemCleartextOption = "pem-pkcs8-cleartext" keyFormatLibp2pCleartextOption = "libp2p-protobuf-cleartext" - keyEncryptionPasswordOptionName = "password" ) var keyExportCmd = &cmds.Command{ @@ -164,9 +161,6 @@ path can be specified with '--output=' or '-o='. Options: []cmds.Option{ cmds.StringOption(outputOptionName, "o", "The path where the output should be stored."), cmds.StringOption(keyFormatOptionName, "f", "The format of the exported private key.").WithDefault(keyFormatLibp2pCleartextOption), - cmds.StringOption(keyEncryptionPasswordOptionName, "p", "The password to encrypt the exported key with (for the encrypted variant only)."), - // FIXME(BLOCKING): change default to keyFormatPemEncryptedOption once it - // is implemented and the sharness tests (if any) are adapted. }, NoRemote: true, Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error { @@ -206,7 +200,7 @@ path can be specified with '--output=' or '-o='. exportFormat, _ := req.Options[keyFormatOptionName].(string) var formattedKey []byte switch exportFormat { - case keyFormatPemEncryptedOption, keyFormatPemCleartextOption: + case keyFormatPemCleartextOption: stdKey, err := crypto.PrivKeyToStdKey(sk) if err != nil { return fmt.Errorf("converting libp2p private key to std Go key: %w", err) @@ -255,7 +249,7 @@ path can be specified with '--output=' or '-o='. if outPath == "" { var fileExtension string switch exportFormat { - case keyFormatPemEncryptedOption, keyFormatPemCleartextOption: + case keyFormatPemCleartextOption: fileExtension = "pem" case keyFormatLibp2pCleartextOption: fileExtension = "key" @@ -273,36 +267,16 @@ path can be specified with '--output=' or '-o='. defer file.Close() switch exportFormat { - case keyFormatPemEncryptedOption, keyFormatPemCleartextOption: + case keyFormatPemCleartextOption: privKeyBytes, err := ioutil.ReadAll(outReader) if err != nil { return err } - var pemBlock *pem.Block - if exportFormat == keyFormatPemEncryptedOption { - keyEncPassword, ok := req.Options[keyEncryptionPasswordOptionName].(string) - if !ok { - return fmt.Errorf("missing password to encrypt the key with, set it with --%s", - keyEncryptionPasswordOptionName) - } - // FIXME(BLOCKING): Using deprecated security function. - pemBlock, err = x509.EncryptPEMBlock(rand.Reader, - "ENCRYPTED PRIVATE KEY", - privKeyBytes, - []byte(keyEncPassword), - x509.PEMCipherAES256) - if err != nil { - return fmt.Errorf("encrypting PEM block: %w", err) - } - } else { // cleartext - pemBlock = &pem.Block{ - Type: "PRIVATE KEY", - Bytes: privKeyBytes, - } - } - - err = pem.Encode(file, pemBlock) + err = pem.Encode(file, &pem.Block{ + Type: "PRIVATE KEY", + Bytes: privKeyBytes, + }) if err != nil { return fmt.Errorf("encoding PEM block: %w", err) } @@ -326,8 +300,6 @@ var keyImportCmd = &cmds.Command{ Options: []cmds.Option{ ke.OptionIPNSBase, cmds.StringOption(keyFormatOptionName, "f", "The format of the private key to import.").WithDefault(keyFormatLibp2pCleartextOption), - // FIXME: Attempt to figure out the import format. - cmds.StringOption(keyEncryptionPasswordOptionName, "p", "The password to decrypt the imported key with (for the encrypted variant only)."), }, Arguments: []cmds.Argument{ cmds.StringArg("name", true, false, "name to associate with key in keychain"), @@ -359,33 +331,17 @@ var keyImportCmd = &cmds.Command{ importFormat, _ := req.Options[keyFormatOptionName].(string) var sk crypto.PrivKey switch importFormat { - case keyFormatPemEncryptedOption, keyFormatPemCleartextOption: + case keyFormatPemCleartextOption: pemBlock, rest := pem.Decode(data) if pemBlock == nil { return fmt.Errorf("PEM block not found in input data:\n%s", rest) } - if pemBlock.Type != "PRIVATE KEY" && pemBlock.Type != "ENCRYPTED PRIVATE KEY" { - return fmt.Errorf("expected [ENCRYPTED] PRIVATE KEY type in PEM block but got: %s", pemBlock.Type) - } - - var privKeyBytes []byte - if importFormat == keyFormatPemEncryptedOption { - keyDecPassword, ok := req.Options[keyEncryptionPasswordOptionName].(string) - if !ok { - return fmt.Errorf("missing password to decrypt the key with, set it with --%s", - keyEncryptionPasswordOptionName) - } - privKeyBytes, err = x509.DecryptPEMBlock(pemBlock, - []byte(keyDecPassword)) - if err != nil { - return fmt.Errorf("decrypting PEM block: %w", err) - } - } else { // cleartext - privKeyBytes = pemBlock.Bytes + if pemBlock.Type != "PRIVATE KEY" { + return fmt.Errorf("expected PRIVATE KEY type in PEM block but got: %s", pemBlock.Type) } - stdKey, err := x509.ParsePKCS8PrivateKey(privKeyBytes) + stdKey, err := x509.ParsePKCS8PrivateKey(pemBlock.Bytes) if err != nil { return fmt.Errorf("parsing PKCS8 format: %w", err) } diff --git a/test/sharness/t0165-keystore.sh b/test/sharness/t0165-keystore.sh index dad93dd2913..7a2d9545f71 100755 --- a/test/sharness/t0165-keystore.sh +++ b/test/sharness/t0165-keystore.sh @@ -205,7 +205,6 @@ test_check_ed25519_sk() { test_key_import_export_all_formats() { KEY_NAME=$1 test_key_import_export $KEY_NAME pem-pkcs8-cleartext - test_key_import_export $KEY_NAME pem-pkcs8-encrypted test_key_import_export $KEY_NAME libp2p-protobuf-cleartext } @@ -214,9 +213,6 @@ test_key_import_export() { KEY_NAME=$1 FORMAT=$2 ORIG_KEY="generated_$KEY_NAME" - if [ $FORMAT == "pem-pkcs8-encrypted" ]; then - KEY_PASSWORD="--password=fake-test-password" - fi if [ $FORMAT == "libp2p-protobuf-cleartext" ]; then FILE_EXT="key" else @@ -224,9 +220,9 @@ test_key_import_export() { fi test_expect_success "export and import $KEY_NAME with format $FORMAT" ' - ipfs key export $ORIG_KEY --format=$FORMAT $KEY_PASSWORD && + ipfs key export $ORIG_KEY --format=$FORMAT && ipfs key rm $ORIG_KEY && - ipfs key import $ORIG_KEY $ORIG_KEY.$FILE_EXT --format=$FORMAT $KEY_PASSWORD > imported_key_id && + ipfs key import $ORIG_KEY $ORIG_KEY.$FILE_EXT --format=$FORMAT > imported_key_id && test_cmp ${KEY_NAME}_id imported_key_id ' } From 63dba3823d885aac55cb7d47cd5c5c083a2e3e28 Mon Sep 17 00:00:00 2001 From: Lucas Molas Date: Wed, 19 Jan 2022 10:20:36 -0300 Subject: [PATCH 3/9] go fmt --- core/commands/keystore.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/commands/keystore.go b/core/commands/keystore.go index f33f9de3c13..35a42974b99 100644 --- a/core/commands/keystore.go +++ b/core/commands/keystore.go @@ -140,9 +140,9 @@ var keyGenCmd = &cmds.Command{ const ( // Key format options used both for importing and exporting. - keyFormatOptionName = "format" - keyFormatPemCleartextOption = "pem-pkcs8-cleartext" - keyFormatLibp2pCleartextOption = "libp2p-protobuf-cleartext" + keyFormatOptionName = "format" + keyFormatPemCleartextOption = "pem-pkcs8-cleartext" + keyFormatLibp2pCleartextOption = "libp2p-protobuf-cleartext" ) var keyExportCmd = &cmds.Command{ From 23d7a476a4dc9ae37d055f08aa7f50b4e04b26eb Mon Sep 17 00:00:00 2001 From: Marcin Rataj Date: Wed, 19 Jan 2022 15:39:00 +0100 Subject: [PATCH 4/9] docs(cli): PEM import/export This adds helptext informing users about PEM support and lets user know when they forgot to pass --format=pem-pkcs8-cleartext while importing a PEM file. --- core/commands/keystore.go | 32 ++++++++++++++++++++++++++++---- test/sharness/t0165-keystore.sh | 6 +++--- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/core/commands/keystore.go b/core/commands/keystore.go index 35a42974b99..614f71c99bd 100644 --- a/core/commands/keystore.go +++ b/core/commands/keystore.go @@ -153,6 +153,13 @@ Exports a named libp2p key to disk. By default, the output will be stored at './.key', but an alternate path can be specified with '--output=' or '-o='. + +It is possible to export a private key to interoperable PEM PKCS8 format by passing +explicit '--format=pem-pkcs8-cleartext'. Produced PEM file can then be consumed +by other software. For example, using openssl to get a PEM with public key: + + $ ipfs key export testkey --format=pem-pkcs8-cleartext -o privkey.pem + $ openssl pkey -in privkey.pem -pubout > pubkey.pem `, }, Arguments: []cmds.Argument{ @@ -160,7 +167,7 @@ path can be specified with '--output=' or '-o='. }, Options: []cmds.Option{ cmds.StringOption(outputOptionName, "o", "The path where the output should be stored."), - cmds.StringOption(keyFormatOptionName, "f", "The format of the exported private key.").WithDefault(keyFormatLibp2pCleartextOption), + cmds.StringOption(keyFormatOptionName, "f", "The format of the exported private key, libp2p-protobuf-cleartext or pem-pkcs8-cleartext.").WithDefault(keyFormatLibp2pCleartextOption), }, NoRemote: true, Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error { @@ -296,10 +303,22 @@ path can be specified with '--output=' or '-o='. var keyImportCmd = &cmds.Command{ Helptext: cmds.HelpText{ Tagline: "Import a key and prints imported key id", + ShortDescription: ` +Imports a key and stores it under provided name. + +By default, the is assumed to be in 'libp2p-protobuf-cleartext' format, +however it is possible to import private keys wrapped in interoperable PEM PKCS8 +by passing '--format=pem-pkcs8-cleartext'. + +PEM format allows for key generation outside of IPFS node: + + $ openssl genpkey -algorithm ED25519 > ed25519.pem + $ ipfs key import test-openssl -f pem-pkcs8-cleartext ed25519.pem +`, }, Options: []cmds.Option{ ke.OptionIPNSBase, - cmds.StringOption(keyFormatOptionName, "f", "The format of the private key to import.").WithDefault(keyFormatLibp2pCleartextOption), + cmds.StringOption(keyFormatOptionName, "f", "The format of the private key to import, libp2p-protobuf-cleartext or pem-pkcs8-cleartext.").WithDefault(keyFormatLibp2pCleartextOption), }, Arguments: []cmds.Argument{ cmds.StringArg("name", true, false, "name to associate with key in keychain"), @@ -354,13 +373,18 @@ var keyImportCmd = &cmds.Command{ sk, _, err = crypto.KeyPairFromStdKey(stdKey) if err != nil { - return fmt.Errorf("converting std Go key to libp2p key : %w", err) + return fmt.Errorf("converting std Go key to libp2p key: %w", err) } case keyFormatLibp2pCleartextOption: sk, err = crypto.UnmarshalPrivateKey(data) if err != nil { - return err + // check if data is PEM, if so, provide user with hint + pemBlock, _ := pem.Decode(data) + if pemBlock != nil { + return fmt.Errorf("unexpected PEM block for format=%s: try again with format=%s", keyFormatLibp2pCleartextOption, keyFormatPemCleartextOption) + } + return fmt.Errorf("unable to unmarshall format=%s: %w", keyFormatLibp2pCleartextOption, err) } default: diff --git a/test/sharness/t0165-keystore.sh b/test/sharness/t0165-keystore.sh index 7a2d9545f71..8d989f2ff6c 100755 --- a/test/sharness/t0165-keystore.sh +++ b/test/sharness/t0165-keystore.sh @@ -213,10 +213,10 @@ test_key_import_export() { KEY_NAME=$1 FORMAT=$2 ORIG_KEY="generated_$KEY_NAME" - if [ $FORMAT == "libp2p-protobuf-cleartext" ]; then - FILE_EXT="key" - else + if [ $FORMAT == "pem-pkcs8-cleartext" ]; then FILE_EXT="pem" + else + FILE_EXT="key" fi test_expect_success "export and import $KEY_NAME with format $FORMAT" ' From 87e103514b066be27baf7bb2a548eab8a4d06970 Mon Sep 17 00:00:00 2001 From: Lucas Molas Date: Fri, 4 Feb 2022 11:06:32 -0300 Subject: [PATCH 5/9] Update core/commands/keystore.go Co-authored-by: Gus Eggert --- core/commands/keystore.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/commands/keystore.go b/core/commands/keystore.go index 614f71c99bd..4635c441f22 100644 --- a/core/commands/keystore.go +++ b/core/commands/keystore.go @@ -304,7 +304,7 @@ var keyImportCmd = &cmds.Command{ Helptext: cmds.HelpText{ Tagline: "Import a key and prints imported key id", ShortDescription: ` -Imports a key and stores it under provided name. +Imports a key and stores it under the provided name. By default, the is assumed to be in 'libp2p-protobuf-cleartext' format, however it is possible to import private keys wrapped in interoperable PEM PKCS8 From b1ef20290ca9a730c574d32feb49f1bc921d8a73 Mon Sep 17 00:00:00 2001 From: Lucas Molas Date: Fri, 4 Feb 2022 11:06:42 -0300 Subject: [PATCH 6/9] Update core/commands/keystore.go Co-authored-by: Gus Eggert --- core/commands/keystore.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/commands/keystore.go b/core/commands/keystore.go index 4635c441f22..bad40a21189 100644 --- a/core/commands/keystore.go +++ b/core/commands/keystore.go @@ -306,7 +306,7 @@ var keyImportCmd = &cmds.Command{ ShortDescription: ` Imports a key and stores it under the provided name. -By default, the is assumed to be in 'libp2p-protobuf-cleartext' format, +By default, the key is assumed to be in 'libp2p-protobuf-cleartext' format, however it is possible to import private keys wrapped in interoperable PEM PKCS8 by passing '--format=pem-pkcs8-cleartext'. From 1d7e794a7b9697a20ffeec5a88795c86e97324cf Mon Sep 17 00:00:00 2001 From: Lucas Molas Date: Fri, 4 Feb 2022 11:06:53 -0300 Subject: [PATCH 7/9] Update core/commands/keystore.go Co-authored-by: Gus Eggert --- core/commands/keystore.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/commands/keystore.go b/core/commands/keystore.go index bad40a21189..ebe6c555e5f 100644 --- a/core/commands/keystore.go +++ b/core/commands/keystore.go @@ -310,7 +310,7 @@ By default, the key is assumed to be in 'libp2p-protobuf-cleartext' format, however it is possible to import private keys wrapped in interoperable PEM PKCS8 by passing '--format=pem-pkcs8-cleartext'. -PEM format allows for key generation outside of IPFS node: +The PEM format allows for key generation outside of the IPFS node: $ openssl genpkey -algorithm ED25519 > ed25519.pem $ ipfs key import test-openssl -f pem-pkcs8-cleartext ed25519.pem From c7481ecad71f83262fdbf51ca0bf4394fc278e91 Mon Sep 17 00:00:00 2001 From: Lucas Molas Date: Fri, 4 Feb 2022 11:07:12 -0300 Subject: [PATCH 8/9] Update core/commands/keystore.go Co-authored-by: Gus Eggert --- core/commands/keystore.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/commands/keystore.go b/core/commands/keystore.go index ebe6c555e5f..11f62fe5046 100644 --- a/core/commands/keystore.go +++ b/core/commands/keystore.go @@ -154,9 +154,9 @@ Exports a named libp2p key to disk. By default, the output will be stored at './.key', but an alternate path can be specified with '--output=' or '-o='. -It is possible to export a private key to interoperable PEM PKCS8 format by passing -explicit '--format=pem-pkcs8-cleartext'. Produced PEM file can then be consumed -by other software. For example, using openssl to get a PEM with public key: +It is possible to export a private key to interoperable PEM PKCS8 format by explicitly +passing '--format=pem-pkcs8-cleartext'. The resulting PEM file can then be consumed +elsewhere. For example, using openssl to get a PEM with public key: $ ipfs key export testkey --format=pem-pkcs8-cleartext -o privkey.pem $ openssl pkey -in privkey.pem -pubout > pubkey.pem From 5c047628610fd357f915b34d1902d9ec1b0781bc Mon Sep 17 00:00:00 2001 From: Lucas Molas Date: Thu, 10 Feb 2022 12:20:15 -0300 Subject: [PATCH 9/9] add openssl compatibility test --- test/sharness/t0165-keystore-data/README.md | 8 ++++ .../t0165-keystore-data/openssl_ed25519.pem | 3 ++ .../t0165-keystore-data/openssl_rsa.pem | 28 ++++++++++++++ test/sharness/t0165-keystore.sh | 37 +++++++++++++++++++ 4 files changed, 76 insertions(+) create mode 100644 test/sharness/t0165-keystore-data/README.md create mode 100644 test/sharness/t0165-keystore-data/openssl_ed25519.pem create mode 100644 test/sharness/t0165-keystore-data/openssl_rsa.pem diff --git a/test/sharness/t0165-keystore-data/README.md b/test/sharness/t0165-keystore-data/README.md new file mode 100644 index 00000000000..33c77fbd376 --- /dev/null +++ b/test/sharness/t0165-keystore-data/README.md @@ -0,0 +1,8 @@ +# OpenSSL generated keys for import/export tests + +Created with commands: + +```bash +openssl genpkey -algorithm ED25519 > openssl_ed25519.pem +openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 > openssl_rsa.pem +``` diff --git a/test/sharness/t0165-keystore-data/openssl_ed25519.pem b/test/sharness/t0165-keystore-data/openssl_ed25519.pem new file mode 100644 index 00000000000..387972c5265 --- /dev/null +++ b/test/sharness/t0165-keystore-data/openssl_ed25519.pem @@ -0,0 +1,3 @@ +-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIJ2M1na2f3dRm4b1FcAQvsn7q08+XfBZcr4MgH4yiBdz +-----END PRIVATE KEY----- diff --git a/test/sharness/t0165-keystore-data/openssl_rsa.pem b/test/sharness/t0165-keystore-data/openssl_rsa.pem new file mode 100644 index 00000000000..34d365bed2e --- /dev/null +++ b/test/sharness/t0165-keystore-data/openssl_rsa.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDSaJB9EKnShOs6 +sbGkB40crn72yNKXj5OBPS2wBDTHWwxyhTB0qJirOT2QYW2DmR/4lPfVk5/f4CJ7 +xIHUBJRoC+NTwqHit24DQBd00tNG4EnKn2Dad/arZ/nEVshkKiGXn0qXxiHHsaCn +X/pnVPU4+O7fdfUlz2EKf3Og/ocRCFrdMsULR2QwDc0YWsY8ngrcKegyFCbKjXjo +zvfbGevCDPlhKaZLxRy0PHnON00YC4KO6d77XpbECFvsE1aG1RxYQX0Zjr+i8UvD +UJp/YCoRNEX54/wKpGebMUrFse5K9hBsFen/wCsPnOsYPSb9g8qyoYRDBnr9sIe1 +9MxFTMy/AgMBAAECggEAKXu2KQI1CS1tlzfbdySJ/MKmg49afckv4sYmENLzeO6J +iLabtBRdbTyu151t0wlIlWEBb9lYJvJwuggnNJ7mh5D4c9YmxqU1imyDc2PxhcLI +qas8lDYcqvSn+L7HaYAo+VTNhxjoJg/uRbGVk/PbGS1zIxmFiLvXPROdv3sPNBsf +EYMDH9q7/8DI6dNBQPxtTKlTDLDsTezbkNFQ74znlXgQYcfY1mXljcRtbJqhQJT3 +uppktESPwLRmqtT9H+v9nCtQR6OLmAmLWNgMrSdGKBsSsgJwv2xfpNMffwd84dtT +uGrS2K+BY0TH2q+Xx04r18GLCst3U5MBSklyHQ/mwQKBgQDqnxNOnK41/n/Q8X4a +/TUnZBx/JHiCoQoa06AsMxFgOvV3ycR+Z9lwb5I5BsicH1GUcHIxSY3mCyd4fLwE +FC0QIyNhPJ5oFKh0Oynjm+79VE8v7kK2qqRL4zUpaCXEsSOrhRsCY0/WQdMUPVsh +okXDUIv37G9KUcjdrhNVpGK3oQKBgQDllK7augIhmlQZTdSLTgmuzhYsXdSGDML/ +Bx48q7OvPhvZIIOsygLGhtcBk2xG6PN1yP44cx9dvcTnzxU6TEblO5P8TWY0BSNj +ZuC5wdxLwc3KUdLd9JLR7qcbjqndDruE01rQFVQ3MDbyB1+VrJgiVHIEomJJrKGm +FQ+314moXwKBgQDL90sDlnZk/kED1k15DRN+kSus5HnXpkRwmfWvNx4t+FOZtdCa +y5Fei8Akz17rStbTIwZDDtzLVnsT5exV52xdkQ6a4+YaOYtQsHZ0JwWXOgo1cv6Q +ary2NGns+1uKKS0HWYnng4rOix8Dg2uMS9Q2PfnQqLz/cSYcgc7RLz2awQKBgQDd +HSaLYztKQeldtahPwwlwYuzYLkbSFNh559EnfffBgIAxzy8C7E1gB95sliBi61oQ +x1SR6c776hoLaVd4np5picgt6B3XXFuJETy/rAcQr8gUZFpDi5sctk4cLHtNfTL9 +6tI8N061GKrS0GcvMNwVtF9cN0mSy8GkxAQvfFgI4QKBgQC4NVimIPptfFckulAL +/t0vkdLhCRr1+UFNhgsQJhCZpfWZK4x8If6Jru/eiU7ywEsL6fHE2ENvyoTjV33g +b9yJ7SV4zkz4VhBxc3p26SIvBgLqtHwH8IkIonlbfQFoEAg1iOneLvimPy0YGHsG ++bTwwlAJJhctILkFtAbooeAQVQ== +-----END PRIVATE KEY----- diff --git a/test/sharness/t0165-keystore.sh b/test/sharness/t0165-keystore.sh index 8d989f2ff6c..4729a7e2e20 100755 --- a/test/sharness/t0165-keystore.sh +++ b/test/sharness/t0165-keystore.sh @@ -72,6 +72,8 @@ ipfs key rm key_ed25519 test_key_import_export_all_formats ed25519_key + test_openssl_compatibility_all_types + test_expect_success "test export file option" ' ipfs key export generated_rsa_key -o=named_rsa_export_file && test_cmp generated_rsa_key.key named_rsa_export_file && @@ -173,6 +175,8 @@ ipfs key rm key_ed25519 test_key_import_export_all_formats ed25519_key + test_openssl_compatibility_all_types + test_expect_success "key export over HTTP /api/v0/key/export is not possible" ' ipfs key gen nohttpexporttest_key --type=ed25519 && curl -X POST -sI "http://$API_ADDR/api/v0/key/export&arg=nohttpexporttest_key" | grep -q "^HTTP/1.1 404 Not Found" @@ -227,6 +231,39 @@ test_key_import_export() { ' } +# Test the entire import/export cycle with a openssl-generated key. +# 1. Import openssl key with PEM format. +# 2. Export key with libp2p format. +# 3. Reimport key. +# 4. Now exported with PEM format. +# 5. Compare with original openssl key. +# 6. Clean up. +test_openssl_compatibility() { + local KEY_NAME FORMAT + KEY_NAME=$1 + + test_expect_success "import and export $KEY_NAME with all formats" ' + ipfs key import test-openssl -f pem-pkcs8-cleartext $KEY_NAME > /dev/null && + ipfs key export test-openssl -f libp2p-protobuf-cleartext -o $KEY_NAME.libp2p.key && + ipfs key rm test-openssl && + + ipfs key import test-openssl -f libp2p-protobuf-cleartext $KEY_NAME.libp2p.key > /dev/null && + ipfs key export test-openssl -f pem-pkcs8-cleartext -o $KEY_NAME.ipfs-exported.pem && + ipfs key rm test-openssl && + + test_cmp $KEY_NAME $KEY_NAME.ipfs-exported.pem && + + rm $KEY_NAME.libp2p.key && + rm $KEY_NAME.ipfs-exported.pem + ' +} + +test_openssl_compatibility_all_types() { + test_openssl_compatibility ../t0165-keystore-data/openssl_ed25519.pem + test_openssl_compatibility ../t0165-keystore-data/openssl_rsa.pem +} + + test_key_cmd test_done