Class NostrEventSignatures

java.lang.Object
xyz.tcheeric.cashu.voucher.nostr.NostrEventSignatures

public final class NostrEventSignatures extends Object
Verifies that a Nostr event was signed by the key it claims to come from.

Why this is here rather than in the library

nostr-java signs events but ships no verifier, so a reader has nothing to call. The voucher ledger read path had a corresponding hole: it trusted the pubkey field and the status carried by whatever the relay returned (audit H-13). A relay is an untrusted transport, and the status is what the online double-spend check depends on, so a hostile relay could flip a REDEEMED voucher back to ACTIVE and enable a second redemption.

What NIP-01 requires

The event id is the SHA-256 of a canonical serialisation of the event, and sig is a BIP-340 Schnorr signature over those 32 id bytes by the pubkey.

Why the id must be recomputed

An earlier version of this class took the id straight off the wire and checked only that the signature matched it, on the argument that callers also pin the author. That argument is wrong, and the two checks are orthogonal: pinning the author says who signed, recomputing the id says what they signed.

The ledger is public, so a genuine issuer event for a voucher is world-readable. An attacker copies its id, sig and pubkey verbatim onto an event whose tags say status=ACTIVE and whose created_at is bumped. Every field the old check looked at is genuine, so it verified; the status and content, which no check covered, are the attacker's. Because the read path takes the newest authentic event, the forgery wins, and a REDEEMED voucher is spendable again. That is the double-spend this class exists to prevent.

So the id is recomputed from pubkey, created_at, kind, tags and content and compared against the carried id before the signature is checked. The computation is nostr-java's own EventSerializer, not a reimplementation, so it cannot drift from the form the publish path produces. Note that GenericEvent.update() is unusable here: it overwrites created_at with the current time, which would both destroy the field being authenticated and make the check trivially pass.

  • Method Summary

    Modifier and Type
    Method
    Description
    static boolean
    verify(@NonNull nostr.event.impl.GenericEvent event)
    Whether the event's id matches its contents and its signature is a valid BIP-340 signature over that id by its author.

    Methods inherited from class java.lang.Object

    clone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait
  • Method Details

    • verify

      public static boolean verify(@NonNull @NonNull nostr.event.impl.GenericEvent event)
      Whether the event's id matches its contents and its signature is a valid BIP-340 signature over that id by its author.
      Parameters:
      event - the event to check
      Returns:
      true when the event is authentic in both senses