Files

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
}