mirror of
https://github.com/ipfs/kubo.git
synced 2026-02-21 10:27:46 +08:00
feat: ipfs key sign|verify (#10235)
This commit is contained in:
parent
7b05b5dd33
commit
8ab2de5ff0
@ -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
|
||||
}
|
||||
|
||||
@ -164,6 +164,8 @@ func TestCommands(t *testing.T) {
|
||||
"/key/rename",
|
||||
"/key/rm",
|
||||
"/key/rotate",
|
||||
"/key/sign",
|
||||
"/key/verify",
|
||||
"/log",
|
||||
"/log/level",
|
||||
"/log/ls",
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user