309 lines
7.3 KiB
Go
309 lines
7.3 KiB
Go
package identity
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
|
"github.com/decred/dcrd/dcrec/secp256k1/v4/ecdsa"
|
|
)
|
|
|
|
func TestGenerateUIDIsDeterministicFromPubkey(t *testing.T) {
|
|
id, err := Generate()
|
|
if err != nil {
|
|
t.Fatalf("generate: %v", err)
|
|
}
|
|
if !strings.HasPrefix(id.UID, "z") {
|
|
t.Fatalf("uid missing multibase 'z' prefix: %q", id.UID)
|
|
}
|
|
if id.Curve != CurveSecp256k1 {
|
|
t.Fatalf("curve = %q", id.Curve)
|
|
}
|
|
if len(id.PrivateKeyHex) != 64 {
|
|
t.Fatalf("privkey hex len = %d", len(id.PrivateKeyHex))
|
|
}
|
|
if len(id.PublicKeyHex) != 66 {
|
|
t.Fatalf("compressed pubkey hex len = %d", len(id.PublicKeyHex))
|
|
}
|
|
|
|
// Reimporting yields the same uid.
|
|
imported, err := Import(id.PrivateKeyHex)
|
|
if err != nil {
|
|
t.Fatalf("import: %v", err)
|
|
}
|
|
if imported.UID != id.UID {
|
|
t.Fatalf("uid changed after import roundtrip: %q vs %q", id.UID, imported.UID)
|
|
}
|
|
if imported.PublicKeyHex != id.PublicKeyHex {
|
|
t.Fatalf("pubkey changed after import roundtrip")
|
|
}
|
|
}
|
|
|
|
func TestImportRejectsBadInput(t *testing.T) {
|
|
if _, err := Import(""); err == nil {
|
|
t.Fatal("empty hex should error")
|
|
}
|
|
if _, err := Import("zz"); err == nil {
|
|
t.Fatal("non-hex should error")
|
|
}
|
|
if _, err := Import("00"); err == nil {
|
|
t.Fatal("wrong-length hex should error")
|
|
}
|
|
if _, err := Import(strings.Repeat("00", 32)); err == nil {
|
|
t.Fatal("zero scalar should error")
|
|
}
|
|
// Hex with 0x prefix should work.
|
|
id1, err := Generate()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
id2, err := Import("0x" + id1.PrivateKeyHex)
|
|
if err != nil {
|
|
t.Fatalf("0x-prefixed hex: %v", err)
|
|
}
|
|
if id1.UID != id2.UID {
|
|
t.Fatal("uid mismatch through 0x prefix")
|
|
}
|
|
}
|
|
|
|
func TestSaveLoadRoundtrip(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "identity.json")
|
|
|
|
if Exists(path) {
|
|
t.Fatal("file should not exist yet")
|
|
}
|
|
if got, _ := Load(path); got != nil {
|
|
t.Fatal("Load on missing file should return nil")
|
|
}
|
|
|
|
id, err := Generate()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := Save(path, id); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !Exists(path) {
|
|
t.Fatal("file should exist after Save")
|
|
}
|
|
|
|
loaded, err := Load(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if loaded == nil || loaded.UID != id.UID || loaded.PrivateKeyHex != id.PrivateKeyHex {
|
|
t.Fatalf("roundtrip mismatch: %+v vs %+v", id, loaded)
|
|
}
|
|
|
|
// File should be 0600 (private key on disk).
|
|
info, err := os.Stat(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if info.Mode().Perm() != 0600 {
|
|
t.Errorf("identity file mode = %o, want 0600", info.Mode().Perm())
|
|
}
|
|
|
|
if err := Delete(path); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if Exists(path) {
|
|
t.Fatal("file should not exist after Delete")
|
|
}
|
|
// Delete is idempotent.
|
|
if err := Delete(path); err != nil {
|
|
t.Fatalf("Delete on missing file should be nil: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSignAndVerify(t *testing.T) {
|
|
id, err := Generate()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
msg := []byte("hello user data layer")
|
|
sig, err := Sign(id, msg)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(sig) == 0 {
|
|
t.Fatal("empty sig")
|
|
}
|
|
|
|
// Verify externally with the public key, matching what meta-watch will do.
|
|
pubBytes, err := hex.DecodeString(id.PublicKeyHex)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
pub, err := secp256k1.ParsePubKey(pubBytes)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
parsedSig, err := ecdsa.ParseDERSignature(sig)
|
|
if err != nil {
|
|
t.Fatalf("parse DER sig: %v", err)
|
|
}
|
|
digest := sha256.Sum256(msg)
|
|
if !parsedSig.Verify(digest[:], pub) {
|
|
t.Fatal("signature did not verify")
|
|
}
|
|
|
|
// Flipping a byte must break verification.
|
|
tampered := append([]byte{}, msg...)
|
|
tampered[0] ^= 0x01
|
|
digest2 := sha256.Sum256(tampered)
|
|
if parsedSig.Verify(digest2[:], pub) {
|
|
t.Fatal("signature verified on tampered message")
|
|
}
|
|
}
|
|
|
|
func TestDeriveAEADKeyIsDeterministic(t *testing.T) {
|
|
id, err := Generate()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
k1, err := DeriveAEADKey(id)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
k2, err := DeriveAEADKey(id)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !bytesEqual(k1, k2) {
|
|
t.Fatal("AEAD key derivation is non-deterministic")
|
|
}
|
|
if len(k1) != aeadKeyLen {
|
|
t.Fatalf("AEAD key len = %d, want %d", len(k1), aeadKeyLen)
|
|
}
|
|
|
|
// Different identity → different key.
|
|
other, _ := Generate()
|
|
k3, _ := DeriveAEADKey(other)
|
|
if bytesEqual(k1, k3) {
|
|
t.Fatal("two distinct identities derived the same AEAD key")
|
|
}
|
|
}
|
|
|
|
func TestAccountStoreRoundtrip(t *testing.T) {
|
|
dir := filepath.Join(t.TempDir(), "accounts")
|
|
|
|
// Empty store.
|
|
list, err := List(dir)
|
|
if err != nil {
|
|
t.Fatalf("List on missing dir: %v", err)
|
|
}
|
|
if len(list) != 0 {
|
|
t.Fatalf("expected empty store, got %d", len(list))
|
|
}
|
|
|
|
a, _ := Generate()
|
|
b, _ := Generate()
|
|
if err := SaveAccount(dir, a); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := SaveAccount(dir, b); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if !ExistsByUID(dir, a.UID) || !ExistsByUID(dir, b.UID) {
|
|
t.Fatal("both accounts should exist")
|
|
}
|
|
|
|
list, err = List(dir)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(list) != 2 {
|
|
t.Fatalf("expected 2 accounts, got %d", len(list))
|
|
}
|
|
|
|
got, err := LoadByUID(dir, a.UID)
|
|
if err != nil || got == nil || got.PrivateKeyHex != a.PrivateKeyHex {
|
|
t.Fatalf("LoadByUID roundtrip failed: %+v err=%v", got, err)
|
|
}
|
|
|
|
// Missing uid → (nil, nil).
|
|
if got, err := LoadByUID(dir, "zdoesnotexist"); err != nil || got != nil {
|
|
t.Fatalf("LoadByUID on missing should be (nil,nil): %+v %v", got, err)
|
|
}
|
|
|
|
// Delete one; the other remains. Delete is idempotent.
|
|
if err := DeleteByUID(dir, a.UID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if ExistsByUID(dir, a.UID) {
|
|
t.Fatal("account should be gone after delete")
|
|
}
|
|
if err := DeleteByUID(dir, a.UID); err != nil {
|
|
t.Fatalf("delete should be idempotent: %v", err)
|
|
}
|
|
list, _ = List(dir)
|
|
if len(list) != 1 || list[0].UID != b.UID {
|
|
t.Fatalf("expected only b remaining, got %+v", list)
|
|
}
|
|
}
|
|
|
|
func TestAccountFilePathRejectsTraversal(t *testing.T) {
|
|
for _, bad := range []string{"", "../escape", "a/b", "z..", strings.Repeat("z", 200)} {
|
|
if _, err := accountFilePath("/tmp/x", bad); err == nil {
|
|
t.Fatalf("expected rejection of uid %q", bad)
|
|
}
|
|
}
|
|
// A real uid is accepted.
|
|
id, _ := Generate()
|
|
if _, err := accountFilePath("/tmp/x", id.UID); err != nil {
|
|
t.Fatalf("real uid rejected: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestMigrateLegacy(t *testing.T) {
|
|
base := t.TempDir()
|
|
legacy := filepath.Join(base, "identity.json")
|
|
accounts := filepath.Join(base, "accounts")
|
|
|
|
// No legacy file → no-op.
|
|
if migrated, err := MigrateLegacy(legacy, accounts); err != nil || migrated {
|
|
t.Fatalf("expected no-op migration, got migrated=%v err=%v", migrated, err)
|
|
}
|
|
|
|
id, _ := Generate()
|
|
if err := Save(legacy, id); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
migrated, err := MigrateLegacy(legacy, accounts)
|
|
if err != nil || !migrated {
|
|
t.Fatalf("expected migration to run: migrated=%v err=%v", migrated, err)
|
|
}
|
|
if Exists(legacy) {
|
|
t.Fatal("legacy file should be removed after migration")
|
|
}
|
|
got, err := LoadByUID(accounts, id.UID)
|
|
if err != nil || got == nil || got.UID != id.UID {
|
|
t.Fatalf("migrated account not found: %+v err=%v", got, err)
|
|
}
|
|
|
|
// Second run is a clean no-op (legacy already gone).
|
|
if migrated, err := MigrateLegacy(legacy, accounts); err != nil || migrated {
|
|
t.Fatalf("second migration should no-op: migrated=%v err=%v", migrated, err)
|
|
}
|
|
}
|
|
|
|
func bytesEqual(a, b []byte) bool {
|
|
if len(a) != len(b) {
|
|
return false
|
|
}
|
|
for i := range a {
|
|
if a[i] != b[i] {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|