mirror of
https://github.com/PR0M3TH3AN/bitgate.git
synced 2026-10-29 09:31:00 +00:00
feat(verify): optional NIP-01 signature verification, on by default in the widget
Closes the gap the audit left open: administrative state fails closed without a verifier, and BitGate shipped none. @bitgate/verify is a separate package so core, nostr, and runtime keep no crypto dependency and can be audited without one. The widget bundles it, so the drop-in path is verified with no extra step; verify="off" disables it, sets data-unverified on the element, and emits bitgate:unverified — meant for local development, and meant to be visible. Verification recomputes the event id before checking the signature. Checking only the signature would let a forged event pass by reusing a genuine signature over an unrelated id, which is the failure mode that matters here. createVerifier also rejects events dated far in the future. A far-future created_at wins replaceable-event selection indefinitely and pins stale state in place; the signature is perfectly valid, so only a clock check catches it. Found while testing: #adminIngestAllowed checked that a verifier *exists*, not that this event was verified — so the public ingestEvent path, which "bring your own relays" integrations use, bypassed verification entirely. Verification now runs inside ingestEvent, with an already-verified flag for the loaders. An async verifier cannot be awaited there and is treated as unverified; ingestVerified() awaits properly. Conformance validated against 120 live events from a public relay across kinds 0, 1, 3 and 10002 — 59 with non-ASCII content and 71 containing quotes, newlines or tabs, which is where a serialization mismatch would surface. All 120 verified, zero id mismatches. Bundle grows 72K -> 120K. 855 tests; audit clean.
This commit is contained in:
1 parent
94ef6a72f5
commit
3b95a63058
14 files changed
+596
-12
No files matched your search
@@ -335,11 +335,21 @@ export class GovernanceRuntime extends Emitter {
|
||||
* @param {NostrEvent} event
|
||||
* @returns {boolean} Whether the event was recognized
|
||||
*/
|
||||
ingestEvent(event) {
|
||||
ingestEvent(event, { verified = false } = {}) {
|
||||
if (!event || typeof event.kind !== "number") {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verification here rather than only in the loaders: ingestEvent is public,
|
||||
// and an application feeding events from its own relay pool — a documented
|
||||
// use case — would otherwise get none. Internal callers that have already
|
||||
// verified pass `verified: true` to avoid checking twice.
|
||||
if (!verified && !this.#passesVerification(event)) {
|
||||
this.diagnostics.rejectedSignatures += 1;
|
||||
this.emit("rejected", { reason: "invalid-signature", id: event.id });
|
||||
return false;
|
||||
}
|
||||
|
||||
this.diagnostics.eventsIngested += 1;
|
||||
|
||||
if (event.kind === REPORT_KIND) {
|
||||
@@ -463,6 +473,50 @@ export class GovernanceRuntime extends Emitter {
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
/**
|
||||
* Run the configured verifier synchronously.
|
||||
*
|
||||
* A verifier that returns a promise cannot be awaited on this path, so it is
|
||||
* treated as unverified: callers with an async verifier must use the loaders
|
||||
* or `ingestVerified()`, both of which await properly. Failing closed is the
|
||||
* only safe reading of "I could not check this".
|
||||
*
|
||||
* @param {NostrEvent} event
|
||||
* @returns {boolean}
|
||||
*/
|
||||
#passesVerification(event) {
|
||||
if (!this.verifySignature) {
|
||||
// No verifier configured: trust signals still flow, and administrative
|
||||
// documents are refused separately by #adminIngestAllowed().
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
const result = this.verifySignature(event);
|
||||
if (typeof result === "boolean") {
|
||||
return result;
|
||||
}
|
||||
return false;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ingest an event, awaiting an asynchronous verifier when one is configured.
|
||||
* @param {NostrEvent} event
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async ingestVerified(event) {
|
||||
if (this.verifySignature) {
|
||||
const [ok] = await this.#verified([event]);
|
||||
if (!ok) {
|
||||
this.diagnostics.rejectedSignatures += 1;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return this.ingestEvent(event, { verified: true });
|
||||
}
|
||||
|
||||
#adminIngestAllowed() {
|
||||
if (this.verifySignature || this.trustUnsignedEvents) {
|
||||
return true;
|
||||
@@ -603,7 +657,7 @@ export class GovernanceRuntime extends Emitter {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return this.ingestEvent(event);
|
||||
return this.ingestEvent(event, { verified: true });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -678,7 +732,7 @@ export class GovernanceRuntime extends Emitter {
|
||||
|
||||
const effective = Array.from(selectReplaceable(await this.#verified(events)).values());
|
||||
for (const event of effective) {
|
||||
this.ingestEvent(event);
|
||||
this.ingestEvent(event, { verified: true });
|
||||
}
|
||||
|
||||
this.stale = false;
|
||||
@@ -757,7 +811,7 @@ export class GovernanceRuntime extends Emitter {
|
||||
}
|
||||
|
||||
for (const event of selectReplaceable(await this.#verified(events)).values()) {
|
||||
if (this.ingestEvent(event)) {
|
||||
if (this.ingestEvent(event, { verified: true })) {
|
||||
learned += 1;
|
||||
}
|
||||
}
|
||||
@@ -787,7 +841,7 @@ export class GovernanceRuntime extends Emitter {
|
||||
}
|
||||
|
||||
for (const event of selectReplaceable(await this.#verified(events)).values()) {
|
||||
this.ingestEvent(event);
|
||||
this.ingestEvent(event, { verified: true });
|
||||
}
|
||||
|
||||
return this.trust.contacts.size;
|
||||
@@ -838,7 +892,7 @@ export class GovernanceRuntime extends Emitter {
|
||||
}
|
||||
|
||||
for (const event of selectReplaceable(await this.#verified(events)).values()) {
|
||||
if (this.ingestEvent(event)) {
|
||||
if (this.ingestEvent(event, { verified: true })) {
|
||||
ingested += 1;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user