Skip to content

feat(badges): surface the per-RP gating nullifier + branded type + drift gate (crypto-core M5)#13

Merged
AtHeartEngineer merged 1 commit into
mainfrom
feat/crypto-core-phase4-sdk-nullifier
Jul 5, 2026
Merged

feat(badges): surface the per-RP gating nullifier + branded type + drift gate (crypto-core M5)#13
AtHeartEngineer merged 1 commit into
mainfrom
feat/crypto-core-phase4-sdk-nullifier

Conversation

@AtHeartEngineer

@AtHeartEngineer AtHeartEngineer commented Jul 5, 2026

Copy link
Copy Markdown
Member

The SDK half of crypto-core Phase 4 (M5): the RP-facing verification surface for the per-RP Sybil-dedup nullifier Minister now binds into the signed VC.

Stacked on feat/crypto-core-phase1-badge-schema (the Phase 1 badge-schema branch is not yet on main); this PR's base is that branch so the diff is Phase 4 only.

What changed

  • Branded MinisterGatingNullifier (mnv1:...) — non-interchangeable at compile time with @ministryofmany/nullifier's Poseidon/BN254 string. Two permanently distinct primitives (credential-anchored VOPRF+HMAC gating tag vs account-anchored, circuit-usable Poseidon); no conversion.
  • verify-badge strips nullifier as a third reserved key BEFORE schema.parse, so strict per-type schemas (account-age, social-following, tlsn-attestation) stay strict. Format-checks ^mnv1:[A-Za-z0-9_-]+$ (fail closed on malformed) and surfaces nullifier?: MinisterGatingNullifier on VerifiedBadge. Absent ⇒ undefined (ref-less / pre-M5) — unchanged behavior for badges with no nullifier.
  • README — the gating-nullifier section: binding rule (VC subject tail == id_token sub), the two-primitives table (M3), and the "one credential, not one person" honesty tied to per-type sybilResistance.
  • drift.test.ts — the long-planned cross-package drift gate: pins this registry (slug set, credentialType, sybilResistance, schema shape/strictness) against a frozen transcription of @ministryofmany/shared.
  • Rebuilt dist/ committed (no prepare script).

Verification

  • pnpm exec tsc --noEmit OK
  • pnpm exec vitest run OK: 474 passed across the repo; the M5 additions are verify-badge (20, incl. strict account-age/social-following expose-the-value + malformed fail-closed) and drift (61).
  • pnpm run build OK, dist/ regenerated and committed.

Discreetly compatibility (not repinned in this PR — confirmed)

Discreetly's server verifier (@ministryofmany/minister-verify) delegates to this SDK's verifyMinisterBadge, so it inherits the nullifier strip/surface automatically. Both Discreetly mock issuers emit no nullifier claim, so badges verify with nullifier: undefined and strict schemas are unaffected. @ministryofmany/client is consumed via link:; no repin needed.

@AtHeartEngineer

Copy link
Copy Markdown
Member Author

Audit resolution (crypto-core Phase 4 SDK). Resolved every SDK-side finding:

  • verify-badge: LENGTH-BOUND the mnv1 tail ({20,64}) — a buggy/compromised issuer can no longer stamp an unbounded tag RPs persist in ban/dedup tables (real value is base64url of 32 bytes = 43 chars). Added too-short and over-cap rejection cases.
  • @ministryofmany/nullifier toField: runtime guard rejecting ^mnv1: — the brand only blocked one direction (a branded value widens back to string), so toField(badge.nullifier) would silently bridge the two non-interchangeable primitives (M3). Added tests.
  • badges/drift.test: reserved-key guard mirroring the new provider-side contract (no schema may echo id/issuanceMonth/nullifier).
  • packages/nullifier/README.md: added with the ADR §2.4(b) two-primitives table + never-bridge-from-mnv1 rule.
  • minister-verify: documented that the per-RP nullifier is intentionally dropped in the badge->policy mapping (Discreetly gating follow-up is optional) so it isn't mistaken for wired-through.

Verify: tsc --noEmit clean, vitest run 495 passed, pnpm run build green, dist/ rebuilt and committed.

@AtHeartEngineer
AtHeartEngineer changed the base branch from feat/crypto-core-phase1-badge-schema to main July 5, 2026 04:42
…ift gate

Surface the crypto-core Phase 4 per-RP Sybil nullifier through the SDK:
add the branded `MinisterGatingNullifier` type, strip and format-check the
VC `credentialSubject.nullifier` in verify-badge before the strict parse,
surface it on account-age and social-following, add the `@ministryofmany/
nullifier` derive package, and add the two-sided badge drift gate.

Rebased onto main; dist regenerated from source on main's tsup toolchain.
@AtHeartEngineer
AtHeartEngineer force-pushed the feat/crypto-core-phase4-sdk-nullifier branch from 0f8ed3d to 68efd87 Compare July 5, 2026 04:47
@AtHeartEngineer
AtHeartEngineer merged commit 535c4a6 into main Jul 5, 2026
1 check passed
@AtHeartEngineer
AtHeartEngineer deleted the feat/crypto-core-phase4-sdk-nullifier branch July 5, 2026 04:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant