feat: ipfs key sign|verify (#10235)
Henrique Dias committed
Dec 4, 2023 at 09:51 UTC
8ab2de5ff05e1427a22be868b31efbcd58a6ee13
7 files changed
+498
-322
client/rpc/key.go
+52
@@ -1,6 +1,7 @@
1
package rpc
2
3
import (
4
+ "bytes"
5
"context"
6
"errors"
7
@@ -9,6 +10,7 @@ import (
10
iface "github.com/ipfs/kubo/core/coreiface"
11
caopts "github.com/ipfs/kubo/core/coreiface/options"
12
"github.com/libp2p/go-libp2p/core/peer"
13
+ "github.com/multiformats/go-multibase"
14
)
15
16
type KeyAPI HttpApi
@@ -141,3 +143,53 @@ func (api *KeyAPI) Remove(ctx context.Context, name string) (iface.Key, error) {
143
func (api *KeyAPI) core() *HttpApi {
144
return (*HttpApi)(api)
145
}
146
+
147
+func (api *KeyAPI) Sign(ctx context.Context, name string, data []byte) (iface.Key, []byte, error) {
148
+ var out struct {
149
+ Key keyOutput
150
+ Signature string
151
+ }
152
+
153
+ err := api.core().Request("key/sign").
154
+ Option("key", name).
155
+ FileBody(bytes.NewReader(data)).
156
+ Exec(ctx, &out)
157
+ if err != nil {
158
+ return nil, nil, err
159
+ }
160
+
161
+ key, err := newKey(out.Key.Name, out.Key.Id)
162
+ if err != nil {
163
+ return nil, nil, err
164
+ }
165
+
166
+ _, signature, err := multibase.Decode(out.Signature)
167
+ if err != nil {
168
+ return nil, nil, err
169
+ }
170
+
171
+ return key, signature, nil
172
+}
173
+
174
+func (api *KeyAPI) Verify(ctx context.Context, keyOrName string, signature, data []byte) (iface.Key, bool, error) {
175
+ var out struct {
176
+ Key keyOutput
177
+ SignatureValid bool
178
+ }
179
+
180
+ err := api.core().Request("key/verify").
181
+ Option("key", keyOrName).
182
+ Option("signature", toMultibase(signature)).
183
+ FileBody(bytes.NewReader(data)).
184
+ Exec(ctx, &out)
185
+ if err != nil {
186
+ return nil, false, err
187
+ }
188
+
189
+ key, err := newKey(out.Key.Name, out.Key.Id)
190
+ if err != nil {
191
+ return nil, false, err
192
+ }
193
+
194
+ return key, out.SignatureValid, nil
195
+}
core/commands/commands_test.go
+2
@@ -164,6 +164,8 @@ func TestCommands(t *testing.T) {
164
"/key/rename",
165
"/key/rm",
166
"/key/rotate",
167
+ "/key/sign",
168
+ "/key/verify",
169
"/log",
170
"/log/level",
171
"/log/ls",
core/commands/keystore.go
+136
@@ -24,6 +24,7 @@ import (
24
migrations "github.com/ipfs/kubo/repo/fsrepo/migrations"
25
"github.com/libp2p/go-libp2p/core/crypto"
26
peer "github.com/libp2p/go-libp2p/core/peer"
27
+ mbase "github.com/multiformats/go-multibase"
28
)
29
30
var KeyCmd = &cmds.Command{
@@ -51,6 +52,8 @@ publish'.
52
"rename": keyRenameCmd,
53
"rm": keyRmCmd,
54
"rotate": keyRotateCmd,
55
+ "sign": keySignCmd,
56
+ "verify": keyVerifyCmd,
57
},
58
}
59
@@ -688,6 +691,139 @@ func keyOutputListEncoders() cmds.EncoderFunc {
691
})
692
}
693
694
+type KeySignOutput struct {
695
+ Key KeyOutput
696
+ Signature string
697
+}
698
+
699
+var keySignCmd = &cmds.Command{
700
+ Status: cmds.Experimental,
701
+ Helptext: cmds.HelpText{
702
+ Tagline: "Generates a signature for the given data with a specified key. Useful for proving the key ownership.",
703
+ LongDescription: `
704
+Sign arbitrary bytes, such as to prove ownership of a Peer ID or an IPNS Name.
705
+To avoid signature reuse, the signed payload is always prefixed with
706
+"libp2p-key signed message:".
707
+`,
708
+ },
709
+ Options: []cmds.Option{
710
+ cmds.StringOption("key", "k", "The name of the key to use for signing."),
711
+ ke.OptionIPNSBase,
712
+ },
713
+ Arguments: []cmds.Argument{
714
+ cmds.FileArg("data", true, false, "The data to sign.").EnableStdin(),
715
+ },
716
+ Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
717
+ api, err := cmdenv.GetApi(env, req)
718
+ if err != nil {
719
+ return err
720
+ }
721
+ keyEnc, err := ke.KeyEncoderFromString(req.Options[ke.OptionIPNSBase.Name()].(string))
722
+ if err != nil {
723
+ return err
724
+ }
725
+
726
+ name, _ := req.Options["key"].(string)
727
+
728
+ file, err := cmdenv.GetFileArg(req.Files.Entries())
729
+ if err != nil {
730
+ return err
731
+ }
732
+ defer file.Close()
733
+
734
+ data, err := io.ReadAll(file)
735
+ if err != nil {
736
+ return err
737
+ }
738
+
739
+ key, signature, err := api.Key().Sign(req.Context, name, data)
740
+ if err != nil {
741
+ return err
742
+ }
743
+
744
+ encodedSignature, err := mbase.Encode(mbase.Base64url, signature)
745
+ if err != nil {
746
+ return err
747
+ }
748
+
749
+ return res.Emit(&KeySignOutput{
750
+ Key: KeyOutput{
751
+ Name: key.Name(),
752
+ Id: keyEnc.FormatID(key.ID()),
753
+ },
754
+ Signature: encodedSignature,
755
+ })
756
+ },
757
+ Type: KeySignOutput{},
758
+}
759
+
760
+type KeyVerifyOutput struct {
761
+ Key KeyOutput
762
+ SignatureValid bool
763
+}
764
+
765
+var keyVerifyCmd = &cmds.Command{
766
+ Status: cmds.Experimental,
767
+ Helptext: cmds.HelpText{
768
+ Tagline: "Verify that the given data and signature match.",
769
+ LongDescription: `
770
+Verify if the given data and signatures match. To avoid the signature reuse,
771
+the signed payload is always prefixed with "libp2p-key signed message:".
772
+`,
773
+ },
774
+ Options: []cmds.Option{
775
+ cmds.StringOption("key", "k", "The name of the key to use for signing."),
776
+ cmds.StringOption("signature", "s", "Multibase-encoded signature to verify."),
777
+ ke.OptionIPNSBase,
778
+ },
779
+ Arguments: []cmds.Argument{
780
+ cmds.FileArg("data", true, false, "The data to verify against the given signature.").EnableStdin(),
781
+ },
782
+ Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
783
+ api, err := cmdenv.GetApi(env, req)
784
+ if err != nil {
785
+ return err
786
+ }
787
+ keyEnc, err := ke.KeyEncoderFromString(req.Options[ke.OptionIPNSBase.Name()].(string))
788
+ if err != nil {
789
+ return err
790
+ }
791
+
792
+ name, _ := req.Options["key"].(string)
793
+ encodedSignature, _ := req.Options["signature"].(string)
794
+
795
+ _, signature, err := mbase.Decode(encodedSignature)
796
+ if err != nil {
797
+ return err
798
+ }
799
+
800
+ file, err := cmdenv.GetFileArg(req.Files.Entries())
801
+ if err != nil {
802
+ return err
803
+ }
804
+ defer file.Close()
805
+
806
+ data, err := io.ReadAll(file)
807
+ if err != nil {
808
+ return err
809
+ }
810
+
811
+ key, valid, err := api.Key().Verify(req.Context, name, signature, data)
812
+ if err != nil {
813
+ return err
814
+ }
815
+
816
+ return res.Emit(&KeyVerifyOutput{
817
+ Key: KeyOutput{
818
+ Name: key.Name(),
819
+ Id: keyEnc.FormatID(key.ID()),
820
+ },
821
+ SignatureValid: valid,
822
+ })
823
+ },
824
+ Type: KeyVerifyOutput{},
825
+}
826
+
827
// DaemonNotRunning checks to see if the ipfs repo is locked, indicating that
828
// the daemon is running, and returns and error if the daemon is running.
829
func DaemonNotRunning(req *cmds.Request, env cmds.Environment) error {
core/coreapi/key.go
+80
@@ -262,3 +262,83 @@ func (api *KeyAPI) Self(ctx context.Context) (coreiface.Key, error) {
262
263
return newKey("self", api.identity)
264
}
265
+
266
+const signedMessagePrefix = "libp2p-key signed message:"
267
+
268
+func (api *KeyAPI) Sign(ctx context.Context, name string, data []byte) (coreiface.Key, []byte, error) {
269
+ var (
270
+ sk crypto.PrivKey
271
+ err error
272
+ )
273
+ if name == "" || name == "self" {
274
+ name = "self"
275
+ sk = api.privateKey
276
+ } else {
277
+ sk, err = api.repo.Keystore().Get(name)
278
+ }
279
+ if err != nil {
280
+ return nil, nil, err
281
+ }
282
+
283
+ pid, err := peer.IDFromPrivateKey(sk)
284
+ if err != nil {
285
+ return nil, nil, err
286
+ }
287
+
288
+ key, err := newKey(name, pid)
289
+ if err != nil {
290
+ return nil, nil, err
291
+ }
292
+
293
+ data = append([]byte(signedMessagePrefix), data...)
294
+
295
+ sig, err := sk.Sign(data)
296
+ if err != nil {
297
+ return nil, nil, err
298
+ }
299
+
300
+ return key, sig, nil
301
+}
302
+
303
+func (api *KeyAPI) Verify(ctx context.Context, keyOrName string, signature, data []byte) (coreiface.Key, bool, error) {
304
+ var (
305
+ name string
306
+ pk crypto.PubKey
307
+ err error
308
+ )
309
+ if keyOrName == "" || keyOrName == "self" {
310
+ name = "self"
311
+ pk = api.privateKey.GetPublic()
312
+ } else if sk, err := api.repo.Keystore().Get(keyOrName); err == nil {
313
+ name = keyOrName
314
+ pk = sk.GetPublic()
315
+ } else if ipnsName, err := ipns.NameFromString(keyOrName); err == nil {
316
+ // This works for both IPNS names and Peer IDs.
317
+ name = ""
318
+ pk, err = ipnsName.Peer().ExtractPublicKey()
319
+ if err != nil {
320
+ return nil, false, err
321
+ }
322
+ } else {
323
+ return nil, false, fmt.Errorf("'%q' is not a known key, an IPNS Name, or a valid PeerID", keyOrName)
324
+ }
325
+
326
+ pid, err := peer.IDFromPublicKey(pk)
327
+ if err != nil {
328
+ return nil, false, err
329
+ }
330
+
331
+ key, err := newKey(name, pid)
332
+ if err != nil {
333
+ return nil, false, err
334
+ }
335
+
336
+ data = append([]byte(signedMessagePrefix), data...)
337
+
338
+ valid, err := pk.Verify(data, signature)
339
+ if err != nil {
340
+ return nil, false, err
341
+ }
342
+
343
+ return key, valid, nil
344
+}
core/coreiface/key.go
+8
@@ -40,4 +40,12 @@ type KeyAPI interface {
40
41
// Remove removes keys from keystore. Returns ipns path of the removed key
42
Remove(ctx context.Context, name string) (Key, error)
43
+
44
+ // Sign signs the given data with the key named name. Returns the key used
45
+ // for signing, the signature, and an error.
46
+ Sign(ctx context.Context, name string, data []byte) (Key, []byte, error)
47
+
48
+ // Verify verifies if the given data and signatures match. Returns the key used
49
+ // for verification, whether signature and data match, and an error.
50
+ Verify(ctx context.Context, keyOrName string, signature, data []byte) (Key, bool, error)
51
}
core/coreiface/tests/key.go
+211
-322
@@ -5,10 +5,14 @@ import (
5
"strings"
6
"testing"
7
8
+ "github.com/ipfs/boxo/ipns"
9
"github.com/ipfs/go-cid"
10
iface "github.com/ipfs/kubo/core/coreiface"
11
opt "github.com/ipfs/kubo/core/coreiface/options"
12
+ "github.com/libp2p/go-libp2p/core/peer"
13
mbase "github.com/multiformats/go-multibase"
14
+ "github.com/stretchr/testify/assert"
15
+ "github.com/stretchr/testify/require"
16
)
17
18
func (tp *TestSuite) TestKey(t *testing.T) {
@@ -34,151 +38,90 @@ func (tp *TestSuite) TestKey(t *testing.T) {
38
t.Run("TestRenameOverwrite", tp.TestRenameOverwrite)
39
t.Run("TestRenameSameNameNoForce", tp.TestRenameSameNameNoForce)
40
t.Run("TestRenameSameName", tp.TestRenameSameName)
37
- t.Run("TestRemove", tp.TestRemove)
41
+ t.Run("TestSign", tp.TestSign)
42
+ t.Run("TestVerify", tp.TestVerify)
43
}
44
45
func (tp *TestSuite) TestListSelf(t *testing.T) {
46
ctx, cancel := context.WithCancel(context.Background())
47
defer cancel()
48
+
49
api, err := tp.makeAPI(t, ctx)
44
- if err != nil {
45
- t.Fatal(err)
46
- return
47
- }
50
+ require.NoError(t, err)
51
52
self, err := api.Key().Self(ctx)
50
- if err != nil {
51
- t.Fatal(err)
52
- }
53
+ require.NoError(t, err)
54
55
keys, err := api.Key().List(ctx)
55
- if err != nil {
56
- t.Fatalf("failed to list keys: %s", err)
57
- return
58
- }
59
-
60
- if len(keys) != 1 {
61
- t.Fatalf("there should be 1 key (self), got %d", len(keys))
62
- return
63
- }
64
-
65
- if keys[0].Name() != "self" {
66
- t.Errorf("expected the key to be called 'self', got '%s'", keys[0].Name())
67
- }
68
-
69
- if keys[0].Path().String() != "/ipns/"+iface.FormatKeyID(self.ID()) {
70
- t.Errorf("expected the key to have path '/ipns/%s', got '%s'", iface.FormatKeyID(self.ID()), keys[0].Path().String())
71
- }
56
+ require.NoError(t, err)
57
+ require.Len(t, keys, 1)
58
+ assert.Equal(t, "self", keys[0].Name())
59
+ assert.Equal(t, "/ipns/"+iface.FormatKeyID(self.ID()), keys[0].Path().String())
60
}
61
62
func (tp *TestSuite) TestRenameSelf(t *testing.T) {
63
ctx, cancel := context.WithCancel(context.Background())
64
defer cancel()
65
+
66
api, err := tp.makeAPI(t, ctx)
78
- if err != nil {
79
- t.Fatal(err)
80
- return
81
- }
67
+ require.NoError(t, err)
68
69
_, _, err = api.Key().Rename(ctx, "self", "foo")
84
- if err == nil {
85
- t.Error("expected error to not be nil")
86
- } else {
87
- if !strings.Contains(err.Error(), "cannot rename key with name 'self'") {
88
- t.Fatalf("expected error 'cannot rename key with name 'self'', got '%s'", err.Error())
89
- }
90
- }
70
+ require.ErrorContains(t, err, "cannot rename key with name 'self'")
71
72
_, _, err = api.Key().Rename(ctx, "self", "foo", opt.Key.Force(true))
93
- if err == nil {
94
- t.Error("expected error to not be nil")
95
- } else {
96
- if !strings.Contains(err.Error(), "cannot rename key with name 'self'") {
97
- t.Fatalf("expected error 'cannot rename key with name 'self'', got '%s'", err.Error())
98
- }
99
- }
73
+ require.ErrorContains(t, err, "cannot rename key with name 'self'")
74
}
75
76
func (tp *TestSuite) TestRemoveSelf(t *testing.T) {
77
ctx, cancel := context.WithCancel(context.Background())
78
defer cancel()
79
+
80
api, err := tp.makeAPI(t, ctx)
106
- if err != nil {
107
- t.Fatal(err)
108
- return
109
- }
81
+ require.NoError(t, err)
82
83
_, err = api.Key().Remove(ctx, "self")
112
- if err == nil {
113
- t.Error("expected error to not be nil")
114
- } else {
115
- if !strings.Contains(err.Error(), "cannot remove key with name 'self'") {
116
- t.Fatalf("expected error 'cannot remove key with name 'self'', got '%s'", err.Error())
117
- }
118
- }
84
+ require.ErrorContains(t, err, "cannot remove key with name 'self'")
85
}
86
87
func (tp *TestSuite) TestGenerate(t *testing.T) {
88
ctx, cancel := context.WithCancel(context.Background())
89
defer cancel()
90
+
91
api, err := tp.makeAPI(t, ctx)
125
- if err != nil {
126
- t.Fatal(err)
127
- }
92
+ require.NoError(t, err)
93
94
k, err := api.Key().Generate(ctx, "foo")
130
- if err != nil {
131
- t.Fatal(err)
132
- return
133
- }
134
-
135
- if k.Name() != "foo" {
136
- t.Errorf("expected the key to be called 'foo', got '%s'", k.Name())
137
- }
95
+ require.NoError(t, err)
96
+ require.Equal(t, "foo", k.Name())
97
98
verifyIPNSPath(t, k.Path().String())
99
}
100
142
-func verifyIPNSPath(t *testing.T, p string) bool {
101
+func verifyIPNSPath(t *testing.T, p string) {
102
t.Helper()
144
- if !strings.HasPrefix(p, "/ipns/") {
145
- t.Errorf("path %q does not look like an IPNS path", p)
146
- return false
147
- }
103
+
104
+ require.True(t, strings.HasPrefix(p, "/ipns/"))
105
+
106
k := p[len("/ipns/"):]
107
c, err := cid.Decode(k)
150
- if err != nil {
151
- t.Errorf("failed to decode IPNS key %q (%v)", k, err)
152
- return false
153
- }
108
+ require.NoError(t, err)
109
+
110
b36, err := c.StringOfBase(mbase.Base36)
155
- if err != nil {
156
- t.Fatalf("cid cannot format itself in b36")
157
- return false
158
- }
159
- if b36 != k {
160
- t.Errorf("IPNS key is not base36")
161
- }
162
- return true
111
+ require.NoError(t, err)
112
+ require.Equal(t, k, b36)
113
}
114
115
func (tp *TestSuite) TestGenerateSize(t *testing.T) {
116
ctx, cancel := context.WithCancel(context.Background())
117
defer cancel()
118
+
119
api, err := tp.makeAPI(t, ctx)
169
- if err != nil {
170
- t.Fatal(err)
171
- }
120
+ require.NoError(t, err)
121
122
k, err := api.Key().Generate(ctx, "foo", opt.Key.Size(2048))
174
- if err != nil {
175
- t.Fatal(err)
176
- return
177
- }
178
-
179
- if k.Name() != "foo" {
180
- t.Errorf("expected the key to be called 'foo', got '%s'", k.Name())
181
- }
123
+ require.NoError(t, err)
124
+ require.Equal(t, "foo", k.Name())
125
126
verifyIPNSPath(t, k.Path().String())
127
}
@@ -190,93 +133,47 @@ func (tp *TestSuite) TestGenerateType(t *testing.T) {
133
defer cancel()
134
135
api, err := tp.makeAPI(t, ctx)
193
- if err != nil {
194
- t.Fatal(err)
195
- }
136
+ require.NoError(t, err)
137
138
k, err := api.Key().Generate(ctx, "bar", opt.Key.Type(opt.Ed25519Key))
198
- if err != nil {
199
- t.Fatal(err)
200
- return
201
- }
202
-
203
- if k.Name() != "bar" {
204
- t.Errorf("expected the key to be called 'foo', got '%s'", k.Name())
205
- }
206
-
139
+ require.NoError(t, err)
140
+ require.Equal(t, "bar", k.Name())
141
// Expected to be an inlined identity hash.
208
- if !strings.HasPrefix(k.Path().String(), "/ipns/12") {
209
- t.Errorf("expected the key to be prefixed with '/ipns/12', got '%s'", k.Path().String())
210
- }
142
+ require.True(t, strings.HasPrefix(k.Path().String(), "/ipns/12"))
143
}
144
145
func (tp *TestSuite) TestGenerateExisting(t *testing.T) {
146
ctx, cancel := context.WithCancel(context.Background())
147
defer cancel()
148
+
149
api, err := tp.makeAPI(t, ctx)
217
- if err != nil {
218
- t.Fatal(err)
219
- }
150
+ require.NoError(t, err)
151
152
_, err = api.Key().Generate(ctx, "foo")
222
- if err != nil {
223
- t.Fatal(err)
224
- return
225
- }
153
+ require.NoError(t, err)
154
155
_, err = api.Key().Generate(ctx, "foo")
228
- if err == nil {
229
- t.Error("expected error to not be nil")
230
- } else {
231
- if !strings.Contains(err.Error(), "key with name 'foo' already exists") {
232
- t.Fatalf("expected error 'key with name 'foo' already exists', got '%s'", err.Error())
233
- }
234
- }
156
+ require.ErrorContains(t, err, "key with name 'foo' already exists")
157
158
_, err = api.Key().Generate(ctx, "self")
237
- if err == nil {
238
- t.Error("expected error to not be nil")
239
- } else {
240
- if !strings.Contains(err.Error(), "cannot create key with name 'self'") {
241
- t.Fatalf("expected error 'cannot create key with name 'self'', got '%s'", err.Error())
242
- }
243
- }
159
+ require.ErrorContains(t, err, "cannot create key with name 'self'")
160
}
161
162
func (tp *TestSuite) TestList(t *testing.T) {
163
ctx, cancel := context.WithCancel(context.Background())
164
defer cancel()
165
+
166
api, err := tp.makeAPI(t, ctx)
250
- if err != nil {
251
- t.Fatal(err)
252
- }
167
+ require.NoError(t, err)
168
169
_, err = api.Key().Generate(ctx, "foo")
255
- if err != nil {
256
- t.Fatal(err)
257
- return
258
- }
170
+ require.NoError(t, err)
171
172
l, err := api.Key().List(ctx)
261
- if err != nil {
262
- t.Fatal(err)
263
- return
264
- }
265
-
266
- if len(l) != 2 {
267
- t.Fatalf("expected to get 2 keys, got %d", len(l))
268
- return
269
- }
270
-
271
- if l[0].Name() != "self" {
272
- t.Fatalf("expected key 0 to be called 'self', got '%s'", l[0].Name())
273
- return
274
- }
275
-
276
- if l[1].Name() != "foo" {
277
- t.Fatalf("expected key 1 to be called 'foo', got '%s'", l[1].Name())
278
- return
279
- }
173
+ require.NoError(t, err)
174
+ require.Len(t, l, 2)
175
+ require.Equal(t, "self", l[0].Name())
176
+ require.Equal(t, "foo", l[1].Name())
177
178
verifyIPNSPath(t, l[0].Path().String())
179
verifyIPNSPath(t, l[1].Path().String())
@@ -285,254 +182,246 @@ func (tp *TestSuite) TestList(t *testing.T) {
182
func (tp *TestSuite) TestRename(t *testing.T) {
183
ctx, cancel := context.WithCancel(context.Background())
184
defer cancel()
185
+
186
api, err := tp.makeAPI(t, ctx)
289
- if err != nil {
290
- t.Fatal(err)
291
- }
187
+ require.NoError(t, err)
188
189
_, err = api.Key().Generate(ctx, "foo")
294
- if err != nil {
295
- t.Fatal(err)
296
- return
297
- }
190
+ require.NoError(t, err)
191
192
k, overwrote, err := api.Key().Rename(ctx, "foo", "bar")
300
- if err != nil {
301
- t.Fatal(err)
302
- return
303
- }
304
-
305
- if overwrote {
306
- t.Error("overwrote should be false")
307
- }
308
-
309
- if k.Name() != "bar" {
310
- t.Errorf("returned key should be called 'bar', got '%s'", k.Name())
311
- }
193
+ require.NoError(t, err)
194
+ assert.False(t, overwrote)
195
+ assert.Equal(t, "bar", k.Name())
196
}
197
198
func (tp *TestSuite) TestRenameToSelf(t *testing.T) {
199
ctx, cancel := context.WithCancel(context.Background())
200
defer cancel()
201
+
202
api, err := tp.makeAPI(t, ctx)
318
- if err != nil {
319
- t.Fatal(err)
320
- }
203
+ require.NoError(t, err)
204
205
_, err = api.Key().Generate(ctx, "foo")
323
- if err != nil {
324
- t.Fatal(err)
325
- return
326
- }
206
+ require.NoError(t, err)
207
208
_, _, err = api.Key().Rename(ctx, "foo", "self")
329
- if err == nil {
330
- t.Error("expected error to not be nil")
331
- } else {
332
- if !strings.Contains(err.Error(), "cannot overwrite key with name 'self'") {
333
- t.Fatalf("expected error 'cannot overwrite key with name 'self'', got '%s'", err.Error())
334
- }
335
- }
209
+ require.ErrorContains(t, err, "cannot overwrite key with name 'self'")
210
}
211
212
func (tp *TestSuite) TestRenameToSelfForce(t *testing.T) {
213
ctx, cancel := context.WithCancel(context.Background())
214
defer cancel()
215
+
216
api, err := tp.makeAPI(t, ctx)
342
- if err != nil {
343
- t.Fatal(err)
344
- }
217
+ require.NoError(t, err)
218
219
_, err = api.Key().Generate(ctx, "foo")
347
- if err != nil {
348
- t.Fatal(err)
349
- return
350
- }
220
+ require.NoError(t, err)
221
222
_, _, err = api.Key().Rename(ctx, "foo", "self", opt.Key.Force(true))
353
- if err == nil {
354
- t.Error("expected error to not be nil")
355
- } else {
356
- if !strings.Contains(err.Error(), "cannot overwrite key with name 'self'") {
357
- t.Fatalf("expected error 'cannot overwrite key with name 'self'', got '%s'", err.Error())
358
- }
359
- }
223
+ require.ErrorContains(t, err, "cannot overwrite key with name 'self'")
224
}
225
226
func (tp *TestSuite) TestRenameOverwriteNoForce(t *testing.T) {
227
ctx, cancel := context.WithCancel(context.Background())
228
defer cancel()
229
+
230
api, err := tp.makeAPI(t, ctx)
366
- if err != nil {
367
- t.Fatal(err)
368
- }
231
+ require.NoError(t, err)
232
233
_, err = api.Key().Generate(ctx, "foo")
371
- if err != nil {
372
- t.Fatal(err)
373
- return
374
- }
234
+ require.NoError(t, err)
235
236
_, err = api.Key().Generate(ctx, "bar")
377
- if err != nil {
378
- t.Fatal(err)
379
- return
380
- }
237
+ require.NoError(t, err)
238
239
_, _, err = api.Key().Rename(ctx, "foo", "bar")
383
- if err == nil {
384
- t.Error("expected error to not be nil")
385
- } else {
386
- if !strings.Contains(err.Error(), "key by that name already exists, refusing to overwrite") {
387
- t.Fatalf("expected error 'key by that name already exists, refusing to overwrite', got '%s'", err.Error())
388
- }
389
- }
240
+ require.ErrorContains(t, err, "key by that name already exists, refusing to overwrite")
241
}
242
243
func (tp *TestSuite) TestRenameOverwrite(t *testing.T) {
244
ctx, cancel := context.WithCancel(context.Background())
245
defer cancel()
246
+
247
api, err := tp.makeAPI(t, ctx)
396
- if err != nil {
397
- t.Fatal(err)
398
- }
248
+ require.NoError(t, err)
249
250
kfoo, err := api.Key().Generate(ctx, "foo")
401
- if err != nil {
402
- t.Fatal(err)
403
- return
404
- }
251
+ require.NoError(t, err)
252
253
_, err = api.Key().Generate(ctx, "bar")
407
- if err != nil {
408
- t.Fatal(err)
409
- return
410
- }
254
+ require.NoError(t, err)
255
256
k, overwrote, err := api.Key().Rename(ctx, "foo", "bar", opt.Key.Force(true))
413
- if err != nil {
414
- t.Fatal(err)
415
- return
416
- }
417
-
418
- if !overwrote {
419
- t.Error("overwrote should be true")
420
- }
421
-
422
- if k.Name() != "bar" {
423
- t.Errorf("returned key should be called 'bar', got '%s'", k.Name())
424
- }
425
-
426
- if k.Path().String() != kfoo.Path().String() {
427
- t.Errorf("k and kfoo should have equal paths, '%s'!='%s'", k.Path().String(), kfoo.Path().String())
428
- }
257
+ require.NoError(t, err)
258
+ require.True(t, overwrote)
259
+ assert.Equal(t, "bar", k.Name())
260
+ assert.Equal(t, kfoo.Path().String(), k.Path().String())
261
}
262
263
func (tp *TestSuite) TestRenameSameNameNoForce(t *testing.T) {
264
ctx, cancel := context.WithCancel(context.Background())
265
defer cancel()
266
+
267
api, err := tp.makeAPI(t, ctx)
435
- if err != nil {
436
- t.Fatal(err)
437
- }
268
+ require.NoError(t, err)
269
270
_, err = api.Key().Generate(ctx, "foo")
440
- if err != nil {
441
- t.Fatal(err)
442
- return
443
- }
271
+ require.NoError(t, err)
272
273
k, overwrote, err := api.Key().Rename(ctx, "foo", "foo")
446
- if err != nil {
447
- t.Fatal(err)
448
- return
449
- }
450
-
451
- if overwrote {
452
- t.Error("overwrote should be false")
453
- }
454
-
455
- if k.Name() != "foo" {
456
- t.Errorf("returned key should be called 'foo', got '%s'", k.Name())
457
- }
274
+ require.NoError(t, err)
275
+ assert.False(t, overwrote)
276
+ assert.Equal(t, "foo", k.Name())
277
}
278
279
func (tp *TestSuite) TestRenameSameName(t *testing.T) {
280
ctx, cancel := context.WithCancel(context.Background())
281
defer cancel()
282
+
283
api, err := tp.makeAPI(t, ctx)
464
- if err != nil {
465
- t.Fatal(err)
466
- }
284
+ require.NoError(t, err)
285
286
_, err = api.Key().Generate(ctx, "foo")
469
- if err != nil {
470
- t.Fatal(err)
471
- return
472
- }
287
+ require.NoError(t, err)
288
289
k, overwrote, err := api.Key().Rename(ctx, "foo", "foo", opt.Key.Force(true))
475
- if err != nil {
476
- t.Fatal(err)
477
- return
478
- }
479
-
480
- if overwrote {
481
- t.Error("overwrote should be false")
482
- }
483
-
484
- if k.Name() != "foo" {
485
- t.Errorf("returned key should be called 'foo', got '%s'", k.Name())
486
- }
290
+ require.NoError(t, err)
291
+ assert.False(t, overwrote)
292
+ assert.Equal(t, "foo", k.Name())
293
}
294
295
func (tp *TestSuite) TestRemove(t *testing.T) {
296
ctx, cancel := context.WithCancel(context.Background())
297
defer cancel()
298
+
299
api, err := tp.makeAPI(t, ctx)
493
- if err != nil {
494
- t.Fatal(err)
495
- }
300
+ require.NoError(t, err)
301
302
k, err := api.Key().Generate(ctx, "foo")
498
- if err != nil {
499
- t.Fatal(err)
500
- return
501
- }
303
+ require.NoError(t, err)
304
305
l, err := api.Key().List(ctx)
504
- if err != nil {
505
- t.Fatal(err)
506
- return
507
- }
508
-
509
- if len(l) != 2 {
510
- t.Fatalf("expected to get 2 keys, got %d", len(l))
511
- return
512
- }
306
+ require.NoError(t, err)
307
+ require.Len(t, l, 2)
308
309
p, err := api.Key().Remove(ctx, "foo")
515
- if err != nil {
516
- t.Fatal(err)
517
- return
518
- }
519
-
520
- if k.Path().String() != p.Path().String() {
521
- t.Errorf("k and p should have equal paths, '%s'!='%s'", k.Path().String(), p.Path().String())
522
- }
310
+ require.NoError(t, err)
311
+ assert.Equal(t, p.Path().String(), k.Path().String())
312
313
l, err = api.Key().List(ctx)
525
- if err != nil {
526
- t.Fatal(err)
527
- return
528
- }
529
-
530
- if len(l) != 1 {
531
- t.Fatalf("expected to get 1 key, got %d", len(l))
532
- return
533
- }
534
-
535
- if l[0].Name() != "self" {
536
- t.Errorf("expected the key to be called 'self', got '%s'", l[0].Name())
537
- }
314
+ require.NoError(t, err)
315
+ require.Len(t, l, 1)
316
+ assert.Equal(t, "self", l[0].Name())
317
+}
318
+
319
+func (tp *TestSuite) TestSign(t *testing.T) {
320
+ ctx, cancel := context.WithCancel(context.Background())
321
+ defer cancel()
322
+
323
+ api, err := tp.makeAPI(t, ctx)
324
+ require.NoError(t, err)
325
+
326
+ key1, err := api.Key().Generate(ctx, "foo", opt.Key.Type(opt.Ed25519Key))
327
+ require.NoError(t, err)
328
+
329
+ data := []byte("hello world")
330
+
331
+ key2, signature, err := api.Key().Sign(ctx, "foo", data)
332
+ require.NoError(t, err)
333
+
334
+ require.Equal(t, key1.Name(), key2.Name())
335
+ require.Equal(t, key1.ID(), key2.ID())
336
+
337
+ pk, err := key1.ID().ExtractPublicKey()
338
+ require.NoError(t, err)
339
+
340
+ valid, err := pk.Verify(append([]byte("libp2p-key signed message:"), data...), signature)
341
+ require.NoError(t, err)
342
+ require.True(t, valid)
343
+}
344
+
345
+func (tp *TestSuite) TestVerify(t *testing.T) {
346
+ t.Parallel()
347
+
348
+ t.Run("Verify Own Key", func(t *testing.T) {
349
+ t.Parallel()
350
+
351
+ ctx, cancel := context.WithCancel(context.Background())
352
+ defer cancel()
353
+
354
+ api, err := tp.makeAPI(t, ctx)
355
+ require.NoError(t, err)
356
+
357
+ _, err = api.Key().Generate(ctx, "foo", opt.Key.Type(opt.Ed25519Key))
358
+ require.NoError(t, err)
359
+
360
+ data := []byte("hello world")
361
+
362
+ _, signature, err := api.Key().Sign(ctx, "foo", data)
363
+ require.NoError(t, err)
364
+
365
+ _, valid, err := api.Key().Verify(ctx, "foo", signature, data)
366
+ require.NoError(t, err)
367
+ require.True(t, valid)
368
+ })
369
+
370
+ t.Run("Verify Self", func(t *testing.T) {
371
+ t.Parallel()
372
+
373
+ ctx, cancel := context.WithCancel(context.Background())
374
+ defer cancel()
375
+
376
+ api, err := tp.makeAPIWithIdentityAndOffline(t, ctx)
377
+ require.NoError(t, err)
378
+
379
+ data := []byte("hello world")
380
+
381
+ _, signature, err := api.Key().Sign(ctx, "", data)
382
+ require.NoError(t, err)
383
+
384
+ _, valid, err := api.Key().Verify(ctx, "", signature, data)
385
+ require.NoError(t, err)
386
+ require.True(t, valid)
387
+ })
388
+
389
+ t.Run("Verify With Key In Different Formats", func(t *testing.T) {
390
+ t.Parallel()
391
+
392
+ // Spin some node and get signature out.
393
+ ctx, cancel := context.WithCancel(context.Background())
394
+ defer cancel()
395
+
396
+ api, err := tp.makeAPI(t, ctx)
397
+ require.NoError(t, err)
398
+
399
+ key, err := api.Key().Generate(ctx, "foo", opt.Key.Type(opt.Ed25519Key))
400
+ require.NoError(t, err)
401
+
402
+ data := []byte("hello world")
403
+
404
+ _, signature, err := api.Key().Sign(ctx, "foo", data)
405
+ require.NoError(t, err)
406
+
407
+ for _, testCase := range [][]string{
408
+ {"Base58 Encoded Peer ID", key.ID().String()},
409
+ {"CIDv1 Encoded Peer ID", peer.ToCid(key.ID()).String()},
410
+ {"CIDv1 Encoded IPNS Name", ipns.NameFromPeer(key.ID()).String()},
411
+ {"Prefixed IPNS Path", ipns.NameFromPeer(key.ID()).AsPath().String()},
412
+ } {
413
+ t.Run(testCase[0], func(t *testing.T) {
414
+ ctx, cancel := context.WithCancel(context.Background())
415
+ defer cancel()
416
+
417
+ // Spin new node.
418
+ api, err := tp.makeAPI(t, ctx)
419
+ require.NoError(t, err)
420
+
421
+ _, valid, err := api.Key().Verify(ctx, testCase[1], signature, data)
422
+ require.NoError(t, err)
423
+ require.True(t, valid)
424
+ })
425
+ }
426
+ })
427
}
docs/changelogs/v0.25.md
+9
@@ -10,6 +10,7 @@
10
- [RPC `API.Authorizations`](#rpc-apiauthorizations)
11
- [MPLEX Removal](#mplex-removal)
12
- [Graphsync Experiment Removal](#graphsync-experiment-removal)
13
+ - [Commands `ipfs key sign` and `ipfs key verify`](#commands-ipfs-key-sign-and-ipfs-key-verify)
14
- [📝 Changelog](#-changelog)
15
- [👨👩👧👦 Contributors](#-contributors)
16
@@ -55,6 +56,14 @@ to update Kubo because some dependency changed and it fails to build anymore.
56
57
For more information see https://github.com/ipfs/kubo/pull/9747.
58
59
+##### Commands `ipfs key sign` and `ipfs key verify`
60
+
61
+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:`.
62
+
63
+These commands are also both available through the RPC client and implemented in `client/rpc`.
64
+
65
+For more information see https://github.com/ipfs/kubo/issues/10230.
66
+
67
### 📝 Changelog
68
69
### 👨👩👧👦 Contributors