Append-only signed event log; persist Dingtian input pushes
Implement the core anti-fraud primitive: an append-only, hash-chained, signed event log (the schema + types predated this; the writer/signer are new). - EventLog (apps/server): serialized append, monotonic index, prevHash chain, signature; verifyChain() detects tamper/reorder/delete. No update/delete paths. - Signer abstraction (packages/shared) over the ATECC608 secure element, with a SoftwareSigner (HMAC, EVENT_SIGNING_KEY) shipped now since the chip is still open-question #6. Documented: software signer is tamper-evident but NOT unforgeable-by-owner. - Add ParkingEventType "input_received" for raw device inputs (not yet a vehicle_entry, which the entry flow will append later). - Read API: GET /api/events; integrity self-check: GET /api/events/verify (admin). Verified on hardware: shorting the Dingtian inputs produced signed, chained input_received events; verifyChain ok; direct DB tamper/delete detected. NOTE: the log captures host-originated actions only. Out-of-band relay actuation (sniffed relay_pw, string protocol, ip_watchdog) produces no event by design -- the control is reconciliation vs. an independent witness, which is not yet built. See wiki/concepts/append-only-event-chain.md.
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
import { createHmac, timingSafeEqual } from "node:crypto";
|
||||
import type { Signer } from "@parking/shared";
|
||||
|
||||
// Concrete signers for the append-only event chain. The Signer interface is the
|
||||
// abstraction over the ATECC608 secure element (open-question #6 — chip not yet
|
||||
// confirmed wired). Until the chip is present we use a software HMAC signer:
|
||||
// it makes the chain self-consistent + tamper-evident, but is NOT unforgeable by
|
||||
// someone who owns the host (only the ATECC608's non-extractable key is). The
|
||||
// swap to hardware is a new Signer impl — no event-log changes.
|
||||
// See wiki/concepts/append-only-event-chain.md and wiki/entities/atecc608.md.
|
||||
|
||||
/** HMAC-SHA256 software signer. Key from env; fail fast if missing in prod. */
|
||||
export class SoftwareSigner implements Signer {
|
||||
readonly keyId: string;
|
||||
readonly #key: Buffer;
|
||||
|
||||
constructor(secret: string, keyId = "sw-hmac-v1") {
|
||||
this.#key = Buffer.from(secret, "utf8");
|
||||
this.keyId = keyId;
|
||||
}
|
||||
|
||||
sign(payload: string): string {
|
||||
return createHmac("sha256", this.#key).update(payload, "utf8").digest("hex");
|
||||
}
|
||||
|
||||
verify(payload: string, signature: string): boolean {
|
||||
const expected = this.sign(payload);
|
||||
// Constant-time compare; bail on length mismatch (timingSafeEqual throws).
|
||||
if (expected.length !== signature.length) return false;
|
||||
return timingSafeEqual(Buffer.from(expected, "hex"), Buffer.from(signature, "hex"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the process signer. Uses EVENT_SIGNING_KEY (HMAC secret). Falls back to
|
||||
* the JWT secret only as a last resort so dev works out of the box — logged as a
|
||||
* warning, because reusing the auth secret for event signing is not ideal.
|
||||
*
|
||||
* TODO(atecc608): when the secure element is wired, return an Atecc608Signer here
|
||||
* (keyId "atecc608-slotN"); existing events stay verifiable via their stored keyId.
|
||||
*/
|
||||
export function buildSigner(log?: { warn: (msg: string) => void }): Signer {
|
||||
const dedicated = process.env.EVENT_SIGNING_KEY;
|
||||
if (dedicated && dedicated.length >= 16) {
|
||||
return new SoftwareSigner(dedicated);
|
||||
}
|
||||
const jwtSecret = process.env.JWT_SECRET;
|
||||
if (jwtSecret && jwtSecret.length >= 16) {
|
||||
log?.warn(
|
||||
"event signing: EVENT_SIGNING_KEY unset — falling back to JWT_SECRET. Set a dedicated key (and wire the ATECC608) before production.",
|
||||
);
|
||||
return new SoftwareSigner(jwtSecret, "sw-hmac-jwtfallback");
|
||||
}
|
||||
throw new Error(
|
||||
"event signing: no signing key. Set EVENT_SIGNING_KEY (>=16 chars) for the append-only event chain.",
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user