@@ -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,68 @@ 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
+ // FIXME(BLOCKING): At least verify some interoperability
310
+ // manually with something like `openssl pkcs8 -in private_key.pem`
311
+ // failing with:
312
+ // ```
313
+ // Error reading key
314
+ // error:0D0680A8:asn1 encoding routines:asn1_check_tlen:wrong tag:../crypto/asn1/tasn_dec.c:1130:
315
+ // error:0D07803A:asn1 encoding routines:asn1_item_embed_d2i:nested asn1 error:../crypto/asn1/tasn_dec.c:290:Type=X509_ALGOR
316
+ // error:0D08303A:asn1 encoding routines:asn1_template_noexp_d2i:nested asn1 error:../crypto/asn1/tasn_dec.c:627:Field=algor, Type=X509_SIG
317
+ // error:0906700D:PEM routines:PEM_ASN1_read_bio:ASN1 lib:../crypto/pem/pem_oth.c:33:
318
+ // ```
319
+ //
320
+ // For the moment checked with https://8gwifi.org/PemParserFunctions.jsp,
321
+ // which is working for RSA but failing for the ed25519 case with:
322
+ // ```
323
+ // java.lang.Exception: Error Performing Parsing java.lang.Exception:
324
+ // org.bouncycastle.openssl.PEMException:
325
+ // unable to convert key pair:
326
+ // no such algorithm: 1.3.101.112 for provider BC
327
+ // ```
328
+ // which is indeed the code per RFC:
329
+ // https://datatracker.ietf.org/doc/html/rfc8410#section-3
330
+ // (so this is probably a problem of the decoding package of the web page).
331
+
332
+ case keyFormatLibp2pCleartextOption :
333
+ _ , err = io .Copy (file , outReader )
334
+ if err != nil {
335
+ return err
336
+ }
227
337
}
228
338
229
339
return nil
@@ -237,6 +347,9 @@ var keyImportCmd = &cmds.Command{
237
347
},
238
348
Options : []cmds.Option {
239
349
ke .OptionIPNSBase ,
350
+ cmds .StringOption (keyFormatOptionName , "f" , "The format of the private key to import." ).WithDefault (keyFormatLibp2pCleartextOption ),
351
+ // FIXME: Attempt to figure out the import format.
352
+ cmds .StringOption (keyEncryptionPasswordOptionName , "p" , "The password to decrypt the imported key with (for the encrypted variant only)." ),
240
353
},
241
354
Arguments : []cmds.Argument {
242
355
cmds .StringArg ("name" , true , false , "name to associate with key in keychain" ),
@@ -265,9 +378,59 @@ var keyImportCmd = &cmds.Command{
265
378
return err
266
379
}
267
380
268
- sk , err := crypto .UnmarshalPrivateKey (data )
269
- if err != nil {
270
- return err
381
+ importFormat , _ := req .Options [keyFormatOptionName ].(string )
382
+ var sk crypto.PrivKey
383
+ switch importFormat {
384
+ case keyFormatPemEncryptedOption , keyFormatPemCleartextOption :
385
+ pemBlock , rest := pem .Decode (data )
386
+ if pemBlock == nil {
387
+ return fmt .Errorf ("PEM block not found in input data:\n %s" , rest )
388
+ }
389
+
390
+ if pemBlock .Type != "PRIVATE KEY" && pemBlock .Type != "ENCRYPTED PRIVATE KEY" {
391
+ return fmt .Errorf ("expected [ENCRYPTED] PRIVATE KEY type in PEM block but got: %s" , pemBlock .Type )
392
+ }
393
+
394
+ var privKeyBytes []byte
395
+ if importFormat == keyFormatPemEncryptedOption {
396
+ keyDecPassword , ok := req .Options [keyEncryptionPasswordOptionName ].(string )
397
+ if ! ok {
398
+ return fmt .Errorf ("missing password to decrypt the key with, set it with --%s" ,
399
+ keyEncryptionPasswordOptionName )
400
+ }
401
+ privKeyBytes , err = x509 .DecryptPEMBlock (pemBlock ,
402
+ []byte (keyDecPassword ))
403
+ if err != nil {
404
+ return fmt .Errorf ("decrypting PEM block: %w" , err )
405
+ }
406
+ } else { // cleartext
407
+ privKeyBytes = pemBlock .Bytes
408
+ }
409
+
410
+ stdKey , err := x509 .ParsePKCS8PrivateKey (privKeyBytes )
411
+ if err != nil {
412
+ return fmt .Errorf ("parsing PKCS8 format: %w" , err )
413
+ }
414
+
415
+ // In case ed25519.PrivateKey is returned we need the pointer for
416
+ // conversion to libp2p (see export command for more details).
417
+ if ed25519KeyPointer , ok := stdKey .(ed25519.PrivateKey ); ok {
418
+ stdKey = & ed25519KeyPointer
419
+ }
420
+
421
+ sk , _ , err = crypto .KeyPairFromStdKey (stdKey )
422
+ if err != nil {
423
+ return fmt .Errorf ("converting std Go key to libp2p key : %w" , err )
424
+
425
+ }
426
+ case keyFormatLibp2pCleartextOption :
427
+ sk , err = crypto .UnmarshalPrivateKey (data )
428
+ if err != nil {
429
+ return err
430
+ }
431
+
432
+ default :
433
+ return fmt .Errorf ("unrecognized import format: %s" , importFormat )
271
434
}
272
435
273
436
cfgRoot , err := cmdenv .GetConfigRoot (env )
0 commit comments