feat: ipfs key sign|verify (#10235)

This commit is contained in:
Henrique Dias 2023-12-04 09:51:26 +01:00 committed by GitHub
parent 7b05b5dd33
commit 8ab2de5ff0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 498 additions and 322 deletions

View File

@ -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
}

View File

@ -164,6 +164,8 @@ func TestCommands(t *testing.T) {
"/key/rename",
"/key/rm",
"/key/rotate",
"/key/sign",
"/key/verify",
"/log",
"/log/level",
"/log/ls",

View File

@ -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 {

View File

@ -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
}

View File

@ -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)
}

View File

@ -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)
})
}
})
}

View File

@ -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