Skip to content
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: ipfs key sign|verify #10235

Merged
merged 7 commits into from
Dec 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions client/rpc/key.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package rpc

import (
"bytes"
"context"
"errors"

Expand All @@ -9,6 +10,7 @@ import (
iface "github.com/ipfs/kubo/core/coreiface"
caopts "github.com/ipfs/kubo/core/coreiface/options"
"github.com/libp2p/go-libp2p/core/peer"
"github.com/multiformats/go-multibase"
)

type KeyAPI HttpApi
Expand Down Expand Up @@ -141,3 +143,53 @@ func (api *KeyAPI) Remove(ctx context.Context, name string) (iface.Key, error) {
func (api *KeyAPI) core() *HttpApi {
return (*HttpApi)(api)
}

func (api *KeyAPI) Sign(ctx context.Context, name string, data []byte) (iface.Key, []byte, error) {
var out struct {
Key keyOutput
Signature string
}

err := api.core().Request("key/sign").
Option("key", name).
FileBody(bytes.NewReader(data)).
Exec(ctx, &out)
if err != nil {
return nil, nil, err
}

key, err := newKey(out.Key.Name, out.Key.Id)
if err != nil {
return nil, nil, err
}

_, signature, err := multibase.Decode(out.Signature)
if err != nil {
return nil, nil, err
}

return key, signature, nil
}

func (api *KeyAPI) Verify(ctx context.Context, keyOrName string, signature, data []byte) (iface.Key, bool, error) {
var out struct {
Key keyOutput
SignatureValid bool
}

err := api.core().Request("key/verify").
Option("key", keyOrName).
Option("signature", toMultibase(signature)).
Comment on lines +181 to +182
Copy link
Contributor

@Jorropo Jorropo Nov 30, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we devise a human readable combined scheme ?
$PEERID-$SIG ?
Can't use PEERID for RSA but else should be fine.


Else for ECC you can omit the public key entirely and do public key recovery (so we would print the signer's PeerID instead of saying correct or incorrect).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean the signature to include the peer ID such that we do not need to provide the key?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, forget the ECC thing it's not what we are trying to do right now.

Instead of -key Qmfoo -sig zMM... Qmfoo-zMM ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Jorropo I see how that could be nice: a single signature that already includes the key, a single value to pass around. However, I think whoever is trying to verify the signature always knows which Peer ID they want to verify it against.

It would also make it harder to choose your own key by name to verify, no?

Just trying to see what would be best.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand what you mean, if it already contains the key, why do I need to specify the peer id ?

Copy link
Member

@lidel lidel Dec 1, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We want this API to accept key as a pet name (ipfs key list) or the actual libp2p-key (peerid).

Combining key and sig into a single arg makes the meaning of mfoo-mbar-mbuz ambiguous because we allow - in pet names AND mbase64url:

  • is the key name mfoo or mfoo-mbar?
  • is the signature mbar-mbuz or mbuz? (mind that - is part of base64url alphabet)

Someone would have to invent additional syntax or parsing rules for this.
My suggestion is to avoid sinking unnecessary time into answering these questions and writing tests for all edge cases – keep it simple, use separate params.

FileBody(bytes.NewReader(data)).
Exec(ctx, &out)
if err != nil {
return nil, false, err
}

key, err := newKey(out.Key.Name, out.Key.Id)
if err != nil {
return nil, false, err
}

return key, out.SignatureValid, nil
}
2 changes: 2 additions & 0 deletions core/commands/commands_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,8 @@ func TestCommands(t *testing.T) {
"/key/rename",
"/key/rm",
"/key/rotate",
"/key/sign",
"/key/verify",
"/log",
"/log/level",
"/log/ls",
Expand Down
136 changes: 136 additions & 0 deletions core/commands/keystore.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
migrations "github.com/ipfs/kubo/repo/fsrepo/migrations"
"github.com/libp2p/go-libp2p/core/crypto"
peer "github.com/libp2p/go-libp2p/core/peer"
mbase "github.com/multiformats/go-multibase"
)

var KeyCmd = &cmds.Command{
Expand Down Expand Up @@ -51,6 +52,8 @@ publish'.
"rename": keyRenameCmd,
"rm": keyRmCmd,
"rotate": keyRotateCmd,
"sign": keySignCmd,
"verify": keyVerifyCmd,
},
}

Expand Down Expand Up @@ -688,6 +691,139 @@ func keyOutputListEncoders() cmds.EncoderFunc {
})
}

type KeySignOutput struct {
Key KeyOutput
Signature string
}

var keySignCmd = &cmds.Command{
Status: cmds.Experimental,
Helptext: cmds.HelpText{
Tagline: "Generates a signature for the given data with a specified key. Useful for proving the key ownership.",
LongDescription: `
Sign arbitrary bytes, such as to prove ownership of a Peer ID or an IPNS Name.
To avoid signature reuse, the signed payload is always prefixed with
"libp2p-key signed message:".
`,
},
Options: []cmds.Option{
cmds.StringOption("key", "k", "The name of the key to use for signing."),
ke.OptionIPNSBase,
},
Arguments: []cmds.Argument{
cmds.FileArg("data", true, false, "The data to sign.").EnableStdin(),
},
Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
api, err := cmdenv.GetApi(env, req)
if err != nil {
return err
}
keyEnc, err := ke.KeyEncoderFromString(req.Options[ke.OptionIPNSBase.Name()].(string))
if err != nil {
return err
}

name, _ := req.Options["key"].(string)

file, err := cmdenv.GetFileArg(req.Files.Entries())
if err != nil {
return err
}
defer file.Close()

data, err := io.ReadAll(file)
if err != nil {
return err
}

key, signature, err := api.Key().Sign(req.Context, name, data)
if err != nil {
return err
}

encodedSignature, err := mbase.Encode(mbase.Base64url, signature)
if err != nil {
return err
}

return res.Emit(&KeySignOutput{
Key: KeyOutput{
Name: key.Name(),
Id: keyEnc.FormatID(key.ID()),
},
Signature: encodedSignature,
})
},
Type: KeySignOutput{},
}

type KeyVerifyOutput struct {
Key KeyOutput
SignatureValid bool
}

var keyVerifyCmd = &cmds.Command{
Status: cmds.Experimental,
Helptext: cmds.HelpText{
Tagline: "Verify that the given data and signature match.",
LongDescription: `
Verify if the given data and signatures match. To avoid the signature reuse,
the signed payload is always prefixed with "libp2p-key signed message:".
`,
},
Options: []cmds.Option{
cmds.StringOption("key", "k", "The name of the key to use for signing."),
cmds.StringOption("signature", "s", "Multibase-encoded signature to verify."),
ke.OptionIPNSBase,
},
Arguments: []cmds.Argument{
cmds.FileArg("data", true, false, "The data to verify against the given signature.").EnableStdin(),
},
Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
api, err := cmdenv.GetApi(env, req)
if err != nil {
return err
}
keyEnc, err := ke.KeyEncoderFromString(req.Options[ke.OptionIPNSBase.Name()].(string))
if err != nil {
return err
}

name, _ := req.Options["key"].(string)
encodedSignature, _ := req.Options["signature"].(string)

_, signature, err := mbase.Decode(encodedSignature)
if err != nil {
return err
}

file, err := cmdenv.GetFileArg(req.Files.Entries())
if err != nil {
return err
}
defer file.Close()

data, err := io.ReadAll(file)
if err != nil {
return err
}

key, valid, err := api.Key().Verify(req.Context, name, signature, data)
if err != nil {
return err
}

return res.Emit(&KeyVerifyOutput{
Key: KeyOutput{
Name: key.Name(),
Id: keyEnc.FormatID(key.ID()),
},
SignatureValid: valid,
})
},
Type: KeyVerifyOutput{},
}

// DaemonNotRunning checks to see if the ipfs repo is locked, indicating that
// the daemon is running, and returns and error if the daemon is running.
func DaemonNotRunning(req *cmds.Request, env cmds.Environment) error {
Expand Down
80 changes: 80 additions & 0 deletions core/coreapi/key.go
Original file line number Diff line number Diff line change
Expand Up @@ -262,3 +262,83 @@ func (api *KeyAPI) Self(ctx context.Context) (coreiface.Key, error) {

return newKey("self", api.identity)
}

const signedMessagePrefix = "libp2p-key signed message:"

func (api *KeyAPI) Sign(ctx context.Context, name string, data []byte) (coreiface.Key, []byte, error) {
var (
sk crypto.PrivKey
err error
)
if name == "" || name == "self" {
name = "self"
sk = api.privateKey
} else {
sk, err = api.repo.Keystore().Get(name)
}
if err != nil {
return nil, nil, err
}

pid, err := peer.IDFromPrivateKey(sk)
if err != nil {
return nil, nil, err
}

key, err := newKey(name, pid)
if err != nil {
return nil, nil, err
}

data = append([]byte(signedMessagePrefix), data...)

sig, err := sk.Sign(data)
if err != nil {
return nil, nil, err
}

return key, sig, nil
}

func (api *KeyAPI) Verify(ctx context.Context, keyOrName string, signature, data []byte) (coreiface.Key, bool, error) {
var (
name string
pk crypto.PubKey
err error
)
if keyOrName == "" || keyOrName == "self" {
name = "self"
pk = api.privateKey.GetPublic()
} else if sk, err := api.repo.Keystore().Get(keyOrName); err == nil {
name = keyOrName
pk = sk.GetPublic()
} else if ipnsName, err := ipns.NameFromString(keyOrName); err == nil {
// This works for both IPNS names and Peer IDs.
name = ""
pk, err = ipnsName.Peer().ExtractPublicKey()
if err != nil {
return nil, false, err
}
} else {
return nil, false, fmt.Errorf("'%q' is not a known key, an IPNS Name, or a valid PeerID", keyOrName)
}

pid, err := peer.IDFromPublicKey(pk)
if err != nil {
return nil, false, err
}

key, err := newKey(name, pid)
if err != nil {
return nil, false, err
}

data = append([]byte(signedMessagePrefix), data...)

valid, err := pk.Verify(data, signature)
if err != nil {
return nil, false, err
}

return key, valid, nil
}
8 changes: 8 additions & 0 deletions core/coreiface/key.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,12 @@ type KeyAPI interface {

// Remove removes keys from keystore. Returns ipns path of the removed key
Remove(ctx context.Context, name string) (Key, error)

// Sign signs the given data with the key named name. Returns the key used
// for signing, the signature, and an error.
Sign(ctx context.Context, name string, data []byte) (Key, []byte, error)

// Verify verifies if the given data and signatures match. Returns the key used
// for verification, whether signature and data match, and an error.
Verify(ctx context.Context, keyOrName string, signature, data []byte) (Key, bool, error)
}
Loading