diff options
| author | bndw <ben@bdw.to> | 2026-02-13 17:35:32 -0800 |
|---|---|---|
| committer | bndw <ben@bdw.to> | 2026-02-13 17:35:32 -0800 |
| commit | 581ceecbf046f99b39885c74e2780a5320e5b15e (patch) | |
| tree | c82dcaddb4f555d5051684221881e36f7fe3f718 /internal/nostr/keys_test.go | |
| parent | 06b9b13274825f797523935494a1b5225f0e0862 (diff) | |
feat: add Nostr protocol implementation (internal/nostr, internal/websocket)
Diffstat (limited to 'internal/nostr/keys_test.go')
| -rw-r--r-- | internal/nostr/keys_test.go | 333 |
1 files changed, 333 insertions, 0 deletions
diff --git a/internal/nostr/keys_test.go b/internal/nostr/keys_test.go new file mode 100644 index 0000000..6c3dd3d --- /dev/null +++ b/internal/nostr/keys_test.go | |||
| @@ -0,0 +1,333 @@ | |||
| 1 | package nostr | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "encoding/hex" | ||
| 5 | "strings" | ||
| 6 | "testing" | ||
| 7 | ) | ||
| 8 | |||
| 9 | func TestGenerateKey(t *testing.T) { | ||
| 10 | key1, err := GenerateKey() | ||
| 11 | if err != nil { | ||
| 12 | t.Fatalf("GenerateKey() error = %v", err) | ||
| 13 | } | ||
| 14 | |||
| 15 | if !key1.CanSign() { | ||
| 16 | t.Error("Generated key should be able to sign") | ||
| 17 | } | ||
| 18 | |||
| 19 | // Private key should be 64 hex characters | ||
| 20 | if len(key1.Private()) != 64 { | ||
| 21 | t.Errorf("Private() length = %d, want 64", len(key1.Private())) | ||
| 22 | } | ||
| 23 | |||
| 24 | // Public key should be 64 hex characters | ||
| 25 | if len(key1.Public()) != 64 { | ||
| 26 | t.Errorf("Public() length = %d, want 64", len(key1.Public())) | ||
| 27 | } | ||
| 28 | |||
| 29 | // Should be valid hex | ||
| 30 | if _, err := hex.DecodeString(key1.Private()); err != nil { | ||
| 31 | t.Errorf("Private() is not valid hex: %v", err) | ||
| 32 | } | ||
| 33 | if _, err := hex.DecodeString(key1.Public()); err != nil { | ||
| 34 | t.Errorf("Public() is not valid hex: %v", err) | ||
| 35 | } | ||
| 36 | |||
| 37 | // Keys should be unique | ||
| 38 | key2, err := GenerateKey() | ||
| 39 | if err != nil { | ||
| 40 | t.Fatalf("GenerateKey() second call error = %v", err) | ||
| 41 | } | ||
| 42 | if key1.Private() == key2.Private() { | ||
| 43 | t.Error("GenerateKey() returned same private key twice") | ||
| 44 | } | ||
| 45 | } | ||
| 46 | |||
| 47 | func TestKeyNpubNsec(t *testing.T) { | ||
| 48 | key, err := GenerateKey() | ||
| 49 | if err != nil { | ||
| 50 | t.Fatalf("GenerateKey() error = %v", err) | ||
| 51 | } | ||
| 52 | |||
| 53 | npub := key.Npub() | ||
| 54 | nsec := key.Nsec() | ||
| 55 | |||
| 56 | // Check prefixes | ||
| 57 | if !strings.HasPrefix(npub, "npub1") { | ||
| 58 | t.Errorf("Npub() = %s, want prefix 'npub1'", npub) | ||
| 59 | } | ||
| 60 | if !strings.HasPrefix(nsec, "nsec1") { | ||
| 61 | t.Errorf("Nsec() = %s, want prefix 'nsec1'", nsec) | ||
| 62 | } | ||
| 63 | |||
| 64 | // Should be able to parse them back | ||
| 65 | keyFromNsec, err := ParseKey(nsec) | ||
| 66 | if err != nil { | ||
| 67 | t.Fatalf("ParseKey(nsec) error = %v", err) | ||
| 68 | } | ||
| 69 | if keyFromNsec.Private() != key.Private() { | ||
| 70 | t.Error("ParseKey(nsec) did not restore original private key") | ||
| 71 | } | ||
| 72 | |||
| 73 | keyFromNpub, err := ParsePublicKey(npub) | ||
| 74 | if err != nil { | ||
| 75 | t.Fatalf("ParsePublicKey(npub) error = %v", err) | ||
| 76 | } | ||
| 77 | if keyFromNpub.Public() != key.Public() { | ||
| 78 | t.Error("ParsePublicKey(npub) did not restore original public key") | ||
| 79 | } | ||
| 80 | } | ||
| 81 | |||
| 82 | func TestParseKey(t *testing.T) { | ||
| 83 | // Known test vector | ||
| 84 | hexKey := "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" | ||
| 85 | |||
| 86 | key, err := ParseKey(hexKey) | ||
| 87 | if err != nil { | ||
| 88 | t.Fatalf("ParseKey(hex) error = %v", err) | ||
| 89 | } | ||
| 90 | |||
| 91 | if !key.CanSign() { | ||
| 92 | t.Error("ParseKey should return key that can sign") | ||
| 93 | } | ||
| 94 | |||
| 95 | if key.Private() != hexKey { | ||
| 96 | t.Errorf("Private() = %s, want %s", key.Private(), hexKey) | ||
| 97 | } | ||
| 98 | |||
| 99 | // Parse the nsec back | ||
| 100 | nsec := key.Nsec() | ||
| 101 | key2, err := ParseKey(nsec) | ||
| 102 | if err != nil { | ||
| 103 | t.Fatalf("ParseKey(nsec) error = %v", err) | ||
| 104 | } | ||
| 105 | if key2.Private() != hexKey { | ||
| 106 | t.Error("Round-trip through nsec failed") | ||
| 107 | } | ||
| 108 | } | ||
| 109 | |||
| 110 | func TestParseKeyErrors(t *testing.T) { | ||
| 111 | tests := []struct { | ||
| 112 | name string | ||
| 113 | key string | ||
| 114 | }{ | ||
| 115 | {"invalid hex", "not-hex"}, | ||
| 116 | {"too short", "0123456789abcdef"}, | ||
| 117 | {"too long", "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef00"}, | ||
| 118 | {"invalid nsec", "nsec1invalid"}, | ||
| 119 | } | ||
| 120 | |||
| 121 | for _, tt := range tests { | ||
| 122 | t.Run(tt.name, func(t *testing.T) { | ||
| 123 | _, err := ParseKey(tt.key) | ||
| 124 | if err == nil { | ||
| 125 | t.Error("ParseKey() expected error, got nil") | ||
| 126 | } | ||
| 127 | }) | ||
| 128 | } | ||
| 129 | } | ||
| 130 | |||
| 131 | func TestParsePublicKey(t *testing.T) { | ||
| 132 | // Generate a key and extract public | ||
| 133 | fullKey, _ := GenerateKey() | ||
| 134 | pubHex := fullKey.Public() | ||
| 135 | |||
| 136 | // Parse public key from hex | ||
| 137 | key, err := ParsePublicKey(pubHex) | ||
| 138 | if err != nil { | ||
| 139 | t.Fatalf("ParsePublicKey(hex) error = %v", err) | ||
| 140 | } | ||
| 141 | |||
| 142 | if key.CanSign() { | ||
| 143 | t.Error("ParsePublicKey should return key that cannot sign") | ||
| 144 | } | ||
| 145 | |||
| 146 | if key.Public() != pubHex { | ||
| 147 | t.Errorf("Public() = %s, want %s", key.Public(), pubHex) | ||
| 148 | } | ||
| 149 | |||
| 150 | if key.Private() != "" { | ||
| 151 | t.Error("Private() should return empty string for public-only key") | ||
| 152 | } | ||
| 153 | |||
| 154 | if key.Nsec() != "" { | ||
| 155 | t.Error("Nsec() should return empty string for public-only key") | ||
| 156 | } | ||
| 157 | |||
| 158 | // Parse from npub | ||
| 159 | npub := fullKey.Npub() | ||
| 160 | key2, err := ParsePublicKey(npub) | ||
| 161 | if err != nil { | ||
| 162 | t.Fatalf("ParsePublicKey(npub) error = %v", err) | ||
| 163 | } | ||
| 164 | if key2.Public() != pubHex { | ||
| 165 | t.Error("ParsePublicKey(npub) did not restore correct public key") | ||
| 166 | } | ||
| 167 | } | ||
| 168 | |||
| 169 | func TestParsePublicKeyErrors(t *testing.T) { | ||
| 170 | tests := []struct { | ||
| 171 | name string | ||
| 172 | key string | ||
| 173 | }{ | ||
| 174 | {"invalid hex", "not-hex"}, | ||
| 175 | {"too short", "0123456789abcdef"}, | ||
| 176 | {"invalid npub", "npub1invalid"}, | ||
| 177 | } | ||
| 178 | |||
| 179 | for _, tt := range tests { | ||
| 180 | t.Run(tt.name, func(t *testing.T) { | ||
| 181 | _, err := ParsePublicKey(tt.key) | ||
| 182 | if err == nil { | ||
| 183 | t.Error("ParsePublicKey() expected error, got nil") | ||
| 184 | } | ||
| 185 | }) | ||
| 186 | } | ||
| 187 | } | ||
| 188 | |||
| 189 | func TestKeySign(t *testing.T) { | ||
| 190 | key, err := GenerateKey() | ||
| 191 | if err != nil { | ||
| 192 | t.Fatalf("GenerateKey() error = %v", err) | ||
| 193 | } | ||
| 194 | |||
| 195 | event := &Event{ | ||
| 196 | CreatedAt: 1704067200, | ||
| 197 | Kind: 1, | ||
| 198 | Tags: Tags{}, | ||
| 199 | Content: "Test message", | ||
| 200 | } | ||
| 201 | |||
| 202 | if err := key.Sign(event); err != nil { | ||
| 203 | t.Fatalf("Sign() error = %v", err) | ||
| 204 | } | ||
| 205 | |||
| 206 | // Check that all fields are set | ||
| 207 | if event.PubKey == "" { | ||
| 208 | t.Error("Sign() did not set PubKey") | ||
| 209 | } | ||
| 210 | if event.ID == "" { | ||
| 211 | t.Error("Sign() did not set ID") | ||
| 212 | } | ||
| 213 | if event.Sig == "" { | ||
| 214 | t.Error("Sign() did not set Sig") | ||
| 215 | } | ||
| 216 | |||
| 217 | // PubKey should match | ||
| 218 | if event.PubKey != key.Public() { | ||
| 219 | t.Errorf("PubKey = %s, want %s", event.PubKey, key.Public()) | ||
| 220 | } | ||
| 221 | |||
| 222 | // Signature should be 128 hex characters (64 bytes) | ||
| 223 | if len(event.Sig) != 128 { | ||
| 224 | t.Errorf("Signature length = %d, want 128", len(event.Sig)) | ||
| 225 | } | ||
| 226 | } | ||
| 227 | |||
| 228 | func TestKeySignPublicOnlyError(t *testing.T) { | ||
| 229 | fullKey, _ := GenerateKey() | ||
| 230 | pubOnlyKey, _ := ParsePublicKey(fullKey.Public()) | ||
| 231 | |||
| 232 | event := &Event{ | ||
| 233 | CreatedAt: 1704067200, | ||
| 234 | Kind: 1, | ||
| 235 | Tags: Tags{}, | ||
| 236 | Content: "Test", | ||
| 237 | } | ||
| 238 | |||
| 239 | err := pubOnlyKey.Sign(event) | ||
| 240 | if err == nil { | ||
| 241 | t.Error("Sign() with public-only key should return error") | ||
| 242 | } | ||
| 243 | } | ||
| 244 | |||
| 245 | func TestEventVerify(t *testing.T) { | ||
| 246 | key, err := GenerateKey() | ||
| 247 | if err != nil { | ||
| 248 | t.Fatalf("GenerateKey() error = %v", err) | ||
| 249 | } | ||
| 250 | |||
| 251 | event := &Event{ | ||
| 252 | CreatedAt: 1704067200, | ||
| 253 | Kind: 1, | ||
| 254 | Tags: Tags{{"test", "value"}}, | ||
| 255 | Content: "Test message for verification", | ||
| 256 | } | ||
| 257 | |||
| 258 | if err := key.Sign(event); err != nil { | ||
| 259 | t.Fatalf("Sign() error = %v", err) | ||
| 260 | } | ||
| 261 | |||
| 262 | if !event.Verify() { | ||
| 263 | t.Error("Verify() returned false for valid signature") | ||
| 264 | } | ||
| 265 | } | ||
| 266 | |||
| 267 | func TestEventVerifyInvalid(t *testing.T) { | ||
| 268 | key, err := GenerateKey() | ||
| 269 | if err != nil { | ||
| 270 | t.Fatalf("GenerateKey() error = %v", err) | ||
| 271 | } | ||
| 272 | |||
| 273 | event := &Event{ | ||
| 274 | CreatedAt: 1704067200, | ||
| 275 | Kind: 1, | ||
| 276 | Tags: Tags{}, | ||
| 277 | Content: "Test message", | ||
| 278 | } | ||
| 279 | |||
| 280 | if err := key.Sign(event); err != nil { | ||
| 281 | t.Fatalf("Sign() error = %v", err) | ||
| 282 | } | ||
| 283 | |||
| 284 | // Corrupt the content (ID becomes invalid) | ||
| 285 | event.Content = "Modified content" | ||
| 286 | if event.Verify() { | ||
| 287 | t.Error("Verify() returned true for modified content") | ||
| 288 | } | ||
| 289 | |||
| 290 | // Restore content but corrupt signature | ||
| 291 | event.Content = "Test message" | ||
| 292 | event.SetID() | ||
| 293 | event.Sig = "0000000000000000000000000000000000000000000000000000000000000000" + | ||
| 294 | "0000000000000000000000000000000000000000000000000000000000000000" | ||
| 295 | if event.Verify() { | ||
| 296 | t.Error("Verify() returned true for invalid signature") | ||
| 297 | } | ||
| 298 | } | ||
| 299 | |||
| 300 | func TestSignAndVerifyRoundTrip(t *testing.T) { | ||
| 301 | // Generate key | ||
| 302 | key, err := GenerateKey() | ||
| 303 | if err != nil { | ||
| 304 | t.Fatalf("GenerateKey() error = %v", err) | ||
| 305 | } | ||
| 306 | |||
| 307 | // Create and sign event | ||
| 308 | event := &Event{ | ||
| 309 | CreatedAt: 1704067200, | ||
| 310 | Kind: KindTextNote, | ||
| 311 | Tags: Tags{{"t", "test"}}, | ||
| 312 | Content: "Integration test message", | ||
| 313 | } | ||
| 314 | |||
| 315 | if err := key.Sign(event); err != nil { | ||
| 316 | t.Fatalf("Sign() error = %v", err) | ||
| 317 | } | ||
| 318 | |||
| 319 | // Verify public key matches | ||
| 320 | if event.PubKey != key.Public() { | ||
| 321 | t.Errorf("Signed event PubKey = %s, want %s", event.PubKey, key.Public()) | ||
| 322 | } | ||
| 323 | |||
| 324 | // Verify the signature | ||
| 325 | if !event.Verify() { | ||
| 326 | t.Error("Verify() failed for freshly signed event") | ||
| 327 | } | ||
| 328 | |||
| 329 | // Check ID is correct | ||
| 330 | if !event.CheckID() { | ||
| 331 | t.Error("CheckID() failed for freshly signed event") | ||
| 332 | } | ||
| 333 | } | ||
