From 8ab2de5ff05e1427a22be868b31efbcd58a6ee13 Mon Sep 17 00:00:00 2001 From: Henrique Dias Date: Mon, 4 Dec 2023 09:51:26 +0100 Subject: [PATCH] feat: ipfs key sign|verify (#10235) --- client/rpc/key.go | 52 ++++ core/commands/commands_test.go | 2 + core/commands/keystore.go | 136 +++++++++ core/coreapi/key.go | 80 +++++ core/coreiface/key.go | 8 + core/coreiface/tests/key.go | 533 +++++++++++++-------------------- docs/changelogs/v0.25.md | 9 + 7 files changed, 498 insertions(+), 322 deletions(-) diff --git a/client/rpc/key.go b/client/rpc/key.go index daddffb2399..710d9fb06d2 100644 --- a/client/rpc/key.go +++ b/client/rpc/key.go @@ -1,6 +1,7 @@ package rpc import ( + "bytes" "context" "errors" @@ -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 @@ -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 +} diff --git a/core/commands/commands_test.go b/core/commands/commands_test.go index a73a0338e50..a34aab4488f 100644 --- a/core/commands/commands_test.go +++ b/core/commands/commands_test.go @@ -164,6 +164,8 @@ func TestCommands(t *testing.T) { "/key/rename", "/key/rm", "/key/rotate", + "/key/sign", + "/key/verify", "/log", "/log/level", "/log/ls", diff --git a/core/commands/keystore.go b/core/commands/keystore.go index 2ad2f7dbd03..a86fb281af3 100644 --- a/core/commands/keystore.go +++ b/core/commands/keystore.go @@ -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{ @@ -51,6 +52,8 @@ publish'. "rename": keyRenameCmd, "rm": keyRmCmd, "rotate": keyRotateCmd, + "sign": keySignCmd, + "verify": keyVerifyCmd, }, } @@ -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 { diff --git a/core/coreapi/key.go b/core/coreapi/key.go index e78868067c7..a6101dae826 100644 --- a/core/coreapi/key.go +++ b/core/coreapi/key.go @@ -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 +} diff --git a/core/coreiface/key.go b/core/coreiface/key.go index 9d61cc95b33..6125e593b84 100644 --- a/core/coreiface/key.go +++ b/core/coreiface/key.go @@ -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) } diff --git a/core/coreiface/tests/key.go b/core/coreiface/tests/key.go index c4c86b74859..90936b0e2a4 100644 --- a/core/coreiface/tests/key.go +++ b/core/coreiface/tests/key.go @@ -5,10 +5,14 @@ import ( "strings" "testing" + "github.com/ipfs/boxo/ipns" "github.com/ipfs/go-cid" iface "github.com/ipfs/kubo/core/coreiface" opt "github.com/ipfs/kubo/core/coreiface/options" + "github.com/libp2p/go-libp2p/core/peer" mbase "github.com/multiformats/go-multibase" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func (tp *TestSuite) TestKey(t *testing.T) { @@ -34,151 +38,90 @@ func (tp *TestSuite) TestKey(t *testing.T) { t.Run("TestRenameOverwrite", tp.TestRenameOverwrite) t.Run("TestRenameSameNameNoForce", tp.TestRenameSameNameNoForce) t.Run("TestRenameSameName", tp.TestRenameSameName) - t.Run("TestRemove", tp.TestRemove) + t.Run("TestSign", tp.TestSign) + t.Run("TestVerify", tp.TestVerify) } func (tp *TestSuite) TestListSelf(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() + api, err := tp.makeAPI(t, ctx) - if err != nil { - t.Fatal(err) - return - } + require.NoError(t, err) self, err := api.Key().Self(ctx) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) keys, err := api.Key().List(ctx) - if err != nil { - t.Fatalf("failed to list keys: %s", err) - return - } - - if len(keys) != 1 { - t.Fatalf("there should be 1 key (self), got %d", len(keys)) - return - } - - if keys[0].Name() != "self" { - t.Errorf("expected the key to be called 'self', got '%s'", keys[0].Name()) - } - - if keys[0].Path().String() != "/ipns/"+iface.FormatKeyID(self.ID()) { - t.Errorf("expected the key to have path '/ipns/%s', got '%s'", iface.FormatKeyID(self.ID()), keys[0].Path().String()) - } + require.NoError(t, err) + require.Len(t, keys, 1) + assert.Equal(t, "self", keys[0].Name()) + assert.Equal(t, "/ipns/"+iface.FormatKeyID(self.ID()), keys[0].Path().String()) } func (tp *TestSuite) TestRenameSelf(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() + api, err := tp.makeAPI(t, ctx) - if err != nil { - t.Fatal(err) - return - } + require.NoError(t, err) _, _, err = api.Key().Rename(ctx, "self", "foo") - if err == nil { - t.Error("expected error to not be nil") - } else { - if !strings.Contains(err.Error(), "cannot rename key with name 'self'") { - t.Fatalf("expected error 'cannot rename key with name 'self'', got '%s'", err.Error()) - } - } + require.ErrorContains(t, err, "cannot rename key with name 'self'") _, _, err = api.Key().Rename(ctx, "self", "foo", opt.Key.Force(true)) - if err == nil { - t.Error("expected error to not be nil") - } else { - if !strings.Contains(err.Error(), "cannot rename key with name 'self'") { - t.Fatalf("expected error 'cannot rename key with name 'self'', got '%s'", err.Error()) - } - } + require.ErrorContains(t, err, "cannot rename key with name 'self'") } func (tp *TestSuite) TestRemoveSelf(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() + api, err := tp.makeAPI(t, ctx) - if err != nil { - t.Fatal(err) - return - } + require.NoError(t, err) _, err = api.Key().Remove(ctx, "self") - if err == nil { - t.Error("expected error to not be nil") - } else { - if !strings.Contains(err.Error(), "cannot remove key with name 'self'") { - t.Fatalf("expected error 'cannot remove key with name 'self'', got '%s'", err.Error()) - } - } + require.ErrorContains(t, err, "cannot remove key with name 'self'") } func (tp *TestSuite) TestGenerate(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() + api, err := tp.makeAPI(t, ctx) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) k, err := api.Key().Generate(ctx, "foo") - if err != nil { - t.Fatal(err) - return - } - - if k.Name() != "foo" { - t.Errorf("expected the key to be called 'foo', got '%s'", k.Name()) - } + require.NoError(t, err) + require.Equal(t, "foo", k.Name()) verifyIPNSPath(t, k.Path().String()) } -func verifyIPNSPath(t *testing.T, p string) bool { +func verifyIPNSPath(t *testing.T, p string) { t.Helper() - if !strings.HasPrefix(p, "/ipns/") { - t.Errorf("path %q does not look like an IPNS path", p) - return false - } + + require.True(t, strings.HasPrefix(p, "/ipns/")) + k := p[len("/ipns/"):] c, err := cid.Decode(k) - if err != nil { - t.Errorf("failed to decode IPNS key %q (%v)", k, err) - return false - } + require.NoError(t, err) + b36, err := c.StringOfBase(mbase.Base36) - if err != nil { - t.Fatalf("cid cannot format itself in b36") - return false - } - if b36 != k { - t.Errorf("IPNS key is not base36") - } - return true + require.NoError(t, err) + require.Equal(t, k, b36) } func (tp *TestSuite) TestGenerateSize(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() + api, err := tp.makeAPI(t, ctx) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) k, err := api.Key().Generate(ctx, "foo", opt.Key.Size(2048)) - if err != nil { - t.Fatal(err) - return - } - - if k.Name() != "foo" { - t.Errorf("expected the key to be called 'foo', got '%s'", k.Name()) - } + require.NoError(t, err) + require.Equal(t, "foo", k.Name()) verifyIPNSPath(t, k.Path().String()) } @@ -190,93 +133,47 @@ func (tp *TestSuite) TestGenerateType(t *testing.T) { defer cancel() api, err := tp.makeAPI(t, ctx) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) k, err := api.Key().Generate(ctx, "bar", opt.Key.Type(opt.Ed25519Key)) - if err != nil { - t.Fatal(err) - return - } - - if k.Name() != "bar" { - t.Errorf("expected the key to be called 'foo', got '%s'", k.Name()) - } - + require.NoError(t, err) + require.Equal(t, "bar", k.Name()) // Expected to be an inlined identity hash. - if !strings.HasPrefix(k.Path().String(), "/ipns/12") { - t.Errorf("expected the key to be prefixed with '/ipns/12', got '%s'", k.Path().String()) - } + require.True(t, strings.HasPrefix(k.Path().String(), "/ipns/12")) } func (tp *TestSuite) TestGenerateExisting(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() + api, err := tp.makeAPI(t, ctx) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) _, err = api.Key().Generate(ctx, "foo") - if err != nil { - t.Fatal(err) - return - } + require.NoError(t, err) _, err = api.Key().Generate(ctx, "foo") - if err == nil { - t.Error("expected error to not be nil") - } else { - if !strings.Contains(err.Error(), "key with name 'foo' already exists") { - t.Fatalf("expected error 'key with name 'foo' already exists', got '%s'", err.Error()) - } - } + require.ErrorContains(t, err, "key with name 'foo' already exists") _, err = api.Key().Generate(ctx, "self") - if err == nil { - t.Error("expected error to not be nil") - } else { - if !strings.Contains(err.Error(), "cannot create key with name 'self'") { - t.Fatalf("expected error 'cannot create key with name 'self'', got '%s'", err.Error()) - } - } + require.ErrorContains(t, err, "cannot create key with name 'self'") } func (tp *TestSuite) TestList(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() + api, err := tp.makeAPI(t, ctx) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) _, err = api.Key().Generate(ctx, "foo") - if err != nil { - t.Fatal(err) - return - } + require.NoError(t, err) l, err := api.Key().List(ctx) - if err != nil { - t.Fatal(err) - return - } - - if len(l) != 2 { - t.Fatalf("expected to get 2 keys, got %d", len(l)) - return - } - - if l[0].Name() != "self" { - t.Fatalf("expected key 0 to be called 'self', got '%s'", l[0].Name()) - return - } - - if l[1].Name() != "foo" { - t.Fatalf("expected key 1 to be called 'foo', got '%s'", l[1].Name()) - return - } + require.NoError(t, err) + require.Len(t, l, 2) + require.Equal(t, "self", l[0].Name()) + require.Equal(t, "foo", l[1].Name()) verifyIPNSPath(t, l[0].Path().String()) verifyIPNSPath(t, l[1].Path().String()) @@ -285,254 +182,246 @@ func (tp *TestSuite) TestList(t *testing.T) { func (tp *TestSuite) TestRename(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() + api, err := tp.makeAPI(t, ctx) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) _, err = api.Key().Generate(ctx, "foo") - if err != nil { - t.Fatal(err) - return - } + require.NoError(t, err) k, overwrote, err := api.Key().Rename(ctx, "foo", "bar") - if err != nil { - t.Fatal(err) - return - } - - if overwrote { - t.Error("overwrote should be false") - } - - if k.Name() != "bar" { - t.Errorf("returned key should be called 'bar', got '%s'", k.Name()) - } + require.NoError(t, err) + assert.False(t, overwrote) + assert.Equal(t, "bar", k.Name()) } func (tp *TestSuite) TestRenameToSelf(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() + api, err := tp.makeAPI(t, ctx) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) _, err = api.Key().Generate(ctx, "foo") - if err != nil { - t.Fatal(err) - return - } + require.NoError(t, err) _, _, err = api.Key().Rename(ctx, "foo", "self") - if err == nil { - t.Error("expected error to not be nil") - } else { - if !strings.Contains(err.Error(), "cannot overwrite key with name 'self'") { - t.Fatalf("expected error 'cannot overwrite key with name 'self'', got '%s'", err.Error()) - } - } + require.ErrorContains(t, err, "cannot overwrite key with name 'self'") } func (tp *TestSuite) TestRenameToSelfForce(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() + api, err := tp.makeAPI(t, ctx) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) _, err = api.Key().Generate(ctx, "foo") - if err != nil { - t.Fatal(err) - return - } + require.NoError(t, err) _, _, err = api.Key().Rename(ctx, "foo", "self", opt.Key.Force(true)) - if err == nil { - t.Error("expected error to not be nil") - } else { - if !strings.Contains(err.Error(), "cannot overwrite key with name 'self'") { - t.Fatalf("expected error 'cannot overwrite key with name 'self'', got '%s'", err.Error()) - } - } + require.ErrorContains(t, err, "cannot overwrite key with name 'self'") } func (tp *TestSuite) TestRenameOverwriteNoForce(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() + api, err := tp.makeAPI(t, ctx) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) _, err = api.Key().Generate(ctx, "foo") - if err != nil { - t.Fatal(err) - return - } + require.NoError(t, err) _, err = api.Key().Generate(ctx, "bar") - if err != nil { - t.Fatal(err) - return - } + require.NoError(t, err) _, _, err = api.Key().Rename(ctx, "foo", "bar") - if err == nil { - t.Error("expected error to not be nil") - } else { - if !strings.Contains(err.Error(), "key by that name already exists, refusing to overwrite") { - t.Fatalf("expected error 'key by that name already exists, refusing to overwrite', got '%s'", err.Error()) - } - } + require.ErrorContains(t, err, "key by that name already exists, refusing to overwrite") } func (tp *TestSuite) TestRenameOverwrite(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() + api, err := tp.makeAPI(t, ctx) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) kfoo, err := api.Key().Generate(ctx, "foo") - if err != nil { - t.Fatal(err) - return - } + require.NoError(t, err) _, err = api.Key().Generate(ctx, "bar") - if err != nil { - t.Fatal(err) - return - } + require.NoError(t, err) k, overwrote, err := api.Key().Rename(ctx, "foo", "bar", opt.Key.Force(true)) - if err != nil { - t.Fatal(err) - return - } - - if !overwrote { - t.Error("overwrote should be true") - } - - if k.Name() != "bar" { - t.Errorf("returned key should be called 'bar', got '%s'", k.Name()) - } - - if k.Path().String() != kfoo.Path().String() { - t.Errorf("k and kfoo should have equal paths, '%s'!='%s'", k.Path().String(), kfoo.Path().String()) - } + require.NoError(t, err) + require.True(t, overwrote) + assert.Equal(t, "bar", k.Name()) + assert.Equal(t, kfoo.Path().String(), k.Path().String()) } func (tp *TestSuite) TestRenameSameNameNoForce(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() + api, err := tp.makeAPI(t, ctx) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) _, err = api.Key().Generate(ctx, "foo") - if err != nil { - t.Fatal(err) - return - } + require.NoError(t, err) k, overwrote, err := api.Key().Rename(ctx, "foo", "foo") - if err != nil { - t.Fatal(err) - return - } - - if overwrote { - t.Error("overwrote should be false") - } - - if k.Name() != "foo" { - t.Errorf("returned key should be called 'foo', got '%s'", k.Name()) - } + require.NoError(t, err) + assert.False(t, overwrote) + assert.Equal(t, "foo", k.Name()) } func (tp *TestSuite) TestRenameSameName(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() + api, err := tp.makeAPI(t, ctx) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) _, err = api.Key().Generate(ctx, "foo") - if err != nil { - t.Fatal(err) - return - } + require.NoError(t, err) k, overwrote, err := api.Key().Rename(ctx, "foo", "foo", opt.Key.Force(true)) - if err != nil { - t.Fatal(err) - return - } - - if overwrote { - t.Error("overwrote should be false") - } - - if k.Name() != "foo" { - t.Errorf("returned key should be called 'foo', got '%s'", k.Name()) - } + require.NoError(t, err) + assert.False(t, overwrote) + assert.Equal(t, "foo", k.Name()) } func (tp *TestSuite) TestRemove(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() + api, err := tp.makeAPI(t, ctx) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) k, err := api.Key().Generate(ctx, "foo") - if err != nil { - t.Fatal(err) - return - } + require.NoError(t, err) l, err := api.Key().List(ctx) - if err != nil { - t.Fatal(err) - return - } - - if len(l) != 2 { - t.Fatalf("expected to get 2 keys, got %d", len(l)) - return - } + require.NoError(t, err) + require.Len(t, l, 2) p, err := api.Key().Remove(ctx, "foo") - if err != nil { - t.Fatal(err) - return - } - - if k.Path().String() != p.Path().String() { - t.Errorf("k and p should have equal paths, '%s'!='%s'", k.Path().String(), p.Path().String()) - } + require.NoError(t, err) + assert.Equal(t, p.Path().String(), k.Path().String()) l, err = api.Key().List(ctx) - if err != nil { - t.Fatal(err) - return - } - - if len(l) != 1 { - t.Fatalf("expected to get 1 key, got %d", len(l)) - return - } - - if l[0].Name() != "self" { - t.Errorf("expected the key to be called 'self', got '%s'", l[0].Name()) - } + require.NoError(t, err) + require.Len(t, l, 1) + assert.Equal(t, "self", l[0].Name()) +} + +func (tp *TestSuite) TestSign(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + api, err := tp.makeAPI(t, ctx) + require.NoError(t, err) + + key1, err := api.Key().Generate(ctx, "foo", opt.Key.Type(opt.Ed25519Key)) + require.NoError(t, err) + + data := []byte("hello world") + + key2, signature, err := api.Key().Sign(ctx, "foo", data) + require.NoError(t, err) + + require.Equal(t, key1.Name(), key2.Name()) + require.Equal(t, key1.ID(), key2.ID()) + + pk, err := key1.ID().ExtractPublicKey() + require.NoError(t, err) + + valid, err := pk.Verify(append([]byte("libp2p-key signed message:"), data...), signature) + require.NoError(t, err) + require.True(t, valid) +} + +func (tp *TestSuite) TestVerify(t *testing.T) { + t.Parallel() + + t.Run("Verify Own Key", func(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + api, err := tp.makeAPI(t, ctx) + require.NoError(t, err) + + _, err = api.Key().Generate(ctx, "foo", opt.Key.Type(opt.Ed25519Key)) + require.NoError(t, err) + + data := []byte("hello world") + + _, signature, err := api.Key().Sign(ctx, "foo", data) + require.NoError(t, err) + + _, valid, err := api.Key().Verify(ctx, "foo", signature, data) + require.NoError(t, err) + require.True(t, valid) + }) + + t.Run("Verify Self", func(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + api, err := tp.makeAPIWithIdentityAndOffline(t, ctx) + require.NoError(t, err) + + data := []byte("hello world") + + _, signature, err := api.Key().Sign(ctx, "", data) + require.NoError(t, err) + + _, valid, err := api.Key().Verify(ctx, "", signature, data) + require.NoError(t, err) + require.True(t, valid) + }) + + t.Run("Verify With Key In Different Formats", func(t *testing.T) { + t.Parallel() + + // Spin some node and get signature out. + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + api, err := tp.makeAPI(t, ctx) + require.NoError(t, err) + + key, err := api.Key().Generate(ctx, "foo", opt.Key.Type(opt.Ed25519Key)) + require.NoError(t, err) + + data := []byte("hello world") + + _, signature, err := api.Key().Sign(ctx, "foo", data) + require.NoError(t, err) + + for _, testCase := range [][]string{ + {"Base58 Encoded Peer ID", key.ID().String()}, + {"CIDv1 Encoded Peer ID", peer.ToCid(key.ID()).String()}, + {"CIDv1 Encoded IPNS Name", ipns.NameFromPeer(key.ID()).String()}, + {"Prefixed IPNS Path", ipns.NameFromPeer(key.ID()).AsPath().String()}, + } { + t.Run(testCase[0], func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // Spin new node. + api, err := tp.makeAPI(t, ctx) + require.NoError(t, err) + + _, valid, err := api.Key().Verify(ctx, testCase[1], signature, data) + require.NoError(t, err) + require.True(t, valid) + }) + } + }) } diff --git a/docs/changelogs/v0.25.md b/docs/changelogs/v0.25.md index ed8275f2f60..059e437b230 100644 --- a/docs/changelogs/v0.25.md +++ b/docs/changelogs/v0.25.md @@ -10,6 +10,7 @@ - [RPC `API.Authorizations`](#rpc-apiauthorizations) - [MPLEX Removal](#mplex-removal) - [Graphsync Experiment Removal](#graphsync-experiment-removal) + - [Commands `ipfs key sign` and `ipfs key verify`](#commands-ipfs-key-sign-and-ipfs-key-verify) - [๐Ÿ“ Changelog](#-changelog) - [๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ Contributors](#-contributors) @@ -55,6 +56,14 @@ to update Kubo because some dependency changed and it fails to build anymore. For more information see https://github.com/ipfs/kubo/pull/9747. +##### Commands `ipfs key sign` and `ipfs key verify` + +This allows the Kubo node to sign arbitrary bytes to prove ownership of a PeerID or an IPNS Name. To avoid signature reuse, the signed payload is always prefixed with `libp2p-key signed message:`. + +These commands are also both available through the RPC client and implemented in `client/rpc`. + +For more information see https://github.com/ipfs/kubo/issues/10230. + ### ๐Ÿ“ Changelog ### ๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ Contributors