| 1 | import assert from "node:assert/strict" ; |
| 2 | import test from "node:test" ; |
| 3 | import {finalizeEvent, generateSecretKey, getPublicKey, verifyEvent} from "nostr-tools/pure" ; |
| 4 | import {sharedSigning} from "./test-signing.mjs" ; |
| 5 | |
| 6 | const secret = generateSecretKey(); |
| 7 | const reorder = event => ({content: event.content, tags: event.tags, created_at: event.created_at, kind: event.kind, pubkey: event.pubkey, id: event.id, sig: event.sig}); |
| 8 | |
| 9 | function setup(signEvent) { |
| 10 | const window = {nostr: {signEvent}, NostrSigner: {verifyEvent}}; |
| 11 | const tiny = {}; |
| 12 | return sharedSigning(window, tiny); |
| 13 | } |
| 14 | |
| 15 | test("signing accepts a valid event when a signer reorders object fields" , async () => { |
| 16 | const signing = setup(async unsigned => reorder(finalizeEvent(structuredClone(unsigned), secret))); |
| 17 | const event = await signing.signEvent({kind: 10002, created_at: 1, content: "" , tags: [["r" , "wss://relay.example" ]]}); |
| 18 | assert.equal(verifyEvent(event), true); |
| 19 | }); |
| 20 | |
| 21 | test("signing still rejects a signer that changes an unsigned field" , async () => { |
| 22 | const signing = setup(async unsigned => { |
| 23 | const event = structuredClone(unsigned); |
| 24 | event.content = "changed" ; |
| 25 | return finalizeEvent(event, secret); |
| 26 | }); |
| 27 | await assert.rejects(signing.signEvent({kind: 10002, created_at: 1, content: "" , tags: []}), /invalid or changed/); |
| 28 | }); |
| 29 | |
| 30 | test("signing compares against a snapshot when the caller mutates its input" , async () => { |
| 31 | let received; |
| 32 | const signing = setup(async unsigned => { |
| 33 | received = unsigned; |
| 34 | return finalizeEvent(structuredClone(unsigned), secret); |
| 35 | }); |
| 36 | const unsigned = {kind: 10002, created_at: 1, content: "" , tags: []}; |
| 37 | const pending = signing.signEvent(unsigned); |
| 38 | unsigned.tags.push(["r" , "wss://changed.example" ]); |
| 39 | const event = await pending; |
| 40 | assert.equal(received.tags.length, 0); |
| 41 | assert.equal(event.tags.length, 0); |
| 42 | }); |
| 43 | |
| 44 | test("signing rejects an invalid signature even when every requested field matches" , async () => { |
| 45 | const signing = setup(async unsigned => ({...JSON.parse(JSON.stringify(finalizeEvent(structuredClone(unsigned), secret))), sig: "0" .repeat(128)})); |
| 46 | await assert.rejects(signing.signEvent({content: "" , tags: [], created_at: 1, kind: 10050}), /invalid or changed/); |
| 47 | }); |
| 48 | |
| 49 | test("signing preserves a requested author and rejects a different signer" , async () => { |
| 50 | const signing = setup(async unsigned => reorder(finalizeEvent(structuredClone(unsigned), secret))); |
| 51 | const unsigned = {pubkey: getPublicKey(secret), content: "" , tags: [], created_at: 1, kind: 10002}; |
| 52 | assert.equal((await signing.signEvent(unsigned)).pubkey, unsigned.pubkey); |
| 53 | await assert.rejects(signing.signEvent({...unsigned, pubkey: getPublicKey(generateSecretKey())}), /invalid or changed/); |
| 54 | }); |
| 55 | |
| 56 | test("signing rejects reordered tags and signer-side mutations" , async () => { |
| 57 | const signing = setup(async unsigned => { |
| 58 | unsigned.tags.reverse(); |
| 59 | return finalizeEvent(structuredClone(unsigned), secret); |
| 60 | }); |
| 61 | await assert.rejects(signing.signEvent({content: "" , tags: [["r" , "wss://first.example" ], ["r" , "wss://second.example" ]], created_at: 1, kind: 10002}), /invalid or changed/); |
| 62 | }); |