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