@cryptotaxi247 / kubo / commits / fe788cae9

feat(cmds): add cleartext PEM/PKCS8 for key import/export (#8616)

* feat(cmds): add PEM/PKCS8 for key import/export Co-authored-by: Marcin Rataj <lidel@lidel.org> Co-authored-by: Gus Eggert <gus@gus.dev>

Lucas Molas committed Feb 10, 2022 at 12:45 UTC fe788cae989dff25aad6933619bac2816b493863
5 files changed +239 -29
core/commands/keystore.go
+132 -11
@@ -2,6 +2,9 @@ package commands
2
3 import (
4 "bytes"
5 + "crypto/ed25519"
6 + "crypto/x509"
7 + "encoding/pem"
8 "fmt"
9 "io"
10 "io/ioutil"
@@ -135,6 +138,13 @@ var keyGenCmd = &cmds.Command{
138 Type: KeyOutput{},
139 }
140
141 +const (
142 + // Key format options used both for importing and exporting.
143 + keyFormatOptionName = "format"
144 + keyFormatPemCleartextOption = "pem-pkcs8-cleartext"
145 + keyFormatLibp2pCleartextOption = "libp2p-protobuf-cleartext"
146 +)
147 +
148 var keyExportCmd = &cmds.Command{
149 Helptext: cmds.HelpText{
150 Tagline: "Export a keypair",
@@ -143,6 +153,13 @@ Exports a named libp2p key to disk.
153
154 By default, the output will be stored at './<key-name>.key', but an alternate
155 path can be specified with '--output=<path>' or '-o=<path>'.
156 +
157 +It is possible to export a private key to interoperable PEM PKCS8 format by explicitly
158 +passing '--format=pem-pkcs8-cleartext'. The resulting PEM file can then be consumed
159 +elsewhere. For example, using openssl to get a PEM with public key:
160 +
161 + $ ipfs key export testkey --format=pem-pkcs8-cleartext -o privkey.pem
162 + $ openssl pkey -in privkey.pem -pubout > pubkey.pem
163 `,
164 },
165 Arguments: []cmds.Argument{
@@ -150,6 +167,7 @@ path can be specified with '--output=<path>' or '-o=<path>'.
167 },
168 Options: []cmds.Option{
169 cmds.StringOption(outputOptionName, "o", "The path where the output should be stored."),
170 + cmds.StringOption(keyFormatOptionName, "f", "The format of the exported private key, libp2p-protobuf-cleartext or pem-pkcs8-cleartext.").WithDefault(keyFormatLibp2pCleartextOption),
171 },
172 NoRemote: true,
173 Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
@@ -186,12 +204,38 @@ path can be specified with '--output=<path>' or '-o=<path>'.
204 return fmt.Errorf("key with name '%s' doesn't exist", name)
205 }
206
189 - encoded, err := crypto.MarshalPrivateKey(sk)
190 - if err != nil {
191 - return err
207 + exportFormat, _ := req.Options[keyFormatOptionName].(string)
208 + var formattedKey []byte
209 + switch exportFormat {
210 + case keyFormatPemCleartextOption:
211 + stdKey, err := crypto.PrivKeyToStdKey(sk)
212 + if err != nil {
213 + return fmt.Errorf("converting libp2p private key to std Go key: %w", err)
214 +
215 + }
216 + // For some reason the ed25519.PrivateKey does not use pointer
217 + // receivers, so we need to convert it for MarshalPKCS8PrivateKey.
218 + // (We should probably change this upstream in PrivKeyToStdKey).
219 + if ed25519KeyPointer, ok := stdKey.(*ed25519.PrivateKey); ok {
220 + stdKey = *ed25519KeyPointer
221 + }
222 + // This function supports a restricted list of public key algorithms,
223 + // but we generate and use only the RSA and ed25519 types that are on that list.
224 + formattedKey, err = x509.MarshalPKCS8PrivateKey(stdKey)
225 + if err != nil {
226 + return fmt.Errorf("marshalling key to PKCS8 format: %w", err)
227 + }
228 +
229 + case keyFormatLibp2pCleartextOption:
230 + formattedKey, err = crypto.MarshalPrivateKey(sk)
231 + if err != nil {
232 + return err
233 + }
234 + default:
235 + return fmt.Errorf("unrecognized export format: %s", exportFormat)
236 }
237
194 - return res.Emit(bytes.NewReader(encoded))
238 + return res.Emit(bytes.NewReader(formattedKey))
239 },
240 PostRun: cmds.PostRunMap{
241 cmds.CLI: func(res cmds.Response, re cmds.ResponseEmitter) error {
@@ -208,8 +252,16 @@ path can be specified with '--output=<path>' or '-o=<path>'.
252 }
253
254 outPath, _ := req.Options[outputOptionName].(string)
255 + exportFormat, _ := req.Options[keyFormatOptionName].(string)
256 if outPath == "" {
212 - trimmed := strings.TrimRight(fmt.Sprintf("%s.key", req.Arguments[0]), "/")
257 + var fileExtension string
258 + switch exportFormat {
259 + case keyFormatPemCleartextOption:
260 + fileExtension = "pem"
261 + case keyFormatLibp2pCleartextOption:
262 + fileExtension = "key"
263 + }
264 + trimmed := strings.TrimRight(fmt.Sprintf("%s.%s", req.Arguments[0], fileExtension), "/")
265 _, outPath = filepath.Split(trimmed)
266 outPath = filepath.Clean(outPath)
267 }
@@ -221,9 +273,26 @@ path can be specified with '--output=<path>' or '-o=<path>'.
273 }
274 defer file.Close()
275
224 - _, err = io.Copy(file, outReader)
225 - if err != nil {
226 - return err
276 + switch exportFormat {
277 + case keyFormatPemCleartextOption:
278 + privKeyBytes, err := ioutil.ReadAll(outReader)
279 + if err != nil {
280 + return err
281 + }
282 +
283 + err = pem.Encode(file, &pem.Block{
284 + Type: "PRIVATE KEY",
285 + Bytes: privKeyBytes,
286 + })
287 + if err != nil {
288 + return fmt.Errorf("encoding PEM block: %w", err)
289 + }
290 +
291 + case keyFormatLibp2pCleartextOption:
292 + _, err = io.Copy(file, outReader)
293 + if err != nil {
294 + return err
295 + }
296 }
297
298 return nil
@@ -234,9 +303,22 @@ path can be specified with '--output=<path>' or '-o=<path>'.
303 var keyImportCmd = &cmds.Command{
304 Helptext: cmds.HelpText{
305 Tagline: "Import a key and prints imported key id",
306 + ShortDescription: `
307 +Imports a key and stores it under the provided name.
308 +
309 +By default, the key is assumed to be in 'libp2p-protobuf-cleartext' format,
310 +however it is possible to import private keys wrapped in interoperable PEM PKCS8
311 +by passing '--format=pem-pkcs8-cleartext'.
312 +
313 +The PEM format allows for key generation outside of the IPFS node:
314 +
315 + $ openssl genpkey -algorithm ED25519 > ed25519.pem
316 + $ ipfs key import test-openssl -f pem-pkcs8-cleartext ed25519.pem
317 +`,
318 },
319 Options: []cmds.Option{
320 ke.OptionIPNSBase,
321 + cmds.StringOption(keyFormatOptionName, "f", "The format of the private key to import, libp2p-protobuf-cleartext or pem-pkcs8-cleartext.").WithDefault(keyFormatLibp2pCleartextOption),
322 },
323 Arguments: []cmds.Argument{
324 cmds.StringArg("name", true, false, "name to associate with key in keychain"),
@@ -265,9 +347,48 @@ var keyImportCmd = &cmds.Command{
347 return err
348 }
349
268 - sk, err := crypto.UnmarshalPrivateKey(data)
269 - if err != nil {
270 - return err
350 + importFormat, _ := req.Options[keyFormatOptionName].(string)
351 + var sk crypto.PrivKey
352 + switch importFormat {
353 + case keyFormatPemCleartextOption:
354 + pemBlock, rest := pem.Decode(data)
355 + if pemBlock == nil {
356 + return fmt.Errorf("PEM block not found in input data:\n%s", rest)
357 + }
358 +
359 + if pemBlock.Type != "PRIVATE KEY" {
360 + return fmt.Errorf("expected PRIVATE KEY type in PEM block but got: %s", pemBlock.Type)
361 + }
362 +
363 + stdKey, err := x509.ParsePKCS8PrivateKey(pemBlock.Bytes)
364 + if err != nil {
365 + return fmt.Errorf("parsing PKCS8 format: %w", err)
366 + }
367 +
368 + // In case ed25519.PrivateKey is returned we need the pointer for
369 + // conversion to libp2p (see export command for more details).
370 + if ed25519KeyPointer, ok := stdKey.(ed25519.PrivateKey); ok {
371 + stdKey = &ed25519KeyPointer
372 + }
373 +
374 + sk, _, err = crypto.KeyPairFromStdKey(stdKey)
375 + if err != nil {
376 + return fmt.Errorf("converting std Go key to libp2p key: %w", err)
377 +
378 + }
379 + case keyFormatLibp2pCleartextOption:
380 + sk, err = crypto.UnmarshalPrivateKey(data)
381 + if err != nil {
382 + // check if data is PEM, if so, provide user with hint
383 + pemBlock, _ := pem.Decode(data)
384 + if pemBlock != nil {
385 + return fmt.Errorf("unexpected PEM block for format=%s: try again with format=%s", keyFormatLibp2pCleartextOption, keyFormatPemCleartextOption)
386 + }
387 + return fmt.Errorf("unable to unmarshall format=%s: %w", keyFormatLibp2pCleartextOption, err)
388 + }
389 +
390 + default:
391 + return fmt.Errorf("unrecognized import format: %s", importFormat)
392 }
393
394 cfgRoot, err := cmdenv.GetConfigRoot(env)
test/sharness/t0165-keystore-data/README.md new
+8
@@ -0,0 +1,8 @@
1 +# OpenSSL generated keys for import/export tests
2 +
3 +Created with commands:
4 +
5 +```bash
6 +openssl genpkey -algorithm ED25519 > openssl_ed25519.pem
7 +openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 > openssl_rsa.pem
8 +```
test/sharness/t0165-keystore-data/openssl_ed25519.pem new
+3
@@ -0,0 +1,3 @@
1 +-----BEGIN PRIVATE KEY-----
2 +MC4CAQAwBQYDK2VwBCIEIJ2M1na2f3dRm4b1FcAQvsn7q08+XfBZcr4MgH4yiBdz
3 +-----END PRIVATE KEY-----
test/sharness/t0165-keystore-data/openssl_rsa.pem new
+28
@@ -0,0 +1,28 @@
1 +-----BEGIN PRIVATE KEY-----
2 +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDSaJB9EKnShOs6
3 +sbGkB40crn72yNKXj5OBPS2wBDTHWwxyhTB0qJirOT2QYW2DmR/4lPfVk5/f4CJ7
4 +xIHUBJRoC+NTwqHit24DQBd00tNG4EnKn2Dad/arZ/nEVshkKiGXn0qXxiHHsaCn
5 +X/pnVPU4+O7fdfUlz2EKf3Og/ocRCFrdMsULR2QwDc0YWsY8ngrcKegyFCbKjXjo
6 +zvfbGevCDPlhKaZLxRy0PHnON00YC4KO6d77XpbECFvsE1aG1RxYQX0Zjr+i8UvD
7 +UJp/YCoRNEX54/wKpGebMUrFse5K9hBsFen/wCsPnOsYPSb9g8qyoYRDBnr9sIe1
8 +9MxFTMy/AgMBAAECggEAKXu2KQI1CS1tlzfbdySJ/MKmg49afckv4sYmENLzeO6J
9 +iLabtBRdbTyu151t0wlIlWEBb9lYJvJwuggnNJ7mh5D4c9YmxqU1imyDc2PxhcLI
10 +qas8lDYcqvSn+L7HaYAo+VTNhxjoJg/uRbGVk/PbGS1zIxmFiLvXPROdv3sPNBsf
11 +EYMDH9q7/8DI6dNBQPxtTKlTDLDsTezbkNFQ74znlXgQYcfY1mXljcRtbJqhQJT3
12 +uppktESPwLRmqtT9H+v9nCtQR6OLmAmLWNgMrSdGKBsSsgJwv2xfpNMffwd84dtT
13 +uGrS2K+BY0TH2q+Xx04r18GLCst3U5MBSklyHQ/mwQKBgQDqnxNOnK41/n/Q8X4a
14 +/TUnZBx/JHiCoQoa06AsMxFgOvV3ycR+Z9lwb5I5BsicH1GUcHIxSY3mCyd4fLwE
15 +FC0QIyNhPJ5oFKh0Oynjm+79VE8v7kK2qqRL4zUpaCXEsSOrhRsCY0/WQdMUPVsh
16 +okXDUIv37G9KUcjdrhNVpGK3oQKBgQDllK7augIhmlQZTdSLTgmuzhYsXdSGDML/
17 +Bx48q7OvPhvZIIOsygLGhtcBk2xG6PN1yP44cx9dvcTnzxU6TEblO5P8TWY0BSNj
18 +ZuC5wdxLwc3KUdLd9JLR7qcbjqndDruE01rQFVQ3MDbyB1+VrJgiVHIEomJJrKGm
19 +FQ+314moXwKBgQDL90sDlnZk/kED1k15DRN+kSus5HnXpkRwmfWvNx4t+FOZtdCa
20 +y5Fei8Akz17rStbTIwZDDtzLVnsT5exV52xdkQ6a4+YaOYtQsHZ0JwWXOgo1cv6Q
21 +ary2NGns+1uKKS0HWYnng4rOix8Dg2uMS9Q2PfnQqLz/cSYcgc7RLz2awQKBgQDd
22 +HSaLYztKQeldtahPwwlwYuzYLkbSFNh559EnfffBgIAxzy8C7E1gB95sliBi61oQ
23 +x1SR6c776hoLaVd4np5picgt6B3XXFuJETy/rAcQr8gUZFpDi5sctk4cLHtNfTL9
24 +6tI8N061GKrS0GcvMNwVtF9cN0mSy8GkxAQvfFgI4QKBgQC4NVimIPptfFckulAL
25 +/t0vkdLhCRr1+UFNhgsQJhCZpfWZK4x8If6Jru/eiU7ywEsL6fHE2ENvyoTjV33g
26 +b9yJ7SV4zkz4VhBxc3p26SIvBgLqtHwH8IkIonlbfQFoEAg1iOneLvimPy0YGHsG
27 ++bTwwlAJJhctILkFtAbooeAQVQ==
28 +-----END PRIVATE KEY-----
test/sharness/t0165-keystore.sh
+68 -18
@@ -63,24 +63,16 @@ ipfs key rm key_ed25519
63 echo $rsahash > rsa_key_id
64 '
65
66 + test_key_import_export_all_formats rsa_key
67 +
68 test_expect_success "create a new ed25519 key" '
69 edhash=$(ipfs key gen generated_ed25519_key --type=ed25519)
70 echo $edhash > ed25519_key_id
71 '
72
71 - test_expect_success "export and import rsa key" '
72 - ipfs key export generated_rsa_key &&
73 - ipfs key rm generated_rsa_key &&
74 - ipfs key import generated_rsa_key generated_rsa_key.key > roundtrip_rsa_key_id &&
75 - test_cmp rsa_key_id roundtrip_rsa_key_id
76 - '
73 + test_key_import_export_all_formats ed25519_key
74
78 - test_expect_success "export and import ed25519 key" '
79 - ipfs key export generated_ed25519_key &&
80 - ipfs key rm generated_ed25519_key &&
81 - ipfs key import generated_ed25519_key generated_ed25519_key.key > roundtrip_ed25519_key_id &&
82 - test_cmp ed25519_key_id roundtrip_ed25519_key_id
83 - '
75 + test_openssl_compatibility_all_types
76
77 test_expect_success "test export file option" '
78 ipfs key export generated_rsa_key -o=named_rsa_export_file &&
@@ -176,15 +168,15 @@ ipfs key rm key_ed25519
168 '
169
170 # export works directly on the keystore present in IPFS_PATH
179 - test_expect_success "export and import ed25519 key while daemon is running" '
180 - edhash=$(ipfs key gen exported_ed25519_key --type=ed25519)
171 + test_expect_success "prepare ed25519 key while daemon is running" '
172 + edhash=$(ipfs key gen generated_ed25519_key --type=ed25519)
173 echo $edhash > ed25519_key_id
182 - ipfs key export exported_ed25519_key &&
183 - ipfs key rm exported_ed25519_key &&
184 - ipfs key import exported_ed25519_key exported_ed25519_key.key > roundtrip_ed25519_key_id &&
185 - test_cmp ed25519_key_id roundtrip_ed25519_key_id
174 '
175
176 + test_key_import_export_all_formats ed25519_key
177 +
178 + test_openssl_compatibility_all_types
179 +
180 test_expect_success "key export over HTTP /api/v0/key/export is not possible" '
181 ipfs key gen nohttpexporttest_key --type=ed25519 &&
182 curl -X POST -sI "http://$API_ADDR/api/v0/key/export&arg=nohttpexporttest_key" | grep -q "^HTTP/1.1 404 Not Found"
@@ -214,6 +206,64 @@ test_check_ed25519_sk() {
206 }
207 }
208
209 +test_key_import_export_all_formats() {
210 + KEY_NAME=$1
211 + test_key_import_export $KEY_NAME pem-pkcs8-cleartext
212 + test_key_import_export $KEY_NAME libp2p-protobuf-cleartext
213 +}
214 +
215 +test_key_import_export() {
216 + local KEY_NAME FORMAT
217 + KEY_NAME=$1
218 + FORMAT=$2
219 + ORIG_KEY="generated_$KEY_NAME"
220 + if [ $FORMAT == "pem-pkcs8-cleartext" ]; then
221 + FILE_EXT="pem"
222 + else
223 + FILE_EXT="key"
224 + fi
225 +
226 + test_expect_success "export and import $KEY_NAME with format $FORMAT" '
227 + ipfs key export $ORIG_KEY --format=$FORMAT &&
228 + ipfs key rm $ORIG_KEY &&
229 + ipfs key import $ORIG_KEY $ORIG_KEY.$FILE_EXT --format=$FORMAT > imported_key_id &&
230 + test_cmp ${KEY_NAME}_id imported_key_id
231 + '
232 +}
233 +
234 +# Test the entire import/export cycle with a openssl-generated key.
235 +# 1. Import openssl key with PEM format.
236 +# 2. Export key with libp2p format.
237 +# 3. Reimport key.
238 +# 4. Now exported with PEM format.
239 +# 5. Compare with original openssl key.
240 +# 6. Clean up.
241 +test_openssl_compatibility() {
242 + local KEY_NAME FORMAT
243 + KEY_NAME=$1
244 +
245 + test_expect_success "import and export $KEY_NAME with all formats" '
246 + ipfs key import test-openssl -f pem-pkcs8-cleartext $KEY_NAME > /dev/null &&
247 + ipfs key export test-openssl -f libp2p-protobuf-cleartext -o $KEY_NAME.libp2p.key &&
248 + ipfs key rm test-openssl &&
249 +
250 + ipfs key import test-openssl -f libp2p-protobuf-cleartext $KEY_NAME.libp2p.key > /dev/null &&
251 + ipfs key export test-openssl -f pem-pkcs8-cleartext -o $KEY_NAME.ipfs-exported.pem &&
252 + ipfs key rm test-openssl &&
253 +
254 + test_cmp $KEY_NAME $KEY_NAME.ipfs-exported.pem &&
255 +
256 + rm $KEY_NAME.libp2p.key &&
257 + rm $KEY_NAME.ipfs-exported.pem
258 + '
259 +}
260 +
261 +test_openssl_compatibility_all_types() {
262 + test_openssl_compatibility ../t0165-keystore-data/openssl_ed25519.pem
263 + test_openssl_compatibility ../t0165-keystore-data/openssl_rsa.pem
264 +}
265 +
266 +
267 test_key_cmd
268
269 test_done