-
-
Notifications
You must be signed in to change notification settings - Fork 3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(cmds): add cleartext PEM/PKCS8 for key import/export #8616
Changes from all commits
f7a59cc
53dd92d
63dba38
23d7a47
87e1035
b1ef202
1d7e794
c7481ec
5c04762
6f43ed6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,9 @@ package commands | |
|
||
import ( | ||
"bytes" | ||
"crypto/ed25519" | ||
"crypto/x509" | ||
"encoding/pem" | ||
"fmt" | ||
"io" | ||
"io/ioutil" | ||
|
@@ -135,6 +138,13 @@ var keyGenCmd = &cmds.Command{ | |
Type: KeyOutput{}, | ||
} | ||
|
||
const ( | ||
// Key format options used both for importing and exporting. | ||
keyFormatOptionName = "format" | ||
keyFormatPemCleartextOption = "pem-pkcs8-cleartext" | ||
keyFormatLibp2pCleartextOption = "libp2p-protobuf-cleartext" | ||
) | ||
|
||
var keyExportCmd = &cmds.Command{ | ||
Helptext: cmds.HelpText{ | ||
Tagline: "Export a keypair", | ||
|
@@ -143,13 +153,21 @@ Exports a named libp2p key to disk. | |
|
||
By default, the output will be stored at './<key-name>.key', but an alternate | ||
path can be specified with '--output=<path>' or '-o=<path>'. | ||
|
||
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 | ||
`, | ||
}, | ||
Arguments: []cmds.Argument{ | ||
cmds.StringArg("name", true, false, "name of key to export").EnableStdin(), | ||
}, | ||
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, libp2p-protobuf-cleartext or pem-pkcs8-cleartext.").WithDefault(keyFormatLibp2pCleartextOption), | ||
}, | ||
NoRemote: true, | ||
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>'. | |
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 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 +252,16 @@ path can be specified with '--output=<path>' or '-o=<path>'. | |
} | ||
|
||
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 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 +273,26 @@ path can be specified with '--output=<path>' or '-o=<path>'. | |
} | ||
defer file.Close() | ||
|
||
_, err = io.Copy(file, outReader) | ||
if err != nil { | ||
return err | ||
switch exportFormat { | ||
case keyFormatPemCleartextOption: | ||
privKeyBytes, err := ioutil.ReadAll(outReader) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
err = pem.Encode(file, &pem.Block{ | ||
Type: "PRIVATE KEY", | ||
Bytes: privKeyBytes, | ||
}) | ||
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 | ||
|
@@ -234,9 +303,22 @@ path can be specified with '--output=<path>' or '-o=<path>'. | |
var keyImportCmd = &cmds.Command{ | ||
Helptext: cmds.HelpText{ | ||
Tagline: "Import a key and prints imported key id", | ||
ShortDescription: ` | ||
Imports a key and stores it under the provided name. | ||
|
||
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'. | ||
|
||
The PEM format allows for key generation outside of the IPFS node: | ||
|
||
$ openssl genpkey -algorithm ED25519 > ed25519.pem | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We don't have to add Hopefully should be relatively straightforward, but if not it's not strictly necessary. |
||
$ ipfs key import test-openssl -f pem-pkcs8-cleartext ed25519.pem | ||
Comment on lines
+315
to
+316
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This example is the best remainder of why we're doing this. It's pretty cool to be able to easily interoperate with |
||
`, | ||
}, | ||
Options: []cmds.Option{ | ||
ke.OptionIPNSBase, | ||
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"), | ||
|
@@ -265,9 +347,48 @@ 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 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" { | ||
return fmt.Errorf("expected PRIVATE KEY type in PEM block but got: %s", pemBlock.Type) | ||
} | ||
|
||
stdKey, err := x509.ParsePKCS8PrivateKey(pemBlock.Bytes) | ||
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 { | ||
// 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: | ||
return fmt.Errorf("unrecognized import format: %s", importFormat) | ||
} | ||
|
||
cfgRoot, err := cmdenv.GetConfigRoot(env) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
-----BEGIN PRIVATE KEY----- | ||
MC4CAQAwBQYDK2VwBCIEIJ2M1na2f3dRm4b1FcAQvsn7q08+XfBZcr4MgH4yiBdz | ||
-----END PRIVATE KEY----- |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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----- |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wow, very weird that
MarshalPKCS8PrivateKey
has docs calling out ed25519 pointers as special: