Skip to content

Commit

Permalink
feat: ipfs key sign|verify (#10235)
Browse files Browse the repository at this point in the history
  • Loading branch information
hacdias committed Dec 4, 2023
1 parent 7b05b5d commit 8ab2de5
Show file tree
Hide file tree
Showing 7 changed files with 498 additions and 322 deletions.
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)).
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)
}

0 comments on commit 8ab2de5

Please sign in to comment.