LOCAL-TRUE · not on-chain

MPS-0029 · Compact caller identity

Auth Forge Studio

World-class educational lab for the Midnight auth trap: ownPublicKey() is a prover-supplied witness — not bound to the transaction signer. Any assert(ownPublicKey() == owner) is theater. Prefer witness-derived keys (example-bboard / create-mn-app --template bboard). By @kshot9000 · nightdream.xyz.

Honest scope: LOCAL-TRUE browser persistence (schema v2 localStorage, multi-tab sync, JSON export/import). Does not run Compact, a proof server, or Lace makeTransfer. Does not claim GitHub Pages is live until Actions are enabled. SHA-256 “derive” here is a teaching stand-in — verify against official Compact crypto before shipping. LOCAL-TRUE ≠ on-chain.

Attack journey

Labeled phases for the unsafe forge path. Error / reject is a first-class state — never silent.

  1. 1
    Idle No unsafe contract deployed
  2. 2
    Deployed owner ← claimed ownPublicKey
  3. 3
    Forged Mallory bypasses admin gate
  4. 4
    Witness-bound Safe path rejects forger

Phase: idle — deploy the unsafe contract to begin.

The rule in one screen

Safe uses of ownPublicKey() are token routing after a real auth gate — never the gate itself.

Unsafe

Auth via ownPublicKey()

Prover picks any 32-byte value. Assertions that compare it to a stored owner are theater.

  • assert(ownPublicKey() == admin)
  • Blacklists keyed only by ownPublicKey()
  • Role maps filled from ownPublicKey() at register
Safe

Witness-derived identity

Caller proves knowledge of a secret. Derive a public commitment in-circuit (bboard / zkloan pattern).

  • witness localSecretKey(): Bytes<32>
  • publicKey(sk) / persistentHash with domain sep
  • ownPublicKey() only to route shielded coins

Public claim rail

Anyone can assert “I am Alice” by stuffing Alice’s pk into the witness.

0% forge pressure

Secret-bound rail

Without Alice’s secret, Mallory’s derived pk never matches owner.

100% integrity (until safe deploy)

Forge theater simulated

Local model of a naive “owner = ownPublicKey” contract. No Compact runtime — educational only.

1. Deploy (honest Alice)

Alice deploys and stores her claimed public key as owner.

64 hex chars · 32 bytes

Not deployed.

2. Attack (Mallory)

Mallory calls adminOnly() while supplying Alice’s public key as her “ownPublicKey”.

Waiting…

3. Same call with witness binding

With a secret-bound owner, Mallory cannot reproduce Alice’s derived key without Alice’s secret.

Safe contract not deployed.


        

Auth scorecard

Interactive checklist — tick what your Compact design actually does. Local only; nothing leaves the browser.

Check items to grade your design.

Safe local bulletin board

bboard-inspired LOCAL-TRUE board: posts bind to publicKey(localSecret). Schema v2 localStorage · BroadcastChannel sync · JSON export/import. Not on-chain.

Donate + @kshot9000 · sensitive secrets may be in exports

Your local secret

Generated in-browser. Never sent anywhere. Mimics localSecretKey witness.

— pk: —

Post message

0 / 280

Board (0)

    Wallet discovery

    Enumerate Object.entries(window.midnight). Do not hardcode mnLace — keys are often UUIDs. Read-only — no connect / transfer.

    Not scanned yet.

    Compact snippets

    Illustrative — verify against current Compact docs / example-bboard before shipping.

    ✗ Forgeable
    export ledger owner: ZswapCoinPublicKey;
    
    constructor() {
      owner = ownPublicKey(); // anyone can later claim this
    }
    
    export circuit adminOnly(): [] {
      assert(ownPublicKey() == owner, "not owner"); // bypassable
    }
    ✓ Witness-derived (bboard-style)
    witness localSecretKey(): Bytes<32>;
    
    export ledger owner: Bytes<32>;
    
    constructor() {
      owner = publicKey(localSecretKey());
    }
    
    export circuit adminOnly(): [] {
      assert(
        disclose(publicKey(localSecretKey()) == owner),
        "not owner"
      );
    }

    Prefer starters: example-bboard · create-mn-app · Counter is archived. Official security: security best practices.