@@ -2,6 +2,10 @@ package commands
2
2
3
3
import (
4
4
"bytes"
5
+ "crypto/ed25519"
6
+ "crypto/rand"
7
+ "crypto/x509"
8
+ "encoding/pem"
5
9
"fmt"
6
10
"io"
7
11
"io/ioutil"
@@ -135,6 +139,15 @@ var keyGenCmd = &cmds.Command{
135
139
Type : KeyOutput {},
136
140
}
137
141
142
+ const (
143
+ // Key format options used both for importing and exporting.
144
+ keyFormatOptionName = "format"
145
+ keyFormatPemEncryptedOption = "pem-pkcs8-encrypted"
146
+ keyFormatPemCleartextOption = "pem-pkcs8-cleartext"
147
+ keyFormatLibp2pCleartextOption = "libp2p-protobuf-cleartext"
148
+ keyEncryptionPasswordOptionName = "password"
149
+ )
150
+
138
151
var keyExportCmd = & cmds.Command {
139
152
Helptext : cmds.HelpText {
140
153
Tagline : "Export a keypair" ,
@@ -150,6 +163,10 @@ path can be specified with '--output=<path>' or '-o=<path>'.
150
163
},
151
164
Options : []cmds.Option {
152
165
cmds .StringOption (outputOptionName , "o" , "The path where the output should be stored." ),
166
+ cmds .StringOption (keyFormatOptionName , "f" , "The format of the exported private key." ).WithDefault (keyFormatLibp2pCleartextOption ),
167
+ cmds .StringOption (keyEncryptionPasswordOptionName , "p" , "The password to encrypt the exported key with (for the encrypted variant only)." ),
168
+ // FIXME(BLOCKING): change default to keyFormatPemEncryptedOption once it
169
+ // is implemented and the sharness tests (if any) are adapted.
153
170
},
154
171
NoRemote : true ,
155
172
Run : func (req * cmds.Request , res cmds.ResponseEmitter , env cmds.Environment ) error {
@@ -186,12 +203,38 @@ path can be specified with '--output=<path>' or '-o=<path>'.
186
203
return fmt .Errorf ("key with name '%s' doesn't exist" , name )
187
204
}
188
205
189
- encoded , err := crypto .MarshalPrivateKey (sk )
190
- if err != nil {
191
- return err
206
+ exportFormat , _ := req .Options [keyFormatOptionName ].(string )
207
+ var formattedKey []byte
208
+ switch exportFormat {
209
+ case keyFormatPemEncryptedOption , keyFormatPemCleartextOption :
210
+ stdKey , err := crypto .PrivKeyToStdKey (sk )
211
+ if err != nil {
212
+ return fmt .Errorf ("converting libp2p private key to std Go key: %w" , err )
213
+
214
+ }
215
+ // For some reason the ed25519.PrivateKey does not use pointer
216
+ // receivers, so we need to convert it for MarshalPKCS8PrivateKey.
217
+ // (We should probably change this upstream in PrivKeyToStdKey).
218
+ if ed25519KeyPointer , ok := stdKey .(* ed25519.PrivateKey ); ok {
219
+ stdKey = * ed25519KeyPointer
220
+ }
221
+ // This function supports a restricted list of public key algorithms,
222
+ // but we generate and use only the RSA and ed25519 types that are on that list.
223
+ formattedKey , err = x509 .MarshalPKCS8PrivateKey (stdKey )
224
+ if err != nil {
225
+ return fmt .Errorf ("marshalling key to PKCS8 format: %w" , err )
226
+ }
227
+
228
+ case keyFormatLibp2pCleartextOption :
229
+ formattedKey , err = crypto .MarshalPrivateKey (sk )
230
+ if err != nil {
231
+ return err
232
+ }
233
+ default :
234
+ return fmt .Errorf ("unrecognized export format: %s" , exportFormat )
192
235
}
193
236
194
- return res .Emit (bytes .NewReader (encoded ))
237
+ return res .Emit (bytes .NewReader (formattedKey ))
195
238
},
196
239
PostRun : cmds.PostRunMap {
197
240
cmds .CLI : func (res cmds.Response , re cmds.ResponseEmitter ) error {
@@ -208,8 +251,16 @@ path can be specified with '--output=<path>' or '-o=<path>'.
208
251
}
209
252
210
253
outPath , _ := req .Options [outputOptionName ].(string )
254
+ exportFormat , _ := req .Options [keyFormatOptionName ].(string )
211
255
if outPath == "" {
212
- trimmed := strings .TrimRight (fmt .Sprintf ("%s.key" , req .Arguments [0 ]), "/" )
256
+ var fileExtension string
257
+ switch exportFormat {
258
+ case keyFormatPemEncryptedOption , keyFormatPemCleartextOption :
259
+ fileExtension = "pem"
260
+ case keyFormatLibp2pCleartextOption :
261
+ fileExtension = "key"
262
+ }
263
+ trimmed := strings .TrimRight (fmt .Sprintf ("%s.%s" , req .Arguments [0 ], fileExtension ), "/" )
213
264
_ , outPath = filepath .Split (trimmed )
214
265
outPath = filepath .Clean (outPath )
215
266
}
@@ -221,9 +272,46 @@ path can be specified with '--output=<path>' or '-o=<path>'.
221
272
}
222
273
defer file .Close ()
223
274
224
- _ , err = io .Copy (file , outReader )
225
- if err != nil {
226
- return err
275
+ switch exportFormat {
276
+ case keyFormatPemEncryptedOption , keyFormatPemCleartextOption :
277
+ privKeyBytes , err := ioutil .ReadAll (outReader )
278
+ if err != nil {
279
+ return err
280
+ }
281
+
282
+ var pemBlock * pem.Block
283
+ if exportFormat == keyFormatPemEncryptedOption {
284
+ keyEncPassword , ok := req .Options [keyEncryptionPasswordOptionName ].(string )
285
+ if ! ok {
286
+ return fmt .Errorf ("missing password to encrypt the key with, set it with --%s" ,
287
+ keyEncryptionPasswordOptionName )
288
+ }
289
+ // FIXME(BLOCKING): Using deprecated security function.
290
+ pemBlock , err = x509 .EncryptPEMBlock (rand .Reader ,
291
+ "ENCRYPTED PRIVATE KEY" ,
292
+ privKeyBytes ,
293
+ []byte (keyEncPassword ),
294
+ x509 .PEMCipherAES256 )
295
+ if err != nil {
296
+ return fmt .Errorf ("encrypting PEM block: %w" , err )
297
+ }
298
+ } else { // cleartext
299
+ pemBlock = & pem.Block {
300
+ Type : "PRIVATE KEY" ,
301
+ Bytes : privKeyBytes ,
302
+ }
303
+ }
304
+
305
+ err = pem .Encode (file , pemBlock )
306
+ if err != nil {
307
+ return fmt .Errorf ("encoding PEM block: %w" , err )
308
+ }
309
+
310
+ case keyFormatLibp2pCleartextOption :
311
+ _ , err = io .Copy (file , outReader )
312
+ if err != nil {
313
+ return err
314
+ }
227
315
}
228
316
229
317
return nil
@@ -237,6 +325,9 @@ var keyImportCmd = &cmds.Command{
237
325
},
238
326
Options : []cmds.Option {
239
327
ke .OptionIPNSBase ,
328
+ cmds .StringOption (keyFormatOptionName , "f" , "The format of the private key to import." ).WithDefault (keyFormatLibp2pCleartextOption ),
329
+ // FIXME: Attempt to figure out the import format.
330
+ cmds .StringOption (keyEncryptionPasswordOptionName , "p" , "The password to decrypt the imported key with (for the encrypted variant only)." ),
240
331
},
241
332
Arguments : []cmds.Argument {
242
333
cmds .StringArg ("name" , true , false , "name to associate with key in keychain" ),
@@ -265,9 +356,59 @@ var keyImportCmd = &cmds.Command{
265
356
return err
266
357
}
267
358
268
- sk , err := crypto .UnmarshalPrivateKey (data )
269
- if err != nil {
270
- return err
359
+ importFormat , _ := req .Options [keyFormatOptionName ].(string )
360
+ var sk crypto.PrivKey
361
+ switch importFormat {
362
+ case keyFormatPemEncryptedOption , keyFormatPemCleartextOption :
363
+ pemBlock , rest := pem .Decode (data )
364
+ if pemBlock == nil {
365
+ return fmt .Errorf ("PEM block not found in input data:\n %s" , rest )
366
+ }
367
+
368
+ if pemBlock .Type != "PRIVATE KEY" && pemBlock .Type != "ENCRYPTED PRIVATE KEY" {
369
+ return fmt .Errorf ("expected [ENCRYPTED] PRIVATE KEY type in PEM block but got: %s" , pemBlock .Type )
370
+ }
371
+
372
+ var privKeyBytes []byte
373
+ if importFormat == keyFormatPemEncryptedOption {
374
+ keyDecPassword , ok := req .Options [keyEncryptionPasswordOptionName ].(string )
375
+ if ! ok {
376
+ return fmt .Errorf ("missing password to decrypt the key with, set it with --%s" ,
377
+ keyEncryptionPasswordOptionName )
378
+ }
379
+ privKeyBytes , err = x509 .DecryptPEMBlock (pemBlock ,
380
+ []byte (keyDecPassword ))
381
+ if err != nil {
382
+ return fmt .Errorf ("decrypting PEM block: %w" , err )
383
+ }
384
+ } else { // cleartext
385
+ privKeyBytes = pemBlock .Bytes
386
+ }
387
+
388
+ stdKey , err := x509 .ParsePKCS8PrivateKey (privKeyBytes )
389
+ if err != nil {
390
+ return fmt .Errorf ("parsing PKCS8 format: %w" , err )
391
+ }
392
+
393
+ // In case ed25519.PrivateKey is returned we need the pointer for
394
+ // conversion to libp2p (see export command for more details).
395
+ if ed25519KeyPointer , ok := stdKey .(ed25519.PrivateKey ); ok {
396
+ stdKey = & ed25519KeyPointer
397
+ }
398
+
399
+ sk , _ , err = crypto .KeyPairFromStdKey (stdKey )
400
+ if err != nil {
401
+ return fmt .Errorf ("converting std Go key to libp2p key : %w" , err )
402
+
403
+ }
404
+ case keyFormatLibp2pCleartextOption :
405
+ sk , err = crypto .UnmarshalPrivateKey (data )
406
+ if err != nil {
407
+ return err
408
+ }
409
+
410
+ default :
411
+ return fmt .Errorf ("unrecognized import format: %s" , importFormat )
271
412
}
272
413
273
414
cfgRoot , err := cmdenv .GetConfigRoot (env )
0 commit comments